2929 LangGraph workflow with nodes, edges, and conditional routing.
3030"""
3131
32+ import json
33+
3234# Standard library
3335import os
3436from datetime import datetime
3537from typing import Literal , Optional
3638
3739# Local - Import operations registry for automatic tool discovery
38- from api_decorators import get_langchain_tools
40+ from api_decorators import get_operations
3941
40- # Third-party - LangChain and LangGraph
41- from langchain_openai import AzureChatOpenAI
42- from langgraph .prebuilt import create_react_agent
42+ # Third-party - OpenAI SDK
43+ from openai import OpenAI
4344
4445# Third-party - Pydantic for validation
4546from pydantic import BaseModel , Field , field_validator
@@ -172,22 +173,43 @@ def __init__(self):
172173 "environment variables. See .env.example for template."
173174 )
174175
175- # Initialize Azure OpenAI client
176- self .llm = AzureChatOpenAI (
177- azure_endpoint = AZURE_OPENAI_ENDPOINT ,
178- api_key = AZURE_OPENAI_API_KEY , # type: ignore
179- azure_deployment = AZURE_OPENAI_DEPLOYMENT ,
180- api_version = AZURE_OPENAI_API_VERSION ,
181- temperature = 0.7 ,
176+ # Initialize OpenAI client with Azure endpoint
177+ self .client = OpenAI (
178+ base_url = AZURE_OPENAI_ENDPOINT ,
179+ api_key = AZURE_OPENAI_API_KEY ,
182180 )
181+ self .model = AZURE_OPENAI_DEPLOYMENT
183182
184183 # Load tools from operation registry
185184 # These are automatically generated from @operation decorated functions
186- self .tools = get_langchain_tools ()
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
187209
188210 async def run_agent (self , request : AgentRequest ) -> AgentResponse :
189211 """
190- Run a ReAct agent with the given request.
212+ Run a ReAct agent with the given request using native OpenAI SDK .
191213
192214 The agent uses a ReAct (Reasoning + Acting) loop:
193215 1. Receive user prompt
@@ -208,46 +230,90 @@ async def run_agent(self, request: AgentRequest) -> AgentResponse:
208230 ValueError: If agent execution fails
209231 """
210232 try :
211- # Create ReAct agent with automatic tool discovery
212- # The create_react_agent function builds a pre-configured
213- # LangGraph workflow that implements the ReAct pattern
214- agent = create_react_agent (self .llm , self .tools )
215-
216- # Execute agent with user prompt
217- # Add system message to guide the agent's behavior
218- # The agent will autonomously choose which tools to use
233+ # System message to guide the agent's behavior
219234 system_msg = (
220235 "You are a helpful task management assistant. "
221236 "You can create, update, delete, and list tasks. "
222237 "When asked to create tasks, use descriptive titles. "
223238 "Always confirm what you've done and show the results."
224239 )
225240
226- result = await agent .ainvoke (
227- {"messages" : [("system" , system_msg ), ("user" , request .prompt )]}
228- )
229-
230- # Extract the agent's final response
231- # LangGraph returns a state dict with message history
232- final_message = result ["messages" ][- 1 ]
233- agent_output = final_message .content if hasattr (final_message , 'content' ) else str (final_message )
241+ messages : list = [
242+ {"role" : "system" , "content" : system_msg },
243+ {"role" : "user" , "content" : request .prompt }
244+ ]
234245
235- # Track which tools were used (for debugging/monitoring)
236246 tools_used = []
237- for msg in result ["messages" ]:
238- if hasattr (msg , 'tool_calls' ) and msg .tool_calls :
239- tools_used .extend ([tc ['name' ] for tc in msg .tool_calls ])
247+ max_iterations = 10
248+
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+ })
304+
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 )
240308
241309 return AgentResponse (
242- result = agent_output ,
310+ result = final_content ,
243311 agent_type = request .agent_type ,
244- tools_used = list (set (tools_used )), # Deduplicate
312+ tools_used = list (set (tools_used )),
245313 created_at = datetime .now ()
246314 )
247315
248316 except Exception as e :
249- # Return error response instead of raising
250- # This allows the API to return a proper error to the client
251317 return AgentResponse (
252318 result = "Agent execution failed. See error field for details." ,
253319 agent_type = request .agent_type ,
0 commit comments