Skip to content

Commit 576eec9

Browse files
committed
feat: Refactor AgentService to use OpenAI SDK and enhance tool integration
Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent 8f17965 commit 576eec9

1 file changed

Lines changed: 103 additions & 37 deletions

File tree

backend/agents.py

Lines changed: 103 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,18 @@
2929
LangGraph workflow with nodes, edges, and conditional routing.
3030
"""
3131

32+
import json
33+
3234
# Standard library
3335
import os
3436
from datetime import datetime
3537
from 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
4546
from 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

Comments
 (0)