From b9393d70721e802f665bf8aebae3bffa8bcf7124 Mon Sep 17 00:00:00 2001 From: jsdio Date: Sun, 15 Oct 2023 07:59:24 +0300 Subject: [PATCH] Changes by aleksandr --- src/assistant/lib/agent/openai_functions.py | 11 +- src/assistant/lib/agent/repositories.py | 2 +- src/assistant/lib/agent/services.py | 200 ++++++++++++++++-- .../api/v1/handlers/voice_responce_handler.py | 22 +- src/assistant/lib/app/app.py | 4 +- .../lib/app/split_settings/openai.py | 1 + .../lib/app/split_settings/postgres.py | 2 +- src/assistant/lib/models/__init__.py | 8 +- src/assistant/pyproject.toml | 21 +- 9 files changed, 214 insertions(+), 57 deletions(-) diff --git a/src/assistant/lib/agent/openai_functions.py b/src/assistant/lib/agent/openai_functions.py index e1d99a4..1237148 100644 --- a/src/assistant/lib/agent/openai_functions.py +++ b/src/assistant/lib/agent/openai_functions.py @@ -36,18 +36,19 @@ class OpenAIFunctions: .order_by(orm_models.FilmWork.embeddings.cosine_distance(embedded_description.root)) .limit(5) ) - neighbours = session.scalars(stmt) - for neighbour in await neighbours: + response = await session.execute(stmt) + neighbours = response.scalars() + for neighbour in neighbours: result.append(models.Movie(**neighbour.__dict__)) return result except sqlalchemy.exc.SQLAlchemyError as error: self.logger.exception("Error: %s", error) @langchain.agents.tool - def get_movie_by_id(self, id: uuid.UUID) -> models.Movie | None: + def get_movie_by_id(self, id: uuid.UUID = None) -> models.Movie | None: """Provide a movie data by movie id.""" - self.logger.info("Request to get movie by id: %s", id) - return None + # self.logger.info("Request to get movie by id: %s", id) + return f"hello world {id}" @langchain.agents.tool def get_similar_movies(self, id: uuid.UUID) -> list[models.Movie] | None: diff --git a/src/assistant/lib/agent/repositories.py b/src/assistant/lib/agent/repositories.py index c119a89..e6a011b 100644 --- a/src/assistant/lib/agent/repositories.py +++ b/src/assistant/lib/agent/repositories.py @@ -26,7 +26,7 @@ class EmbeddingRepository: ) # type: ignore[reportGeneralTypeIssues] return models.Embedding(**response["data"][0]["embedding"]) except openai.error.OpenAIError: - self.logger.exception("Failed to get async embedding for: %s", text) + self.logger.exception("Failed to get sync embedding for: %s", text) async def aget_embedding(self, text: str, model: str = "text-embedding-ada-002") -> models.Embedding | None: """Get the embedding for a given text.[Async]""" diff --git a/src/assistant/lib/agent/services.py b/src/assistant/lib/agent/services.py index 046e35d..52aa3b3 100644 --- a/src/assistant/lib/agent/services.py +++ b/src/assistant/lib/agent/services.py @@ -1,5 +1,6 @@ import asyncio import logging +import typing import uuid import fastapi @@ -11,7 +12,7 @@ import langchain.prompts import langchain.schema import langchain.tools.render -import assistant.lib.models.movies as movies +import lib.models as models import lib.agent.openai_functions as openai_functions import lib.app.settings as app_settings @@ -23,6 +24,121 @@ class AgentService: def __init__(self, settings: app_settings.Settings, tools: openai_functions.OpenAIFunctions) -> None: self.settings = settings self.tools = tools + self.llm = langchain.chat_models.ChatOpenAI( + temperature=self.settings.openai.agent_temperature, + openai_api_key=self.settings.openai.api_key.get_secret_value() + ) + self.chat_repository = chat_repository + self.logger = logging.getLogger(__name__) + + async def get_chat_session_id(self, request: models.RequestLastSessionId) -> uuid.UUID: + session_id = self.chat_repository.get_last_session_id(request) + if not session_id: + session_id = uuid.uuid4() + return session_id + + async def artem_process_request(self, request: models.AgentCreateRequestModel) -> models.AgentCreateResponseModel: + # Get session ID + session_request = models.RequestLastSessionId( + channel=request.channel, + user_id=request.user_id, + minutes_ago=3 + ) + session_id = await self.chat_repository.get_last_session_id(session_request) + if not session_id: + print("NO PREVIOUS CHATS") + session_id = uuid.uuid4() + print("FOUND CHAT:", ) + print("SID:", session_id) + + tools = [ + langchain.tools.Tool( + name="GetMovieByDescription", + func=self.tools.get_movie_by_description, + coroutine=self.tools.get_movie_by_description, + description="Get a movie by description" + ), + ] + + llm = langchain.chat_models.ChatOpenAI(temperature=self.settings.openai.agent_temperature, openai_api_key=self.settings.openai.api_key.get_secret_value()) + + chat_history = langchain.memory.ChatMessageHistory() + # chat_history = [] + chat_history_name = f"{chat_history=}".partition("=")[0] + request_chat_history = models.RequestChatHistory(session_id=session_id) + chat_history_source = await self.chat_repository.get_messages_by_sid(request_chat_history) + for entry in chat_history_source: + # chat_history.append(langchain.schema.messages.HumanMessage(content=first_question)) + # chat_history.append(langchain.schema.messages.AIMessage(content=first_result["output"])) + + if entry.content["role"] == "user": + chat_history.append(langchain.schema.messages.HumanMessage(content=entry.content["content"])) + elif entry.content["role"] == "agent": + chat_history.append(langchain.schema.messages.AIMessage(content=entry.content["content"])) + + # chat_history = [entry.model_dump() for entry in chat_history_source] + memory_buffer = langchain.memory.ConversationBufferMemory(memory_key=chat_history_name,chat_memory=chat_history) + + print("CHAT HISTORY:", chat_history) + + # chat_history_name = f"{chat_history=}".partition("=")[0] + + + prompt = langchain.prompts.ChatPromptTemplate.from_messages( + [ + ( + "system", + "Act as an advanced AI assistant with extensive capabilities, you have a vast knowledge base about movies and their related aspects. If you are asked about a movie, please use provided functions to retrive data about movies. You can receive a question in any language. Translate it into English. If you don't know the answer, just say that you don't know, don't try to make up an answer. Be concise. ", + ), + langchain.prompts.MessagesPlaceholder(variable_name=chat_history_name), + ("user", "{input}"), + langchain.prompts.MessagesPlaceholder(variable_name="agent_scratchpad"), + ] + ) + + llm_with_tools = llm.bind( + functions=[langchain.tools.render.format_tool_to_openai_function(tool) for tool in tools] + ) + + agent = ( + { + "input": lambda _: _["input"], + "agent_scratchpad": lambda _: langchain.agents.format_scratchpad.format_to_openai_functions( + _["intermediate_steps"] + ), + "chat_history": lambda _: _["chat_history"], + } + | prompt + | llm_with_tools + | langchain.agents.output_parsers.OpenAIFunctionsAgentOutputParser() + ) + + agent_executor = langchain.agents.AgentExecutor(agent=agent, tools=tools, verbose=True, memory=memory_buffer) + response = await agent_executor.ainvoke({"input": request.text, "chat_history": chat_history}) + print("AI RESPONSE:", response) + user_request = models.RequestChatMessage( + session_id=session_id, + user_id=request.user_id, + channel=request.channel, + message={"role": "user", "content": request.text} + ) + ai_response = models.RequestChatMessage( + session_id=session_id, + user_id=request.user_id, + channel=request.channel, + message={"role": "assistant", "content": response["output"]} + ) + + await self.chat_repository.add_message(user_request) + await self.chat_repository.add_message(ai_response) + + return response + + + # TODO: Добавить запрос для процессинга запроса с памятью+ + # TODO: Улучшить промпт+ + # TODO: Возможно, надо добавить Chain на перевод + async def process_request(self, request: models.AgentCreateRequestModel) -> models.AgentCreateResponseModel: @@ -55,26 +171,70 @@ class AgentService: return response_model - return await agent_executor.ainvoke({"input": first_question, "chat_history": chat_history}) +async def main(): + import lib.agent.repositories as agent_repositories + import lib.clients as clients + + postgres_client = clients.AsyncPostgresClient(app_settings.Settings()) + embedding_repository = agent_repositories.EmbeddingRepository(app_settings.Settings()) + chat_repository = _chat_repository.ChatHistoryRepository(postgres_client.get_async_session()) + + agent_service = AgentService( + settings=app_settings.Settings(), + tools=openai_functions.OpenAIFunctions( + repository=embedding_repository, + pg_async_session=postgres_client.get_async_session(), + ), + chat_repository=chat_repository + ) + + # question = "What is the movie about a famous country singer meet a talented singer and songwriter who works as a waitress?" + request_1 = models.AgentCreateRequestModel( + channel="telegram", + user_id="123", + text="What is the movie about a famous country singer meet a talented singer and songwriter who works as a waitress?" + ) + request_2 = models.AgentCreateRequestModel( + channel="telegram", + user_id="123", + text="So what is the rating of the movie? Do you recommend it?" + ) + request_3 = models.AgentCreateRequestModel( + channel="telegram", + user_id="123", + text="What are the similar movies?" + ) + + response = await agent_service.artem_process_request(request_1) + response = await agent_service.artem_process_request(request_2) + response = await agent_service.artem_process_request(request_3) -# async def main(): -# agent_executor = langchain.agents.AgentExecutor(agent=agent, tools=tools, verbose=True) - -# # first_question = "What is the movie where halfling bring the ring to the volcano?" -# first_question = ( -# "What is the movie about a famous country singer meet a talented singer and songwriter who works as a waitress?" -# ) -# second_question = "So what is the rating of the movie? Do you recommend it?" -# third_question = "What are the similar movies?" -# first_result = await agent_executor.ainvoke({"input": first_question, "chat_history": chat_history}) -# chat_history.append(langchain.schema.messages.HumanMessage(content=first_question)) -# chat_history.append(langchain.schema.messages.AIMessage(content=first_result["output"])) -# second_result = await agent_executor.ainvoke({"input": second_question, "chat_history": chat_history}) -# chat_history.append(langchain.schema.messages.HumanMessage(content=second_question)) -# chat_history.append(langchain.schema.messages.AIMessage(content=second_result["output"])) -# final_result = await agent_executor.ainvoke({"input": third_question, "chat_history": chat_history}) -# if __name__ == "__main__": -# asyncio.run(main()) + # response = await agent_service.artem_process_request(question) + # question = "Highly Rated Titanic Movies" + # request = models.AgentCreateRequestModel(text=question) + # film_results = await agent_service.process_request(request=request) + + # result = [agent_service.tools.get_movie_by_id(id=film.id) for film in film_results] + + # agent_executor = langchain.agents.AgentExecutor(agent=agent, tools=tools, verbose=True) + # + # # first_question = "What is the movie where halfling bring the ring to the volcano?" + # first_question = ( + # "What is the movie about a famous country singer meet a talented singer and songwriter who works as a waitress?" + # ) + # second_question = "So what is the rating of the movie? Do you recommend it?" + # third_question = "What are the similar movies?" + # first_result = await agent_executor.ainvoke({"input": first_question, "chat_history": chat_history}) + # chat_history.append(langchain.schema.messages.HumanMessage(content=first_question)) + # chat_history.append(langchain.schema.messages.AIMessage(content=first_result["output"])) + # second_result = await agent_executor.ainvoke({"input": second_question, "chat_history": chat_history}) + # chat_history.append(langchain.schema.messages.HumanMessage(content=second_question)) + # chat_history.append(langchain.schema.messages.AIMessage(content=second_result["output"])) + # final_result = await agent_executor.ainvoke({"input": third_question, "chat_history": chat_history}) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/assistant/lib/api/v1/handlers/voice_responce_handler.py b/src/assistant/lib/api/v1/handlers/voice_responce_handler.py index 01660cc..80d8ecc 100644 --- a/src/assistant/lib/api/v1/handlers/voice_responce_handler.py +++ b/src/assistant/lib/api/v1/handlers/voice_responce_handler.py @@ -5,18 +5,18 @@ import fastapi import lib.stt.services as stt_services -# import lib.tts.services as tts_service -# import lib.models as models +import lib.tts.services as tts_service +import lib.models as models class VoiceResponseHandler: def __init__( self, stt: stt_services.SpeechService, - # tts: tts_service.TTSService, + tts: tts_service.TTSService, ): self.stt = stt - # self.tts = tts + self.tts = tts self.router = fastapi.APIRouter() self.router.add_api_route( "/", @@ -36,10 +36,10 @@ class VoiceResponseHandler: # TODO: Добавить обработку текста через клиента openai # TODO: Добавить синтез речи через клиента tts # TODO: Заменить заглушку на реальный ответ - # response = await self.tts.get_audio_as_bytes( - # models.TTSCreateRequestModel( - # text=voice_text, - # ) - # ) - # return fastapi.responses.StreamingResponse(io.BytesIO(response.audio_content), media_type="audio/ogg") - return fastapi.responses.StreamingResponse(io.BytesIO(voice), media_type="audio/ogg") + response = await self.tts.get_audio_as_bytes( + models.TTSCreateRequestModel( + text=voice_text, + ) + ) + return fastapi.responses.StreamingResponse(io.BytesIO(response.audio_content), media_type="audio/ogg") + # return fastapi.responses.StreamingResponse(io.BytesIO(voice), media_type="audio/ogg") diff --git a/src/assistant/lib/app/app.py b/src/assistant/lib/app/app.py index a060a20..3ad25c1 100644 --- a/src/assistant/lib/app/app.py +++ b/src/assistant/lib/app/app.py @@ -117,7 +117,7 @@ class Application: models.VoiceModelProvidersEnum.ELEVEN_LABS: tts_eleven_labs_repository, }, ) - + # Handlers logger.info("Initializing handlers") @@ -127,7 +127,7 @@ class Application: # TODO: объявить сервисы tts и openai и добавить их в voice_response_handler voice_response_handler = api_v1_handlers.VoiceResponseHandler( stt=stt_service, - # tts=tts_service, # TODO + tts=tts_service, ).router logger.info("Creating application") diff --git a/src/assistant/lib/app/split_settings/openai.py b/src/assistant/lib/app/split_settings/openai.py index 235b940..765040d 100644 --- a/src/assistant/lib/app/split_settings/openai.py +++ b/src/assistant/lib/app/split_settings/openai.py @@ -16,3 +16,4 @@ class OpenaiSettings(pydantic_settings.BaseSettings): default=..., validation_alias=pydantic.AliasChoices("api_key", "openai_api_key") ) stt_model: str = "whisper-1" + agent_temperature: float = 0.7 diff --git a/src/assistant/lib/app/split_settings/postgres.py b/src/assistant/lib/app/split_settings/postgres.py index e055c19..e70b2be 100644 --- a/src/assistant/lib/app/split_settings/postgres.py +++ b/src/assistant/lib/app/split_settings/postgres.py @@ -41,4 +41,4 @@ class PostgresSettings(pydantic_settings.BaseSettings): @property def dsn_as_safe_url(self) -> str: - return f"{self.driver}://{self.user}:***@{self.host}:{self.port}" + return f"{self.driver}://{self.user}:***@{self.host}:{self.port}/{self.db_name}" diff --git a/src/assistant/lib/models/__init__.py b/src/assistant/lib/models/__init__.py index 3990c57..01c3fc9 100644 --- a/src/assistant/lib/models/__init__.py +++ b/src/assistant/lib/models/__init__.py @@ -3,18 +3,19 @@ from .embedding import Embedding from .movies import Movie from .token import Token from .tts import * +from .agent import * -__all__ = ["Embedding", "Message", "Movie", "RequestChatHistory", "RequestChatMessage", "RequestLastSessionId", "Token"] +# __all__ = ["Embedding", "Message", "Movie", "RequestChatHistory", "RequestChatMessage", "RequestLastSessionId", "Token"] __all__ = [ "AVAILABLE_MODELS_TYPE", - "Base", + # "Base", "BaseLanguageCodesEnum", "BaseVoiceModel", "ElevenLabsLanguageCodesEnum", "ElevenLabsListVoiceModelsModel", "ElevenLabsVoiceModel", - "IdCreatedUpdatedBaseMixin", + # "IdCreatedUpdatedBaseMixin", "LANGUAGE_CODES_ENUM_TYPE", "LIST_VOICE_MODELS_TYPE", "TTSCreateRequestModel", @@ -25,4 +26,5 @@ __all__ = [ "YandexLanguageCodesEnum", "YandexListVoiceModelsModel", "YandexVoiceModel", + "AgentCreateRequestModel", ] diff --git a/src/assistant/pyproject.toml b/src/assistant/pyproject.toml index b59205d..dda90a7 100644 --- a/src/assistant/pyproject.toml +++ b/src/assistant/pyproject.toml @@ -13,7 +13,7 @@ profile = "black" py_version = "311" [tool.poetry] -authors = ["ijaric@gmail.com", "jsdio@jsdio.ru"] +authors = ["jsdio@jsdio.ru"] description = "" name = "fastapi_project" readme = "README.md" @@ -22,29 +22,23 @@ version = "0.1.0" [tool.poetry.dependencies] alembic = "^1.12.0" asyncpg = "^0.28.0" -dill = "^0.3.7" -faker = "^19.10.0" fastapi = "0.103.1" greenlet = "^2.0.2" httpx = "^0.25.0" -langchain = "^0.0.312" -openai = "^0.28.1" -pgvector = "^0.2.3" multidict = "^6.0.4" -openai = "^0.28.1" -orjson = "3.9.7" +orjson = "^3.9.7" psycopg2-binary = "^2.9.9" pydantic = {extras = ["email"], version = "^2.3.0"} pydantic-settings = "^2.0.3" -pytest = "^7.4.2" pytest-asyncio = "^0.21.1" python = "^3.11" python-jose = "^3.3.0" -python-magic = "^0.4.27" -python-multipart = "^0.0.6" sqlalchemy = "^2.0.20" uvicorn = "^0.23.2" -wrapt = "^1.15.0" +pgvector = "^0.2.3" +python-magic = "^0.4.27" +openai = "^0.28.1" +python-multipart = "^0.0.6" [tool.poetry.dev-dependencies] black = "^23.7.0" @@ -98,9 +92,8 @@ variable-rgx = "^_{0,2}[a-z][a-z0-9_]*$" [tool.pyright] exclude = [ - ".venv", - "alembic" ".pytest_cache", + ".venv" ] pythonPlatform = "All" pythonVersion = "3.11"