From 66d2511e6df1f80b64ed6d22730cd789bbe40379 Mon Sep 17 00:00:00 2001 From: Nam Le Date: Sun, 15 Jun 2025 20:15:10 +0200 Subject: [PATCH] Add FastAPI app to communicate with LangGraph Server Allow sending chat message, human review command & get chat history using simple requests --- api/Dockerfile | 12 ++++ api/__init__.py | 0 api/main.py | 93 ++++++++++++++++++++++++++++ api/test_main.py | 11 ++++ compose.yaml | 4 ++ pyproject.toml | 7 ++- Dockerfile => src/chatbot/Dockerfile | 0 src/chatbot/__about__.py | 2 +- src/chatbot/graph.py | 3 - 9 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 api/Dockerfile create mode 100644 api/__init__.py create mode 100644 api/main.py create mode 100644 api/test_main.py rename Dockerfile => src/chatbot/Dockerfile (100%) diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..0ea30a6 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12 + +WORKDIR /code +COPY ./pyproject.toml /code/pyproject.toml +# Get files required by hatch to create the environment +COPY ./src/chatbot/__about__.py /code/src/chatbot/__about__.py +COPY ./README.md /code/README.md +RUN python -m pip install --upgrade pip && pip install hatch +RUN hatch env create +COPY ./api /code/api + +CMD ["hatch", "run", "uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8080"] \ No newline at end of file diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/main.py b/api/main.py new file mode 100644 index 0000000..e20e204 --- /dev/null +++ b/api/main.py @@ -0,0 +1,93 @@ +from fastapi import FastAPI +from langgraph_sdk import get_client +from pydantic import BaseModel + +app = FastAPI() + +langgraph_client = get_client(url="http://langgraph-api:8000") + + +class ChatMessage(BaseModel): + text: str + model_config = { + "json_schema_extra": { + "examples": [ + { + "text": "Hello", + } + ] + } + } + + +class HumanReview(BaseModel): + action: str + data: str + model_config = { + "json_schema_extra": { + "examples": [ + { + "action": "feedback", + "data": "That's not what I meant! Please try again.", + }, + { + "action": "continue", + "data": "", + }, + ] + } + } + + +@app.get("/") +async def read_main(): + return {"msg": "Hello! Welcome to the LangGraph Chat API"} + + +@app.get("/chat") +async def list_chat_threads(): + """ + List all chat threads. + """ + return await langgraph_client.threads.search( + metadata={"graph_id": "chat"}, + ) + + +@app.get("/chat/{thread_id}") +async def get_chat_history(thread_id: str): + """ + Get the chat history for the given thread. + """ + return await langgraph_client.threads.get(thread_id=thread_id) + + +@app.post("/chat/{thread_id}") +async def chat_with_thread(thread_id: str, message: ChatMessage): + """ + Take message from user and return response from chatbot. + If chat thread does not exist, create a new thread. + """ + return await langgraph_client.runs.wait( + thread_id=thread_id, + assistant_id="chat", + input={"messages": [{"role": "user", "content": message.text}]}, + if_not_exists="create", + ) + + +@app.post("/chat/{thread_id}/human_review") +async def human_review(thread_id: str, review: HumanReview): + """ + Submit human review to resume interrupted thread. + """ + return await langgraph_client.runs.wait( + thread_id=thread_id, + assistant_id="chat", + command={ + "resume": { + "action": review.action, + "data": review.data, + } + }, + ) diff --git a/api/test_main.py b/api/test_main.py new file mode 100644 index 0000000..b9b5e31 --- /dev/null +++ b/api/test_main.py @@ -0,0 +1,11 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_main(): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"msg": "Hello! Welcome to the LangGraph Chat API"} diff --git a/compose.yaml b/compose.yaml index 53a0ee5..775e718 100644 --- a/compose.yaml +++ b/compose.yaml @@ -39,3 +39,7 @@ services: environment: REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable + api: + image: chatbot-api:latest + ports: + - "8080:8080" diff --git a/pyproject.toml b/pyproject.toml index 8c310ca..1c22d5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,9 @@ dependencies = [ "pydantic", "python-dotenv", "langchain-tavily", - "grandalf" + "grandalf", + "fastapi[standard]", + "uvicorn[standard]", ] [project.urls] @@ -66,7 +68,7 @@ lint.ignore = ["E501"] exclude = ["__pycache__", "build", "dist", ".venv"] [tool.coverage.run] -source_pkgs = ["chatbot", "tests"] +source_pkgs = ["chatbot", "tests", "api"] branch = true parallel = true omit = [ @@ -76,6 +78,7 @@ omit = [ [tool.coverage.paths] chatbot = ["src/chatbot", "*/chatbot/src/chatbot"] tests = ["tests", "*/chatbot/tests"] +api = ["api", "*/chatbot/api"] [tool.coverage.report] exclude_lines = [ diff --git a/Dockerfile b/src/chatbot/Dockerfile similarity index 100% rename from Dockerfile rename to src/chatbot/Dockerfile diff --git a/src/chatbot/__about__.py b/src/chatbot/__about__.py index cbf8518..01e50b9 100644 --- a/src/chatbot/__about__.py +++ b/src/chatbot/__about__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2025-present Nam Le # # SPDX-License-Identifier: MIT -__version__ = "0.0.5" +__version__ = "0.0.6" diff --git a/src/chatbot/graph.py b/src/chatbot/graph.py index 20149a3..661762c 100644 --- a/src/chatbot/graph.py +++ b/src/chatbot/graph.py @@ -2,7 +2,6 @@ from dotenv import load_dotenv from langchain_core.messages import AIMessage -from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import END, START, StateGraph from chatbot.utils.nodes.human_review import human_review_node @@ -42,8 +41,6 @@ def route_after_chatbot(state: State) -> Literal[END, "human_review", "tools"]: ) graph_builder.add_edge("tools", "chatbot") -memory = MemorySaver() -# graph = graph_builder.compile(checkpointer=memory) # In memory saver should not be used when deployed using LangGraph graph = graph_builder.compile() graph.get_graph().print_ascii()