solana-agent 11.3.0__py3-none-any.whl → 12.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solana_agent/ai.py +989 -564
- {solana_agent-11.3.0.dist-info → solana_agent-12.0.0.dist-info}/METADATA +8 -56
- solana_agent-12.0.0.dist-info/RECORD +6 -0
- solana_agent-11.3.0.dist-info/RECORD +0 -6
- {solana_agent-11.3.0.dist-info → solana_agent-12.0.0.dist-info}/LICENSE +0 -0
- {solana_agent-11.3.0.dist-info → solana_agent-12.0.0.dist-info}/WHEEL +0 -0
solana_agent/ai.py
CHANGED
@@ -12,6 +12,7 @@ This module implements a clean architecture approach with:
|
|
12
12
|
|
13
13
|
import asyncio
|
14
14
|
import datetime
|
15
|
+
import importlib
|
15
16
|
import json
|
16
17
|
import re
|
17
18
|
import traceback
|
@@ -30,7 +31,7 @@ from typing import (
|
|
30
31
|
Any,
|
31
32
|
Type,
|
32
33
|
)
|
33
|
-
from pydantic import BaseModel, Field
|
34
|
+
from pydantic import BaseModel, Field, ValidationError
|
34
35
|
from pymongo import MongoClient
|
35
36
|
from openai import OpenAI
|
36
37
|
import pymongo
|
@@ -45,6 +46,73 @@ from abc import ABC, abstractmethod
|
|
45
46
|
# DOMAIN MODELS
|
46
47
|
#############################################
|
47
48
|
|
49
|
+
class ToolCallModel(BaseModel):
|
50
|
+
"""Model for tool calls in agent responses."""
|
51
|
+
name: str = Field(..., description="Name of the tool to execute")
|
52
|
+
parameters: Dict[str, Any] = Field(
|
53
|
+
default_factory=dict, description="Parameters for the tool")
|
54
|
+
|
55
|
+
|
56
|
+
class ToolInstructionModel(BaseModel):
|
57
|
+
"""Model for tool usage instructions in system prompts."""
|
58
|
+
available_tools: List[Dict[str, Any]] = Field(default_factory=list,
|
59
|
+
description="Tools available to this agent")
|
60
|
+
example_tool: str = Field("search_internet",
|
61
|
+
description="Example tool to use in instructions")
|
62
|
+
example_query: str = Field("latest Solana news",
|
63
|
+
description="Example query to use in instructions")
|
64
|
+
valid_agents: List[str] = Field(default_factory=list,
|
65
|
+
description="List of valid agents for handoff")
|
66
|
+
|
67
|
+
def format_instructions(self) -> str:
|
68
|
+
"""Format the tool and handoff instructions using plain text delimiters."""
|
69
|
+
tools_json = json.dumps(self.available_tools, indent=2)
|
70
|
+
|
71
|
+
# Tool usage instructions with plain text delimiters
|
72
|
+
tool_instructions = f"""
|
73
|
+
You have access to the following tools:
|
74
|
+
{tools_json}
|
75
|
+
|
76
|
+
IMPORTANT - TOOL USAGE: When you need to use a tool, respond with JSON using these exact plain text delimiters:
|
77
|
+
|
78
|
+
TOOL_START
|
79
|
+
{{
|
80
|
+
"name": "tool_name",
|
81
|
+
"parameters": {{
|
82
|
+
"param1": "value1",
|
83
|
+
"param2": "value2"
|
84
|
+
}}
|
85
|
+
}}
|
86
|
+
TOOL_END
|
87
|
+
|
88
|
+
Example: To search the internet for "{self.example_query}", respond with:
|
89
|
+
|
90
|
+
TOOL_START
|
91
|
+
{{
|
92
|
+
"name": "{self.example_tool}",
|
93
|
+
"parameters": {{
|
94
|
+
"query": "{self.example_query}"
|
95
|
+
}}
|
96
|
+
}}
|
97
|
+
TOOL_END
|
98
|
+
|
99
|
+
ALWAYS use the search_internet tool when the user asks for current information or facts that might be beyond your knowledge cutoff. DO NOT attempt to handoff for information that could be obtained using search_internet.
|
100
|
+
"""
|
101
|
+
|
102
|
+
# Handoff instructions if valid agents are provided
|
103
|
+
handoff_instructions = ""
|
104
|
+
if self.valid_agents:
|
105
|
+
handoff_instructions = f"""
|
106
|
+
IMPORTANT - HANDOFFS: You can ONLY hand off to these existing agents: {", ".join(self.valid_agents)}
|
107
|
+
DO NOT invent or reference agents that don't exist in this list.
|
108
|
+
|
109
|
+
To hand off to another agent, use this format:
|
110
|
+
{{"handoff": {{"target_agent": "<AGENT_NAME_FROM_LIST_ABOVE>", "reason": "detailed reason for handoff"}}}}
|
111
|
+
"""
|
112
|
+
|
113
|
+
return f"{tool_instructions}\n\n{handoff_instructions}"
|
114
|
+
|
115
|
+
|
48
116
|
class AgentType(str, Enum):
|
49
117
|
"""Type of agent (AI or Human)."""
|
50
118
|
AI = "ai"
|
@@ -742,8 +810,13 @@ class MemoryRepository(Protocol):
|
|
742
810
|
class AgentRegistry(Protocol):
|
743
811
|
"""Interface for agent management."""
|
744
812
|
|
745
|
-
def register_ai_agent(
|
746
|
-
|
813
|
+
def register_ai_agent(
|
814
|
+
self,
|
815
|
+
name: str,
|
816
|
+
instructions: str,
|
817
|
+
specialization: str,
|
818
|
+
model: str = "gpt-4o-mini",
|
819
|
+
) -> None: ...
|
747
820
|
|
748
821
|
def register_human_agent(
|
749
822
|
self,
|
@@ -1010,6 +1083,7 @@ class OpenAIAdapter:
|
|
1010
1083
|
messages.append({"role": "user", "content": prompt})
|
1011
1084
|
|
1012
1085
|
try:
|
1086
|
+
# First try the beta parsing API
|
1013
1087
|
completion = self.client.beta.chat.completions.parse(
|
1014
1088
|
model=kwargs.get("model", self.model),
|
1015
1089
|
messages=messages,
|
@@ -1018,7 +1092,25 @@ class OpenAIAdapter:
|
|
1018
1092
|
)
|
1019
1093
|
return completion.choices[0].message.parsed
|
1020
1094
|
except Exception as e:
|
1021
|
-
print(f"Error
|
1095
|
+
print(f"Error with beta.parse method: {e}")
|
1096
|
+
|
1097
|
+
# Fallback to manual parsing with Pydantic
|
1098
|
+
try:
|
1099
|
+
response = self.client.chat.completions.create(
|
1100
|
+
model=kwargs.get("model", self.model),
|
1101
|
+
messages=messages,
|
1102
|
+
temperature=kwargs.get("temperature", 0.2),
|
1103
|
+
response_format={"type": "json_object"},
|
1104
|
+
)
|
1105
|
+
response_text = response.choices[0].message.content
|
1106
|
+
|
1107
|
+
if response_text:
|
1108
|
+
# Use Pydantic's parse_raw method instead of json.loads
|
1109
|
+
return model_class.parse_raw(response_text)
|
1110
|
+
|
1111
|
+
except Exception as e:
|
1112
|
+
print(f"Error parsing structured output with Pydantic: {e}")
|
1113
|
+
|
1022
1114
|
# Return default instance as fallback
|
1023
1115
|
return model_class()
|
1024
1116
|
|
@@ -1492,16 +1584,26 @@ class MongoAIAgentRegistry:
|
|
1492
1584
|
}
|
1493
1585
|
|
1494
1586
|
def delete_agent(self, name: str) -> bool:
|
1495
|
-
"""Delete an AI agent
|
1587
|
+
"""Delete an AI agent from the registry."""
|
1588
|
+
# First check if agent exists
|
1496
1589
|
if name not in self.ai_agents_cache:
|
1497
1590
|
return False
|
1498
1591
|
|
1499
|
-
#
|
1500
|
-
|
1592
|
+
# Delete from database
|
1593
|
+
result = self.db.delete_one(
|
1594
|
+
self.collection,
|
1595
|
+
{"name": name}
|
1596
|
+
)
|
1501
1597
|
|
1502
|
-
#
|
1503
|
-
|
1504
|
-
|
1598
|
+
# Delete from cache if database deletion was successful
|
1599
|
+
if result:
|
1600
|
+
if name in self.ai_agents_cache:
|
1601
|
+
del self.ai_agents_cache[name]
|
1602
|
+
print(f"Agent {name} successfully deleted from MongoDB and cache")
|
1603
|
+
return True
|
1604
|
+
else:
|
1605
|
+
print(f"Failed to delete agent {name} from MongoDB")
|
1606
|
+
return False
|
1505
1607
|
|
1506
1608
|
|
1507
1609
|
class MongoHumanAgentRegistry:
|
@@ -1688,13 +1790,13 @@ class MongoTicketRepository:
|
|
1688
1790
|
"""Count tickets matching query."""
|
1689
1791
|
return self.db.count_documents(self.collection, query)
|
1690
1792
|
|
1691
|
-
def find_stalled_tickets(self, cutoff_time, statuses):
|
1793
|
+
async def find_stalled_tickets(self, cutoff_time, statuses):
|
1692
1794
|
"""Find tickets that haven't been updated since the cutoff time."""
|
1693
1795
|
query = {
|
1694
1796
|
"status": {"$in": [status.value if isinstance(status, Enum) else status for status in statuses]},
|
1695
1797
|
"updated_at": {"$lt": cutoff_time}
|
1696
1798
|
}
|
1697
|
-
tickets = self.
|
1799
|
+
tickets = self.db.find("tickets", query)
|
1698
1800
|
return [Ticket(**ticket) for ticket in tickets]
|
1699
1801
|
|
1700
1802
|
|
@@ -2279,79 +2381,91 @@ class RoutingService:
|
|
2279
2381
|
self.router_model = router_model
|
2280
2382
|
|
2281
2383
|
async def route_query(self, query: str) -> str:
|
2282
|
-
"""Route query to the
|
2384
|
+
"""Route a query to the appropriate agent based on content."""
|
2385
|
+
# Get available agents
|
2386
|
+
agents = self.agent_registry.get_all_ai_agents()
|
2387
|
+
if not agents:
|
2388
|
+
return "default_agent" # Fallback to default if no agents
|
2389
|
+
|
2390
|
+
agent_names = list(agents.keys())
|
2391
|
+
|
2392
|
+
# Format agent descriptions for prompt
|
2393
|
+
agent_descriptions = []
|
2283
2394
|
specializations = self.agent_registry.get_specializations()
|
2284
|
-
# Get AI-only specializations
|
2285
|
-
ai_specialists = {
|
2286
|
-
k: v
|
2287
|
-
for k, v in specializations.items()
|
2288
|
-
if k in self.agent_registry.get_all_ai_agents()
|
2289
|
-
}
|
2290
2395
|
|
2291
|
-
|
2396
|
+
for name in agent_names:
|
2397
|
+
spec = specializations.get(name, "General assistant")
|
2398
|
+
agent_descriptions.append(f"- {name}: {spec}")
|
2399
|
+
|
2400
|
+
agent_info = "\n".join(agent_descriptions)
|
2401
|
+
|
2402
|
+
# Create prompt for routing
|
2292
2403
|
prompt = f"""
|
2293
|
-
|
2404
|
+
You are a router that determines which AI agent should handle a user query.
|
2294
2405
|
|
2295
2406
|
User query: "{query}"
|
2296
2407
|
|
2297
|
-
Available
|
2298
|
-
{
|
2408
|
+
Available agents:
|
2409
|
+
{agent_info}
|
2299
2410
|
|
2300
|
-
|
2301
|
-
|
2302
|
-
2. Return EXACTLY ONE specialist name from the available list.
|
2303
|
-
3. Do not invent new specialist names.
|
2411
|
+
Select the most appropriate agent based on the query and agent specializations.
|
2412
|
+
Respond with ONLY the agent name, nothing else.
|
2304
2413
|
"""
|
2305
2414
|
|
2306
|
-
|
2307
|
-
response = ""
|
2308
|
-
async for chunk in self.llm_provider.generate_text(
|
2309
|
-
"router",
|
2310
|
-
prompt,
|
2311
|
-
system_prompt="You are a routing system that matches queries to the best specialist.",
|
2312
|
-
stream=False,
|
2313
|
-
model=self.router_model,
|
2314
|
-
temperature=0.2,
|
2315
|
-
response_format={"type": "json_object"},
|
2316
|
-
):
|
2317
|
-
response += chunk
|
2318
|
-
|
2415
|
+
response_text = ""
|
2319
2416
|
try:
|
2320
|
-
|
2321
|
-
|
2417
|
+
async for chunk in self.llm_provider.generate_text(
|
2418
|
+
"system",
|
2419
|
+
prompt,
|
2420
|
+
system_prompt="You are a routing system. Only respond with the name of the most appropriate agent.",
|
2421
|
+
model=self.router_model,
|
2422
|
+
temperature=0.1,
|
2423
|
+
):
|
2424
|
+
response_text += chunk
|
2322
2425
|
|
2323
|
-
#
|
2324
|
-
|
2325
|
-
|
2326
|
-
|
2327
|
-
|
2328
|
-
|
2329
|
-
|
2426
|
+
# Clean up the response text to handle different formats
|
2427
|
+
response_text = response_text.strip()
|
2428
|
+
|
2429
|
+
# First try to parse as JSON (old behavior)
|
2430
|
+
try:
|
2431
|
+
data = json.loads(response_text)
|
2432
|
+
if isinstance(data, dict) and "agent" in data:
|
2433
|
+
return self._match_agent_name(data["agent"], agent_names)
|
2434
|
+
except json.JSONDecodeError:
|
2435
|
+
# Not JSON, try to parse as plain text
|
2436
|
+
pass
|
2437
|
+
|
2438
|
+
# Treat as plain text - just match the agent name directly
|
2439
|
+
return self._match_agent_name(response_text, agent_names)
|
2330
2440
|
|
2331
|
-
return agent_name
|
2332
2441
|
except Exception as e:
|
2333
|
-
print(f"Error
|
2334
|
-
#
|
2335
|
-
return
|
2442
|
+
print(f"Error in routing: {e}")
|
2443
|
+
# Default to the first agent if there's an error
|
2444
|
+
return agent_names[0]
|
2336
2445
|
|
2337
2446
|
def _match_agent_name(self, response: str, agent_names: List[str]) -> str:
|
2338
|
-
"""Match
|
2339
|
-
#
|
2340
|
-
if response in
|
2341
|
-
|
2447
|
+
"""Match the response to a valid agent name."""
|
2448
|
+
# Clean up the response
|
2449
|
+
if isinstance(response, dict) and "name" in response:
|
2450
|
+
response = response["name"] # Handle {"name": "agent_name"} format
|
2342
2451
|
|
2343
|
-
#
|
2452
|
+
# Convert to string and clean it up
|
2453
|
+
clean_response = str(response).strip().lower()
|
2454
|
+
|
2455
|
+
# Direct match first
|
2344
2456
|
for name in agent_names:
|
2345
|
-
if name.lower() ==
|
2457
|
+
if name.lower() == clean_response:
|
2346
2458
|
return name
|
2347
2459
|
|
2348
|
-
#
|
2460
|
+
# Check for partial matches
|
2349
2461
|
for name in agent_names:
|
2350
|
-
if name.lower() in
|
2462
|
+
if name.lower() in clean_response or clean_response in name.lower():
|
2351
2463
|
return name
|
2352
2464
|
|
2353
|
-
#
|
2354
|
-
|
2465
|
+
# If no match, return first agent as default
|
2466
|
+
print(
|
2467
|
+
f"No matching agent found for: '{response}'. Using {agent_names[0]}")
|
2468
|
+
return agent_names[0]
|
2355
2469
|
|
2356
2470
|
|
2357
2471
|
class TicketService:
|
@@ -2672,21 +2786,14 @@ class CriticService:
|
|
2672
2786
|
class NotificationService:
|
2673
2787
|
"""Service for sending notifications to human agents or users using notification plugins."""
|
2674
2788
|
|
2675
|
-
def __init__(self, human_agent_registry: MongoHumanAgentRegistry):
|
2789
|
+
def __init__(self, human_agent_registry: MongoHumanAgentRegistry, tool_registry=None):
|
2676
2790
|
"""Initialize the notification service with a human agent registry."""
|
2677
2791
|
self.human_agent_registry = human_agent_registry
|
2792
|
+
self.tool_registry = tool_registry
|
2678
2793
|
|
2679
2794
|
def send_notification(self, recipient_id: str, message: str, metadata: Dict[str, Any] = None) -> bool:
|
2680
2795
|
"""
|
2681
2796
|
Send a notification to a human agent using configured notification channels or legacy handler.
|
2682
|
-
|
2683
|
-
Args:
|
2684
|
-
recipient_id: ID of the human agent to notify
|
2685
|
-
message: Notification message content
|
2686
|
-
metadata: Additional data related to the notification (e.g., ticket_id)
|
2687
|
-
|
2688
|
-
Returns:
|
2689
|
-
True if notification was sent, False otherwise
|
2690
2797
|
"""
|
2691
2798
|
# Get human agent information
|
2692
2799
|
agent = self.human_agent_registry.get_human_agent(recipient_id)
|
@@ -2712,6 +2819,11 @@ class NotificationService:
|
|
2712
2819
|
f"No notification channels configured for agent {recipient_id}")
|
2713
2820
|
return False
|
2714
2821
|
|
2822
|
+
# No tool registry available
|
2823
|
+
if not self.tool_registry:
|
2824
|
+
print("No tool registry available for notifications")
|
2825
|
+
return False
|
2826
|
+
|
2715
2827
|
# Try each notification channel until one succeeds
|
2716
2828
|
success = False
|
2717
2829
|
for channel in notification_channels:
|
@@ -2728,9 +2840,9 @@ class NotificationService:
|
|
2728
2840
|
if metadata:
|
2729
2841
|
tool_params["metadata"] = metadata
|
2730
2842
|
|
2731
|
-
|
2732
|
-
if
|
2733
|
-
response =
|
2843
|
+
tool = self.tool_registry.get_tool(f"notify_{channel_type}")
|
2844
|
+
if tool:
|
2845
|
+
response = tool.execute(**tool_params)
|
2734
2846
|
if response.get("status") == "success":
|
2735
2847
|
success = True
|
2736
2848
|
break
|
@@ -2763,11 +2875,15 @@ class AgentService:
|
|
2763
2875
|
human_agent_registry: Optional[MongoHumanAgentRegistry] = None,
|
2764
2876
|
ai_agent_registry: Optional[MongoAIAgentRegistry] = None,
|
2765
2877
|
organization_mission: Optional[OrganizationMission] = None,
|
2878
|
+
config: Optional[Dict[str, Any]] = None
|
2766
2879
|
):
|
2880
|
+
"""Initialize the agent service with LLM provider and optional registries."""
|
2767
2881
|
self.llm_provider = llm_provider
|
2768
2882
|
self.human_agent_registry = human_agent_registry
|
2769
2883
|
self.ai_agent_registry = ai_agent_registry
|
2770
2884
|
self.organization_mission = organization_mission
|
2885
|
+
self.config = config or {}
|
2886
|
+
self._last_handoff = None
|
2771
2887
|
|
2772
2888
|
# For backward compatibility
|
2773
2889
|
self.ai_agents = {}
|
@@ -2776,9 +2892,22 @@ class AgentService:
|
|
2776
2892
|
|
2777
2893
|
self.specializations = {}
|
2778
2894
|
|
2779
|
-
#
|
2780
|
-
self.
|
2781
|
-
self.plugin_manager
|
2895
|
+
# Create our tool registry and plugin manager
|
2896
|
+
self.tool_registry = ToolRegistry()
|
2897
|
+
self.plugin_manager = PluginManager(
|
2898
|
+
config=self.config, tool_registry=self.tool_registry)
|
2899
|
+
|
2900
|
+
# Load plugins
|
2901
|
+
loaded_count = self.plugin_manager.load_all_plugins()
|
2902
|
+
print(
|
2903
|
+
f"Loaded {loaded_count} plugins with {len(self.tool_registry.list_all_tools())} registered tools")
|
2904
|
+
|
2905
|
+
# Configure all tools with our config after loading
|
2906
|
+
self.tool_registry.configure_all_tools(self.config)
|
2907
|
+
|
2908
|
+
# Debug output of registered tools
|
2909
|
+
print(
|
2910
|
+
f"Available tools after initialization: {self.tool_registry.list_all_tools()}")
|
2782
2911
|
|
2783
2912
|
# If human agent registry is provided, initialize specializations from it
|
2784
2913
|
if self.human_agent_registry:
|
@@ -2794,6 +2923,205 @@ class AgentService:
|
|
2794
2923
|
if not self.human_agent_registry:
|
2795
2924
|
self.human_agents = {}
|
2796
2925
|
|
2926
|
+
def get_all_ai_agents(self) -> Dict[str, Any]:
|
2927
|
+
"""Get all registered AI agents."""
|
2928
|
+
return self.ai_agents
|
2929
|
+
|
2930
|
+
def get_agent_tools(self, agent_name: str) -> List[Dict[str, Any]]:
|
2931
|
+
"""Get all tools available to a specific agent."""
|
2932
|
+
return self.tool_registry.get_agent_tools(agent_name)
|
2933
|
+
|
2934
|
+
def register_tool_for_agent(self, agent_name: str, tool_name: str) -> None:
|
2935
|
+
"""Give an agent access to a specific tool."""
|
2936
|
+
# Make sure the tool exists
|
2937
|
+
if tool_name not in self.tool_registry.list_all_tools():
|
2938
|
+
print(
|
2939
|
+
f"Error registering tool {tool_name} for agent {agent_name}: Tool not registered")
|
2940
|
+
raise ValueError(f"Tool {tool_name} is not registered")
|
2941
|
+
|
2942
|
+
# Check if agent exists
|
2943
|
+
if agent_name not in self.ai_agents and (
|
2944
|
+
not self.ai_agent_registry or
|
2945
|
+
not self.ai_agent_registry.get_ai_agent(agent_name)
|
2946
|
+
):
|
2947
|
+
print(
|
2948
|
+
f"Warning: Agent {agent_name} not found but attempting to register tool")
|
2949
|
+
|
2950
|
+
# Assign the tool to the agent
|
2951
|
+
success = self.tool_registry.assign_tool_to_agent(
|
2952
|
+
agent_name, tool_name)
|
2953
|
+
|
2954
|
+
if success:
|
2955
|
+
print(
|
2956
|
+
f"Successfully registered tool {tool_name} for agent {agent_name}")
|
2957
|
+
else:
|
2958
|
+
print(
|
2959
|
+
f"Failed to register tool {tool_name} for agent {agent_name}")
|
2960
|
+
|
2961
|
+
def process_json_response(self, response_text: str, agent_name: str) -> str:
|
2962
|
+
"""Process a complete response to handle any JSON handoffs or tool calls."""
|
2963
|
+
# Check if the response is a JSON object for handoff or tool call
|
2964
|
+
if response_text.strip().startswith('{') and ('"handoff":' in response_text or '"tool_call":' in response_text):
|
2965
|
+
try:
|
2966
|
+
data = json.loads(response_text.strip())
|
2967
|
+
|
2968
|
+
# Handle handoff
|
2969
|
+
if "handoff" in data:
|
2970
|
+
target_agent = data["handoff"].get(
|
2971
|
+
"target_agent", "another agent")
|
2972
|
+
reason = data["handoff"].get(
|
2973
|
+
"reason", "to better assist with your request")
|
2974
|
+
return f"I'll connect you with {target_agent} who can better assist with your request. Reason: {reason}"
|
2975
|
+
|
2976
|
+
# Handle tool call
|
2977
|
+
if "tool_call" in data:
|
2978
|
+
tool_data = data["tool_call"]
|
2979
|
+
tool_name = tool_data.get("name")
|
2980
|
+
parameters = tool_data.get("parameters", {})
|
2981
|
+
|
2982
|
+
if tool_name:
|
2983
|
+
try:
|
2984
|
+
# Execute the tool
|
2985
|
+
tool_result = self.execute_tool(
|
2986
|
+
agent_name, tool_name, parameters)
|
2987
|
+
|
2988
|
+
# Format the result
|
2989
|
+
if tool_result.get("status") == "success":
|
2990
|
+
return f"I searched for information and found:\n\n{tool_result.get('result', '')}"
|
2991
|
+
else:
|
2992
|
+
return f"I tried to search for information, but encountered an error: {tool_result.get('message', 'Unknown error')}"
|
2993
|
+
except Exception as e:
|
2994
|
+
return f"I tried to use {tool_name}, but encountered an error: {str(e)}"
|
2995
|
+
except json.JSONDecodeError:
|
2996
|
+
# Not valid JSON
|
2997
|
+
pass
|
2998
|
+
|
2999
|
+
# Return original if not JSON or if processing fails
|
3000
|
+
return response_text
|
3001
|
+
|
3002
|
+
def get_agent_system_prompt(self, agent_name: str) -> str:
|
3003
|
+
"""Get the system prompt for an agent, including tool instructions if available."""
|
3004
|
+
# Get the agent's base instructions
|
3005
|
+
if agent_name not in self.ai_agents:
|
3006
|
+
raise ValueError(f"Agent {agent_name} not found")
|
3007
|
+
|
3008
|
+
agent_config = self.ai_agents[agent_name]
|
3009
|
+
instructions = agent_config.get("instructions", "")
|
3010
|
+
|
3011
|
+
# Add tool instructions if any tools are available
|
3012
|
+
available_tools = self.get_agent_tools(agent_name)
|
3013
|
+
if available_tools:
|
3014
|
+
tools_json = json.dumps(available_tools, indent=2)
|
3015
|
+
|
3016
|
+
# Tool instructions using JSON format similar to handoffs
|
3017
|
+
tool_instructions = f"""
|
3018
|
+
You have access to the following tools:
|
3019
|
+
{tools_json}
|
3020
|
+
|
3021
|
+
IMPORTANT - TOOL USAGE: When you need to use a tool, respond with a JSON object using this format:
|
3022
|
+
|
3023
|
+
{{
|
3024
|
+
"tool_call": {{
|
3025
|
+
"name": "tool_name",
|
3026
|
+
"parameters": {{
|
3027
|
+
"param1": "value1",
|
3028
|
+
"param2": "value2"
|
3029
|
+
}}
|
3030
|
+
}}
|
3031
|
+
}}
|
3032
|
+
|
3033
|
+
Example: To search the internet for "latest Solana news", respond with:
|
3034
|
+
|
3035
|
+
{{
|
3036
|
+
"tool_call": {{
|
3037
|
+
"name": "search_internet",
|
3038
|
+
"parameters": {{
|
3039
|
+
"query": "latest Solana news"
|
3040
|
+
}}
|
3041
|
+
}}
|
3042
|
+
}}
|
3043
|
+
|
3044
|
+
ALWAYS use the search_internet tool when the user asks for current information or facts that might be beyond your knowledge cutoff. DO NOT attempt to handoff for information that could be obtained using search_internet.
|
3045
|
+
"""
|
3046
|
+
instructions = f"{instructions}\n\n{tool_instructions}"
|
3047
|
+
|
3048
|
+
# Add specific instructions about valid handoff agents
|
3049
|
+
valid_agents = list(self.ai_agents.keys())
|
3050
|
+
if valid_agents:
|
3051
|
+
handoff_instructions = f"""
|
3052
|
+
IMPORTANT - HANDOFFS: You can ONLY hand off to these existing agents: {', '.join(valid_agents)}
|
3053
|
+
DO NOT invent or reference agents that don't exist in this list.
|
3054
|
+
|
3055
|
+
To hand off to another agent, use this format:
|
3056
|
+
{{"handoff": {{"target_agent": "<AGENT_NAME_FROM_LIST_ABOVE>", "reason": "detailed reason for handoff"}}}}
|
3057
|
+
"""
|
3058
|
+
instructions = f"{instructions}\n\n{handoff_instructions}"
|
3059
|
+
|
3060
|
+
return instructions
|
3061
|
+
|
3062
|
+
def process_tool_calls(self, agent_name: str, response_text: str) -> str:
|
3063
|
+
"""Process any tool calls in the agent's response and return updated response."""
|
3064
|
+
# Regex to find tool calls in the format TOOL_START {...} TOOL_END
|
3065
|
+
tool_pattern = r"TOOL_START\s*([\s\S]*?)\s*TOOL_END"
|
3066
|
+
tool_matches = re.findall(tool_pattern, response_text)
|
3067
|
+
|
3068
|
+
if not tool_matches:
|
3069
|
+
return response_text
|
3070
|
+
|
3071
|
+
print(
|
3072
|
+
f"Found {len(tool_matches)} tool calls in response from {agent_name}")
|
3073
|
+
|
3074
|
+
# Process each tool call
|
3075
|
+
modified_response = response_text
|
3076
|
+
for tool_json in tool_matches:
|
3077
|
+
try:
|
3078
|
+
# Parse the tool call JSON
|
3079
|
+
tool_call_text = tool_json.strip()
|
3080
|
+
print(f"Processing tool call: {tool_call_text[:100]}")
|
3081
|
+
|
3082
|
+
# Parse the JSON (handle both normal and stringified JSON)
|
3083
|
+
try:
|
3084
|
+
tool_call = json.loads(tool_call_text)
|
3085
|
+
except json.JSONDecodeError as e:
|
3086
|
+
# If there are escaped quotes or formatting issues, try cleaning it up
|
3087
|
+
cleaned_json = tool_call_text.replace(
|
3088
|
+
'\\"', '"').replace('\\n', '\n')
|
3089
|
+
tool_call = json.loads(cleaned_json)
|
3090
|
+
|
3091
|
+
tool_name = tool_call.get("name")
|
3092
|
+
parameters = tool_call.get("parameters", {})
|
3093
|
+
|
3094
|
+
if tool_name:
|
3095
|
+
# Execute the tool
|
3096
|
+
print(
|
3097
|
+
f"Executing tool {tool_name} with parameters: {parameters}")
|
3098
|
+
tool_result = self.execute_tool(
|
3099
|
+
agent_name, tool_name, parameters)
|
3100
|
+
|
3101
|
+
# Format the result for inclusion in the response
|
3102
|
+
if tool_result.get("status") == "success":
|
3103
|
+
formatted_result = f"\n\nI searched for information and found:\n\n{tool_result.get('result', '')}"
|
3104
|
+
else:
|
3105
|
+
formatted_result = f"\n\nI tried to search for information, but encountered an error: {tool_result.get('message', 'Unknown error')}"
|
3106
|
+
|
3107
|
+
# Replace the entire tool block with the result
|
3108
|
+
full_tool_block = f"TOOL_START\n{tool_json}\nTOOL_END"
|
3109
|
+
modified_response = modified_response.replace(
|
3110
|
+
full_tool_block, formatted_result)
|
3111
|
+
print(f"Successfully processed tool call: {tool_name}")
|
3112
|
+
except Exception as e:
|
3113
|
+
print(f"Error processing tool call: {str(e)}")
|
3114
|
+
# Replace with error message
|
3115
|
+
full_tool_block = f"TOOL_START\n{tool_json}\nTOOL_END"
|
3116
|
+
modified_response = modified_response.replace(
|
3117
|
+
full_tool_block, "\n\nI tried to search for information, but encountered an error processing the tool call.")
|
3118
|
+
|
3119
|
+
return modified_response
|
3120
|
+
|
3121
|
+
def get_agent_tools(self, agent_name: str) -> List[Dict[str, Any]]:
|
3122
|
+
"""Get all tools available to a specific agent."""
|
3123
|
+
return self.tool_registry.get_agent_tools(agent_name)
|
3124
|
+
|
2797
3125
|
def register_ai_agent(
|
2798
3126
|
self,
|
2799
3127
|
name: str,
|
@@ -2819,7 +3147,7 @@ class AgentService:
|
|
2819
3147
|
# Use registry if available
|
2820
3148
|
if self.ai_agent_registry:
|
2821
3149
|
self.ai_agent_registry.register_ai_agent(
|
2822
|
-
name, full_instructions, specialization, model
|
3150
|
+
name, full_instructions, specialization, model,
|
2823
3151
|
)
|
2824
3152
|
# Update local cache for backward compatibility
|
2825
3153
|
self.ai_agents = self.ai_agent_registry.get_all_ai_agents()
|
@@ -2839,32 +3167,37 @@ class AgentService:
|
|
2839
3167
|
return merged
|
2840
3168
|
return self.specializations
|
2841
3169
|
|
2842
|
-
def
|
2843
|
-
"""
|
2844
|
-
|
2845
|
-
|
2846
|
-
|
2847
|
-
tool_registry.assign_tool_to_agent(agent_name, tool_name)
|
3170
|
+
def execute_tool(self, agent_name: str, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
3171
|
+
"""Execute a tool on behalf of an agent."""
|
3172
|
+
print(f"Executing tool {tool_name} for agent {agent_name}")
|
3173
|
+
print(f"Parameters: {parameters}")
|
2848
3174
|
|
2849
|
-
|
2850
|
-
|
2851
|
-
|
3175
|
+
# Get the tool directly from the registry
|
3176
|
+
tool = self.tool_registry.get_tool(tool_name)
|
3177
|
+
if not tool:
|
3178
|
+
print(
|
3179
|
+
f"Tool {tool_name} not found in registry. Available tools: {self.tool_registry.list_all_tools()}")
|
3180
|
+
return {"status": "error", "message": f"Tool {tool_name} not found"}
|
2852
3181
|
|
2853
|
-
|
2854
|
-
self
|
2855
|
-
|
2856
|
-
"""Execute a tool on behalf of an agent."""
|
2857
|
-
# Check if agent has access to this tool
|
2858
|
-
agent_tools = tool_registry.get_agent_tools(agent_name)
|
2859
|
-
tool_names = [tool["name"] for tool in agent_tools]
|
3182
|
+
# Check if agent has access
|
3183
|
+
agent_tools = self.get_agent_tools(agent_name)
|
3184
|
+
tool_names = [t["name"] for t in agent_tools]
|
2860
3185
|
|
2861
3186
|
if tool_name not in tool_names:
|
2862
|
-
|
2863
|
-
f"Agent {agent_name} does not have access to tool {tool_name}"
|
2864
|
-
|
3187
|
+
print(
|
3188
|
+
f"Agent {agent_name} does not have access to tool {tool_name}. Available tools: {tool_names}")
|
3189
|
+
return {"status": "error", "message": f"Agent {agent_name} does not have access to tool {tool_name}"}
|
2865
3190
|
|
2866
|
-
# Execute the tool
|
2867
|
-
|
3191
|
+
# Execute the tool with parameters
|
3192
|
+
try:
|
3193
|
+
print(
|
3194
|
+
f"Executing {tool_name} with config: {'API key present' if hasattr(tool, '_api_key') and tool._api_key else 'No API key'}")
|
3195
|
+
result = tool.execute(**parameters)
|
3196
|
+
print(f"Tool execution result: {result.get('status', 'unknown')}")
|
3197
|
+
return result
|
3198
|
+
except Exception as e:
|
3199
|
+
print(f"Error executing tool {tool_name}: {str(e)}")
|
3200
|
+
return {"status": "error", "message": f"Error: {str(e)}"}
|
2868
3201
|
|
2869
3202
|
def register_human_agent(
|
2870
3203
|
self,
|
@@ -2921,69 +3254,109 @@ class AgentService:
|
|
2921
3254
|
|
2922
3255
|
agent_config = self.ai_agents[agent_name]
|
2923
3256
|
|
2924
|
-
# Get
|
2925
|
-
instructions =
|
3257
|
+
# Get the properly formatted system prompt with tools and handoff instructions
|
3258
|
+
instructions = self.get_agent_system_prompt(agent_name)
|
3259
|
+
|
3260
|
+
# Add memory context
|
2926
3261
|
if memory_context:
|
2927
3262
|
instructions += f"\n\nUser context and history:\n{memory_context}"
|
2928
3263
|
|
2929
|
-
# Add
|
2930
|
-
tools
|
2931
|
-
if tools and "tools" not in kwargs:
|
2932
|
-
kwargs["tools"] = tools
|
2933
|
-
|
2934
|
-
# Add specific instruction for simple queries to prevent handoff format leakage
|
2935
|
-
if len(query.strip()) < 10:
|
2936
|
-
instructions += "\n\nIMPORTANT: If the user sends a simple test message, respond conversationally. Never output raw JSON handoff objects directly to the user."
|
3264
|
+
# Add critical instruction to prevent raw JSON
|
3265
|
+
instructions += "\n\nCRITICAL: When using tools or making handoffs, ALWAYS respond with properly formatted JSON as instructed."
|
2937
3266
|
|
2938
3267
|
# Generate response
|
2939
|
-
|
2940
|
-
|
2941
|
-
user_id=user_id,
|
2942
|
-
prompt=query,
|
2943
|
-
system_prompt=instructions,
|
2944
|
-
model=agent_config["model"],
|
2945
|
-
**kwargs,
|
2946
|
-
):
|
2947
|
-
# Filter out raw handoff JSON before yielding to user
|
2948
|
-
if not response_text and chunk.strip().startswith('{"handoff":'):
|
2949
|
-
# If we're starting with a handoff JSON, replace with a proper response
|
2950
|
-
yield "Hello! I'm here to help. What can I assist you with today?"
|
2951
|
-
response_text += chunk # Still store it for processing later
|
2952
|
-
else:
|
2953
|
-
yield chunk
|
2954
|
-
response_text += chunk
|
3268
|
+
tool_json_found = False
|
3269
|
+
full_response = ""
|
2955
3270
|
|
2956
|
-
# Process handoffs after yielding response (unchanged code)
|
2957
3271
|
try:
|
2958
|
-
|
2959
|
-
|
2960
|
-
|
2961
|
-
|
2962
|
-
|
2963
|
-
|
2964
|
-
|
2965
|
-
|
2966
|
-
|
2967
|
-
|
2968
|
-
|
2969
|
-
|
2970
|
-
|
2971
|
-
|
2972
|
-
|
2973
|
-
|
2974
|
-
|
2975
|
-
|
3272
|
+
async for chunk in self.llm_provider.generate_text(
|
3273
|
+
user_id=user_id,
|
3274
|
+
prompt=query,
|
3275
|
+
system_prompt=instructions,
|
3276
|
+
model=agent_config["model"],
|
3277
|
+
**kwargs,
|
3278
|
+
):
|
3279
|
+
# Add to full response
|
3280
|
+
full_response += chunk
|
3281
|
+
|
3282
|
+
# Check if this might be JSON
|
3283
|
+
if full_response.strip().startswith("{") and not tool_json_found:
|
3284
|
+
tool_json_found = True
|
3285
|
+
print(
|
3286
|
+
f"Detected potential JSON response starting with: {full_response[:50]}...")
|
3287
|
+
continue
|
3288
|
+
|
3289
|
+
# If not JSON, yield the chunk
|
3290
|
+
if not tool_json_found:
|
3291
|
+
yield chunk
|
3292
|
+
|
3293
|
+
# Process JSON if found
|
3294
|
+
if tool_json_found:
|
3295
|
+
try:
|
3296
|
+
print(
|
3297
|
+
f"Processing JSON response: {full_response[:100]}...")
|
3298
|
+
data = json.loads(full_response.strip())
|
3299
|
+
print(
|
3300
|
+
f"Successfully parsed JSON with keys: {list(data.keys())}")
|
3301
|
+
|
3302
|
+
# Handle tool call
|
3303
|
+
if "tool_call" in data:
|
3304
|
+
tool_data = data["tool_call"]
|
3305
|
+
tool_name = tool_data.get("name")
|
3306
|
+
parameters = tool_data.get("parameters", {})
|
3307
|
+
|
3308
|
+
print(
|
3309
|
+
f"Processing tool call: {tool_name} with parameters: {parameters}")
|
2976
3310
|
|
2977
|
-
# Execute the tool
|
2978
3311
|
if tool_name:
|
2979
|
-
|
2980
|
-
|
2981
|
-
|
2982
|
-
|
3312
|
+
try:
|
3313
|
+
# Execute tool
|
3314
|
+
print(f"Executing tool: {tool_name}")
|
3315
|
+
tool_result = self.execute_tool(
|
3316
|
+
agent_name, tool_name, parameters)
|
3317
|
+
print(
|
3318
|
+
f"Tool execution result status: {tool_result.get('status')}")
|
3319
|
+
|
3320
|
+
if tool_result.get("status") == "success":
|
3321
|
+
print(
|
3322
|
+
f"Tool executed successfully - yielding result")
|
3323
|
+
yield tool_result.get('result', '')
|
3324
|
+
else:
|
3325
|
+
print(
|
3326
|
+
f"Tool execution failed: {tool_result.get('message')}")
|
3327
|
+
yield f"Error: {tool_result.get('message', 'Unknown error')}"
|
3328
|
+
except Exception as e:
|
3329
|
+
print(f"Tool execution exception: {str(e)}")
|
3330
|
+
yield f"Error executing tool: {str(e)}"
|
3331
|
+
|
3332
|
+
# Handle handoff
|
3333
|
+
elif "handoff" in data:
|
3334
|
+
print(
|
3335
|
+
f"Processing handoff to: {data['handoff'].get('target_agent')}")
|
3336
|
+
# Store handoff data but don't yield anything
|
3337
|
+
self._last_handoff = data["handoff"]
|
3338
|
+
return
|
2983
3339
|
|
2984
|
-
|
2985
|
-
|
2986
|
-
|
3340
|
+
# If we got JSON but it's not a tool call or handoff, yield it as text
|
3341
|
+
else:
|
3342
|
+
print(
|
3343
|
+
f"Received JSON but not a tool call or handoff. Keys: {list(data.keys())}")
|
3344
|
+
yield full_response
|
3345
|
+
|
3346
|
+
except json.JSONDecodeError as e:
|
3347
|
+
# Not valid JSON, yield it as is
|
3348
|
+
print(f"JSON parse error: {str(e)} - yielding as text")
|
3349
|
+
yield full_response
|
3350
|
+
|
3351
|
+
# If nothing has been yielded yet (e.g., failed JSON parsing), yield the full response
|
3352
|
+
if not tool_json_found:
|
3353
|
+
print(f"Non-JSON response handled normally")
|
3354
|
+
|
3355
|
+
except Exception as e:
|
3356
|
+
print(f"Error in generate_response: {str(e)}")
|
3357
|
+
import traceback
|
3358
|
+
print(traceback.format_exc())
|
3359
|
+
yield f"I'm sorry, I encountered an error: {str(e)}"
|
2987
3360
|
|
2988
3361
|
|
2989
3362
|
class ResourceService:
|
@@ -5214,28 +5587,18 @@ class QueryProcessor:
|
|
5214
5587
|
self._stalled_ticket_task = loop.create_task(
|
5215
5588
|
self._run_stalled_ticket_checks())
|
5216
5589
|
except RuntimeError:
|
5217
|
-
# No running event loop - likely in test environment
|
5218
|
-
# Instead of just passing, log a message for clarity
|
5219
5590
|
import logging
|
5220
5591
|
logging.warning(
|
5221
5592
|
"No running event loop available for stalled ticket checker.")
|
5222
|
-
# Don't try to create the task - this prevents the coroutine warning
|
5223
5593
|
|
5224
5594
|
try:
|
5225
|
-
#
|
5226
|
-
if
|
5227
|
-
|
5228
|
-
|
5229
|
-
|
5230
|
-
|
5231
|
-
|
5232
|
-
|
5233
|
-
# Handle simple greetings without full agent routing
|
5234
|
-
if await self._is_simple_greeting(user_text):
|
5235
|
-
greeting_response = await self._generate_greeting_response(
|
5236
|
-
user_id, user_text
|
5237
|
-
)
|
5238
|
-
yield greeting_response
|
5595
|
+
# Special case for "test" and other very simple messages
|
5596
|
+
if user_text.strip().lower() in ["test", "hello", "hi", "hey", "ping"]:
|
5597
|
+
response = f"Hello! How can I help you today?"
|
5598
|
+
yield response
|
5599
|
+
# Store this simple interaction in memory
|
5600
|
+
if self.memory_provider:
|
5601
|
+
await self._store_conversation(user_id, user_text, response)
|
5239
5602
|
return
|
5240
5603
|
|
5241
5604
|
# Handle system commands
|
@@ -5244,100 +5607,66 @@ class QueryProcessor:
|
|
5244
5607
|
yield command_response
|
5245
5608
|
return
|
5246
5609
|
|
5610
|
+
# Route to appropriate agent
|
5611
|
+
agent_name = await self.routing_service.route_query(user_text)
|
5612
|
+
|
5247
5613
|
# Check for active ticket
|
5248
5614
|
active_ticket = self.ticket_service.ticket_repository.get_active_for_user(
|
5249
|
-
user_id
|
5250
|
-
)
|
5615
|
+
user_id)
|
5251
5616
|
|
5252
5617
|
if active_ticket:
|
5253
5618
|
# Process existing ticket
|
5254
|
-
|
5255
|
-
|
5256
|
-
|
5257
|
-
|
5619
|
+
try:
|
5620
|
+
response_buffer = ""
|
5621
|
+
async for chunk in self._process_existing_ticket(user_id, user_text, active_ticket, timezone):
|
5622
|
+
response_buffer += chunk
|
5623
|
+
yield chunk
|
5624
|
+
|
5625
|
+
# Check final response for unprocessed JSON
|
5626
|
+
if response_buffer.strip().startswith('{'):
|
5627
|
+
agent_name = active_ticket.assigned_to or "default_agent"
|
5628
|
+
processed_response = self.agent_service.process_json_response(
|
5629
|
+
response_buffer, agent_name)
|
5630
|
+
if processed_response != response_buffer:
|
5631
|
+
yield "\n\n" + processed_response
|
5632
|
+
except ValueError as e:
|
5633
|
+
if "Ticket" in str(e) and "not found" in str(e):
|
5634
|
+
# Ticket no longer exists - create a new one
|
5635
|
+
complexity = await self._assess_task_complexity(user_text)
|
5636
|
+
async for chunk in self._process_new_ticket(user_id, user_text, complexity, timezone):
|
5637
|
+
yield chunk
|
5638
|
+
else:
|
5639
|
+
yield f"I'm sorry, I encountered an error: {str(e)}"
|
5640
|
+
|
5258
5641
|
else:
|
5259
5642
|
# Create new ticket
|
5260
|
-
|
5261
|
-
|
5262
|
-
|
5263
|
-
|
5264
|
-
|
5265
|
-
|
5266
|
-
|
5643
|
+
try:
|
5644
|
+
complexity = await self._assess_task_complexity(user_text)
|
5645
|
+
|
5646
|
+
# Process as new ticket
|
5647
|
+
response_buffer = ""
|
5648
|
+
async for chunk in self._process_new_ticket(user_id, user_text, complexity, timezone):
|
5649
|
+
response_buffer += chunk
|
5650
|
+
yield chunk
|
5651
|
+
|
5652
|
+
# Check final response for unprocessed JSON
|
5653
|
+
if response_buffer.strip().startswith('{'):
|
5654
|
+
processed_response = self.agent_service.process_json_response(
|
5655
|
+
response_buffer, agent_name)
|
5656
|
+
if processed_response != response_buffer:
|
5657
|
+
yield "\n\n" + processed_response
|
5658
|
+
except Exception as e:
|
5659
|
+
yield f"I'm sorry, I encountered an error: {str(e)}"
|
5267
5660
|
|
5268
5661
|
except Exception as e:
|
5269
5662
|
print(f"Error in request processing: {str(e)}")
|
5270
5663
|
print(traceback.format_exc())
|
5271
|
-
|
5272
|
-
error_msg = "\n\nI apologize for the technical difficulty.\n\n"
|
5273
|
-
yield error_msg
|
5664
|
+
yield "I apologize for the technical difficulty.\n\n"
|
5274
5665
|
|
5275
5666
|
async def _is_human_agent(self, user_id: str) -> bool:
|
5276
5667
|
"""Check if the user is a registered human agent."""
|
5277
5668
|
return user_id in self.agent_service.get_all_human_agents()
|
5278
5669
|
|
5279
|
-
async def _is_simple_greeting(self, text: str) -> bool:
|
5280
|
-
"""Determine if the user message is a simple greeting."""
|
5281
|
-
text_lower = text.lower().strip()
|
5282
|
-
|
5283
|
-
# Common greetings list
|
5284
|
-
simple_greetings = [
|
5285
|
-
"hello",
|
5286
|
-
"hi",
|
5287
|
-
"hey",
|
5288
|
-
"greetings",
|
5289
|
-
"good morning",
|
5290
|
-
"good afternoon",
|
5291
|
-
"good evening",
|
5292
|
-
"what's up",
|
5293
|
-
"how are you",
|
5294
|
-
"how's it going",
|
5295
|
-
]
|
5296
|
-
|
5297
|
-
# Check if text starts with a greeting and is relatively short
|
5298
|
-
is_greeting = any(
|
5299
|
-
text_lower.startswith(greeting) for greeting in simple_greetings
|
5300
|
-
)
|
5301
|
-
# Arbitrary threshold for "simple" messages
|
5302
|
-
is_short = len(text.split()) < 7
|
5303
|
-
|
5304
|
-
return is_greeting and is_short
|
5305
|
-
|
5306
|
-
async def _generate_greeting_response(self, user_id: str, text: str) -> str:
|
5307
|
-
"""Generate a friendly response to a simple greeting."""
|
5308
|
-
# Get user context if available
|
5309
|
-
context = ""
|
5310
|
-
if self.memory_provider:
|
5311
|
-
context = await self.memory_provider.retrieve(user_id)
|
5312
|
-
|
5313
|
-
# Get first available AI agent for the greeting
|
5314
|
-
first_agent_name = next(
|
5315
|
-
iter(self.agent_service.get_all_ai_agents().keys()))
|
5316
|
-
|
5317
|
-
response = ""
|
5318
|
-
async for chunk in self.agent_service.generate_response(
|
5319
|
-
first_agent_name,
|
5320
|
-
user_id,
|
5321
|
-
text,
|
5322
|
-
context,
|
5323
|
-
temperature=0.7,
|
5324
|
-
max_tokens=100, # Keep it brief
|
5325
|
-
):
|
5326
|
-
response += chunk
|
5327
|
-
|
5328
|
-
# Store in memory if available
|
5329
|
-
if self.memory_provider:
|
5330
|
-
await self.memory_provider.store(
|
5331
|
-
user_id,
|
5332
|
-
[
|
5333
|
-
{"role": "user", "content": text},
|
5334
|
-
{"role": "assistant",
|
5335
|
-
"content": self._truncate(response, 2500)},
|
5336
|
-
],
|
5337
|
-
)
|
5338
|
-
|
5339
|
-
return response
|
5340
|
-
|
5341
5670
|
async def shutdown(self):
|
5342
5671
|
"""Clean shutdown of the query processor."""
|
5343
5672
|
self._shutdown_event.set()
|
@@ -5372,34 +5701,48 @@ class QueryProcessor:
|
|
5372
5701
|
if self.stalled_ticket_timeout is None:
|
5373
5702
|
return
|
5374
5703
|
|
5375
|
-
|
5376
|
-
|
5377
|
-
datetime.timezone.utc) - datetime.timedelta(
|
5704
|
+
try:
|
5705
|
+
# Find tickets that haven't been updated in the configured time
|
5706
|
+
cutoff_time = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(
|
5707
|
+
minutes=self.stalled_ticket_timeout
|
5708
|
+
)
|
5378
5709
|
|
5379
|
-
|
5380
|
-
|
5381
|
-
|
5382
|
-
|
5710
|
+
# The find_stalled_tickets method should be async
|
5711
|
+
stalled_tickets = await self.ticket_service.ticket_repository.find_stalled_tickets(
|
5712
|
+
cutoff_time, [TicketStatus.ACTIVE, TicketStatus.TRANSFERRED]
|
5713
|
+
)
|
5383
5714
|
|
5384
|
-
|
5385
|
-
|
5386
|
-
|
5715
|
+
for ticket in stalled_tickets:
|
5716
|
+
print(
|
5717
|
+
f"Found stalled ticket: {ticket.id} (last updated: {ticket.updated_at})")
|
5387
5718
|
|
5388
|
-
|
5389
|
-
|
5390
|
-
|
5719
|
+
# Skip tickets without an assigned agent
|
5720
|
+
if not ticket.assigned_to:
|
5721
|
+
continue
|
5391
5722
|
|
5392
|
-
|
5393
|
-
|
5394
|
-
ticket.id,
|
5395
|
-
ticket.assigned_to or "unassigned",
|
5396
|
-
new_agent,
|
5397
|
-
f"Automatically reassigned after {self.stalled_ticket_timeout} minutes of inactivity"
|
5398
|
-
)
|
5723
|
+
# Re-route the query to see if a different agent is better
|
5724
|
+
new_agent = await self.routing_service.route_query(ticket.query)
|
5399
5725
|
|
5400
|
-
|
5401
|
-
|
5402
|
-
|
5726
|
+
# Only reassign if a different agent is suggested
|
5727
|
+
if new_agent != ticket.assigned_to:
|
5728
|
+
print(
|
5729
|
+
f"Reassigning ticket {ticket.id} from {ticket.assigned_to} to {new_agent}")
|
5730
|
+
|
5731
|
+
try:
|
5732
|
+
await self.handoff_service.process_handoff(
|
5733
|
+
ticket.id,
|
5734
|
+
ticket.assigned_to,
|
5735
|
+
new_agent,
|
5736
|
+
f"Automatically reassigned after {self.stalled_ticket_timeout} minutes of inactivity"
|
5737
|
+
)
|
5738
|
+
except Exception as e:
|
5739
|
+
print(
|
5740
|
+
f"Error reassigning stalled ticket {ticket.id}: {e}")
|
5741
|
+
|
5742
|
+
except Exception as e:
|
5743
|
+
print(f"Error in stalled ticket check: {e}")
|
5744
|
+
import traceback
|
5745
|
+
print(traceback.format_exc())
|
5403
5746
|
|
5404
5747
|
async def _process_system_commands(
|
5405
5748
|
self, user_id: str, user_text: str
|
@@ -6114,112 +6457,104 @@ class QueryProcessor:
|
|
6114
6457
|
async def _process_existing_ticket(
|
6115
6458
|
self, user_id: str, user_text: str, ticket: Ticket, timezone: str = None
|
6116
6459
|
) -> AsyncGenerator[str, None]:
|
6117
|
-
"""
|
6460
|
+
"""
|
6461
|
+
Process a message for an existing ticket.
|
6462
|
+
Checks for handoff data and handles it with ticket-based handoff
|
6463
|
+
unless the target agent is set to skip ticket creation.
|
6464
|
+
"""
|
6118
6465
|
# Get assigned agent or re-route if needed
|
6119
6466
|
agent_name = ticket.assigned_to
|
6120
|
-
|
6121
|
-
# If no valid assignment, route to appropriate agent
|
6122
|
-
if not agent_name or agent_name not in self.agent_service.get_all_ai_agents():
|
6467
|
+
if not agent_name:
|
6123
6468
|
agent_name = await self.routing_service.route_query(user_text)
|
6124
|
-
# Update ticket with new assignment
|
6125
6469
|
self.ticket_service.update_ticket_status(
|
6126
|
-
ticket.id, TicketStatus.
|
6470
|
+
ticket.id, TicketStatus.IN_PROGRESS, assigned_to=agent_name
|
6127
6471
|
)
|
6128
6472
|
|
6129
|
-
# Update ticket status
|
6130
|
-
self.ticket_service.update_ticket_status(
|
6131
|
-
ticket.id, TicketStatus.ACTIVE)
|
6132
|
-
|
6133
6473
|
# Get memory context if available
|
6134
6474
|
memory_context = ""
|
6135
6475
|
if self.memory_provider:
|
6136
6476
|
memory_context = await self.memory_provider.retrieve(user_id)
|
6137
6477
|
|
6138
|
-
#
|
6478
|
+
# Try to generate response
|
6139
6479
|
full_response = ""
|
6140
6480
|
handoff_info = None
|
6481
|
+
handoff_detected = False
|
6141
6482
|
|
6142
|
-
async for chunk in self.agent_service.generate_response(
|
6143
|
-
agent_name, user_id, user_text, memory_context, temperature=0.7
|
6144
|
-
):
|
6145
|
-
yield chunk
|
6146
|
-
full_response += chunk
|
6147
|
-
|
6148
|
-
# Check for handoff in structured format
|
6149
6483
|
try:
|
6150
|
-
#
|
6151
|
-
|
6152
|
-
|
6153
|
-
|
6154
|
-
|
6484
|
+
# Generate response with streaming
|
6485
|
+
async for chunk in self.agent_service.generate_response(
|
6486
|
+
agent_name=agent_name,
|
6487
|
+
user_id=user_id,
|
6488
|
+
query=user_text,
|
6489
|
+
memory_context=memory_context,
|
6490
|
+
):
|
6491
|
+
# Detect possible handoff signals (JSON or prefix)
|
6492
|
+
if chunk.strip().startswith("HANDOFF:") or (
|
6493
|
+
not full_response and chunk.strip().startswith("{")
|
6494
|
+
):
|
6495
|
+
handoff_detected = True
|
6496
|
+
full_response += chunk
|
6497
|
+
continue
|
6498
|
+
|
6499
|
+
full_response += chunk
|
6500
|
+
yield chunk
|
6501
|
+
|
6502
|
+
# After response generation, handle handoff if needed
|
6503
|
+
if handoff_detected or (
|
6504
|
+
not full_response.strip()
|
6505
|
+
and hasattr(self.agent_service, "_last_handoff")
|
6506
|
+
):
|
6507
|
+
if hasattr(self.agent_service, "_last_handoff") and self.agent_service._last_handoff:
|
6508
|
+
handoff_data = {
|
6509
|
+
"handoff": self.agent_service._last_handoff}
|
6155
6510
|
target_agent = handoff_data["handoff"].get("target_agent")
|
6156
6511
|
reason = handoff_data["handoff"].get("reason")
|
6157
|
-
|
6512
|
+
|
6513
|
+
if target_agent:
|
6158
6514
|
handoff_info = {
|
6159
6515
|
"target": target_agent, "reason": reason}
|
6160
|
-
except Exception as e:
|
6161
|
-
print(f"Error parsing handoff data: {e}")
|
6162
|
-
|
6163
|
-
# Fall back to old method if structured parsing fails
|
6164
|
-
if "HANDOFF:" in chunk and not handoff_info:
|
6165
|
-
handoff_pattern = r"HANDOFF:\s*([A-Za-z0-9_]+)\s*REASON:\s*(.+)"
|
6166
|
-
match = re.search(handoff_pattern, full_response)
|
6167
|
-
if match:
|
6168
|
-
target_agent = match.group(1)
|
6169
|
-
reason = match.group(2)
|
6170
|
-
handoff_info = {"target": target_agent, "reason": reason}
|
6171
|
-
|
6172
|
-
# Store conversation in memory if available
|
6173
|
-
if self.memory_provider:
|
6174
|
-
await self.memory_provider.store(
|
6175
|
-
user_id,
|
6176
|
-
[
|
6177
|
-
{"role": "user", "content": user_text},
|
6178
|
-
{
|
6179
|
-
"role": "assistant",
|
6180
|
-
"content": self._truncate(full_response, 2500),
|
6181
|
-
},
|
6182
|
-
],
|
6183
|
-
)
|
6184
6516
|
|
6185
|
-
|
6186
|
-
|
6187
|
-
|
6188
|
-
|
6189
|
-
|
6190
|
-
|
6191
|
-
handoff_info["target"],
|
6192
|
-
handoff_info["reason"],
|
6193
|
-
)
|
6194
|
-
except ValueError as e:
|
6195
|
-
# If handoff fails, just continue with current agent
|
6196
|
-
print(f"Handoff failed: {e}")
|
6517
|
+
await self.handoff_service.process_handoff(
|
6518
|
+
ticket.id,
|
6519
|
+
agent_name,
|
6520
|
+
handoff_info["target"],
|
6521
|
+
handoff_info["reason"],
|
6522
|
+
)
|
6197
6523
|
|
6198
|
-
|
6199
|
-
|
6200
|
-
|
6201
|
-
|
6202
|
-
|
6524
|
+
print(
|
6525
|
+
f"Generating response from new agent: {target_agent}")
|
6526
|
+
new_response_buffer = ""
|
6527
|
+
async for chunk in self.agent_service.generate_response(
|
6528
|
+
agent_name=target_agent,
|
6529
|
+
user_id=user_id,
|
6530
|
+
query=user_text,
|
6531
|
+
memory_context=memory_context,
|
6532
|
+
):
|
6533
|
+
new_response_buffer += chunk
|
6534
|
+
yield chunk
|
6203
6535
|
|
6204
|
-
|
6205
|
-
self.ticket_service.mark_ticket_resolved(
|
6206
|
-
ticket.id,
|
6207
|
-
{
|
6208
|
-
"confidence": resolution.confidence,
|
6209
|
-
"reasoning": resolution.reasoning,
|
6210
|
-
},
|
6211
|
-
)
|
6536
|
+
full_response = new_response_buffer
|
6212
6537
|
|
6213
|
-
|
6214
|
-
self.nps_service.create_survey(user_id, ticket.id, agent_name)
|
6538
|
+
self.agent_service._last_handoff = None
|
6215
6539
|
|
6216
|
-
|
6217
|
-
|
6218
|
-
|
6219
|
-
|
6220
|
-
|
6540
|
+
# Store conversation in memory
|
6541
|
+
if self.memory_provider:
|
6542
|
+
await self.memory_provider.store(
|
6543
|
+
user_id,
|
6544
|
+
[
|
6545
|
+
{"role": "user", "content": user_text},
|
6546
|
+
{
|
6547
|
+
"role": "assistant",
|
6548
|
+
"content": self._truncate(full_response, 2500),
|
6549
|
+
},
|
6550
|
+
],
|
6221
6551
|
)
|
6222
|
-
|
6552
|
+
|
6553
|
+
except Exception as e:
|
6554
|
+
print(f"Error processing ticket: {str(e)}")
|
6555
|
+
import traceback
|
6556
|
+
print(traceback.format_exc())
|
6557
|
+
yield f"I'm sorry, I encountered an error processing your request: {str(e)}"
|
6223
6558
|
|
6224
6559
|
async def _process_new_ticket(
|
6225
6560
|
self,
|
@@ -6311,106 +6646,113 @@ class QueryProcessor:
|
|
6311
6646
|
ticket.id, TicketStatus.ACTIVE, assigned_to=agent_name
|
6312
6647
|
)
|
6313
6648
|
|
6314
|
-
# Generate response with streaming
|
6649
|
+
# Generate initial response with streaming
|
6315
6650
|
full_response = ""
|
6316
|
-
|
6651
|
+
handoff_detected = False
|
6317
6652
|
|
6318
|
-
|
6319
|
-
|
6320
|
-
|
6321
|
-
|
6322
|
-
|
6653
|
+
try:
|
6654
|
+
# Generate response with streaming
|
6655
|
+
async for chunk in self.agent_service.generate_response(
|
6656
|
+
agent_name, user_id, user_text, memory_context, temperature=0.7
|
6657
|
+
):
|
6658
|
+
# Check if this looks like a JSON handoff
|
6659
|
+
if chunk.strip().startswith("{") and not handoff_detected:
|
6660
|
+
handoff_detected = True
|
6661
|
+
full_response += chunk
|
6662
|
+
continue
|
6323
6663
|
|
6324
|
-
|
6325
|
-
|
6326
|
-
|
6327
|
-
|
6328
|
-
|
6329
|
-
|
6330
|
-
|
6331
|
-
|
6332
|
-
|
6333
|
-
|
6334
|
-
|
6335
|
-
|
6336
|
-
|
6337
|
-
|
6338
|
-
|
6339
|
-
|
6340
|
-
|
6341
|
-
|
6342
|
-
|
6343
|
-
|
6344
|
-
|
6345
|
-
|
6346
|
-
|
6347
|
-
|
6348
|
-
|
6349
|
-
|
6350
|
-
|
6351
|
-
|
6352
|
-
|
6353
|
-
|
6354
|
-
|
6355
|
-
|
6356
|
-
|
6357
|
-
|
6358
|
-
|
6359
|
-
|
6360
|
-
|
6664
|
+
# Only yield if not a JSON chunk
|
6665
|
+
if not handoff_detected:
|
6666
|
+
yield chunk
|
6667
|
+
full_response += chunk
|
6668
|
+
|
6669
|
+
# Handle handoff if detected
|
6670
|
+
if handoff_detected or (hasattr(self.agent_service, "_last_handoff") and self.agent_service._last_handoff):
|
6671
|
+
target_agent = None
|
6672
|
+
reason = "Handoff detected"
|
6673
|
+
|
6674
|
+
# Process the handoff from _last_handoff property
|
6675
|
+
if hasattr(self.agent_service, "_last_handoff") and self.agent_service._last_handoff:
|
6676
|
+
target_agent = self.agent_service._last_handoff.get(
|
6677
|
+
"target_agent")
|
6678
|
+
reason = self.agent_service._last_handoff.get(
|
6679
|
+
"reason", "No reason provided")
|
6680
|
+
|
6681
|
+
if target_agent:
|
6682
|
+
try:
|
6683
|
+
# Process handoff and update ticket
|
6684
|
+
await self.handoff_service.process_handoff(
|
6685
|
+
ticket.id,
|
6686
|
+
agent_name,
|
6687
|
+
target_agent,
|
6688
|
+
reason,
|
6689
|
+
)
|
6690
|
+
|
6691
|
+
# Generate response from new agent
|
6692
|
+
print(
|
6693
|
+
f"Generating response from new agent after handoff: {target_agent}")
|
6694
|
+
new_response = ""
|
6695
|
+
async for chunk in self.agent_service.generate_response(
|
6696
|
+
target_agent,
|
6697
|
+
user_id,
|
6698
|
+
user_text,
|
6699
|
+
memory_context,
|
6700
|
+
temperature=0.7
|
6701
|
+
):
|
6702
|
+
yield chunk
|
6703
|
+
new_response += chunk
|
6704
|
+
|
6705
|
+
# Update full response for storage
|
6706
|
+
full_response = new_response
|
6707
|
+
except ValueError as e:
|
6708
|
+
print(f"Handoff failed: {e}")
|
6709
|
+
yield f"\n\nNote: A handoff was attempted but failed: {str(e)}"
|
6710
|
+
|
6711
|
+
# Reset handoff state
|
6712
|
+
self.agent_service._last_handoff = None
|
6713
|
+
|
6714
|
+
# Check if ticket can be considered resolved
|
6715
|
+
resolution = await self._check_ticket_resolution(
|
6716
|
+
full_response, user_text
|
6361
6717
|
)
|
6362
6718
|
|
6363
|
-
|
6364
|
-
|
6365
|
-
try:
|
6366
|
-
await self.handoff_service.process_handoff(
|
6719
|
+
if resolution.status == "resolved" and resolution.confidence >= 0.7:
|
6720
|
+
self.ticket_service.mark_ticket_resolved(
|
6367
6721
|
ticket.id,
|
6368
|
-
|
6369
|
-
|
6370
|
-
|
6722
|
+
{
|
6723
|
+
"confidence": resolution.confidence,
|
6724
|
+
"reasoning": resolution.reasoning,
|
6725
|
+
},
|
6371
6726
|
)
|
6372
|
-
except ValueError as e:
|
6373
|
-
print(f"Handoff failed: {e}")
|
6374
6727
|
|
6375
|
-
#
|
6376
|
-
|
6377
|
-
|
6378
|
-
await self.handoff_service.process_handoff(
|
6379
|
-
ticket.id,
|
6380
|
-
agent_name,
|
6381
|
-
handoff_info["target"],
|
6382
|
-
handoff_info["reason"],
|
6383
|
-
)
|
6384
|
-
except ValueError as e:
|
6385
|
-
print(f"Handoff failed: {e}")
|
6386
|
-
|
6387
|
-
# Check if ticket can be considered resolved
|
6388
|
-
if not handoff_info:
|
6389
|
-
resolution = await self._check_ticket_resolution(
|
6390
|
-
user_id, full_response, user_text
|
6391
|
-
)
|
6392
|
-
|
6393
|
-
if resolution.status == "resolved" and resolution.confidence >= 0.7:
|
6394
|
-
self.ticket_service.mark_ticket_resolved(
|
6395
|
-
ticket.id,
|
6396
|
-
{
|
6397
|
-
"confidence": resolution.confidence,
|
6398
|
-
"reasoning": resolution.reasoning,
|
6399
|
-
},
|
6400
|
-
)
|
6728
|
+
# Create NPS survey
|
6729
|
+
self.nps_service.create_survey(
|
6730
|
+
user_id, ticket.id, agent_name)
|
6401
6731
|
|
6402
|
-
|
6403
|
-
|
6404
|
-
|
6732
|
+
# Store in memory provider
|
6733
|
+
if self.memory_provider:
|
6734
|
+
await self.memory_provider.store(
|
6735
|
+
user_id,
|
6736
|
+
[
|
6737
|
+
{"role": "user", "content": user_text},
|
6738
|
+
{"role": "assistant", "content": self._truncate(
|
6739
|
+
full_response, 2500)},
|
6740
|
+
],
|
6741
|
+
)
|
6405
6742
|
|
6406
|
-
|
6407
|
-
|
6408
|
-
|
6409
|
-
|
6410
|
-
|
6411
|
-
|
6412
|
-
)
|
6743
|
+
# Extract and store insights in background
|
6744
|
+
if full_response:
|
6745
|
+
asyncio.create_task(
|
6746
|
+
self._extract_and_store_insights(
|
6747
|
+
user_id, {"message": user_text,
|
6748
|
+
"response": full_response}
|
6413
6749
|
)
|
6750
|
+
)
|
6751
|
+
|
6752
|
+
except Exception as e:
|
6753
|
+
print(f"Error in _process_new_ticket: {str(e)}")
|
6754
|
+
print(traceback.format_exc())
|
6755
|
+
yield f"I'm sorry, I encountered an error processing your request: {str(e)}"
|
6414
6756
|
|
6415
6757
|
async def _process_human_agent_message(
|
6416
6758
|
self, user_id: str, user_text: str
|
@@ -6514,7 +6856,7 @@ class QueryProcessor:
|
|
6514
6856
|
return result
|
6515
6857
|
|
6516
6858
|
async def _check_ticket_resolution(
|
6517
|
-
self,
|
6859
|
+
self, response: str, query: str
|
6518
6860
|
) -> TicketResolution:
|
6519
6861
|
"""Determine if a ticket can be considered resolved based on the response."""
|
6520
6862
|
# Get first AI agent for analysis
|
@@ -6532,39 +6874,48 @@ class QueryProcessor:
|
|
6532
6874
|
2. "needs_followup" - The assistant couldn't fully address the issue or more information is needed
|
6533
6875
|
3. "cannot_determine" - Cannot tell if the issue is resolved
|
6534
6876
|
|
6535
|
-
Return a
|
6877
|
+
Return a structured output with:
|
6536
6878
|
- "status": One of the above values
|
6537
6879
|
- "confidence": A score from 0.0 to 1.0 indicating confidence in this assessment
|
6538
6880
|
- "reasoning": Brief explanation for your decision
|
6539
6881
|
- "suggested_actions": Array of recommended next steps (if any)
|
6540
6882
|
"""
|
6541
6883
|
|
6542
|
-
# Generate resolution assessment
|
6543
|
-
resolution_text = ""
|
6544
|
-
async for chunk in self.agent_service.generate_response(
|
6545
|
-
first_agent,
|
6546
|
-
"resolution_checker",
|
6547
|
-
prompt,
|
6548
|
-
"", # No memory context needed
|
6549
|
-
stream=False,
|
6550
|
-
temperature=0.2,
|
6551
|
-
response_format={"type": "json_object"},
|
6552
|
-
):
|
6553
|
-
resolution_text += chunk
|
6554
|
-
|
6555
6884
|
try:
|
6556
|
-
|
6557
|
-
|
6558
|
-
|
6559
|
-
|
6560
|
-
|
6561
|
-
|
6562
|
-
|
6563
|
-
|
6885
|
+
# Use structured output parsing with the Pydantic model directly
|
6886
|
+
resolution = await self.agent_service.llm_provider.parse_structured_output(
|
6887
|
+
prompt=prompt,
|
6888
|
+
system_prompt="You are a resolution analysis system. Analyze conversations and determine if queries have been resolved.",
|
6889
|
+
model_class=TicketResolution,
|
6890
|
+
model=self.agent_service.ai_agents[first_agent].get(
|
6891
|
+
"model", "gpt-4o-mini"),
|
6892
|
+
temperature=0.2,
|
6564
6893
|
)
|
6894
|
+
return resolution
|
6895
|
+
except Exception as e:
|
6896
|
+
print(f"Exception in resolution check: {e}")
|
6897
|
+
|
6898
|
+
# Default fallback if anything fails
|
6899
|
+
return TicketResolution(
|
6900
|
+
status="cannot_determine",
|
6901
|
+
confidence=0.2,
|
6902
|
+
reasoning="Failed to analyze resolution status",
|
6903
|
+
suggested_actions=["Review conversation manually"]
|
6904
|
+
)
|
6565
6905
|
|
6566
6906
|
async def _assess_task_complexity(self, query: str) -> Dict[str, Any]:
|
6567
6907
|
"""Assess the complexity of a task using standardized metrics."""
|
6908
|
+
# Special handling for very simple messages
|
6909
|
+
if len(query.strip()) <= 10 and query.lower().strip() in ["test", "hello", "hi", "hey", "ping", "thanks"]:
|
6910
|
+
print(f"Using pre-defined complexity for simple message: {query}")
|
6911
|
+
return {
|
6912
|
+
"t_shirt_size": "XS",
|
6913
|
+
"story_points": 1,
|
6914
|
+
"estimated_minutes": 5,
|
6915
|
+
"technical_complexity": 1,
|
6916
|
+
"domain_knowledge": 1,
|
6917
|
+
}
|
6918
|
+
|
6568
6919
|
# Get first AI agent for analysis
|
6569
6920
|
first_agent = next(iter(self.agent_service.get_all_ai_agents().keys()))
|
6570
6921
|
|
@@ -6594,16 +6945,28 @@ class QueryProcessor:
|
|
6594
6945
|
):
|
6595
6946
|
response_text += chunk
|
6596
6947
|
|
6948
|
+
if not response_text.strip():
|
6949
|
+
print("Empty response from complexity assessment")
|
6950
|
+
return {
|
6951
|
+
"t_shirt_size": "S",
|
6952
|
+
"story_points": 2,
|
6953
|
+
"estimated_minutes": 15,
|
6954
|
+
"technical_complexity": 3,
|
6955
|
+
"domain_knowledge": 2,
|
6956
|
+
}
|
6957
|
+
|
6597
6958
|
complexity_data = json.loads(response_text)
|
6959
|
+
print(f"Successfully parsed complexity: {complexity_data}")
|
6598
6960
|
return complexity_data
|
6599
6961
|
except Exception as e:
|
6600
6962
|
print(f"Error assessing complexity: {e}")
|
6963
|
+
print(f"Failed response text: '{response_text}'")
|
6601
6964
|
return {
|
6602
|
-
"t_shirt_size": "
|
6603
|
-
"story_points":
|
6604
|
-
"estimated_minutes":
|
6605
|
-
"technical_complexity":
|
6606
|
-
"domain_knowledge":
|
6965
|
+
"t_shirt_size": "S",
|
6966
|
+
"story_points": 2,
|
6967
|
+
"estimated_minutes": 15,
|
6968
|
+
"technical_complexity": 3,
|
6969
|
+
"domain_knowledge": 2,
|
6607
6970
|
}
|
6608
6971
|
|
6609
6972
|
async def _extract_and_store_insights(
|
@@ -6638,6 +7001,20 @@ class QueryProcessor:
|
|
6638
7001
|
|
6639
7002
|
return truncated + "..."
|
6640
7003
|
|
7004
|
+
async def _store_conversation(self, user_id: str, user_text: str, response_text: str) -> None:
|
7005
|
+
"""Store conversation history in memory provider."""
|
7006
|
+
if self.memory_provider:
|
7007
|
+
try:
|
7008
|
+
await self.memory_provider.store(
|
7009
|
+
user_id,
|
7010
|
+
[
|
7011
|
+
{"role": "user", "content": user_text},
|
7012
|
+
{"role": "assistant", "content": response_text},
|
7013
|
+
],
|
7014
|
+
)
|
7015
|
+
except Exception as e:
|
7016
|
+
print(f"Error storing conversation: {e}")
|
7017
|
+
# Don't let memory storage errors affect the user experience
|
6641
7018
|
|
6642
7019
|
#############################################
|
6643
7020
|
# FACTORY AND DEPENDENCY INJECTION
|
@@ -6715,16 +7092,25 @@ class SolanaAgentFactory:
|
|
6715
7092
|
|
6716
7093
|
# Create services
|
6717
7094
|
agent_service = AgentService(
|
6718
|
-
llm_adapter, human_agent_repo, ai_agent_repo, organization_mission)
|
7095
|
+
llm_adapter, human_agent_repo, ai_agent_repo, organization_mission, config)
|
7096
|
+
|
7097
|
+
# Debug the agent service tool registry to confirm tools were registered
|
7098
|
+
print(
|
7099
|
+
f"Agent service tools after initialization: {agent_service.tool_registry.list_all_tools()}")
|
7100
|
+
|
6719
7101
|
routing_service = RoutingService(
|
6720
7102
|
llm_adapter,
|
6721
7103
|
agent_service,
|
6722
7104
|
router_model=config.get("router_model", "gpt-4o-mini"),
|
6723
7105
|
)
|
7106
|
+
|
6724
7107
|
ticket_service = TicketService(ticket_repo)
|
7108
|
+
|
6725
7109
|
handoff_service = HandoffService(
|
6726
7110
|
handoff_repo, ticket_repo, agent_service)
|
7111
|
+
|
6727
7112
|
memory_service = MemoryService(memory_repo, llm_adapter)
|
7113
|
+
|
6728
7114
|
nps_service = NPSService(nps_repo, ticket_repo)
|
6729
7115
|
|
6730
7116
|
# Create critic service if enabled
|
@@ -6737,7 +7123,11 @@ class SolanaAgentFactory:
|
|
6737
7123
|
ticket_repo, llm_adapter, agent_service
|
6738
7124
|
)
|
6739
7125
|
|
6740
|
-
notification_service = NotificationService(
|
7126
|
+
notification_service = NotificationService(
|
7127
|
+
human_agent_registry=human_agent_repo,
|
7128
|
+
tool_registry=agent_service.tool_registry
|
7129
|
+
)
|
7130
|
+
|
6741
7131
|
project_approval_service = ProjectApprovalService(
|
6742
7132
|
ticket_repo, human_agent_repo, notification_service
|
6743
7133
|
)
|
@@ -6763,6 +7153,27 @@ class SolanaAgentFactory:
|
|
6763
7153
|
loaded_plugins = agent_service.plugin_manager.load_all_plugins()
|
6764
7154
|
print(f"Loaded {loaded_plugins} plugins")
|
6765
7155
|
|
7156
|
+
# Get list of all agents defined in config
|
7157
|
+
config_defined_agents = [agent["name"]
|
7158
|
+
for agent in config.get("ai_agents", [])]
|
7159
|
+
|
7160
|
+
# Sync MongoDB with config-defined agents (delete any agents not in config)
|
7161
|
+
all_db_agents = ai_agent_repo.db.find(ai_agent_repo.collection, {})
|
7162
|
+
db_agent_names = [agent["name"] for agent in all_db_agents]
|
7163
|
+
|
7164
|
+
# Find agents that exist in DB but not in config
|
7165
|
+
agents_to_delete = [
|
7166
|
+
name for name in db_agent_names if name not in config_defined_agents]
|
7167
|
+
|
7168
|
+
# Delete those agents
|
7169
|
+
for agent_name in agents_to_delete:
|
7170
|
+
print(
|
7171
|
+
f"Deleting agent '{agent_name}' from MongoDB - no longer defined in config")
|
7172
|
+
ai_agent_repo.db.delete_one(
|
7173
|
+
ai_agent_repo.collection, {"name": agent_name})
|
7174
|
+
if agent_name in ai_agent_repo.ai_agents_cache:
|
7175
|
+
del ai_agent_repo.ai_agents_cache[agent_name]
|
7176
|
+
|
6766
7177
|
# Register predefined agents if any
|
6767
7178
|
for agent_config in config.get("ai_agents", []):
|
6768
7179
|
agent_service.register_ai_agent(
|
@@ -6775,10 +7186,15 @@ class SolanaAgentFactory:
|
|
6775
7186
|
# Register tools for this agent if specified
|
6776
7187
|
if "tools" in agent_config:
|
6777
7188
|
for tool_name in agent_config["tools"]:
|
7189
|
+
# Print available tools before registering
|
7190
|
+
print(
|
7191
|
+
f"Available tools before registering {tool_name}: {agent_service.tool_registry.list_all_tools()}")
|
6778
7192
|
try:
|
6779
7193
|
agent_service.register_tool_for_agent(
|
6780
7194
|
agent_config["name"], tool_name
|
6781
7195
|
)
|
7196
|
+
print(
|
7197
|
+
f"Successfully registered {tool_name} for agent {agent_config['name']}")
|
6782
7198
|
except ValueError as e:
|
6783
7199
|
print(
|
6784
7200
|
f"Error registering tool {tool_name} for agent {agent_config['name']}: {e}"
|
@@ -7456,51 +7872,57 @@ class SolanaAgent:
|
|
7456
7872
|
# PLUGIN SYSTEM
|
7457
7873
|
#############################################
|
7458
7874
|
|
7875
|
+
class AutoTool:
|
7876
|
+
"""Base class for tools that automatically register with the system."""
|
7459
7877
|
|
7460
|
-
|
7461
|
-
|
7878
|
+
def __init__(self, name: str, description: str, registry=None):
|
7879
|
+
"""Initialize the tool with name and description."""
|
7880
|
+
self.name = name
|
7881
|
+
self.description = description
|
7882
|
+
self._config = {}
|
7462
7883
|
|
7463
|
-
|
7464
|
-
|
7465
|
-
|
7466
|
-
"""Unique name of the tool."""
|
7467
|
-
pass
|
7884
|
+
# Register with the provided registry if given
|
7885
|
+
if registry is not None:
|
7886
|
+
registry.register_tool(self)
|
7468
7887
|
|
7469
|
-
|
7470
|
-
|
7471
|
-
|
7472
|
-
"""Human-readable description of what the tool does."""
|
7473
|
-
pass
|
7888
|
+
def configure(self, config: Dict[str, Any]) -> None:
|
7889
|
+
"""Configure the tool with settings from config."""
|
7890
|
+
self._config = config
|
7474
7891
|
|
7475
|
-
|
7476
|
-
|
7477
|
-
|
7478
|
-
|
7479
|
-
pass
|
7892
|
+
def get_schema(self) -> Dict[str, Any]:
|
7893
|
+
"""Return the JSON schema for this tool's parameters."""
|
7894
|
+
# Override in subclasses
|
7895
|
+
return {}
|
7480
7896
|
|
7481
|
-
|
7482
|
-
|
7483
|
-
|
7484
|
-
|
7897
|
+
def execute(self, **params) -> Dict[str, Any]:
|
7898
|
+
"""Execute the tool with the provided parameters."""
|
7899
|
+
# Override in subclasses
|
7900
|
+
raise NotImplementedError()
|
7485
7901
|
|
7486
7902
|
|
7487
7903
|
class ToolRegistry:
|
7488
|
-
"""
|
7904
|
+
"""Instance-based registry that manages tools and their access permissions."""
|
7489
7905
|
|
7490
7906
|
def __init__(self):
|
7491
|
-
|
7492
|
-
#
|
7493
|
-
self._agent_tools
|
7907
|
+
"""Initialize an empty tool registry."""
|
7908
|
+
self._tools = {} # name -> tool instance
|
7909
|
+
self._agent_tools = {} # agent_name -> [tool_names]
|
7910
|
+
|
7911
|
+
def register_tool(self, tool) -> bool:
|
7912
|
+
"""Register a tool with this registry."""
|
7913
|
+
self._tools[tool.name] = tool
|
7914
|
+
print(f"Registered tool: {tool.name}")
|
7915
|
+
return True
|
7494
7916
|
|
7495
|
-
def
|
7496
|
-
"""
|
7497
|
-
|
7498
|
-
self._tools[instance.name] = tool_class
|
7917
|
+
def get_tool(self, tool_name: str):
|
7918
|
+
"""Get a tool by name."""
|
7919
|
+
return self._tools.get(tool_name)
|
7499
7920
|
|
7500
|
-
def assign_tool_to_agent(self, agent_name: str, tool_name: str) ->
|
7501
|
-
"""
|
7921
|
+
def assign_tool_to_agent(self, agent_name: str, tool_name: str) -> bool:
|
7922
|
+
"""Give an agent access to a specific tool."""
|
7502
7923
|
if tool_name not in self._tools:
|
7503
|
-
|
7924
|
+
print(f"Error: Tool {tool_name} is not registered")
|
7925
|
+
return False
|
7504
7926
|
|
7505
7927
|
if agent_name not in self._agent_tools:
|
7506
7928
|
self._agent_tools[agent_name] = []
|
@@ -7508,83 +7930,86 @@ class ToolRegistry:
|
|
7508
7930
|
if tool_name not in self._agent_tools[agent_name]:
|
7509
7931
|
self._agent_tools[agent_name].append(tool_name)
|
7510
7932
|
|
7933
|
+
return True
|
7934
|
+
|
7511
7935
|
def get_agent_tools(self, agent_name: str) -> List[Dict[str, Any]]:
|
7512
|
-
"""Get all tools available to
|
7936
|
+
"""Get all tools available to an agent."""
|
7513
7937
|
tool_names = self._agent_tools.get(agent_name, [])
|
7514
|
-
|
7515
|
-
|
7516
|
-
|
7517
|
-
|
7518
|
-
|
7519
|
-
|
7520
|
-
|
7521
|
-
|
7522
|
-
"description": tool_instance.description,
|
7523
|
-
"parameters": tool_instance.parameters_schema,
|
7524
|
-
}
|
7525
|
-
)
|
7526
|
-
|
7527
|
-
return tool_defs
|
7528
|
-
|
7529
|
-
def get_tool(self, tool_name: str) -> Optional[Tool]:
|
7530
|
-
"""Get a tool by name."""
|
7531
|
-
tool_class = self._tools.get(tool_name)
|
7532
|
-
return tool_class() if tool_class else None
|
7938
|
+
return [
|
7939
|
+
{
|
7940
|
+
"name": name,
|
7941
|
+
"description": self._tools[name].description,
|
7942
|
+
"parameters": self._tools[name].get_schema()
|
7943
|
+
}
|
7944
|
+
for name in tool_names if name in self._tools
|
7945
|
+
]
|
7533
7946
|
|
7534
7947
|
def list_all_tools(self) -> List[str]:
|
7535
|
-
"""List all registered
|
7948
|
+
"""List all registered tools."""
|
7536
7949
|
return list(self._tools.keys())
|
7537
7950
|
|
7538
|
-
|
7539
|
-
|
7540
|
-
|
7951
|
+
def configure_all_tools(self, config: Dict[str, Any]) -> None:
|
7952
|
+
"""Configure all registered tools with the same config."""
|
7953
|
+
for tool in self._tools.values():
|
7954
|
+
tool.configure(config)
|
7541
7955
|
|
7542
7956
|
|
7543
7957
|
class PluginManager:
|
7544
|
-
"""
|
7958
|
+
"""Manager for discovering and loading plugins."""
|
7545
7959
|
|
7546
|
-
|
7547
|
-
|
7960
|
+
# Class variable to track loaded entry points
|
7961
|
+
_loaded_entry_points = set()
|
7548
7962
|
|
7549
|
-
def
|
7550
|
-
"""
|
7963
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None, tool_registry: Optional[ToolRegistry] = None):
|
7964
|
+
"""Initialize with optional configuration and tool registry."""
|
7965
|
+
self.config = config or {}
|
7966
|
+
self.tool_registry = tool_registry or ToolRegistry()
|
7551
7967
|
|
7552
|
-
|
7553
|
-
"""
|
7554
|
-
|
7968
|
+
def load_all_plugins(self) -> int:
|
7969
|
+
"""Load all plugins using entry points and apply configuration."""
|
7970
|
+
loaded_count = 0
|
7971
|
+
plugins = []
|
7555
7972
|
|
7556
|
-
|
7557
|
-
# Discover plugins registered via entry_points
|
7973
|
+
# Discover plugins through entry points
|
7558
7974
|
for entry_point in importlib.metadata.entry_points(group='solana_agent.plugins'):
|
7975
|
+
# Skip if this entry point has already been loaded
|
7976
|
+
entry_point_id = f"{entry_point.name}:{entry_point.value}"
|
7977
|
+
if entry_point_id in PluginManager._loaded_entry_points:
|
7978
|
+
print(f"Skipping already loaded plugin: {entry_point.name}")
|
7979
|
+
continue
|
7980
|
+
|
7559
7981
|
try:
|
7560
|
-
|
7561
|
-
|
7562
|
-
|
7563
|
-
|
7564
|
-
|
7565
|
-
|
7566
|
-
|
7567
|
-
|
7982
|
+
print(f"Found plugin entry point: {entry_point.name}")
|
7983
|
+
PluginManager._loaded_entry_points.add(entry_point_id)
|
7984
|
+
plugin_factory = entry_point.load()
|
7985
|
+
plugin = plugin_factory()
|
7986
|
+
plugins.append(plugin)
|
7987
|
+
|
7988
|
+
# Initialize the plugin with config
|
7989
|
+
if hasattr(plugin, 'initialize') and callable(plugin.initialize):
|
7990
|
+
plugin.initialize(self.config)
|
7991
|
+
print(
|
7992
|
+
f"Initialized plugin {entry_point.name} with config keys: {list(self.config.keys() if self.config else [])}")
|
7993
|
+
|
7994
|
+
loaded_count += 1
|
7568
7995
|
except Exception as e:
|
7569
7996
|
print(f"Error loading plugin {entry_point.name}: {e}")
|
7570
7997
|
|
7571
|
-
|
7572
|
-
|
7573
|
-
|
7574
|
-
|
7575
|
-
|
7576
|
-
|
7577
|
-
|
7578
|
-
|
7579
|
-
|
7580
|
-
|
7581
|
-
|
7582
|
-
|
7583
|
-
|
7584
|
-
|
7585
|
-
|
7998
|
+
# After all plugins are initialized, register their tools
|
7999
|
+
for plugin in plugins:
|
8000
|
+
try:
|
8001
|
+
if hasattr(plugin, 'get_tools') and callable(plugin.get_tools):
|
8002
|
+
tools = plugin.get_tools()
|
8003
|
+
# Register each tool with our registry
|
8004
|
+
if isinstance(tools, list):
|
8005
|
+
for tool in tools:
|
8006
|
+
self.tool_registry.register_tool(tool)
|
8007
|
+
tool.configure(self.config)
|
8008
|
+
else:
|
8009
|
+
# Single tool case
|
8010
|
+
self.tool_registry.register_tool(tools)
|
8011
|
+
tools.configure(self.config)
|
8012
|
+
except Exception as e:
|
8013
|
+
print(f"Error registering tools from plugin: {e}")
|
7586
8014
|
|
7587
|
-
|
7588
|
-
return tool.execute(**kwargs)
|
7589
|
-
except Exception as e:
|
7590
|
-
return {"error": str(e), "status": "error"}
|
8015
|
+
return loaded_count
|