agercas commited on
Commit
1ffaf53
·
1 Parent(s): 2a3616b

add agents

Browse files
src/agents/{langgraph_agent.py → langgraph_agent_v0.py} RENAMED
File without changes
src/agents/langgraph_agent_v1.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Sequence
2
+ from typing import Annotated, Literal, TypedDict
3
+
4
+ from langchain.chat_models import init_chat_model
5
+ from langchain_community.tools import DuckDuckGoSearchRun, WikipediaQueryRun
6
+ from langchain_community.tools.arxiv import ArxivQueryRun
7
+ from langchain_community.tools.pubmed.tool import PubmedQueryRun
8
+ from langchain_community.tools.semanticscholar.tool import SemanticScholarQueryRun
9
+ from langchain_community.tools.wikidata.tool import WikidataAPIWrapper, WikidataQueryRun
10
+ from langchain_community.utilities import WikipediaAPIWrapper
11
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage
12
+ from langchain_core.runnables import RunnableConfig
13
+ from langchain_core.tools import Tool
14
+ from langchain_experimental.utilities import PythonREPL
15
+ from langgraph.graph import END, StateGraph
16
+ from langgraph.graph.message import add_messages
17
+ from pydantic import BaseModel, Field
18
+
19
+ # Set up tools
20
+ python_repl = PythonREPL()
21
+ repl_tool = Tool(
22
+ name="python_repl",
23
+ description="A Python shell. Use this to execute python commands. Input should be a valid python command. If you want to see the output of a value, you should print it out with `print(...)`.",
24
+ func=python_repl.run,
25
+ )
26
+
27
+ # Initialize all tools
28
+ tools = [
29
+ DuckDuckGoSearchRun(),
30
+ PubmedQueryRun(),
31
+ SemanticScholarQueryRun(),
32
+ ArxivQueryRun(),
33
+ WikidataQueryRun(api_wrapper=WikidataAPIWrapper()),
34
+ WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper()),
35
+ repl_tool,
36
+ ]
37
+
38
+ # Initialize Gemini model
39
+ model = init_chat_model("gemini-2.0-flash", model_provider="google_genai")
40
+ model_with_tools = model.bind_tools(tools)
41
+
42
+ # Create tools lookup
43
+ tools_by_name = {tool.name: tool for tool in tools}
44
+
45
+
46
+ # Pydantic models for structured output
47
+ class ToolSufficiencyResponse(BaseModel):
48
+ """Response for tool sufficiency check"""
49
+
50
+ sufficient: bool = Field(description="Whether the available tools are sufficient to answer the question")
51
+ reasoning: str = Field(description="Brief reasoning for the decision")
52
+
53
+
54
+ class FinalAnswer(BaseModel):
55
+ """Final answer structure"""
56
+
57
+ answer: str = Field(description="The comprehensive answer to the user's question")
58
+
59
+
60
+ # Graph state
61
+ class AgentState(TypedDict):
62
+ """The state of the agent."""
63
+
64
+ messages: Annotated[Sequence[BaseMessage], add_messages]
65
+ llm_call_count: int
66
+ max_llm_calls: int
67
+ question: str | None = None
68
+ answer: FinalAnswer | None = None
69
+ tool_sufficiency: ToolSufficiencyResponse | None = None
70
+
71
+
72
+ # Node functions
73
+ def check_tool_sufficiency(state: AgentState, config: RunnableConfig):
74
+ """Check if available tools are sufficient to answer the question"""
75
+
76
+ question = state["question"]
77
+ question_message = HumanMessage(content=f"Question to analyze: {question}")
78
+
79
+ # Create system prompt for sufficiency check
80
+ available_tools_desc = "\n".join([f"- {tool.name}: {tool.description}" for tool in tools])
81
+
82
+ system_prompt = f"""You are an AI assistant that needs to determine if the available tools are sufficient to answer a user's question.
83
+
84
+ Available tools:
85
+ {available_tools_desc}
86
+
87
+ Your task is to analyze the user's question and determine if these tools provide sufficient capability to answer it comprehensively.
88
+
89
+ Consider:
90
+ - Can the question be answered with web search, academic papers, or computational tools?
91
+ - Does the question require real-time data, personal information, or capabilities not available through these tools?
92
+ - Can you break down the question into parts that these tools can handle?
93
+
94
+ Be generous in your assessment - if there's a reasonable path to answer the question using these tools, respond with sufficient=True."""
95
+
96
+ messages = [SystemMessage(content=system_prompt), question_message]
97
+
98
+ structured_model = model.with_structured_output(ToolSufficiencyResponse)
99
+ response = structured_model.invoke(messages, config)
100
+
101
+ # Add response to messages for context
102
+ response_message = AIMessage(
103
+ content=f"Tool sufficiency check: {'Sufficient' if response.sufficient else 'Insufficient'}. Reasoning: {response.reasoning}"
104
+ )
105
+
106
+ return {"messages": [question_message, response_message], "tool_sufficiency": response}
107
+
108
+
109
+ def call_model(state: AgentState, config: RunnableConfig):
110
+ """Call the model (ReAct agent LLM node)"""
111
+
112
+ system_prompt = SystemMessage(
113
+ content="""You are a helpful AI assistant with access to various tools. Use the tools available to you to answer the user's question comprehensively.
114
+
115
+ Think step by step:
116
+ 1. Analyze what information you need.
117
+ 2. Use appropriate tools to gather that information.
118
+ 3. Synthesize the information to provide a complete answer.
119
+
120
+ IMPORTANT INSTRUCTIONS:
121
+ - Avoid repeating the same tool call with identical parameters.
122
+ - When calling tools, vary your queries and tool arguments to explore different aspects of the question.
123
+ - Use each tool intelligently and purposefully—avoid redundant or uninformative tool calls.
124
+ - Track which tools you've already used and how, so you don't repeat yourself.
125
+
126
+ Be thorough but efficient with your tool usage. Use tools only when needed, and prefer combining information from multiple sources."""
127
+ )
128
+
129
+ response = model_with_tools.invoke([system_prompt] + state["messages"], config)
130
+
131
+ # Increment LLM call count
132
+ new_count = state.get("llm_call_count", 0) + 1
133
+
134
+ return {"messages": [response], "llm_call_count": new_count}
135
+
136
+
137
+ def tool_node(state: AgentState):
138
+ """Execute tools based on the last message's tool calls"""
139
+
140
+ outputs = []
141
+ last_message = state["messages"][-1]
142
+
143
+ for tool_call in last_message.tool_calls:
144
+ try:
145
+ tool_result = tools_by_name[tool_call["name"]].invoke(tool_call["args"])
146
+ outputs.append(
147
+ ToolMessage(
148
+ content=str(tool_result),
149
+ name=tool_call["name"],
150
+ tool_call_id=tool_call["id"],
151
+ )
152
+ )
153
+ except Exception as e:
154
+ outputs.append(
155
+ ToolMessage(
156
+ content=f"Error executing tool {tool_call['name']}: {str(e)}",
157
+ name=tool_call["name"],
158
+ tool_call_id=tool_call["id"],
159
+ )
160
+ )
161
+
162
+ return {"messages": outputs}
163
+
164
+
165
+ def final_answer_node(state: AgentState, config: RunnableConfig):
166
+ """Generate final structured answer based on conversation history"""
167
+
168
+ system_prompt = SystemMessage(
169
+ content="""You are tasked with providing the most concise possible final answer to the user question based on the conversation history and tool usage.
170
+
171
+ CRITICAL INSTRUCTIONS FOR CONCISENESS:
172
+ - If the question asks for a number, provide ONLY the number (e.g., "5", "23", "147")
173
+ - If the question asks for a name, provide ONLY the name (e.g., "John Smith", "Paris")
174
+ - If the question asks for a yes/no, provide ONLY "Yes" or "No"
175
+ - If the question asks for a date, provide ONLY the date (e.g., "2023-05-15", "March 2020")
176
+ - Remove ALL unnecessary words, articles, explanations, or context
177
+ - Do NOT include phrases like "The answer is", "Based on the research", "According to", etc.
178
+ - Provide the absolute minimum text that directly answers the question
179
+ - This is for a benchmark submission where brevity is crucial
180
+
181
+ If the answer cannot be determined from the conversation history, respond with "Unable to determine" (nothing more)."""
182
+ )
183
+
184
+ filtered_messages = []
185
+ for msg in state["messages"]:
186
+ if hasattr(msg, "content") and msg.content and msg.content.strip():
187
+ filtered_messages.append(msg)
188
+
189
+ messages = [system_prompt] + filtered_messages
190
+
191
+ structured_model = model.with_structured_output(FinalAnswer)
192
+ response = structured_model.invoke(messages, config)
193
+
194
+ return {"messages": [AIMessage(content=f"Final Answer: {response.answer}")], "answer": response}
195
+
196
+
197
+ # Edge functions
198
+ def should_continue_sufficiency(state: AgentState) -> Literal["sufficient", "insufficient"]:
199
+ """Decide whether tools are sufficient"""
200
+ # Check if we have a tool sufficiency result
201
+ sufficiency = state["tool_sufficiency"]
202
+
203
+ if sufficiency and hasattr(sufficiency, "sufficient") and sufficiency.sufficient:
204
+ return "sufficient"
205
+ else:
206
+ return "insufficient"
207
+
208
+
209
+ def should_continue_react(state: AgentState) -> Literal["tools", "final_answer"]:
210
+ """Decide whether to continue with ReAct loop or move to final answer"""
211
+
212
+ messages = state["messages"]
213
+ last_message = messages[-1]
214
+
215
+ if hasattr(last_message, "tool_calls") and last_message.tool_calls:
216
+ return "tools"
217
+
218
+ return "final_answer"
219
+
220
+
221
+ def should_continue_after_tools(state: AgentState) -> Literal["agent", "final_answer"]:
222
+ """Decide after tool execution whether to continue or finalize"""
223
+
224
+ llm_call_count = state.get("llm_call_count", 0)
225
+ max_calls = state.get("max_llm_calls", 4)
226
+
227
+ # If we've reached the maximum number of LLM calls, go to final answer
228
+ if llm_call_count >= max_calls:
229
+ return "final_answer"
230
+
231
+ # Otherwise, continue the ReAct loop (go back to agent)
232
+ return "agent"
233
+
234
+
235
+ def create_react_agent_graph():
236
+ """Create and return the compiled ReAct agent graph"""
237
+
238
+ workflow = StateGraph(AgentState)
239
+
240
+ # Add nodes
241
+ workflow.add_node("check_sufficiency", check_tool_sufficiency)
242
+ workflow.add_node("agent", call_model)
243
+ workflow.add_node("tools", tool_node)
244
+ workflow.add_node("final_answer", final_answer_node)
245
+
246
+ # Set entry point
247
+ workflow.set_entry_point("check_sufficiency")
248
+
249
+ # Add conditional edges
250
+ workflow.add_conditional_edges(
251
+ "check_sufficiency", should_continue_sufficiency, {"sufficient": "agent", "insufficient": END}
252
+ )
253
+ workflow.add_conditional_edges("agent", should_continue_react, {"tools": "tools", "final_answer": "final_answer"})
254
+ workflow.add_conditional_edges(
255
+ "tools", should_continue_after_tools, {"agent": "agent", "final_answer": "final_answer"}
256
+ )
257
+
258
+ # Add edges
259
+ workflow.add_edge("final_answer", END)
260
+
261
+ return workflow.compile()
262
+
263
+
264
+ def run_agent(question: str, max_llm_calls: int = 4):
265
+ """Run the ReAct agent with a question"""
266
+
267
+ graph = create_react_agent_graph()
268
+
269
+ initial_state = {"messages": [HumanMessage(content=question)], "llm_call_count": 0, "max_llm_calls": max_llm_calls}
270
+
271
+ # Stream the execution
272
+ print(f"Question: {question}")
273
+ print("=" * 50)
274
+
275
+ for step in graph.stream(initial_state):
276
+ for node, output in step.items():
277
+ print(f"\n--- {node.upper()} ---")
278
+ if "messages" in output and output["messages"]:
279
+ for msg in output["messages"]:
280
+ if hasattr(msg, "content"):
281
+ print(f"{msg.__class__.__name__}: {msg.content}")
282
+ elif hasattr(msg, "tool_calls") and msg.tool_calls:
283
+ print(f"Tool calls: {[tc['name'] for tc in msg.tool_calls]}")
284
+
285
+ if "final_answer" in output:
286
+ print("\nFINAL STRUCTURED ANSWER:")
287
+ print(f"Answer: {output['final_answer'].answer}")
288
+ print(f"Confidence: {output['final_answer'].confidence}")
289
+ print(f"Sources: {output['final_answer'].sources_used}")
src/agents/langgraph_agent_v2.py ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Sequence
2
+ from typing import Annotated, Literal
3
+
4
+ from langchain.chat_models import init_chat_model
5
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage
6
+ from langchain_core.runnables import RunnableConfig
7
+ from langgraph.graph import END, StateGraph
8
+ from langgraph.graph.message import add_messages
9
+ from pydantic import BaseModel, Field
10
+
11
+ from src.agents.models import FeasibilityCheck, FinalAnswer, FinalConclusion, NextStep
12
+ from src.agents.prompts import GAIAPrompts
13
+ from src.agents.tools import tools
14
+
15
+ # Initialize
16
+ model = init_chat_model("gemini-2.0-flash", model_provider="google_genai")
17
+ model_with_tools = model.bind_tools(tools)
18
+
19
+ tools_by_name = {tool.name: tool for tool in tools}
20
+
21
+ prompts = GAIAPrompts()
22
+
23
+
24
+ # Graph state
25
+ class GraphState(BaseModel):
26
+ """The state of the graph"""
27
+
28
+ # History
29
+ history: Annotated[Sequence[BaseMessage], add_messages] = Field(
30
+ default_factory=list
31
+ ) # Complete history with node info
32
+ coordinator_messages: Annotated[Sequence[BaseMessage], add_messages] = Field(
33
+ default_factory=list
34
+ ) # Coordinator-specific messages
35
+ executor_messages: Sequence[BaseMessage] = Field(default_factory=list) # Executor-specific messages
36
+
37
+ # Input
38
+ question: str
39
+
40
+ # Feasibility check
41
+ feasibility: FeasibilityCheck | None = None
42
+
43
+ # Coordinator state
44
+ next_step: NextStep | None = None
45
+ coordinator_conclusion: FinalConclusion | None = None
46
+ coordinator_iterations: int
47
+ coordinator_max_iterations: int
48
+
49
+ # Executor state
50
+ executor_conclusion: FinalConclusion | None = None
51
+ executor_iterations: int
52
+ executor_max_iterations: int
53
+
54
+ # Final answer state
55
+ final_answer: FinalAnswer | None = None
56
+
57
+ def __getitem__(self, item):
58
+ return getattr(self, item)
59
+
60
+
61
+ # Nodes
62
+ def check_feasibility(state: GraphState, config: RunnableConfig):
63
+ """Check if the question is feasible to answer with the available tools"""
64
+
65
+ question = state["question"]
66
+
67
+ system_message = SystemMessage(content=prompts.get_feasibility_check_prompt(tools), node="feasibility")
68
+ question_message = HumanMessage(content=question, node="feasibility")
69
+ messages = [system_message, question_message]
70
+
71
+ structured_model = model.with_structured_output(FeasibilityCheck)
72
+ response = structured_model.invoke(messages, config)
73
+
74
+ response_message = AIMessage(content=str(response), node="feasibility")
75
+ messages += [response_message]
76
+ return {
77
+ "history": messages,
78
+ "feasibility": response,
79
+ }
80
+
81
+
82
+ def coordinator_node(state: GraphState, config: RunnableConfig):
83
+ """Determine the next step in the plan and select appropriate tools"""
84
+
85
+ coordinator_messages = state["coordinator_messages"]
86
+ new_messages = []
87
+
88
+ if not coordinator_messages:
89
+ system_message = SystemMessage(content=prompts.get_coordinator_system_prompt(tools), node="coordinator")
90
+ human_message = HumanMessage(
91
+ content=prompts.get_coordinator_context_prompt(state["question"]), node="coordinator"
92
+ )
93
+ coordinator_messages = [system_message, human_message]
94
+ new_messages = coordinator_messages
95
+
96
+ if state["executor_conclusion"]:
97
+ executor_message = AIMessage(
98
+ content=f"Executor conclusion: {state['executor_conclusion'].conclusion}. Complete text: {str(state['executor_conclusion'])}",
99
+ node="executor",
100
+ )
101
+ coordinator_messages += [executor_message]
102
+ new_messages += [executor_message]
103
+
104
+ # Check if we've reached max iterations
105
+ if (state["next_step"] and state["next_step"].is_final) or (
106
+ state["coordinator_iterations"] >= state["coordinator_max_iterations"]
107
+ ):
108
+ # Generate final conclusion instead of next step
109
+ human_message = HumanMessage(
110
+ content=prompts.get_coordinator_max_iterations_prompt(state["question"]), node="coordinator"
111
+ )
112
+
113
+ structured_model = model.with_structured_output(FinalConclusion)
114
+ response = structured_model.invoke(coordinator_messages + [human_message], config)
115
+ response_message = AIMessage(content=str(response), node="coordinator")
116
+
117
+ new_messages += [human_message, response_message]
118
+ return {
119
+ "history": new_messages,
120
+ "coordinator_messages": new_messages,
121
+ "coordinator_conclusion": response,
122
+ "coordinator_iterations": state["coordinator_iterations"] + 1,
123
+ }
124
+
125
+ structured_model = model.with_structured_output(NextStep)
126
+ response = structured_model.invoke(coordinator_messages, config)
127
+
128
+ response_message = AIMessage(content=str(response), node="coordinator")
129
+ new_messages += [response_message]
130
+
131
+ return {
132
+ "history": new_messages,
133
+ "coordinator_messages": new_messages,
134
+ "coordinator_iterations": state["coordinator_iterations"] + 1,
135
+ "next_step": response,
136
+ "executor_messages": [],
137
+ "executor_conclusion": None,
138
+ "executor_iterations": 0,
139
+ }
140
+
141
+
142
+ def executor_node(state: GraphState, config: RunnableConfig):
143
+ """Plan the execution of the current step using ReAct pattern"""
144
+ if not state["next_step"]:
145
+ return {
146
+ "executor_conclusion": FinalConclusion(conclusion="No next step", partial_results=""),
147
+ "executor_iterations": state["executor_iterations"] + 1,
148
+ }
149
+
150
+ messages = state["executor_messages"]
151
+
152
+ if not messages:
153
+ system_message = SystemMessage(
154
+ content=prompts.get_executor_system_prompt(state["next_step"].tools),
155
+ node="executor",
156
+ )
157
+ human_message = HumanMessage(content=prompts.get_executor_task_prompt(state["next_step"].step), node="executor")
158
+ messages = [system_message, human_message]
159
+
160
+ if state["executor_iterations"] >= state["executor_max_iterations"]:
161
+ # Generate final conclusion and return to coordinator
162
+ human_message = HumanMessage(
163
+ content=prompts.get_executor_max_iterations_prompt(state["next_step"].step),
164
+ node="executor",
165
+ )
166
+
167
+ messages += [human_message]
168
+
169
+ structured_model = model.with_structured_output(FinalConclusion)
170
+ response = structured_model.invoke(messages, config)
171
+
172
+ response_message = AIMessage(
173
+ content=f"Executor conclusion: {str(response)}",
174
+ node="executor",
175
+ )
176
+
177
+ return {
178
+ "history": [human_message, response_message],
179
+ "executor_conclusion": response
180
+ or FinalConclusion(conclusion="Failed to generate conclusion", partial_results=""),
181
+ "executor_iterations": state["executor_iterations"] + 1,
182
+ }
183
+
184
+ selected_tools = [tool for tool in tools if tool.name in state["next_step"].tools]
185
+ model_with_selected_tools = model.bind_tools(selected_tools)
186
+
187
+ response_message = model_with_selected_tools.invoke(messages, config)
188
+ response_message.node = "executor"
189
+
190
+ return {
191
+ "history": response_message,
192
+ "executor_messages": messages + [response_message],
193
+ "executor_iterations": state["executor_iterations"] + 1,
194
+ }
195
+
196
+
197
+ def tool_node(state: GraphState):
198
+ """Execute tools based on the last message's tool calls"""
199
+ outputs = []
200
+ messages = state["executor_messages"]
201
+ last_message = state["executor_messages"][-1]
202
+
203
+ for tool_call in last_message.tool_calls:
204
+ try:
205
+ tool_result = tools_by_name[tool_call["name"]].invoke(tool_call["args"])
206
+ tool_message = ToolMessage(
207
+ content=str(tool_result),
208
+ name=tool_call["name"],
209
+ tool_call_id=tool_call["id"],
210
+ node="tools",
211
+ )
212
+ outputs.append(tool_message)
213
+ except Exception as e:
214
+ tool_message = ToolMessage(
215
+ content=f"Error executing tool {tool_call['name']}: {str(e)}",
216
+ name=tool_call["name"],
217
+ tool_call_id=tool_call["id"],
218
+ node="tools",
219
+ )
220
+ outputs.append(tool_message)
221
+
222
+ return {
223
+ "history": outputs,
224
+ "executor_messages": messages + outputs,
225
+ }
226
+
227
+
228
+ def finalise(state: GraphState, config: RunnableConfig):
229
+ """Generate the final answer based on coordinator history"""
230
+ system_message = SystemMessage(content=prompts.get_finalizer_prompt(), node="finalise")
231
+ messages = [system_message] + state["coordinator_messages"]
232
+
233
+ structured_model = model.with_structured_output(FinalAnswer)
234
+ response = structured_model.invoke(messages, config)
235
+ response_message = AIMessage(content=str(response), node="finalise")
236
+
237
+ return {"history": response_message, "final_answer": response}
238
+
239
+
240
+ # Edges
241
+ def should_continue_after_feasibility(state: GraphState) -> Literal["coordinator", END]:
242
+ """Decide whether to continue with coordination or end"""
243
+ if state["feasibility"] and state["feasibility"].feasible:
244
+ return "coordinator"
245
+ return END
246
+
247
+
248
+ def should_continue_after_coordinator(state: GraphState) -> Literal["executor", "finalise"]:
249
+ """Decide whether to continue with execution or go to final answer"""
250
+ if state["coordinator_conclusion"] or (state["coordinator_iterations"] >= state["coordinator_max_iterations"]):
251
+ return "finalise"
252
+ return "executor"
253
+
254
+
255
+ def should_continue_after_executor(state: GraphState) -> Literal["tools", "coordinator", "executor"]:
256
+ """Decide whether to continue with tools or go back to coordinator"""
257
+ last_message = state["executor_messages"][-1]
258
+ if hasattr(last_message, "tool_calls") and last_message.tool_calls:
259
+ return "tools"
260
+
261
+ if state["executor_conclusion"]:
262
+ return "coordinator"
263
+
264
+ return "executor"
265
+
266
+
267
+ def should_continue_after_tools(state: GraphState) -> Literal["executor"]:
268
+ """Tools always go back to executor"""
269
+ return "executor"
270
+
271
+
272
+ # Graph
273
+ def build_graph():
274
+ """Build the graph"""
275
+ graph = StateGraph(GraphState)
276
+
277
+ # Add nodes
278
+ graph.add_node("check_feasibility", check_feasibility)
279
+ graph.add_node("coordinator", coordinator_node)
280
+ graph.add_node("executor", executor_node)
281
+ graph.add_node("tools", tool_node)
282
+ graph.add_node("finalise", finalise)
283
+
284
+ # Set entry point
285
+ graph.set_entry_point("check_feasibility")
286
+
287
+ # Add edges
288
+ graph.add_conditional_edges(
289
+ "check_feasibility", should_continue_after_feasibility, {"coordinator": "coordinator", END: END}
290
+ )
291
+ graph.add_conditional_edges(
292
+ "coordinator", should_continue_after_coordinator, {"executor": "executor", "finalise": "finalise"}
293
+ )
294
+ graph.add_conditional_edges(
295
+ "executor",
296
+ should_continue_after_executor,
297
+ {"executor": "executor", "tools": "tools", "coordinator": "coordinator"},
298
+ )
299
+ graph.add_conditional_edges(
300
+ "tools",
301
+ should_continue_after_tools,
302
+ {"executor": "executor"},
303
+ )
304
+
305
+ # Finalise node goes to END
306
+ graph.add_edge("finalise", END)
307
+
308
+ return graph.compile()
309
+
310
+
311
+ def run_agent(question: str, coordinator_max_iterations: int = 5, executor_max_iterations: int = 3):
312
+ """Run the agent with a question"""
313
+ graph = build_graph()
314
+
315
+ initial_state = {
316
+ "question": question,
317
+ "history": [],
318
+ "coordinator_messages": [],
319
+ "executor_messages": [],
320
+ "coordinator_iterations": 0,
321
+ "executor_iterations": 0,
322
+ "coordinator_max_iterations": coordinator_max_iterations,
323
+ "executor_max_iterations": executor_max_iterations,
324
+ }
325
+
326
+ # Stream the execution
327
+ print(f"Question: {question}")
328
+ print("=" * 50)
329
+
330
+ for step in graph.stream(initial_state):
331
+ for node, output in step.items():
332
+ print(f"\n--- {node.upper()} ---")
333
+
334
+ # Print history with node information
335
+ if "history" in output and output["history"]:
336
+ print("\nComplete History (with node info):")
337
+ for msg in output["history"]:
338
+ node_info = getattr(msg, "node", "unknown") if hasattr(msg, "node") else "unknown"
339
+ content = getattr(msg, "content", str(msg)) if hasattr(msg, "content") else str(msg)
340
+ print(f"[{node_info}] {msg.__class__.__name__}: {content}")
341
+
342
+ if "coordinator_messages" in output and output["coordinator_messages"]:
343
+ print("\nCoordinator Messages:")
344
+ for msg in output["coordinator_messages"]:
345
+ if hasattr(msg, "content"):
346
+ print(f"{msg.__class__.__name__}: {msg.content}")
347
+
348
+ if "executor_messages" in output and output["executor_messages"]:
349
+ print("\nExecutor Messages:")
350
+ for msg in output["executor_messages"]:
351
+ if hasattr(msg, "content"):
352
+ print(f"{msg.__class__.__name__}: {msg.content}")
353
+
354
+ if "executor_conclusion" in output and output["executor_conclusion"]:
355
+ print("\n=== EXECUTOR CONCLUSION ===")
356
+ print(f"Conclusion: {output['executor_conclusion'].conclusion}")
357
+ print(f"Partial Results: {output['executor_conclusion'].partial_results}")
358
+ print(f"Confidence: {output['executor_conclusion'].confidence}")
359
+
360
+ if "final_answer" in output and output["final_answer"]:
361
+ print("\n=== FINAL ANSWER ===")
362
+ print(f"Answer: {output['final_answer'].answer}")
363
+ print(f"Confidence: {output['final_answer'].confidence}")
364
+ print(f"Reasoning: {output['final_answer'].reasoning}")