From cf681b2260fab39695145d8fd530a370ab9273f1 Mon Sep 17 00:00:00 2001 From: swve Date: Sun, 31 Dec 2023 16:31:43 +0000 Subject: [PATCH] feat: init memory into ai chat messaging --- .devcontainer/devcontainer.json | 1 + apps/api/config/config.py | 11 +++ apps/api/config/config.yaml | 3 + apps/api/requirements.txt | 1 + apps/api/src/routers/ai/ai.py | 20 ++++- apps/api/src/services/ai/ai.py | 105 ++++++++++++++++++++++--- apps/api/src/services/ai/base.py | 34 ++++++-- apps/api/src/services/ai/schemas/ai.py | 5 ++ docker-compose.yml | 5 ++ 9 files changed, 163 insertions(+), 22 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fa53814b..ff600b2a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,6 +8,7 @@ "extensions": [ "eamodio.gitlens", "ms-python.python", + "ms-python.black-formatter", "ms-python.vscode-pylance", "styled-components.vscode-styled-components", "dbaeumer.vscode-eslint", diff --git a/apps/api/config/config.py b/apps/api/config/config.py index d23d5ada..765eebac 100644 --- a/apps/api/config/config.py +++ b/apps/api/config/config.py @@ -55,6 +55,8 @@ class DatabaseConfig(BaseModel): sql_connection_string: Optional[str] mongo_connection_string: Optional[str] +class RedisConfig(BaseModel): + redis_connection_string: Optional[str] class LearnHouseConfig(BaseModel): site_name: str @@ -63,6 +65,7 @@ class LearnHouseConfig(BaseModel): general_config: GeneralConfig hosting_config: HostingConfig database_config: DatabaseConfig + redis_config: RedisConfig security_config: SecurityConfig ai_config: AIConfig @@ -183,6 +186,13 @@ def get_learnhouse_config() -> LearnHouseConfig: "mongo_connection_string" ) + # Redis config + env_redis_connection_string = os.environ.get("LEARNHOUSE_REDIS_CONNECTION_STRING") + redis_connection_string = env_redis_connection_string or yaml_config.get( + "redis_config", {} + ).get("redis_connection_string") + + # AI Config env_openai_api_key = os.environ.get("LEARNHOUSE_OPENAI_API_KEY") env_is_ai_enabled = os.environ.get("LEARNHOUSE_IS_AI_ENABLED") @@ -255,6 +265,7 @@ def get_learnhouse_config() -> LearnHouseConfig: database_config=database_config, security_config=SecurityConfig(auth_jwt_secret_key=auth_jwt_secret_key), ai_config=ai_config, + redis_config=RedisConfig(redis_connection_string=redis_connection_string), ) return config diff --git a/apps/api/config/config.yaml b/apps/api/config/config.yaml index 17bea379..923cc3ac 100644 --- a/apps/api/config/config.yaml +++ b/apps/api/config/config.yaml @@ -27,3 +27,6 @@ hosting_config: database_config: sql_connection_string: postgresql://learnhouse:learnhouse@db:5432/learnhouse mongo_connection_string: mongodb://learnhouse:learnhouse@mongo:27017/ + +redis_config: + redis_connection_string: redis://redis:6379/learnhouse diff --git a/apps/api/requirements.txt b/apps/api/requirements.txt index 0d25add8..5e21e2f0 100644 --- a/apps/api/requirements.txt +++ b/apps/api/requirements.txt @@ -24,3 +24,4 @@ openai chromadb sentence-transformers python-dotenv +redis diff --git a/apps/api/src/routers/ai/ai.py b/apps/api/src/routers/ai/ai.py index a48626d0..d66bb6f8 100644 --- a/apps/api/src/routers/ai/ai.py +++ b/apps/api/src/routers/ai/ai.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, Request from sqlmodel import Session -from src.services.ai.ai import ai_start_activity_chat_session -from src.services.ai.schemas.ai import StartActivityAIChatSession +from src.services.ai.ai import ai_send_activity_chat_message, ai_start_activity_chat_session +from src.services.ai.schemas.ai import ActivityAIChatSessionResponse, SendActivityAIChatMessage, StartActivityAIChatSession from src.core.events.database import get_db_session from src.db.users import PublicUser from src.security.auth import get_current_user @@ -16,10 +16,24 @@ async def api_ai_start_activity_chat_session( chat_session_object: StartActivityAIChatSession, current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), -): +)-> ActivityAIChatSessionResponse: """ Start a new AI Chat session with a Course Activity """ return ai_start_activity_chat_session( request, chat_session_object, current_user, db_session ) + +@router.post("/send/activity_chat_message") +async def api_ai_send_activity_chat_message( + request: Request, + chat_session_object: SendActivityAIChatMessage, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +)-> ActivityAIChatSessionResponse: + """ + Send a message to an AI Chat session with a Course Activity + """ + return ai_send_activity_chat_message( + request, chat_session_object, current_user, db_session + ) \ No newline at end of file diff --git a/apps/api/src/services/ai/ai.py b/apps/api/src/services/ai/ai.py index 058598b0..bc776962 100644 --- a/apps/api/src/services/ai/ai.py +++ b/apps/api/src/services/ai/ai.py @@ -1,13 +1,20 @@ +from uuid import uuid4 from fastapi import Depends, HTTPException, Request +from requests import session from sqlmodel import Session, select from src.db.courses import Course, CourseRead from src.core.events.database import get_db_session from src.db.users import PublicUser from src.db.activities import Activity, ActivityRead from src.security.auth import get_current_user -from src.services.ai.base import ask_ai +from langchain.memory.chat_message_histories import RedisChatMessageHistory +from src.services.ai.base import ask_ai, get_chat_session_history -from src.services.ai.schemas.ai import StartActivityAIChatSession +from src.services.ai.schemas.ai import ( + ActivityAIChatSessionResponse, + SendActivityAIChatMessage, + StartActivityAIChatSession, +) from src.services.courses.activities.utils import ( serialize_activity_text_to_ai_comprehensible_text, structure_activity_content_by_type, @@ -19,7 +26,7 @@ def ai_start_activity_chat_session( chat_session_object: StartActivityAIChatSession, current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), -): +) -> ActivityAIChatSessionResponse: """ Start a new AI Chat session with a Course Activity """ @@ -32,13 +39,14 @@ def ai_start_activity_chat_session( activity = ActivityRead.from_orm(activity) # Get the Course - statement = select(Course).join(Activity).where( - Activity.activity_uuid == chat_session_object.activity_uuid + statement = ( + select(Course) + .join(Activity) + .where(Activity.activity_uuid == chat_session_object.activity_uuid) ) course = db_session.exec(statement).first() course = CourseRead.from_orm(course) - if not activity: raise HTTPException( status_code=404, @@ -50,16 +58,91 @@ def ai_start_activity_chat_session( # Serialize Activity Content Blocks to a text comprehensible by the AI structured = structure_activity_content_by_type(content) - ai_friendly_text = serialize_activity_text_to_ai_comprehensible_text(structured,course,activity) + ai_friendly_text = serialize_activity_text_to_ai_comprehensible_text( + structured, course, activity + ) + + chat_session = get_chat_session_history() response = ask_ai( chat_session_object.message, - [], + chat_session['message_history'], ai_friendly_text, "You are a helpful Education Assistant, and you are helping a student with the associated Course. " "Use the available tools to get context about this question even if the question is not specific enough." - "For context, this is the Course name :" + course.name + " and this is the Lecture name :" + activity.name + "." - "Use your knowledge to help the student." + "For context, this is the Course name :" + + course.name + + " and this is the Lecture name :" + + activity.name + + "." + "Use your knowledge to help the student.", ) - return response['output'] + return ActivityAIChatSessionResponse( + aichat_uuid=chat_session['aichat_uuid'], + activity_uuid=activity.activity_uuid, + message=response["output"], + ) + +def ai_send_activity_chat_message( + request: Request, + chat_session_object: SendActivityAIChatMessage, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +) -> ActivityAIChatSessionResponse: + """ + Start a new AI Chat session with a Course Activity + """ + # Get the Activity + statement = select(Activity).where( + Activity.activity_uuid == chat_session_object.activity_uuid + ) + activity = db_session.exec(statement).first() + + activity = ActivityRead.from_orm(activity) + + # Get the Course + statement = ( + select(Course) + .join(Activity) + .where(Activity.activity_uuid == chat_session_object.activity_uuid) + ) + course = db_session.exec(statement).first() + course = CourseRead.from_orm(course) + + if not activity: + raise HTTPException( + status_code=404, + detail="Activity not found", + ) + + # Get Activity Content Blocks + content = activity.content + + # Serialize Activity Content Blocks to a text comprehensible by the AI + structured = structure_activity_content_by_type(content) + ai_friendly_text = serialize_activity_text_to_ai_comprehensible_text( + structured, course, activity + ) + + chat_session = get_chat_session_history(chat_session_object.aichat_uuid) + + response = ask_ai( + chat_session_object.message, + chat_session['message_history'], + ai_friendly_text, + "You are a helpful Education Assistant, and you are helping a student with the associated Course. " + "Use the available tools to get context about this question even if the question is not specific enough." + "For context, this is the Course name :" + + course.name + + " and this is the Lecture name :" + + activity.name + + "." + "Use your knowledge to help the student if the context is not enough.", + ) + + return ActivityAIChatSessionResponse( + aichat_uuid=chat_session['aichat_uuid'], + activity_uuid=activity.activity_uuid, + message=response["output"], + ) diff --git a/apps/api/src/services/ai/base.py b/apps/api/src/services/ai/base.py index cc5fb140..02674e92 100644 --- a/apps/api/src/services/ai/base.py +++ b/apps/api/src/services/ai/base.py @@ -1,3 +1,5 @@ +from typing import Optional +from uuid import uuid4 from langchain.agents import AgentExecutor from langchain.text_splitter import CharacterTextSplitter from langchain.embeddings.sentence_transformer import SentenceTransformerEmbeddings @@ -5,7 +7,7 @@ from langchain.vectorstores import Chroma from langchain_core.messages import BaseMessage from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent from langchain.prompts import MessagesPlaceholder -from langchain.memory import ConversationBufferMemory +from langchain.memory.chat_message_histories import RedisChatMessageHistory from langchain_core.messages import SystemMessage from langchain.agents.openai_functions_agent.agent_token_buffer_memory import ( AgentTokenBufferMemory, @@ -27,7 +29,7 @@ chat_history = [] def ask_ai( question: str, - chat_history: list[BaseMessage], + message_history, text_reference: str, message_for_the_prompt: str, ): @@ -52,14 +54,13 @@ def ask_ai( ) tools = [tool] - llm = ChatOpenAI( - temperature=0, api_key=openai_api_key - ) + llm = ChatOpenAI(temperature=0, api_key=openai_api_key) memory_key = "history" - memory = AgentTokenBufferMemory(memory_key=memory_key, llm=llm) - + memory = AgentTokenBufferMemory( + memory_key=memory_key, llm=llm, chat_memory=message_history + ) system_message = SystemMessage(content=(message_for_the_prompt)) @@ -70,7 +71,6 @@ def ask_ai( agent = OpenAIFunctionsAgent(llm=llm, tools=tools, prompt=prompt) - agent_executor = AgentExecutor( agent=agent, tools=tools, @@ -80,3 +80,21 @@ def ask_ai( ) return agent_executor({"input": question}) + + +def get_chat_session_history(aichat_uuid: Optional[str] = None): + # Init Message History + session_id = aichat_uuid if aichat_uuid else f"aichat_{uuid4()}" + + LH_CONFIG = get_learnhouse_config() + redis_conn_string = LH_CONFIG.redis_config.redis_connection_string + + if redis_conn_string: + message_history = RedisChatMessageHistory( + url=redis_conn_string, ttl=2160000, session_id=session_id + ) + else: + print("Redis connection string not found, using local memory") + message_history = [] + + return {"message_history": message_history, "aichat_uuid": session_id} diff --git a/apps/api/src/services/ai/schemas/ai.py b/apps/api/src/services/ai/schemas/ai.py index 7e7ae3fc..d63601a5 100644 --- a/apps/api/src/services/ai/schemas/ai.py +++ b/apps/api/src/services/ai/schemas/ai.py @@ -5,6 +5,11 @@ class StartActivityAIChatSession(BaseModel): activity_uuid: str message: str +class ActivityAIChatSessionResponse(BaseModel): + aichat_uuid: str + activity_uuid: str + message: str + class SendActivityAIChatMessage(BaseModel): aichat_uuid: str diff --git a/docker-compose.yml b/docker-compose.yml index a1bd0ef5..1bf90dea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,11 @@ services: - POSTGRES_USER=learnhouse - POSTGRES_PASSWORD=learnhouse - POSTGRES_DB=learnhouse + redis: + image: redis:7.2.3 + restart: always + ports: + - "6379:6379" mongo: image: mongo:5.0 restart: always