Skip to content

Commit 529a1c4

Browse files
authored
OpenAI and ticket feed (#9)
* refactor: update Azure OpenAI configuration and streamline environment variable usage Signed-off-by: Andre Bossard <anbossar@microsoft.com> * feat: integrate FastMCP client for external tool support and add tests Signed-off-by: Andre Bossard <anbossar@microsoft.com> * feat: implement Ticket MCP integration with FastMCP client and add REST endpoints for ticket management Signed-off-by: Andre Bossard <anbossar@microsoft.com> --------- Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent f4378c5 commit 529a1c4

6 files changed

Lines changed: 421 additions & 38 deletions

File tree

.env.example

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
11
# Azure OpenAI Configuration
2-
# Get your credentials from: https://portal.azure.com
2+
# Get your API key from: https://portal.azure.com
33

4-
# Your Azure OpenAI endpoint (e.g., https://your-resource.openai.azure.com/)
5-
AZURE_OPENAI_ENDPOINT=https://can-i-haz-houze-resource.services.ai.azure.com/api/projects/can-i-haz-houze
6-
7-
# Your Azure OpenAI API key
8-
AZURE_OPENAI_API_KEY=your-api-key-here
9-
10-
# The deployment name you created in Azure (e.g., gpt-4, gpt-35-turbo)
11-
AZURE_OPENAI_DEPLOYMENT=gpt-5-mini
12-
13-
# API version - use latest stable version
14-
# See: https://learn.microsoft.com/azure/ai-services/openai/reference#rest-api-versioning
15-
AZURE_OPENAI_API_VERSION=2024-05-01-preview
4+
# Your Azure OpenAI API key (Bearer token)
5+
AZURE_API_KEY=your-api-key-here
166

177
# Optional: Frontend build path override
188
# FRONTEND_DIST=/path/to/custom/frontend/dist

backend/agents.py

Lines changed: 143 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,23 @@
3232
# Standard library
3333
import os
3434
from datetime import datetime
35-
from typing import Literal, Optional
35+
from typing import Any, Literal, Optional
36+
37+
# Load environment variables before anything else
38+
from dotenv import load_dotenv
39+
40+
load_dotenv()
3641

3742
# Local - Import operations registry for automatic tool discovery
3843
from api_decorators import get_langchain_tools
39-
44+
# Third-party - FastMCP client for external MCP servers
45+
from fastmcp import Client as MCPClient
46+
from langchain_core.tools import StructuredTool
4047
# Third-party - LangChain and LangGraph
41-
from langchain_openai import ChatOpenAI
48+
from langchain_openai import AzureChatOpenAI
4249
from langgraph.prebuilt import create_react_agent
43-
4450
# Third-party - Pydantic for validation
45-
from pydantic import BaseModel, Field, field_validator
51+
from pydantic import BaseModel, Field, create_model, field_validator
4652

4753
# ============================================================================
4854
# DATA MODELS - Pydantic for validation and schema generation
@@ -125,15 +131,84 @@ class AgentResponse(BaseModel):
125131

126132

127133
# ============================================================================
128-
# CONFIGURATION - Azure OpenAI settings from environment
134+
# CONFIGURATION - Azure OpenAI settings (hardcoded except API key)
135+
# ============================================================================
136+
137+
# Azure OpenAI configuration - only API key from environment
138+
AZURE_OPENAI_ENDPOINT = "https://can-i-haz-houze-resource.cognitiveservices.azure.com"
139+
AZURE_OPENAI_API_KEY = os.getenv("AZURE_API_KEY", "")
140+
AZURE_OPENAI_DEPLOYMENT = "gpt-5-mini"
141+
AZURE_OPENAI_API_VERSION = "2025-04-01-preview"
142+
143+
# External MCP server URL for ticket management (hardcoded)
144+
TICKET_MCP_SERVER_URL = "https://yodrrscbpxqnslgugwow.supabase.co/functions/v1/mcp/a7f2b8c4-d3e9-4f1a-b5c6-e8d9f0123456"
145+
146+
147+
# ============================================================================
148+
# MCP TOOL CONVERSION HELPERS
129149
# ============================================================================
130150

131-
# Azure OpenAI configuration
132-
# These should be set in .env file (see .env.example)
133-
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT", "")
134-
AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY", "")
135-
AZURE_OPENAI_DEPLOYMENT = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4")
136-
AZURE_OPENAI_API_VERSION = os.getenv("AZURE_OPENAI_API_VERSION", "2024-02-15-preview")
151+
def _json_type_to_python(json_type: str) -> type:
152+
"""Map JSON schema type to Python type."""
153+
mapping = {
154+
"string": str,
155+
"integer": int,
156+
"number": float,
157+
"boolean": bool,
158+
"array": list,
159+
"object": dict,
160+
}
161+
return mapping.get(json_type, str)
162+
163+
164+
def _schema_to_pydantic(name: str, schema: dict) -> type[BaseModel]:
165+
"""Convert JSON schema to Pydantic model for LangChain tool args."""
166+
properties = schema.get("properties", {})
167+
required = set(schema.get("required", []))
168+
169+
fields: dict[str, Any] = {}
170+
for field_name, field_schema in properties.items():
171+
field_type = _json_type_to_python(field_schema.get("type", "string"))
172+
field_desc = field_schema.get("description", f"{field_name} parameter")
173+
174+
if field_name in required:
175+
fields[field_name] = (field_type, Field(description=field_desc))
176+
else:
177+
default = field_schema.get("default")
178+
fields[field_name] = (Optional[field_type], Field(default=default, description=field_desc))
179+
180+
model_name = f"{name.title().replace('_', '').replace('-', '')}Args"
181+
return create_model(model_name, **fields)
182+
183+
184+
def _mcp_tool_to_langchain(mcp_client: MCPClient, tool: Any) -> StructuredTool:
185+
"""
186+
Convert MCP tool to LangChain StructuredTool.
187+
188+
Creates a wrapper that calls the external MCP server via the persistent client.
189+
"""
190+
tool_name = tool.name
191+
tool_desc = tool.description or f"MCP tool: {tool_name}"
192+
input_schema = tool.inputSchema if hasattr(tool, 'inputSchema') else {}
193+
194+
# Create async wrapper that calls MCP server
195+
async def call_mcp_tool(**kwargs) -> str:
196+
result = await mcp_client.call_tool(tool_name, kwargs)
197+
# Extract text from MCP response
198+
if hasattr(result, 'content') and result.content:
199+
texts = [c.text for c in result.content if hasattr(c, 'text')]
200+
return "\n".join(texts) if texts else str(result)
201+
return str(result)
202+
203+
# Build Pydantic model from input schema
204+
args_model = _schema_to_pydantic(tool_name, input_schema)
205+
206+
return StructuredTool(
207+
name=tool_name,
208+
description=tool_desc,
209+
coroutine=call_mcp_tool,
210+
args_schema=args_model,
211+
)
137212

138213

139214
# ============================================================================
@@ -147,6 +222,7 @@ class AgentService:
147222
Consolidated operations:
148223
- Agent initialization with Azure OpenAI
149224
- Tool discovery from @operation registry
225+
- External MCP server tool integration (persistent connection)
150226
- ReAct agent execution with task tools
151227
- Response formatting and error handling
152228
@@ -165,24 +241,66 @@ def __init__(self):
165241
ValueError: If Azure OpenAI configuration is incomplete
166242
"""
167243
# Validate configuration
168-
if not AZURE_OPENAI_ENDPOINT or not AZURE_OPENAI_API_KEY:
244+
if not AZURE_OPENAI_API_KEY:
169245
raise ValueError(
170-
"Azure OpenAI configuration is incomplete. "
171-
"Please set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY "
172-
"environment variables. See .env.example for template."
246+
"Azure OpenAI API key not set. "
247+
"Please set AZURE_API_KEY environment variable."
173248
)
174249

175-
# Initialize ChatOpenAI with Azure endpoint using base_url pattern
176-
self.llm = ChatOpenAI(
177-
base_url=AZURE_OPENAI_ENDPOINT,
250+
# Initialize AzureChatOpenAI with hardcoded endpoint
251+
self.llm = AzureChatOpenAI(
252+
azure_endpoint=AZURE_OPENAI_ENDPOINT,
178253
api_key=AZURE_OPENAI_API_KEY, # type: ignore
179-
model=AZURE_OPENAI_DEPLOYMENT,
180-
temperature=0.7,
254+
azure_deployment=AZURE_OPENAI_DEPLOYMENT,
255+
api_version=AZURE_OPENAI_API_VERSION,
181256
)
182257

183258
# Load LangGraph tools from operation registry
184259
# These are the actual Python functions decorated with @tool
185260
self.tools = get_langchain_tools()
261+
262+
# Ticket MCP client state (lazy initialization)
263+
self._ticket_mcp_client: Optional[MCPClient] = None
264+
self._ticket_mcp_tools_loaded = False
265+
266+
async def _ensure_ticket_mcp_connection(self):
267+
"""
268+
Ensure ticket MCP client is connected and tools are loaded.
269+
270+
Opens a persistent connection to the external ticket MCP server and
271+
converts its tools to LangChain format. Called lazily on first
272+
agent run.
273+
"""
274+
if self._ticket_mcp_tools_loaded:
275+
return
276+
277+
try:
278+
# Create and connect ticket MCP client (keep connection open)
279+
client = MCPClient(TICKET_MCP_SERVER_URL)
280+
await client.__aenter__()
281+
self._ticket_mcp_client = client
282+
283+
# Fetch and convert ticket MCP tools
284+
mcp_tools = await client.list_tools()
285+
for tool in mcp_tools:
286+
lc_tool = _mcp_tool_to_langchain(client, tool)
287+
self.tools.append(lc_tool)
288+
289+
print(f"DEBUG: Loaded {len(mcp_tools)} ticket tools from MCP server {TICKET_MCP_SERVER_URL}")
290+
self._ticket_mcp_tools_loaded = True
291+
292+
except Exception as e:
293+
print(f"WARNING: Failed to load ticket MCP tools from {TICKET_MCP_SERVER_URL}: {e}")
294+
# Continue without ticket MCP tools - local tools still work
295+
296+
async def close(self):
297+
"""Close the ticket MCP client connection."""
298+
if self._ticket_mcp_client:
299+
try:
300+
await self._ticket_mcp_client.__aexit__(None, None, None)
301+
except Exception:
302+
pass
303+
self._ticket_mcp_client = None
186304

187305
async def run_agent(self, request: AgentRequest) -> AgentResponse:
188306
"""
@@ -197,6 +315,7 @@ async def run_agent(self, request: AgentRequest) -> AgentResponse:
197315
198316
All @operation decorated functions are automatically available as LangGraph tools.
199317
LangGraph calls the Python functions directly (not via MCP).
318+
External MCP tools are loaded from the configured MCP server.
200319
201320
Args:
202321
request: AgentRequest with prompt and agent type
@@ -207,6 +326,9 @@ async def run_agent(self, request: AgentRequest) -> AgentResponse:
207326
Raises:
208327
ValueError: If agent execution fails
209328
"""
329+
# Ensure ticket MCP tools are loaded (lazy initialization)
330+
await self._ensure_ticket_mcp_connection()
331+
210332
try:
211333
# Create ReAct agent with LangGraph tools
212334
# The tools are the actual Python functions with @tool decorator

0 commit comments

Comments
 (0)