From 4ed3f4dc2ed2ef921bc128a19a2d7cad018b6f62 Mon Sep 17 00:00:00 2001 From: Marc Fabian Mezger Date: Mon, 1 Jul 2024 17:56:20 +0200 Subject: [PATCH] adding visualization. --- agent/backend/graph.py | 116 ++++++++++++++++++++++++++----- agent/scripts/visualize_graph.py | 15 ++++ agent/utils/utility.py | 11 +++ graph.png | Bin 0 -> 22780 bytes 4 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 agent/scripts/visualize_graph.py create mode 100644 graph.png diff --git a/agent/backend/graph.py b/agent/backend/graph.py index 86543c2..f0eb662 100644 --- a/agent/backend/graph.py +++ b/agent/backend/graph.py @@ -1,8 +1,10 @@ +"""Defining the graph.""" import os from collections.abc import Sequence from typing import Annotated, Literal, TypedDict from langchain_cohere import ChatCohere, CohereEmbeddings +from langchain_community.chat_models.ollama import ChatOllama from langchain_core.documents import Document from langchain_core.language_models import LanguageModelLike from langchain_core.messages import ( @@ -23,7 +25,7 @@ from langgraph.graph import END, StateGraph, add_messages from qdrant_client import QdrantClient -from agent.backend.prompts import COHERE_RESPONSE_TEMPLATE, REPHRASE_TEMPLATE +from agent.backend.prompts import COHERE_RESPONSE_TEMPLATE, REPHRASE_TEMPLATE, RESPONSE_TEMPLATE from agent.utils.utility import format_docs_for_citations OPENAI_MODEL_KEY = "openai_gpt_3_5_turbo" @@ -32,6 +34,9 @@ class AgentState(TypedDict): + + """State of the Agent.""" + query: str documents: list[Document] messages: Annotated[list[BaseMessage], add_messages] @@ -61,6 +66,12 @@ class AgentState(TypedDict): def get_retriever() -> BaseRetriever: + """Create a Vector Database retriever. + + Returns + ------- + BaseRetriever: Qdrant + Cohere Embeddings Retriever + """ embedding = CohereEmbeddings(model="embed-multilingual-v3.0") qdrant_client = QdrantClient("http://localhost", port=6333, api_key=os.getenv("QDRANT_API_KEY"), prefer_grpc=False) @@ -70,6 +81,16 @@ def get_retriever() -> BaseRetriever: def retrieve_documents(state: AgentState) -> AgentState: + """Retrieve documents from the retriever. + + Args: + ---- + state (AgentState): Graph State. + + Returns: + ------- + AgentState: Modified Graph State. + """ retriever = get_retriever() messages = convert_to_messages(state["messages"]) query = messages[-1].content @@ -78,11 +99,21 @@ def retrieve_documents(state: AgentState) -> AgentState: def retrieve_documents_with_chat_history(state: AgentState) -> AgentState: + """Retrieve documents from the retriever with chat history. + + Args: + ---- + state (AgentState): Graph State. + + Returns: + ------- + AgentState: Modified Graph State. + """ retriever = get_retriever() model = llm.with_config(tags=["nostream"]) - CONDENSE_QUESTION_PROMPT = PromptTemplate.from_template(REPHRASE_TEMPLATE) - condense_question_chain = (CONDENSE_QUESTION_PROMPT | model | StrOutputParser()).with_config( + condense_queston_prompt = PromptTemplate.from_template(REPHRASE_TEMPLATE) + condense_question_chain = (condense_queston_prompt | model | StrOutputParser()).with_config( run_name="CondenseQuestion", ) @@ -96,6 +127,12 @@ def retrieve_documents_with_chat_history(state: AgentState) -> AgentState: def route_to_retriever( state: AgentState, ) -> Literal["retriever", "retriever_with_chat_history"]: + """Route to the appropriate retriever based on the state. + + Returns + ------- + Literal["retriever", "retriever_with_chat_history"]: Choosen retriever method. + """ # at this point in the graph execution there is exactly one (i.e. first) message from the user, # so use basic retriever without chat history if len(state["messages"]) == 1: @@ -105,23 +142,35 @@ def route_to_retriever( def get_chat_history(messages: Sequence[BaseMessage]) -> Sequence[BaseMessage]: - chat_history = [] - for message in messages: - if (isinstance(message, AIMessage) and not message.tool_calls) or isinstance(message, HumanMessage): - chat_history.append({"content": message.content, "role": message.type}) - return chat_history + """Append the chat history to the messages. + + Args: + ---- + messages (Sequence[BaseMessage]): Messages from the frontend. + + Returns: + ------- + Sequence[BaseMessage]: Chat history as Langchain messages. + """ + return [ + {"content": message.content, "role": message.type} + for message in messages + if (isinstance(message, AIMessage) and not message.tool_calls) or isinstance(message, HumanMessage) + ] def generate_response(state: AgentState, model: LanguageModelLike, prompt_template: str) -> AgentState: - """Args: + """Create a response from the model. + + Args: ---- - state (AgentState): _description_ - model (LanguageModelLike): _description_ - prompt_template (str): _description_. + state (AgentState): Graph State. + model (LanguageModelLike): Language Model. + prompt_template (str): Template for the prompt. - Returns + Returns: ------- - AgentState: _description_ + AgentState: Modified Graph State. """ prompt = ChatPromptTemplate.from_messages( [ @@ -146,15 +195,47 @@ def generate_response(state: AgentState, model: LanguageModelLike, prompt_templa def generate_response_default(state: AgentState) -> AgentState: + """Generate a response using non cohere model. + + Args: + ---- + state (AgentState): Graph State. + + Returns: + ------- + AgentState: Modified Graph State. + """ return generate_response(state, llm, RESPONSE_TEMPLATE) def generate_response_cohere(state: AgentState) -> AgentState: + """Generate a response using the Cohere model. + + Args: + ---- + state (AgentState): Graph State. + + Returns: + ------- + AgentState: Modified Graph State. + """ model = llm.bind(documents=state["documents"]) return generate_response(state, model, COHERE_RESPONSE_TEMPLATE) -def route_to_response_synthesizer(state: AgentState, config: RunnableConfig) -> Literal["response_synthesizer", "response_synthesizer_cohere"]: +def route_to_response_synthesizer(state: AgentState, config: RunnableConfig) -> Literal["response_synthesizer", "response_synthesizer_cohere"]: # noqa: ARG001 + """Route to the appropriate response synthesizer based on the config. + + Args: + ---- + state (AgentState): Graph State. + config (RunnableConfig): Runnable Config. + + + Returns: + ------- + Literal["response_synthesizer", "response_synthesizer_cohere"]: Choosen response synthesizer method. + """ model_name = config.get("configurable", {}).get("model_name", OPENAI_MODEL_KEY) if model_name == COHERE_MODEL_KEY: return "response_synthesizer_cohere" @@ -162,7 +243,7 @@ def route_to_response_synthesizer(state: AgentState, config: RunnableConfig) -> return "response_synthesizer" -def build_graph(): +def build_graph() -> StateGraph: """Build the graph for the agent. Returns @@ -191,5 +272,6 @@ def build_graph(): return workflow.compile() -# answer = graph.invoke({"messages": [{"role": "human", "content": "wer ist der vater von luke skywalker?"}, {"role": "assistant", "content": "Der Vater von Luke Skywalker war Anakin Skywalker."}, {"role": "human", "content": "und wer ist seine mutter?"}]}) +# answer = graph.invoke({"messages": [{"role": "human", "content": "wer ist der vater von luke skywalker?"}, {"role": "assistant", "content": "Der Vater von Luke +# Skywalker war Anakin Skywalker."}, {"role": "human", "content": "und wer ist seine mutter?"}]}) # logger.info(answer) diff --git a/agent/scripts/visualize_graph.py b/agent/scripts/visualize_graph.py new file mode 100644 index 0000000..9347f2b --- /dev/null +++ b/agent/scripts/visualize_graph.py @@ -0,0 +1,15 @@ +"""Visualizing the Langgraph Graph.""" +from pathlib import Path + +from langchain_core.runnables.graph import MermaidDrawMethod + +from agent.backend.graph import build_graph + +workflow = build_graph() + + +mermaid_graph = workflow.get_graph().draw_mermaid_png(draw_method=MermaidDrawMethod.API) + +# save as png +with Path("graph.png").open("wb") as f: + f.write(mermaid_graph) diff --git a/agent/utils/utility.py b/agent/utils/utility.py index 3ccec97..4e34322 100644 --- a/agent/utils/utility.py +++ b/agent/utils/utility.py @@ -1,5 +1,6 @@ """Utility module.""" import uuid +from collections.abc import Sequence from pathlib import Path from langchain.prompts import PromptTemplate @@ -181,6 +182,16 @@ def extract_text_from_langchain_documents(docs: list[Document]) -> str: def format_docs_for_citations(docs: Sequence[Document]) -> str: + """Format the documents for citations. + + Args: + ---- + docs (Sequence[Document]): Langchain documents from a vectordatabase. + + Returns: + ------- + str: Combined documents in a format suitable for citations. + """ formatted_docs = [] for i, doc in enumerate(docs): doc_string = f"{doc.page_content}" diff --git a/graph.png b/graph.png new file mode 100644 index 0000000000000000000000000000000000000000..8f1863b43a232a783f135c980ac48bb519996bcf GIT binary patch literal 22780 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