11"""
2- Agent Management Module with Azure OpenAI and LangGraph
2+ Agent Management Module with OpenAI and LangGraph
33
44Provides LangGraph-based agents for task automation:
55- Type-safe data models with Pydantic
66- Self-documenting schemas for REST and MCP
7- - Azure OpenAI integration via langchain-openai
7+ - OpenAI integration via langchain-openai
88- ReAct agent pattern with automatic tool discovery
99
1010Following "Grokking Simplicity" and "A Philosophy of Software Design":
3939
4040load_dotenv ()
4141
42+ from uuid import UUID
43+
4244# Ensure operations register before we request LangChain tools
4345import operations # noqa: F401
46+
4447# Local - Import operations registry for automatic tool discovery
4548from api_decorators import get_langchain_tools
49+
50+ # Local CSV service
51+ from csv_data import get_csv_ticket_service
52+
4653# Third-party - FastMCP client for external MCP servers
4754from fastmcp import Client as MCPClient
4855from langchain_core .tools import StructuredTool
56+
4957# Third-party - LangChain and LangGraph
50- from langchain_openai import AzureChatOpenAI
58+ from langchain_openai import ChatOpenAI
5159from langgraph .prebuilt import create_react_agent
60+
5261# Third-party - Pydantic for validation
5362from pydantic import BaseModel , Field , create_model , field_validator
63+ from tickets import TicketStatus
5464
5565# ============================================================================
5666# DATA MODELS - Pydantic for validation and schema generation
@@ -133,14 +143,12 @@ class AgentResponse(BaseModel):
133143
134144
135145# ============================================================================
136- # CONFIGURATION - Azure OpenAI settings (hardcoded except API key)
146+ # CONFIGURATION - OpenAI settings
137147# ============================================================================
138148
139- # Azure OpenAI configuration - only API key from environment
140- AZURE_OPENAI_ENDPOINT = "https://can-i-haz-houze-resource.cognitiveservices.azure.com"
141- AZURE_OPENAI_API_KEY = os .getenv ("AZURE_API_KEY" , "" )
142- AZURE_OPENAI_DEPLOYMENT = os .getenv ("AZURE_OPENAI_DEPLOYMENT" , "gpt-5-mini" )
143- AZURE_OPENAI_API_VERSION = os .getenv ("AZURE_OPENAI_API_VERSION" , "2025-04-01-preview" )
149+ OPENAI_API_KEY = os .getenv ("OPENAI_API_KEY" , "" )
150+ OPENAI_MODEL = os .getenv ("OPENAI_MODEL" , "gpt-4o-mini" )
151+ OPENAI_BASE_URL = os .getenv ("OPENAI_BASE_URL" , "" ) # optional override
144152
145153# External MCP server URL for ticket management (hardcoded)
146154TICKET_MCP_SERVER_URL = "https://yodrrscbpxqnslgugwow.supabase.co/functions/v1/mcp/a7f2b8c4-d3e9-4f1a-b5c6-e8d9f0123456"
@@ -241,7 +249,7 @@ class AgentService:
241249 Agent service handling all LLM agent operations.
242250
243251 Consolidated operations:
244- - Agent initialization with Azure OpenAI
252+ - Agent initialization with OpenAI
245253 - Tool discovery from @operation registry
246254 - External MCP server tool integration (persistent connection)
247255 - ReAct agent execution with task tools
@@ -253,71 +261,39 @@ class AgentService:
253261
254262 def __init__ (self ):
255263 """
256- Initialize the agent service with Azure OpenAI.
264+ Initialize the agent service with OpenAI.
257265
258266 Validates that required environment variables are set and creates
259267 the LLM client for agent execution.
260268
261269 Raises:
262- ValueError: If Azure OpenAI configuration is incomplete
270+ ValueError: If OpenAI configuration is incomplete
263271 """
264272 # Validate configuration
265- if not AZURE_OPENAI_API_KEY :
273+ if not OPENAI_API_KEY :
266274 raise ValueError (
267- "Azure OpenAI API key not set. "
268- "Please set AZURE_API_KEY environment variable."
275+ "OpenAI API key not set. "
276+ "Please set OPENAI_API_KEY environment variable."
269277 )
270278
271- # Initialize AzureChatOpenAI with hardcoded endpoint
272- self .llm = AzureChatOpenAI (
273- azure_endpoint = AZURE_OPENAI_ENDPOINT ,
274- api_key = AZURE_OPENAI_API_KEY , # type: ignore
275- azure_deployment = AZURE_OPENAI_DEPLOYMENT ,
276- api_version = AZURE_OPENAI_API_VERSION ,
279+ # Initialize ChatOpenAI
280+ self .llm = ChatOpenAI (
281+ model = OPENAI_MODEL ,
282+ api_key = OPENAI_API_KEY ,
283+ base_url = OPENAI_BASE_URL or None ,
284+ temperature = 0.0 ,
277285 )
278286
279- # Load LangGraph tools from operation registry
280- # These are the actual Python functions decorated with @tool
281- self .tools = get_langchain_tools ()
282-
283- # Ticket MCP client state (lazy initialization)
287+ # CSV tools only (do not expose operations or external MCP)
288+ self .tools = self ._build_csv_tools ()
289+
290+ # Ticket MCP client state (unused)
284291 self ._ticket_mcp_client : Optional [MCPClient ] = None
285292 self ._ticket_mcp_tools_loaded = False
286293
287294 async def _ensure_ticket_mcp_connection (self ):
288- """
289- Ensure ticket MCP client is connected and tools are loaded.
290-
291- Opens a persistent connection to the external ticket MCP server and
292- converts its tools to LangChain format. Called lazily on first
293- agent run.
294- """
295- if self ._ticket_mcp_tools_loaded :
296- return
297-
298- try :
299- # Create and connect ticket MCP client (keep connection open)
300- client = MCPClient (TICKET_MCP_SERVER_URL )
301- await client .__aenter__ ()
302- self ._ticket_mcp_client = client
303-
304- # Fetch and convert ticket MCP tools
305- mcp_tools = await client .list_tools ()
306- print (f"\n { '=' * 60 } " )
307- print (f"🎫 TICKET MCP SERVER CONNECTED" )
308- print (f"{ '=' * 60 } " )
309- print (f" URL: { TICKET_MCP_SERVER_URL } " )
310- print (f" Tools available: { len (mcp_tools )} " )
311- for tool in mcp_tools :
312- lc_tool = _mcp_tool_to_langchain (client , tool )
313- self .tools .append (lc_tool )
314- print (f" ✓ { tool .name } : { (tool .description or '' )[:60 ]} ..." )
315- print (f"{ '=' * 60 } \n " )
316- self ._ticket_mcp_tools_loaded = True
317-
318- except Exception as e :
319- print (f"WARNING: Failed to load ticket MCP tools from { TICKET_MCP_SERVER_URL } : { e } " )
320- # Continue without ticket MCP tools - local tools still work
295+ """No-op: external MCP tools not exposed."""
296+ return
321297
322298 async def close (self ):
323299 """Close the ticket MCP client connection."""
@@ -327,7 +303,78 @@ async def close(self):
327303 except Exception :
328304 pass
329305 self ._ticket_mcp_client = None
330-
306+
307+ def _build_csv_tools (self ) -> list [StructuredTool ]:
308+ """Build LangChain tools backed by CSVTicketService."""
309+ import json
310+ service = get_csv_ticket_service ()
311+
312+ def _csv_list_tickets (status : str | None = None , assigned_group : str | None = None , has_assignee : bool | None = None ) -> str :
313+ try :
314+ status_enum = TicketStatus (status .lower ()) if status else None
315+ except Exception :
316+ status_enum = None
317+ tickets = service .list_tickets (status = status_enum , assigned_group = assigned_group , has_assignee = has_assignee )
318+ return json .dumps ([t .model_dump () for t in tickets [:200 ]], default = str )
319+
320+ def _csv_get_ticket (ticket_id : str ) -> str :
321+ try :
322+ tid = UUID (ticket_id )
323+ except Exception :
324+ return json .dumps ({"error" : "invalid ticket id" })
325+ ticket = service .get_ticket (tid )
326+ if not ticket :
327+ return json .dumps ({"error" : "not found" })
328+ return json .dumps (ticket .model_dump (), default = str )
329+
330+ def _csv_search_tickets (query : str , limit : int = 50 ) -> str :
331+ q = query .lower ()
332+ tickets = service .list_tickets ()
333+ matched = []
334+ for t in tickets :
335+ text = " " .join ([
336+ t .summary or "" ,
337+ t .description or "" ,
338+ t .notes or "" ,
339+ t .resolution or "" ,
340+ t .requester_name or "" ,
341+ t .assigned_group or "" ,
342+ t .city or "" ,
343+ ]).lower ()
344+ if q in text :
345+ matched .append (t .model_dump ())
346+ if len (matched ) >= limit :
347+ break
348+ return json .dumps (matched , default = str )
349+
350+ def _csv_ticket_fields () -> str :
351+ # Use Ticket model fields as schema
352+ from tickets import Ticket
353+ return json .dumps (list (Ticket .model_fields .keys ()))
354+
355+ return [
356+ StructuredTool .from_function (
357+ func = _csv_list_tickets ,
358+ name = "csv_list_tickets" ,
359+ description = "List tickets from CSV with optional filters: status (new, assigned, in_progress, pending, resolved, closed, cancelled), assigned_group, has_assignee (true/false). Returns JSON array." ,
360+ ),
361+ StructuredTool .from_function (
362+ func = _csv_get_ticket ,
363+ name = "csv_get_ticket" ,
364+ description = "Get a ticket by UUID (id). Returns JSON object including notes/resolution." ,
365+ ),
366+ StructuredTool .from_function (
367+ func = _csv_search_tickets ,
368+ name = "csv_search_tickets" ,
369+ description = "Search tickets by text across summary, description, notes, resolution, requester, group, city. Returns JSON array." ,
370+ ),
371+ StructuredTool .from_function (
372+ func = _csv_ticket_fields ,
373+ name = "csv_ticket_fields" ,
374+ description = "List available ticket fields (schema) as JSON array of field names." ,
375+ ),
376+ ]
377+
331378 async def run_agent (self , request : AgentRequest ) -> AgentResponse :
332379 """
333380 Run a ReAct agent with the given request using LangGraph.
@@ -352,9 +399,6 @@ async def run_agent(self, request: AgentRequest) -> AgentResponse:
352399 Raises:
353400 ValueError: If agent execution fails
354401 """
355- # Ensure ticket MCP tools are loaded (lazy initialization)
356- await self ._ensure_ticket_mcp_connection ()
357-
358402 try :
359403 # Create ReAct agent with LangGraph tools
360404 # The tools are the actual Python functions with @tool decorator
@@ -363,22 +407,17 @@ async def run_agent(self, request: AgentRequest) -> AgentResponse:
363407 # System message to guide the agent's behavior
364408 system_msg = (
365409 "You are a support ticket management assistant. "
366- "Your primary role is to help users manage, search, and evaluate support tickets . "
367- "You MUST use the available tools to perform all actions - NEVER simulate or pretend . "
410+ "Use the CSV tools: csv_list_tickets, csv_search_tickets, csv_get_ticket, csv_ticket_fields . "
411+ "You MUST use the available tools to perform all actions - NEVER simulate or invent data . "
368412 "\n \n "
369413 "TICKET CAPABILITIES:\n "
370- "- Search and list tickets by status, priority, city, service\n "
371- "- Get detailed ticket information and work logs\n "
372- "- Analyze ticket statistics and trends\n "
373- "- Request modifications to tickets (status, priority, assignee, etc.)\n "
374- "- Review and approve/reject modification requests\n "
375- "\n "
376- "TASK CAPABILITIES:\n "
377- "- Create, update, delete, and list tasks\n "
378- "- Get task statistics\n "
414+ "- List tickets with filters (status, assigned_group, has_assignee)\n "
415+ "- Search tickets across summary/description/notes/resolution/requester/group/city\n "
416+ "- Get a ticket by id (UUID) including notes and resolution\n "
417+ "- Inspect available ticket fields (schema)\n "
379418 "\n "
380- "When users ask about tickets, use the ticket tools (list_tickets, get_ticket, search_tickets, etc.) . "
381- "When users ask about tasks, use task tools (create_task, list_tasks, etc.) . "
419+ "When users ask about tickets, call the csv_* tools and summarize results . "
420+ "If a field is not present, state that explicitly . "
382421 "Always confirm actions based on actual tool results."
383422 )
384423
0 commit comments