Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions agentstore/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from langchain_core.messages import AIMessage, HumanMessage
from langgraph.errors import GraphRecursionError
from pydantic import BaseModel, Field

from .config import get_settings
Expand Down Expand Up @@ -78,6 +79,13 @@ def health() -> dict[str, str]:
return {"status": "ok"}


RECURSION_LIMIT_MESSAGE = (
"This question needed more back-and-forth between specialists than we allow per "
"request. Try asking a narrower question, or split it into separate questions — "
"one per topic (sales, inventory, reviews, customer)."
)


@app.post("/api/chat", response_model=ChatResponse)
def chat(req: ChatRequest) -> ChatResponse:
start = time.perf_counter()
Expand All @@ -86,6 +94,17 @@ def chat(req: ChatRequest) -> ChatResponse:
{"messages": [HumanMessage(content=req.query)]},
config={"recursion_limit": RECURSION_LIMIT},
)
except GraphRecursionError:
# Not a server error: the supervisor/worker loop hit its hop budget
# without reaching FINISH (e.g. a question spanning many domains, or
# the LLM bouncing indecisively between specialists). Surface it as a
# normal answer so the UI renders a helpful chat bubble instead of a
# red error toast with an internal LangGraph error message.
return ChatResponse(
answer=RECURSION_LIMIT_MESSAGE,
agent="supervisor",
latency_ms=int((time.perf_counter() - start) * 1000),
)
except Exception as exc: # noqa: BLE001 — bubble up as 500 with a short msg
raise HTTPException(status_code=500, detail=f"graph error: {exc}") from exc

Expand Down
21 changes: 21 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from fastapi.testclient import TestClient
from langchain_core.messages import AIMessage
from langgraph.errors import GraphRecursionError

from agentstore.server import _allow_credentials_for, app

Expand Down Expand Up @@ -45,6 +46,26 @@ def test_chat_rejects_empty_query():
assert r.status_code == 422


def test_chat_handles_recursion_limit_gracefully():
"""A supervisor/worker loop that never reaches FINISH within
RECURSION_LIMIT should surface as a normal answer, not an opaque 500."""

class _StubGraph:
def invoke(self, state, config=None):
raise GraphRecursionError(
"Recursion limit of 8 reached without hitting a stop condition."
)

with patch("agentstore.server.get_graph", return_value=_StubGraph()):
with TestClient(app) as client:
r = client.post("/api/chat", json={"query": "a very complex multi-domain question"})
assert r.status_code == 200
body = r.json()
assert body["agent"] == "supervisor"
assert "narrower" in body["answer"]
assert isinstance(body["latency_ms"], int)


# ── CORS ─────────────────────────────────────────────────────────────────────
#
# Wildcard origin + allow_credentials=True is an unsafe combination: browsers
Expand Down
Loading