ClearCast-AI / agent /graph.py
abhinavvathadi's picture
Fix OpenAI tool request compatibility
baabac6
Raw
History Blame Contribute Delete
2.78 kB
"""LangGraph tool-calling agent for ClearCast.
Architecture : add_messages state, an LLM chatbot node,
ToolNode execution, tools_condition routing, and MemorySaver checkpointing.
The loop is START -> chatbot -> tools -> chatbot until the LLM returns an answer.
"""
from pathlib import Path
from typing import Annotated
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from typing_extensions import TypedDict
from agent.prompts import get_system_prompt
from agent.weather_client import get_langchain_tools
PROJECT_ROOT = Path(__file__).resolve().parents[1]
load_dotenv(dotenv_path=PROJECT_ROOT / ".env", override=False)
class State(TypedDict):
"""Conversation state accumulated by the add_messages reducer."""
# The reducer appends node updates instead of replacing message history.
messages: Annotated[list, add_messages]
async def build_graph():
"""Discover MCP tools and compile"""
tools = await get_langchain_tools()
# Step 2: create the graph builder after defining State.
graph_builder = StateGraph(State)
# Step 3: bind discovered schemas so the model can select MCP tools.
llm = ChatOpenAI(model="gpt-5.5")
llm_with_tools = llm.bind_tools(tools)
def chatbot(state: State):
"""Run the analyst with its stable system prompt and current history."""
system_message = {"role": "system", "content": get_system_prompt()}
response = llm_with_tools.invoke([system_message, *state["messages"]])
return {"messages": [response]}
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", ToolNode(tools=tools))
# Step 4: tools_condition ends the graph when there are no tool calls.
graph_builder.add_conditional_edges("chatbot", tools_condition)
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
# Step 5: thread_id-scoped checkpoints provide demo conversation memory.
return graph_builder.compile(checkpointer=MemorySaver())
def invoke_graph(graph, user_message: str, thread_id: str = "1") -> str:
"""Invoke the compiled graph and return its final natural-language answer."""
# Bound the chatbot/tool loop so a model that repeatedly requests tools
# cannot generate an open-ended number of paid LLM calls.
config = {
"configurable": {"thread_id": thread_id},
"recursion_limit": 10,
}
result = graph.invoke(
{"messages": [{"role": "user", "content": user_message}]},
config=config,
)
return result["messages"][-1].content