Files
socweb/backend/app/graph/builder.py
T
gongzhiyongandClaude Sonnet 4.6 47921d1a45 fix: resolve 6 gen-ui issues — timeline completeness, regen sync, localStorage persistence
## Fixes
- [#3] Status events now add/update a status ActivityNode in timeline (upsertWsStatusNode)
- [#4] Done callback adds done node; error callback adds error node + sets session.status="error"
- [#5] handleRegenerate fully synced with workspace: status/tool/card/done/error all handled
- [#1][#2] localStorage persistence: completed/error sessions auto-saved, lazy-loaded on demand
- [#1] handleSelectConversation preloads workspace sessions from localStorage for history messages
- Refactored completeWsSession to include done ActivityNode in timeline
- Added errorWsSession, upsertWsStatusNode, saveWsToStorage, loadWsFromStorage helpers

## Known limitation
- [#7] workspace_card merge:true not yet used by backend (all cards are append-only for now)
- History workspace recovery depends on localStorage (browser-local, not cross-device)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 03:08:23 +08:00

104 lines
3.6 KiB
Python

"""Build and compile the LangGraph agent.
Phase 1 used a simple single-node StateGraph.
Phase 2 upgrades to create_react_agent (ReAct pattern) with dynamic tool binding.
When no tools are requested, we fall back to a plain single-node graph so the
agent does not produce unnecessary tool-call reasoning.
"""
from __future__ import annotations
from langchain_openai import AzureChatOpenAI
from langgraph.graph import StateGraph
from langgraph.prebuilt import create_react_agent
from app.config import settings
from app.graph.nodes import call_model
from app.graph.state import ChatState
from app.store.memory import get_checkpointer
# Model parameter presets
MODEL_PARAMS: dict[str, dict] = {
"flash": {"max_tokens": 500, "temperature": 0.2},
"pro": {"max_tokens": 4096, "temperature": 0.3},
}
# System prompt that instructs the ReAct agent
SYSTEM_PROMPT = (
"You are SOC Assistant, an enterprise AI assistant. "
"You help users with knowledge base queries, ticket management, "
"and general questions. "
"When the user has enabled specific tools, you may use them if relevant. "
"If you decide not to use an available tool, briefly explain why. "
"Always respond in the same language the user uses. "
"Be concise, accurate, and helpful.\n\n"
"When web search tools are available:\n"
"- Use search_web first to find relevant pages\n"
"- Use read_url to get full content from the most relevant URLs (1-3 max)\n"
"- Use sort_by_relevance to rank results if you have many documents\n"
)
# Cache compiled graphs to avoid re-creation on every request.
# Key: (model, frozenset(tool_names))
_graph_cache: dict[tuple, object] = {}
def _get_llm(model: str) -> AzureChatOpenAI:
"""Create an AzureChatOpenAI instance with preset parameters."""
params = MODEL_PARAMS.get(model, MODEL_PARAMS["flash"])
return AzureChatOpenAI(
azure_endpoint=settings.azure_openai_endpoint,
api_key=settings.azure_openai_api_key,
api_version=settings.azure_openai_api_version,
azure_deployment=settings.azure_openai_deployment,
max_tokens=params["max_tokens"],
temperature=params["temperature"],
streaming=True,
)
# MCP tool names that should not be cached (bound to per-request client)
_MCP_TOOL_NAMES = {"search_web", "read_url", "sort_by_relevance"}
async def get_chat_graph(model: str = "flash", tools: list | None = None):
"""Get or create a compiled graph for the given model and tool set.
When tools are provided, creates a ReAct agent that can call tools.
When no tools, falls back to a simple single-node graph.
MCP tools are bound to a per-request client session, so graphs
containing them are never cached.
"""
tools = tools or []
tool_names = frozenset(t.name for t in tools)
has_mcp_tools = bool(tool_names & _MCP_TOOL_NAMES)
cache_key = (model, tool_names)
if not has_mcp_tools and cache_key in _graph_cache:
return _graph_cache[cache_key]
checkpointer = await get_checkpointer()
llm = _get_llm(model)
if tools:
# ReAct agent with tool calling
graph = create_react_agent(
llm,
tools=tools,
checkpointer=checkpointer,
prompt=SYSTEM_PROMPT,
)
else:
# Simple graph without tools (Phase 1 style)
builder = StateGraph(ChatState)
builder.add_node("agent", call_model)
builder.set_entry_point("agent")
builder.set_finish_point("agent")
graph = builder.compile(checkpointer=checkpointer)
if not has_mcp_tools:
_graph_cache[cache_key] = graph
return graph