From 321f89cdc8b1fd224559db448f35c958cb6f3fe4 Mon Sep 17 00:00:00 2001 From: ks6088ts Date: Sat, 1 Jun 2024 18:10:10 +0900 Subject: [PATCH] add an agent app with LangGraph --- docs/README.md | 2 + frontend/entrypoint.py | 2 + frontend/solutions/agent_langgraph.py | 100 ++++++++++++++++++ frontend/solutions/chat_langchain.py | 9 -- frontend/solutions/internals/__init__.py | 0 .../solutions/internals/tools/__init__.py | 0 .../solutions/internals/tools/examples.py | 25 +++++ frontend/solutions/types.py | 1 + poetry.lock | 16 ++- pyproject.toml | 1 + 10 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 frontend/solutions/agent_langgraph.py create mode 100644 frontend/solutions/internals/__init__.py create mode 100644 frontend/solutions/internals/tools/__init__.py create mode 100644 frontend/solutions/internals/tools/examples.py diff --git a/docs/README.md b/docs/README.md index c3af2e4..4b92d86 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,8 @@ - [Add message history (memory) > In-memory](https://python.langchain.com/v0.1/docs/expression_language/how_to/message_history/#in-memory) - [Tool calling agent](https://python.langchain.com/v0.1/docs/modules/agents/agent_types/tool_calling/) - [LangChain > Tools > Bing Search](https://python.langchain.com/v0.1/docs/integrations/tools/bing_search/) +- [Tool Calling with LangChain](https://blog.langchain.dev/tool-calling-with-langchain/) +- [langchain/cookbook/tool_call_messages.ipynb](https://github.com/langchain-ai/langchain/blob/master/cookbook/tool_call_messages.ipynb?ref=blog.langchain.dev) ## Backend diff --git a/frontend/entrypoint.py b/frontend/entrypoint.py index 14d6fe0..3942830 100644 --- a/frontend/entrypoint.py +++ b/frontend/entrypoint.py @@ -1,6 +1,7 @@ import logging from frontend.solutions import ( + agent_langgraph, azure_ai_vision, azure_storage, chat, @@ -28,6 +29,7 @@ def start( SolutionType.DOCUMENT_INTELLIGENCE.value: document_intelligence.start, SolutionType.AZURE_STORAGE.value: azure_storage.start, SolutionType.AZURE_AI_VISION.value: azure_ai_vision.start, + SolutionType.AGENT_LANGGRAPH.value: agent_langgraph.start, } return solutions[solution_name.upper()]( backend_url=backend_url, diff --git a/frontend/solutions/agent_langgraph.py b/frontend/solutions/agent_langgraph.py new file mode 100644 index 0000000..5c7bcef --- /dev/null +++ b/frontend/solutions/agent_langgraph.py @@ -0,0 +1,100 @@ +import logging +import operator +from collections.abc import Sequence +from os import getenv +from typing import Annotated, TypedDict + +import streamlit as st +from dotenv import load_dotenv +from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage +from langchain_core.runnables import ConfigurableField, RunnableLambda +from langchain_openai import AzureChatOpenAI +from langgraph.graph import END, StateGraph + +from frontend.solutions.internals.tools.examples import add, exponentiate, get_current_weather, multiply + +logger = logging.getLogger(__name__) +load_dotenv("frontend.env") + +tools = [ + multiply, + exponentiate, + add, + get_current_weather, +] +llm = AzureChatOpenAI( + api_key=getenv("AZURE_OPENAI_API_KEY"), + api_version=getenv("AZURE_OPENAI_API_VERSION"), + azure_endpoint=getenv("AZURE_OPENAI_ENDPOINT"), + azure_deployment=getenv("AZURE_OPENAI_GPT_MODEL"), +).bind_tools(tools) + +llm_with_tools = llm.configurable_alternatives( + ConfigurableField(id="llm"), +) + + +class AgentState(TypedDict): + messages: Annotated[Sequence[BaseMessage], operator.add] + + +def should_continue(state): + return "continue" if state["messages"][-1].tool_calls else "end" + + +def call_model(state, config): + return {"messages": [llm_with_tools.invoke(state["messages"], config=config)]} + + +def call_tools(state): + def _invoke_tool(tool_call): + tool = {tool.name: tool for tool in tools}[tool_call["name"]] + return ToolMessage(tool.invoke(tool_call["args"]), tool_call_id=tool_call["id"]) + + tool_executor = RunnableLambda(_invoke_tool) + last_message = state["messages"][-1] + return {"messages": tool_executor.batch(last_message.tool_calls)} + + +def create_graph(): + workflow = StateGraph(AgentState) + workflow.add_node("agent", call_model) + workflow.add_node("action", call_tools) + workflow.set_entry_point("agent") + workflow.add_conditional_edges( + "agent", + should_continue, + { + "continue": "action", + "end": END, + }, + ) + workflow.add_edge("action", "agent") + return workflow.compile() + + +def start( + backend_url: str, + log_level: int, +): + logger.setLevel(log_level) + logger.debug(f"set log level to {log_level}") + + st.title("ChatGPT-like clone") + + graph = create_graph() + + if prompt := st.chat_input("What is up?"): + with st.chat_message("user"): + st.markdown(prompt) + + with st.spinner("Analyzing..."): + with st.chat_message("assistant"): + response = graph.invoke( + { + "messages": [ + HumanMessage(prompt), + ] + } + ) + st.json(response) diff --git a/frontend/solutions/chat_langchain.py b/frontend/solutions/chat_langchain.py index cc28660..60410ca 100644 --- a/frontend/solutions/chat_langchain.py +++ b/frontend/solutions/chat_langchain.py @@ -22,15 +22,6 @@ def get_session_history(session_id: str) -> BaseChatMessageHistory: return store[session_id] -def run_bing_search( - query: str, - k=1, -) -> str: - from langchain_community.utilities import BingSearchAPIWrapper - - return BingSearchAPIWrapper(k=k).run(query=query) - - def start( backend_url: str, log_level: int, diff --git a/frontend/solutions/internals/__init__.py b/frontend/solutions/internals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/solutions/internals/tools/__init__.py b/frontend/solutions/internals/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/solutions/internals/tools/examples.py b/frontend/solutions/internals/tools/examples.py new file mode 100644 index 0000000..297ea55 --- /dev/null +++ b/frontend/solutions/internals/tools/examples.py @@ -0,0 +1,25 @@ +from langchain_core.tools import tool + + +@tool +def multiply(x: float, y: float) -> float: + """Multiply 'x' times 'y'.""" + return x * y + + +@tool +def exponentiate(x: float, y: float) -> float: + """Raise 'x' to the 'y'.""" + return x**y + + +@tool +def add(x: float, y: float) -> float: + """Add 'x' and 'y'.""" + return x + y + + +@tool +def get_current_weather(city: str) -> str: + """Get the current weather in 'city'.""" + return f"The weather in {city} is sunny." diff --git a/frontend/solutions/types.py b/frontend/solutions/types.py index cb5aedf..219d581 100644 --- a/frontend/solutions/types.py +++ b/frontend/solutions/types.py @@ -9,3 +9,4 @@ class SolutionType(Enum): DOCUMENT_INTELLIGENCE = "DOCUMENT_INTELLIGENCE" AZURE_STORAGE = "AZURE_STORAGE" AZURE_AI_VISION = "AZURE_AI_VISION" + AGENT_LANGGRAPH = "AGENT_LANGGRAPH" diff --git a/poetry.lock b/poetry.lock index c846b2c..a575757 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1596,6 +1596,20 @@ langchain-core = ">=0.2.0,<0.3.0" [package.extras] extended-testing = ["beautifulsoup4 (>=4.12.3,<5.0.0)", "lxml (>=4.9.3,<6.0)"] +[[package]] +name = "langgraph" +version = "0.0.60" +description = "langgraph" +optional = false +python-versions = "<4.0,>=3.9.0" +files = [ + {file = "langgraph-0.0.60-py3-none-any.whl", hash = "sha256:c893e5209b9b81172c074f2d5b286edfaaa928a989bdd17c2ba6ec47eb97a48a"}, + {file = "langgraph-0.0.60.tar.gz", hash = "sha256:3e46b8263182dcf93515afc673db12e52510ef88a32a345cba7b8fa34196734a"}, +] + +[package.dependencies] +langchain-core = ">=0.2,<0.3" + [[package]] name = "langsmith" version = "0.1.67" @@ -4244,4 +4258,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "350bbb95e990c2bdeab2052aef359f6df2a73aa72af7456ecfbcf2b24829f29d" +content-hash = "3007c430eebed242c2d1b29d663c7c0fc138bef22683f5a15831ccc71978ee6f" diff --git a/pyproject.toml b/pyproject.toml index a6a3846..d3217a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ langchain = "^0.2.1" langchain-openai = "^0.1.8" langchain-community = "^0.2.1" langsmith = "^0.1.67" +langgraph = "^0.0.60" [tool.poetry.group.azure-functions.dependencies]