@@ -84,8 +84,7 @@ def _env_int(name: str, default: int) -> int:
8484from langchain_core .callbacks import BaseCallbackHandler
8585from langchain_core .tools import StructuredTool
8686
87- # Third-party - LangChain and LangGraph
88- from langchain_openai import ChatOpenAI
87+ # Third-party - LangGraph
8988from langgraph .prebuilt import create_react_agent
9089
9190# Third-party - Pydantic for validation
@@ -173,12 +172,13 @@ class AgentResponse(BaseModel):
173172
174173
175174# ============================================================================
176- # CONFIGURATION - OpenAI settings
175+ # CONFIGURATION - LLM settings (LiteLLM default, OpenAI optional)
177176# ============================================================================
178177
179178OPENAI_API_KEY = os .getenv ("OPENAI_API_KEY" , "" )
180179OPENAI_MODEL = os .getenv ("OPENAI_MODEL" , "gpt-4o-mini" )
181180OPENAI_BASE_URL = os .getenv ("OPENAI_BASE_URL" , "" ) # optional override
181+ LITELLM_MODEL = os .getenv ("LITELLM_MODEL" , "github_copilot/gpt-4o" )
182182OPENAI_CALL_LOGGING_ENABLED = _env_flag ("OPENAI_CALL_LOGGING_ENABLED" , "true" )
183183AGENT_EFFICIENCY_MODE = _env_flag ("AGENT_EFFICIENCY_MODE" , "true" )
184184AGENT_TRACE_ENABLED = _env_flag ("AGENT_TRACE_ENABLED" , "false" )
@@ -409,28 +409,27 @@ class AgentService:
409409
410410 def __init__ (self ):
411411 """
412- Initialize the agent service with OpenAI .
412+ Initialize the agent service.
413413
414- Validates that required environment variables are set and creates
415- the LLM client for agent execution.
416-
417- Raises:
418- ValueError: If OpenAI configuration is incomplete
414+ Uses OpenAI when OPENAI_API_KEY is set, otherwise falls back
415+ to LiteLLM (supports GitHub Copilot, Ollama, etc.).
419416 """
420- # Validate configuration
421- if not OPENAI_API_KEY :
422- raise ValueError (
423- "OpenAI API key not set. "
424- "Please set OPENAI_API_KEY environment variable."
417+ if OPENAI_API_KEY :
418+ from langchain_openai import ChatOpenAI
419+ self .llm = ChatOpenAI (
420+ model = OPENAI_MODEL ,
421+ api_key = OPENAI_API_KEY ,
422+ base_url = OPENAI_BASE_URL or None ,
423+ temperature = 0.0 ,
425424 )
426-
427- # Initialize ChatOpenAI
428- self . llm = ChatOpenAI (
429- model = OPENAI_MODEL ,
430- api_key = OPENAI_API_KEY ,
431- base_url = OPENAI_BASE_URL or None ,
432- temperature = 0.0 ,
433- )
425+ logger . info ( f"AgentService using OpenAI: { OPENAI_MODEL } " )
426+ else :
427+ from langchain_litellm import ChatLiteLLM
428+ self . llm = ChatLiteLLM (
429+ model = LITELLM_MODEL ,
430+ temperature = 0.0 ,
431+ )
432+ logger . info ( f"AgentService using LiteLLM: { LITELLM_MODEL } " )
434433
435434 # CSV tools only (do not expose operations or external MCP)
436435 self .tools = self ._build_csv_tools ()
@@ -561,6 +560,27 @@ def _csv_ticket_fields() -> str:
561560 from tickets import Ticket
562561 return json .dumps (list (Ticket .model_fields .keys ()))
563562
563+ def _csv_ticket_stats () -> str :
564+ """Get aggregated statistics for CSV tickets."""
565+ from collections import Counter
566+ tickets = service .list_tickets ()
567+ by_status = Counter (t .status .value for t in tickets )
568+ by_priority = Counter (t .priority .value for t in tickets )
569+ by_group = Counter (t .assigned_group for t in tickets if t .assigned_group )
570+ unassigned = sum (1 for t in tickets if t .assignee is None and t .assigned_group is not None )
571+ return json .dumps ({
572+ "total" : len (tickets ), "unassigned" : unassigned ,
573+ "by_status" : dict (by_status ), "by_priority" : dict (by_priority ),
574+ "by_group" : dict (by_group .most_common (10 )),
575+ }, default = str )
576+
577+ def _csv_sla_breach_tickets (unassigned_only : bool = True , include_ok : bool = False ) -> str :
578+ """Return tickets at SLA breach risk with pre-computed age and status."""
579+ from tickets import get_sla_breach_report
580+ tickets = service .list_tickets (has_assignee = False if unassigned_only else None )
581+ report = get_sla_breach_report (tickets , reference_time = None , include_ok = include_ok )
582+ return json .dumps (report .model_dump () if hasattr (report , 'model_dump' ) else report , default = str )
583+
564584 return [
565585 StructuredTool .from_function (
566586 func = _csv_list_tickets ,
@@ -604,6 +624,21 @@ def _csv_ticket_fields() -> str:
604624 name = "csv_ticket_fields" ,
605625 description = "List available ticket fields (schema) as JSON array of field names." ,
606626 ),
627+ StructuredTool .from_function (
628+ func = _csv_ticket_stats ,
629+ name = "csv_ticket_stats" ,
630+ description = "Get aggregated statistics: total, unassigned, by_status, by_priority, by_group. Returns JSON." ,
631+ ),
632+ StructuredTool .from_function (
633+ func = _csv_sla_breach_tickets ,
634+ name = "csv_sla_breach_tickets" ,
635+ description = (
636+ "Return tickets at SLA breach risk. Pre-computed age_hours, sla_threshold_hours, breach_status. "
637+ "SLA thresholds: critical=4h, high=24h, medium=72h, low=120h. "
638+ "unassigned_only (default true) filters to group-assigned but no individual. "
639+ "include_ok (default false) adds tickets within SLA window. Returns JSON."
640+ ),
641+ ),
607642 ]
608643
609644 async def run_agent (self , request : AgentRequest ) -> AgentResponse :
0 commit comments