2929 LangGraph workflow with nodes, edges, and conditional routing.
3030"""
3131
32- import json
33-
3432# Standard library
3533import os
3634from datetime import datetime
3735from typing import Literal , Optional
3836
3937# Local - Import operations registry for automatic tool discovery
40- from api_decorators import get_operations
38+ from api_decorators import get_langchain_tools
4139
42- # Third-party - OpenAI SDK
43- from openai import OpenAI
40+ # Third-party - LangChain and LangGraph
41+ from langchain_openai import ChatOpenAI
42+ from langgraph .prebuilt import create_react_agent
4443
4544# Third-party - Pydantic for validation
4645from pydantic import BaseModel , Field , field_validator
@@ -173,43 +172,21 @@ def __init__(self):
173172 "environment variables. See .env.example for template."
174173 )
175174
176- # Initialize OpenAI client with Azure endpoint
177- self .client = OpenAI (
175+ # Initialize ChatOpenAI with Azure endpoint using base_url pattern
176+ self .llm = ChatOpenAI (
178177 base_url = AZURE_OPENAI_ENDPOINT ,
179- api_key = AZURE_OPENAI_API_KEY ,
178+ api_key = AZURE_OPENAI_API_KEY , # type: ignore
179+ model = AZURE_OPENAI_DEPLOYMENT ,
180+ temperature = 0.7 ,
180181 )
181- self .model = AZURE_OPENAI_DEPLOYMENT
182182
183- # Load tools from operation registry
184- # These are automatically generated from @operation decorated functions
185- self .tools = self ._get_tools_for_openai ()
186-
187- def _get_tools_for_openai (self ) -> list [dict ]:
188- """Convert operations to OpenAI function calling format."""
189- operations = get_operations ()
190- tools = []
191-
192- for op_name , op in operations .items ():
193- # Get the input schema from the operation
194- input_schema = op .get_mcp_input_schema ()
195-
196- # Convert operation to OpenAI tool schema
197- tool_def = {
198- "type" : "function" ,
199- "function" : {
200- "name" : op .name ,
201- "description" : op .description or "" ,
202- "parameters" : input_schema
203- }
204- }
205-
206- tools .append (tool_def )
207-
208- return tools
183+ # Load LangGraph tools from operation registry
184+ # These are the actual Python functions decorated with @tool
185+ self .tools = get_langchain_tools ()
209186
210187 async def run_agent (self , request : AgentRequest ) -> AgentResponse :
211188 """
212- Run a ReAct agent with the given request using native OpenAI SDK .
189+ Run a ReAct agent with the given request using LangGraph .
213190
214191 The agent uses a ReAct (Reasoning + Acting) loop:
215192 1. Receive user prompt
@@ -218,7 +195,8 @@ async def run_agent(self, request: AgentRequest) -> AgentResponse:
218195 4. Observe results
219196 5. Repeat until task is complete
220197
221- All @operation decorated functions are automatically available as tools.
198+ All @operation decorated functions are automatically available as LangGraph tools.
199+ LangGraph calls the Python functions directly (not via MCP).
222200
223201 Args:
224202 request: AgentRequest with prompt and agent type
@@ -230,84 +208,57 @@ async def run_agent(self, request: AgentRequest) -> AgentResponse:
230208 ValueError: If agent execution fails
231209 """
232210 try :
211+ # Create ReAct agent with LangGraph tools
212+ # The tools are the actual Python functions with @tool decorator
213+ agent = create_react_agent (self .llm , self .tools )
214+
233215 # System message to guide the agent's behavior
234216 system_msg = (
235217 "You are a helpful task management assistant. "
236- "You can create, update, delete, and list tasks. "
237- "When asked to create tasks, use descriptive titles. "
238- "Always confirm what you've done and show the results."
218+ "You MUST use the available tools to perform all actions. "
219+ "NEVER pretend or simulate creating, updating, or listing tasks - you MUST call the actual tools. "
220+ "Available tools: create_task, update_task, delete_task, list_tasks, get_task, get_task_stats. "
221+ "When asked to create tasks, call create_task for each one. "
222+ "When asked to list tasks, call list_tasks. "
223+ "Always use tools and confirm what you've done based on the tool results."
239224 )
240225
241- messages : list = [
242- {"role" : "system" , "content" : system_msg },
243- {"role" : "user" , "content" : request .prompt }
244- ]
226+ # Execute agent with user prompt
227+ print (f"DEBUG: Running agent with { len (self .tools )} tools" )
228+ print (f"DEBUG: Tools: { [t .name if hasattr (t , 'name' ) else str (t ) for t in self .tools ]} " )
245229
246- tools_used = []
247- max_iterations = 10
230+ result = await agent .ainvoke (
231+ {"messages" : [("system" , system_msg ), ("user" , request .prompt )]}
232+ )
248233
249- # ReAct loop
250- for _ in range (max_iterations ):
251- response = self .client .chat .completions .create (
252- model = self .model ,
253- messages = messages , # type: ignore
254- tools = self .tools , # type: ignore
255- tool_choice = "auto"
256- )
257-
258- message = response .choices [0 ].message
259-
260- # Convert message to dict for appending
261- message_dict : dict = {"role" : "assistant" , "content" : message .content or "" }
262- if message .tool_calls :
263- message_dict ["tool_calls" ] = [
264- {
265- "id" : tc .id ,
266- "type" : tc .type ,
267- "function" : {"name" : tc .function .name , "arguments" : tc .function .arguments } # type: ignore
268- } for tc in message .tool_calls
269- ]
270- messages .append (message_dict )
271-
272- # If no tool calls, we're done
273- if not message .tool_calls :
274- break
275-
276- # Execute tool calls
277- for tool_call in message .tool_calls :
278- function_name = tool_call .function .name # type: ignore
279- tools_used .append (function_name )
280-
281- try :
282- # Parse arguments
283- arguments = json .loads (tool_call .function .arguments ) # type: ignore
284-
285- # Find and execute the operation
286- operations = get_operations ()
287- operation = operations .get (function_name )
288-
289- if operation :
290- result = await operation .handler (** arguments )
291- result_str = json .dumps (result .model_dump () if hasattr (result , 'model_dump' ) else result )
292- else :
293- result_str = json .dumps ({"error" : f"Unknown tool: { function_name } " })
294-
295- except Exception as e :
296- result_str = json .dumps ({"error" : str (e )})
297-
298- # Add tool result to messages
299- messages .append ({
300- "role" : "tool" ,
301- "tool_call_id" : tool_call .id ,
302- "content" : result_str
303- })
234+ print (f"DEBUG: Agent result messages: { len (result ['messages' ])} " )
235+ for i , msg in enumerate (result ["messages" ]):
236+ print (f"DEBUG: Message { i } : type={ type (msg ).__name__ } , has_tool_calls={ hasattr (msg , 'tool_calls' )} " )
237+ if hasattr (msg , 'tool_calls' ) and msg .tool_calls :
238+ print (f"DEBUG: Tool calls: { msg .tool_calls } " )
304239
305- # Get final response
306- last_msg = messages [- 1 ]
307- final_content = last_msg .get ("content" , "" ) if isinstance (last_msg , dict ) else str (last_msg )
240+ # Extract the agent's final response
241+ final_message = result ["messages" ][- 1 ]
242+ agent_output = final_message .content if hasattr (final_message , 'content' ) else str (final_message )
243+
244+ # Track which tools were used
245+ tools_used = []
246+ for msg in result ["messages" ]:
247+ # Check for tool_calls attribute
248+ if hasattr (msg , 'tool_calls' ) and msg .tool_calls :
249+ for tc in msg .tool_calls :
250+ # Extract tool name from tool call
251+ if isinstance (tc , dict ):
252+ tools_used .append (tc .get ('name' , '' ))
253+ else :
254+ tools_used .append (tc ['name' ] if 'name' in tc else str (tc ))
255+ # Also check for ToolMessage type (tool results)
256+ elif hasattr (msg , 'type' ) and msg .type == 'tool' :
257+ if hasattr (msg , 'name' ):
258+ tools_used .append (msg .name )
308259
309260 return AgentResponse (
310- result = final_content ,
261+ result = agent_output ,
311262 agent_type = request .agent_type ,
312263 tools_used = list (set (tools_used )),
313264 created_at = datetime .now ()
0 commit comments