solana-agent 7.0.0__py3-none-any.whl → 8.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 +935 -68
- solana_agent-8.0.0.dist-info/METADATA +248 -0
- solana_agent-8.0.0.dist-info/RECORD +6 -0
- solana_agent-7.0.0.dist-info/METADATA +0 -178
- solana_agent-7.0.0.dist-info/RECORD +0 -6
- {solana_agent-7.0.0.dist-info → solana_agent-8.0.0.dist-info}/LICENSE +0 -0
- {solana_agent-7.0.0.dist-info → solana_agent-8.0.0.dist-info}/WHEEL +0 -0
solana_agent/ai.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import datetime
|
|
3
|
+
import re
|
|
3
4
|
import traceback
|
|
4
5
|
import ntplib
|
|
5
6
|
import json
|
|
@@ -226,6 +227,49 @@ class AI:
|
|
|
226
227
|
self._tools.append(search_internet_tool)
|
|
227
228
|
print("Internet search capability added as default tool")
|
|
228
229
|
|
|
230
|
+
# Automatically add knowledge base search tool if Pinecone is configured
|
|
231
|
+
if pinecone_api_key and pinecone_index_name:
|
|
232
|
+
search_kb_tool = {
|
|
233
|
+
"type": "function",
|
|
234
|
+
"function": {
|
|
235
|
+
"name": "search_kb",
|
|
236
|
+
"description": "Search the knowledge base using Pinecone",
|
|
237
|
+
"parameters": {
|
|
238
|
+
"type": "object",
|
|
239
|
+
"properties": {
|
|
240
|
+
"query": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"description": "Search query to find relevant documents",
|
|
243
|
+
},
|
|
244
|
+
"namespace": {
|
|
245
|
+
"type": "string",
|
|
246
|
+
"description": "Namespace of the Pinecone to search",
|
|
247
|
+
"default": "global",
|
|
248
|
+
},
|
|
249
|
+
"rerank_model": {
|
|
250
|
+
"type": "string",
|
|
251
|
+
"description": "Rerank model to use",
|
|
252
|
+
"enum": ["cohere-rerank-3.5"],
|
|
253
|
+
"default": "cohere-rerank-3.5",
|
|
254
|
+
},
|
|
255
|
+
"inner_limit": {
|
|
256
|
+
"type": "integer",
|
|
257
|
+
"description": "Maximum number of results to rerank",
|
|
258
|
+
"default": 10,
|
|
259
|
+
},
|
|
260
|
+
"limit": {
|
|
261
|
+
"type": "integer",
|
|
262
|
+
"description": "Maximum number of results to return",
|
|
263
|
+
"default": 3,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
"required": ["query"],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
self._tools.append(search_kb_tool)
|
|
271
|
+
print("Knowledge base search capability added as default tool")
|
|
272
|
+
|
|
229
273
|
async def __aenter__(self):
|
|
230
274
|
return self
|
|
231
275
|
|
|
@@ -724,14 +768,14 @@ class AI:
|
|
|
724
768
|
# Add time awareness to instructions with explicit formatting guidance
|
|
725
769
|
time_instructions = f"""
|
|
726
770
|
IMPORTANT: You are time-aware. The current date is {datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")}.
|
|
727
|
-
|
|
771
|
+
|
|
728
772
|
TIME RESPONSE RULES:
|
|
729
773
|
1. When asked about the current time, ONLY use the check_time tool and respond with EXACTLY what it returns
|
|
730
774
|
2. NEVER add UTC time when the check_time tool returns local time
|
|
731
775
|
3. NEVER convert between timezones unless explicitly requested
|
|
732
776
|
4. NEVER mention timezone offsets (like "X hours behind UTC") unless explicitly asked
|
|
733
777
|
5. Local time is the ONLY time that should be mentioned in your response
|
|
734
|
-
|
|
778
|
+
|
|
735
779
|
Default timezone: {default_timezone} (use this when user's timezone is unknown)
|
|
736
780
|
"""
|
|
737
781
|
self._instructions = self._instructions + "\n\n" + time_instructions
|
|
@@ -765,19 +809,20 @@ class AI:
|
|
|
765
809
|
|
|
766
810
|
# Extract structured knowledge
|
|
767
811
|
prompt = f"""
|
|
768
|
-
Based on these search results about "{topic}", extract 3-5 factual insights
|
|
812
|
+
Based on these search results about "{topic}", extract 3-5 factual insights
|
|
769
813
|
worth adding to our collective knowledge.
|
|
770
|
-
|
|
814
|
+
|
|
771
815
|
Search results:
|
|
772
816
|
{search_results}
|
|
773
|
-
|
|
817
|
+
|
|
774
818
|
Format each insight as a JSON object with:
|
|
775
819
|
1. "fact": The factual information
|
|
776
820
|
2. "relevance": Short explanation of why this is generally useful
|
|
777
|
-
|
|
821
|
+
|
|
778
822
|
Return ONLY a valid JSON array. Example:
|
|
779
823
|
[
|
|
780
|
-
{{"fact": "Topic X has property Y",
|
|
824
|
+
{{"fact": "Topic X has property Y",
|
|
825
|
+
"relevance": "Important for understanding Z"}}
|
|
781
826
|
]
|
|
782
827
|
"""
|
|
783
828
|
|
|
@@ -1302,6 +1347,161 @@ class AI:
|
|
|
1302
1347
|
return func
|
|
1303
1348
|
|
|
1304
1349
|
|
|
1350
|
+
class HumanAgent:
|
|
1351
|
+
"""Represents a human operator in the agent swarm."""
|
|
1352
|
+
|
|
1353
|
+
def __init__(
|
|
1354
|
+
self,
|
|
1355
|
+
agent_id: str,
|
|
1356
|
+
name: str,
|
|
1357
|
+
specialization: str,
|
|
1358
|
+
notification_handler: Callable = None,
|
|
1359
|
+
availability_status: Literal["available",
|
|
1360
|
+
"busy", "offline"] = "available",
|
|
1361
|
+
):
|
|
1362
|
+
"""Initialize a human agent.
|
|
1363
|
+
|
|
1364
|
+
Args:
|
|
1365
|
+
agent_id (str): Unique identifier for this human agent
|
|
1366
|
+
name (str): Display name of the human agent
|
|
1367
|
+
specialization (str): Area of expertise description
|
|
1368
|
+
notification_handler (Callable, optional): Function to call when agent receives a handoff
|
|
1369
|
+
availability_status (str): Current availability of the human agent
|
|
1370
|
+
"""
|
|
1371
|
+
self.agent_id = agent_id
|
|
1372
|
+
self.name = name
|
|
1373
|
+
self.specialization = specialization
|
|
1374
|
+
self.notification_handler = notification_handler
|
|
1375
|
+
self.availability_status = availability_status
|
|
1376
|
+
self.current_tickets = {} # Tracks tickets assigned to this human
|
|
1377
|
+
|
|
1378
|
+
async def receive_handoff(
|
|
1379
|
+
self, ticket_id: str, user_id: str, query: str, context: str
|
|
1380
|
+
) -> bool:
|
|
1381
|
+
"""Handle receiving a ticket from an AI agent or another human.
|
|
1382
|
+
|
|
1383
|
+
Args:
|
|
1384
|
+
ticket_id: Unique identifier for this conversation thread
|
|
1385
|
+
user_id: End user identifier
|
|
1386
|
+
query: The user's question or issue
|
|
1387
|
+
context: Conversation context and history
|
|
1388
|
+
|
|
1389
|
+
Returns:
|
|
1390
|
+
bool: Whether the handoff was accepted
|
|
1391
|
+
"""
|
|
1392
|
+
if self.availability_status != "available":
|
|
1393
|
+
return False
|
|
1394
|
+
|
|
1395
|
+
# Add to current tickets
|
|
1396
|
+
self.current_tickets[ticket_id] = {
|
|
1397
|
+
"user_id": user_id,
|
|
1398
|
+
"query": query,
|
|
1399
|
+
"context": context,
|
|
1400
|
+
"status": "pending",
|
|
1401
|
+
"received_at": datetime.datetime.now(datetime.timezone.utc),
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
# Notify the human operator through the configured handler
|
|
1405
|
+
if self.notification_handler:
|
|
1406
|
+
await self.notification_handler(
|
|
1407
|
+
agent_id=self.agent_id,
|
|
1408
|
+
ticket_id=ticket_id,
|
|
1409
|
+
user_id=user_id,
|
|
1410
|
+
query=query,
|
|
1411
|
+
context=context,
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
return True
|
|
1415
|
+
|
|
1416
|
+
async def respond(self, ticket_id: str, response: str) -> Dict[str, Any]:
|
|
1417
|
+
"""Submit a response to a user query.
|
|
1418
|
+
|
|
1419
|
+
Args:
|
|
1420
|
+
ticket_id: The ticket identifier
|
|
1421
|
+
response: The human agent's response text
|
|
1422
|
+
|
|
1423
|
+
Returns:
|
|
1424
|
+
Dict with response details and status
|
|
1425
|
+
"""
|
|
1426
|
+
if ticket_id not in self.current_tickets:
|
|
1427
|
+
return {"status": "error", "message": "Ticket not found"}
|
|
1428
|
+
|
|
1429
|
+
ticket = self.current_tickets[ticket_id]
|
|
1430
|
+
ticket["response"] = response
|
|
1431
|
+
ticket["response_time"] = datetime.datetime.now(datetime.timezone.utc)
|
|
1432
|
+
ticket["status"] = "responded"
|
|
1433
|
+
|
|
1434
|
+
return {
|
|
1435
|
+
"status": "success",
|
|
1436
|
+
"ticket_id": ticket_id,
|
|
1437
|
+
"user_id": ticket["user_id"],
|
|
1438
|
+
"response": response,
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
async def handoff_to(
|
|
1442
|
+
self, ticket_id: str, target_agent_id: str, reason: str
|
|
1443
|
+
) -> bool:
|
|
1444
|
+
"""Hand off a ticket to another agent (AI or human).
|
|
1445
|
+
|
|
1446
|
+
Args:
|
|
1447
|
+
ticket_id: The ticket to hand off
|
|
1448
|
+
target_agent_id: Agent to transfer the ticket to
|
|
1449
|
+
reason: Reason for the handoff
|
|
1450
|
+
|
|
1451
|
+
Returns:
|
|
1452
|
+
bool: Whether handoff was successful
|
|
1453
|
+
"""
|
|
1454
|
+
if ticket_id not in self.current_tickets:
|
|
1455
|
+
return False
|
|
1456
|
+
|
|
1457
|
+
# This just marks it for handoff - the actual handoff is handled by the swarm
|
|
1458
|
+
self.current_tickets[ticket_id]["status"] = "handoff_requested"
|
|
1459
|
+
self.current_tickets[ticket_id]["handoff_target"] = target_agent_id
|
|
1460
|
+
self.current_tickets[ticket_id]["handoff_reason"] = reason
|
|
1461
|
+
|
|
1462
|
+
return True
|
|
1463
|
+
|
|
1464
|
+
def update_availability(
|
|
1465
|
+
self, status: Literal["available", "busy", "offline"]
|
|
1466
|
+
) -> None:
|
|
1467
|
+
"""Update the availability status of this human agent."""
|
|
1468
|
+
self.availability_status = status
|
|
1469
|
+
|
|
1470
|
+
async def check_pending_tickets(self) -> str:
|
|
1471
|
+
"""Get a summary of pending tickets for this human agent."""
|
|
1472
|
+
if not self.current_tickets:
|
|
1473
|
+
return "You have no pending tickets."
|
|
1474
|
+
|
|
1475
|
+
pending_count = sum(
|
|
1476
|
+
1 for t in self.current_tickets.values() if t["status"] == "pending"
|
|
1477
|
+
)
|
|
1478
|
+
result = [f"You have {pending_count} pending tickets"]
|
|
1479
|
+
|
|
1480
|
+
# Add details for each pending ticket
|
|
1481
|
+
for ticket_id, ticket in self.current_tickets.items():
|
|
1482
|
+
if ticket["status"] == "pending":
|
|
1483
|
+
received_time = ticket.get(
|
|
1484
|
+
"received_at", datetime.datetime.now(datetime.timezone.utc)
|
|
1485
|
+
)
|
|
1486
|
+
time_diff = datetime.datetime.now(
|
|
1487
|
+
datetime.timezone.utc) - received_time
|
|
1488
|
+
hours_ago = round(time_diff.total_seconds() / 3600, 1)
|
|
1489
|
+
|
|
1490
|
+
result.append(
|
|
1491
|
+
f"- Ticket {ticket_id[:8]}... from user {ticket['user_id'][:8]}... ({hours_ago}h ago)"
|
|
1492
|
+
)
|
|
1493
|
+
result.append(
|
|
1494
|
+
f" Query: {ticket['query'][:50]}..."
|
|
1495
|
+
if len(ticket["query"]) > 50
|
|
1496
|
+
else f" Query: {ticket['query']}"
|
|
1497
|
+
)
|
|
1498
|
+
|
|
1499
|
+
if pending_count == 0:
|
|
1500
|
+
result.append("No pending tickets requiring your attention.")
|
|
1501
|
+
|
|
1502
|
+
return "\n".join(result)
|
|
1503
|
+
|
|
1504
|
+
|
|
1305
1505
|
class Swarm:
|
|
1306
1506
|
"""An AI Agent Swarm that coordinates specialized AI agents with handoff capabilities."""
|
|
1307
1507
|
|
|
@@ -1382,7 +1582,330 @@ class Swarm:
|
|
|
1382
1582
|
print(
|
|
1383
1583
|
f"MultiAgentSystem initialized with router model: {router_model}")
|
|
1384
1584
|
|
|
1385
|
-
|
|
1585
|
+
def register_human_agent(
|
|
1586
|
+
self,
|
|
1587
|
+
agent_id: str,
|
|
1588
|
+
name: str,
|
|
1589
|
+
specialization: str,
|
|
1590
|
+
notification_handler: Callable = None,
|
|
1591
|
+
) -> HumanAgent:
|
|
1592
|
+
"""Register a human agent with the swarm.
|
|
1593
|
+
|
|
1594
|
+
Args:
|
|
1595
|
+
agent_id: Unique identifier for this human agent
|
|
1596
|
+
name: Display name of the human agent
|
|
1597
|
+
specialization: Description of expertise
|
|
1598
|
+
notification_handler: Function to call when agent receives handoff
|
|
1599
|
+
|
|
1600
|
+
Returns:
|
|
1601
|
+
The created HumanAgent instance
|
|
1602
|
+
"""
|
|
1603
|
+
# Create human agent instance
|
|
1604
|
+
human_agent = HumanAgent(
|
|
1605
|
+
agent_id=agent_id,
|
|
1606
|
+
name=name,
|
|
1607
|
+
specialization=specialization,
|
|
1608
|
+
notification_handler=notification_handler,
|
|
1609
|
+
)
|
|
1610
|
+
|
|
1611
|
+
# Store in humans registry
|
|
1612
|
+
if not hasattr(self, "human_agents"):
|
|
1613
|
+
self.human_agents = {}
|
|
1614
|
+
self.human_agents[agent_id] = human_agent
|
|
1615
|
+
|
|
1616
|
+
# Add human agent to specialization map
|
|
1617
|
+
self.specializations[agent_id] = f"[HUMAN] {specialization}"
|
|
1618
|
+
|
|
1619
|
+
# Create or update the ticket collection
|
|
1620
|
+
if "tickets" not in self.database.db.list_collection_names():
|
|
1621
|
+
self.database.db.create_collection("tickets")
|
|
1622
|
+
self.tickets = self.database.db["tickets"]
|
|
1623
|
+
|
|
1624
|
+
print(
|
|
1625
|
+
f"Registered human agent: {name}, specialization: {specialization[:50]}..."
|
|
1626
|
+
)
|
|
1627
|
+
|
|
1628
|
+
# Update AI agents with human handoff capabilities
|
|
1629
|
+
self._update_all_handoff_capabilities()
|
|
1630
|
+
|
|
1631
|
+
return human_agent
|
|
1632
|
+
|
|
1633
|
+
def _update_all_handoff_capabilities(self):
|
|
1634
|
+
"""Update all agents with current handoff capabilities for both AI and human agents."""
|
|
1635
|
+
# Get all AI agent names
|
|
1636
|
+
ai_agent_names = list(self.agents.keys())
|
|
1637
|
+
|
|
1638
|
+
# Get all human agent names
|
|
1639
|
+
human_agent_names = (
|
|
1640
|
+
list(self.human_agents.keys()) if hasattr(
|
|
1641
|
+
self, "human_agents") else []
|
|
1642
|
+
)
|
|
1643
|
+
|
|
1644
|
+
# For each AI agent, update its handoff tools
|
|
1645
|
+
for agent_name, agent in self.agents.items():
|
|
1646
|
+
# Get available target agents (both AI and human)
|
|
1647
|
+
available_ai_targets = [
|
|
1648
|
+
name for name in ai_agent_names if name != agent_name
|
|
1649
|
+
]
|
|
1650
|
+
available_targets = available_ai_targets + human_agent_names
|
|
1651
|
+
|
|
1652
|
+
# First remove any existing handoff tools
|
|
1653
|
+
agent._tools = [
|
|
1654
|
+
t for t in agent._tools if t["function"]["name"] != "request_handoff"
|
|
1655
|
+
]
|
|
1656
|
+
|
|
1657
|
+
# Create updated handoff tool with both AI and human targets
|
|
1658
|
+
def create_handoff_tool(current_agent_name, available_targets_list):
|
|
1659
|
+
def request_handoff(target_agent: str, reason: str) -> str:
|
|
1660
|
+
"""Request an immediate handoff to another agent (AI or human).
|
|
1661
|
+
This is an INTERNAL SYSTEM TOOL. The user will NOT see your reasoning about the handoff.
|
|
1662
|
+
Use this tool IMMEDIATELY when a query is outside your expertise.
|
|
1663
|
+
|
|
1664
|
+
Args:
|
|
1665
|
+
target_agent: Name of agent to transfer to. MUST be one of: {', '.join(available_targets_list)}.
|
|
1666
|
+
DO NOT INVENT NEW NAMES OR VARIATIONS. Use EXACTLY one of these names.
|
|
1667
|
+
reason: Brief explanation of why this question requires the specialist
|
|
1668
|
+
|
|
1669
|
+
Returns:
|
|
1670
|
+
str: Empty string - the handoff is handled internally
|
|
1671
|
+
"""
|
|
1672
|
+
# Validate target agent exists (either AI or human)
|
|
1673
|
+
is_human_target = target_agent in human_agent_names
|
|
1674
|
+
is_ai_target = target_agent in ai_agent_names
|
|
1675
|
+
|
|
1676
|
+
if not (is_human_target or is_ai_target):
|
|
1677
|
+
print(
|
|
1678
|
+
f"[HANDOFF WARNING] Invalid target '{target_agent}'")
|
|
1679
|
+
if available_targets_list:
|
|
1680
|
+
original_target = target_agent
|
|
1681
|
+
target_agent = available_targets_list[0]
|
|
1682
|
+
print(
|
|
1683
|
+
f"[HANDOFF CORRECTION] Redirecting from '{original_target}' to '{target_agent}'"
|
|
1684
|
+
)
|
|
1685
|
+
else:
|
|
1686
|
+
print(
|
|
1687
|
+
"[HANDOFF ERROR] No valid target agents available")
|
|
1688
|
+
return ""
|
|
1689
|
+
|
|
1690
|
+
print(
|
|
1691
|
+
f"[HANDOFF TOOL CALLED] {current_agent_name} -> {target_agent}: {reason}"
|
|
1692
|
+
)
|
|
1693
|
+
|
|
1694
|
+
# Set handoff info - now includes flag for whether target is human
|
|
1695
|
+
agent._handoff_info = {
|
|
1696
|
+
"target": target_agent,
|
|
1697
|
+
"reason": reason,
|
|
1698
|
+
"is_human_target": is_human_target,
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
# Return empty string - the actual handoff happens in the process method
|
|
1702
|
+
return ""
|
|
1703
|
+
|
|
1704
|
+
return request_handoff
|
|
1705
|
+
|
|
1706
|
+
# Use the factory to create a properly-bound tool function
|
|
1707
|
+
handoff_tool = create_handoff_tool(agent_name, available_targets)
|
|
1708
|
+
|
|
1709
|
+
# Initialize handoff info attribute
|
|
1710
|
+
agent._handoff_info = None
|
|
1711
|
+
|
|
1712
|
+
# Add the updated handoff tool with proper closure
|
|
1713
|
+
agent.add_tool(handoff_tool)
|
|
1714
|
+
|
|
1715
|
+
# Update agent instructions with handoff guidance including human agents
|
|
1716
|
+
ai_handoff_examples = "\n".join(
|
|
1717
|
+
[
|
|
1718
|
+
f" - `{name}` (AI: {self.specializations[name][:40]}...)"
|
|
1719
|
+
for name in available_ai_targets
|
|
1720
|
+
]
|
|
1721
|
+
)
|
|
1722
|
+
human_handoff_examples = "\n".join(
|
|
1723
|
+
[
|
|
1724
|
+
f" - `{name}` (HUMAN: {self.specializations[name].replace('[HUMAN] ', '')[:40]}...)"
|
|
1725
|
+
for name in human_agent_names
|
|
1726
|
+
]
|
|
1727
|
+
)
|
|
1728
|
+
|
|
1729
|
+
handoff_instructions = f"""
|
|
1730
|
+
STRICT HANDOFF GUIDANCE:
|
|
1731
|
+
1. You must use ONLY the EXACT agent names listed below for handoffs.
|
|
1732
|
+
|
|
1733
|
+
AI AGENTS (available immediately):
|
|
1734
|
+
{ai_handoff_examples}
|
|
1735
|
+
|
|
1736
|
+
HUMAN AGENTS (might have response delay):
|
|
1737
|
+
{human_handoff_examples}
|
|
1738
|
+
|
|
1739
|
+
2. DO NOT INVENT OR MODIFY AGENT NAMES.
|
|
1740
|
+
|
|
1741
|
+
3. ONLY these EXACT agent names will work for handoffs: {', '.join(available_targets)}
|
|
1742
|
+
|
|
1743
|
+
4. Use human agents ONLY when:
|
|
1744
|
+
- The question truly requires human judgment or expertise
|
|
1745
|
+
- The user explicitly asks for a human agent
|
|
1746
|
+
- The task involves confidential information that AI shouldn't access
|
|
1747
|
+
"""
|
|
1748
|
+
|
|
1749
|
+
# Update agent instructions with handoff guidance
|
|
1750
|
+
agent._instructions = (
|
|
1751
|
+
re.sub(
|
|
1752
|
+
r"STRICT HANDOFF GUIDANCE:.*?(?=\n\n)",
|
|
1753
|
+
handoff_instructions,
|
|
1754
|
+
agent._instructions,
|
|
1755
|
+
flags=re.DOTALL,
|
|
1756
|
+
)
|
|
1757
|
+
if "STRICT HANDOFF GUIDANCE" in agent._instructions
|
|
1758
|
+
else agent._instructions + "\n\n" + handoff_instructions
|
|
1759
|
+
)
|
|
1760
|
+
|
|
1761
|
+
print("Updated handoff capabilities for all agents with AI and human targets")
|
|
1762
|
+
|
|
1763
|
+
async def process_human_response(
|
|
1764
|
+
self,
|
|
1765
|
+
human_agent_id: str,
|
|
1766
|
+
ticket_id: str,
|
|
1767
|
+
response: str,
|
|
1768
|
+
handoff_to: str = None,
|
|
1769
|
+
handoff_reason: str = None,
|
|
1770
|
+
) -> Dict[str, Any]:
|
|
1771
|
+
"""Process a response from a human agent.
|
|
1772
|
+
|
|
1773
|
+
Args:
|
|
1774
|
+
human_agent_id: ID of the human agent responding
|
|
1775
|
+
ticket_id: Ticket identifier
|
|
1776
|
+
response: Human agent's response text
|
|
1777
|
+
handoff_to: Optional target agent for handoff
|
|
1778
|
+
handoff_reason: Optional reason for handoff
|
|
1779
|
+
|
|
1780
|
+
Returns:
|
|
1781
|
+
Dict with status and details
|
|
1782
|
+
"""
|
|
1783
|
+
# Verify the human agent exists
|
|
1784
|
+
if not hasattr(self, "human_agents") or human_agent_id not in self.human_agents:
|
|
1785
|
+
return {"status": "error", "message": "Human agent not found"}
|
|
1786
|
+
|
|
1787
|
+
human_agent = self.human_agents[human_agent_id]
|
|
1788
|
+
|
|
1789
|
+
# Get the ticket
|
|
1790
|
+
ticket = self.tickets.find_one({"_id": ticket_id})
|
|
1791
|
+
if not ticket:
|
|
1792
|
+
return {"status": "error", "message": "Ticket not found"}
|
|
1793
|
+
|
|
1794
|
+
# Check if ticket is assigned to this agent
|
|
1795
|
+
if ticket.get("assigned_to") != human_agent_id:
|
|
1796
|
+
return {"status": "error", "message": "Ticket not assigned to this agent"}
|
|
1797
|
+
|
|
1798
|
+
# If handoff requested
|
|
1799
|
+
if handoff_to:
|
|
1800
|
+
# Determine if target is human or AI
|
|
1801
|
+
is_human_target = (
|
|
1802
|
+
hasattr(self, "human_agents") and handoff_to in self.human_agents
|
|
1803
|
+
)
|
|
1804
|
+
|
|
1805
|
+
is_ai_target = handoff_to in self.agents
|
|
1806
|
+
|
|
1807
|
+
if not (is_human_target or is_ai_target):
|
|
1808
|
+
return {"status": "error", "message": "Invalid handoff target"}
|
|
1809
|
+
|
|
1810
|
+
# Record the handoff
|
|
1811
|
+
self.handoffs.insert_one(
|
|
1812
|
+
{
|
|
1813
|
+
"ticket_id": ticket_id,
|
|
1814
|
+
"user_id": ticket["user_id"],
|
|
1815
|
+
"from_agent": human_agent_id,
|
|
1816
|
+
"to_agent": handoff_to,
|
|
1817
|
+
"reason": handoff_reason or "Human agent handoff",
|
|
1818
|
+
"query": ticket["query"],
|
|
1819
|
+
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
|
1820
|
+
}
|
|
1821
|
+
)
|
|
1822
|
+
|
|
1823
|
+
# Update ticket status
|
|
1824
|
+
self.tickets.update_one(
|
|
1825
|
+
{"_id": ticket_id},
|
|
1826
|
+
{
|
|
1827
|
+
"$set": {
|
|
1828
|
+
"assigned_to": handoff_to,
|
|
1829
|
+
"status": "transferred",
|
|
1830
|
+
"human_response": response,
|
|
1831
|
+
"handoff_reason": handoff_reason,
|
|
1832
|
+
"updated_at": datetime.datetime.now(datetime.timezone.utc),
|
|
1833
|
+
}
|
|
1834
|
+
},
|
|
1835
|
+
)
|
|
1836
|
+
|
|
1837
|
+
# Process based on target type
|
|
1838
|
+
if is_human_target:
|
|
1839
|
+
# Human-to-human handoff
|
|
1840
|
+
target_human = self.human_agents[handoff_to]
|
|
1841
|
+
|
|
1842
|
+
# Get updated context including the human's response
|
|
1843
|
+
context = (
|
|
1844
|
+
ticket.get("context", "")
|
|
1845
|
+
+ f"\n\nHuman agent {human_agent.name}: {response}"
|
|
1846
|
+
)
|
|
1847
|
+
|
|
1848
|
+
# Try to hand off to the human agent
|
|
1849
|
+
accepted = await target_human.receive_handoff(
|
|
1850
|
+
ticket_id=ticket_id,
|
|
1851
|
+
user_id=ticket["user_id"],
|
|
1852
|
+
query=ticket["query"],
|
|
1853
|
+
context=context,
|
|
1854
|
+
)
|
|
1855
|
+
|
|
1856
|
+
if accepted:
|
|
1857
|
+
return {
|
|
1858
|
+
"status": "success",
|
|
1859
|
+
"message": f"Transferred to human agent {target_human.name}",
|
|
1860
|
+
"ticket_id": ticket_id,
|
|
1861
|
+
}
|
|
1862
|
+
else:
|
|
1863
|
+
return {
|
|
1864
|
+
"status": "warning",
|
|
1865
|
+
"message": f"Human agent {target_human.name} is unavailable",
|
|
1866
|
+
}
|
|
1867
|
+
else:
|
|
1868
|
+
# Human-to-AI handoff
|
|
1869
|
+
target_ai = self.agents[handoff_to]
|
|
1870
|
+
|
|
1871
|
+
# Return details for AI processing
|
|
1872
|
+
return {
|
|
1873
|
+
"status": "success",
|
|
1874
|
+
"message": f"Transferred to AI agent {handoff_to}",
|
|
1875
|
+
"ticket_id": ticket_id,
|
|
1876
|
+
"ai_agent": target_ai,
|
|
1877
|
+
"user_id": ticket["user_id"],
|
|
1878
|
+
"query": ticket["query"],
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
# No handoff - just record the human response
|
|
1882
|
+
self.tickets.update_one(
|
|
1883
|
+
{"_id": ticket_id},
|
|
1884
|
+
{
|
|
1885
|
+
"$set": {
|
|
1886
|
+
"status": "resolved",
|
|
1887
|
+
"human_response": response,
|
|
1888
|
+
"resolved_at": datetime.datetime.now(datetime.timezone.utc),
|
|
1889
|
+
}
|
|
1890
|
+
},
|
|
1891
|
+
)
|
|
1892
|
+
|
|
1893
|
+
# Also record in messages for continuity
|
|
1894
|
+
self.database.save_message(
|
|
1895
|
+
ticket["user_id"],
|
|
1896
|
+
{
|
|
1897
|
+
"message": ticket["query"],
|
|
1898
|
+
"response": response,
|
|
1899
|
+
"human_agent": human_agent_id,
|
|
1900
|
+
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
|
1901
|
+
},
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
return {
|
|
1905
|
+
"status": "success",
|
|
1906
|
+
"message": "Response recorded",
|
|
1907
|
+
"ticket_id": ticket_id,
|
|
1908
|
+
}
|
|
1386
1909
|
|
|
1387
1910
|
async def extract_and_store_insights(
|
|
1388
1911
|
self, user_id: str, conversation: dict
|
|
@@ -1399,7 +1922,7 @@ class Swarm:
|
|
|
1399
1922
|
Review this conversation and extract 0-3 IMPORTANT factual insights worth remembering for future users.
|
|
1400
1923
|
Only extract FACTUAL information that would be valuable across multiple conversations.
|
|
1401
1924
|
Do NOT include opinions, personal preferences, or user-specific details.
|
|
1402
|
-
|
|
1925
|
+
|
|
1403
1926
|
Conversation:
|
|
1404
1927
|
User: {conversation.get('message', '')}
|
|
1405
1928
|
Assistant: {conversation.get('response', '')}
|
|
@@ -1737,11 +2260,11 @@ class Swarm:
|
|
|
1737
2260
|
STRICT HANDOFF GUIDANCE:
|
|
1738
2261
|
1. You must use ONLY the EXACT agent names listed below for handoffs:
|
|
1739
2262
|
{handoff_examples}
|
|
1740
|
-
|
|
2263
|
+
|
|
1741
2264
|
2. DO NOT INVENT, MODIFY, OR CREATE NEW AGENT NAMES like "Smart Contract Developer" or "Technical Expert"
|
|
1742
|
-
|
|
2265
|
+
|
|
1743
2266
|
3. For technical implementation questions, use "developer" (not variations like "developer expert" or "tech specialist")
|
|
1744
|
-
|
|
2267
|
+
|
|
1745
2268
|
4. ONLY these EXACT agent names will work for handoffs: {', '.join(available_targets)}
|
|
1746
2269
|
"""
|
|
1747
2270
|
|
|
@@ -1763,6 +2286,11 @@ class Swarm:
|
|
|
1763
2286
|
timezone (str, optional): User-specific timezone
|
|
1764
2287
|
"""
|
|
1765
2288
|
try:
|
|
2289
|
+
# Handle special ticket management commands
|
|
2290
|
+
if user_text.lower().startswith("!ticket"):
|
|
2291
|
+
yield await self._process_ticket_commands(user_id, user_text)
|
|
2292
|
+
return
|
|
2293
|
+
|
|
1766
2294
|
# Handle special commands
|
|
1767
2295
|
if user_text.strip().lower().startswith("!memory "):
|
|
1768
2296
|
query = user_text[8:].strip()
|
|
@@ -1818,48 +2346,299 @@ class Swarm:
|
|
|
1818
2346
|
print(traceback.format_exc())
|
|
1819
2347
|
yield "\n\nI apologize for the technical difficulty.\n\n"
|
|
1820
2348
|
|
|
2349
|
+
async def _process_ticket_commands(self, user_id: str, command: str) -> str:
|
|
2350
|
+
"""Process ticket management commands directly in chat."""
|
|
2351
|
+
parts = command.strip().split(" ", 2)
|
|
2352
|
+
|
|
2353
|
+
# Check if user is a registered human agent
|
|
2354
|
+
is_human_agent = False
|
|
2355
|
+
human_agent = None
|
|
2356
|
+
if hasattr(self, "human_agents"):
|
|
2357
|
+
for agent_id, agent in self.human_agents.items():
|
|
2358
|
+
if agent_id == user_id:
|
|
2359
|
+
is_human_agent = True
|
|
2360
|
+
human_agent = agent
|
|
2361
|
+
break
|
|
2362
|
+
|
|
2363
|
+
if not is_human_agent:
|
|
2364
|
+
return "⚠️ Only registered human agents can use ticket commands."
|
|
2365
|
+
|
|
2366
|
+
# Process various ticket commands
|
|
2367
|
+
if len(parts) > 1:
|
|
2368
|
+
action = parts[1].lower()
|
|
2369
|
+
|
|
2370
|
+
# List tickets assigned to this human agent
|
|
2371
|
+
if action == "list":
|
|
2372
|
+
tickets = list(
|
|
2373
|
+
self.tickets.find(
|
|
2374
|
+
{"assigned_to": user_id, "status": "pending"})
|
|
2375
|
+
)
|
|
2376
|
+
|
|
2377
|
+
if not tickets:
|
|
2378
|
+
return "📋 You have no pending tickets."
|
|
2379
|
+
|
|
2380
|
+
ticket_list = ["## Your Pending Tickets", ""]
|
|
2381
|
+
for i, ticket in enumerate(tickets, 1):
|
|
2382
|
+
created = ticket.get(
|
|
2383
|
+
"created_at", datetime.datetime.now(
|
|
2384
|
+
datetime.timezone.utc)
|
|
2385
|
+
)
|
|
2386
|
+
time_ago = self._format_time_ago(created)
|
|
2387
|
+
|
|
2388
|
+
ticket_list.append(
|
|
2389
|
+
f"**{i}. Ticket {ticket['_id'][:8]}...** ({time_ago})"
|
|
2390
|
+
)
|
|
2391
|
+
ticket_list.append(
|
|
2392
|
+
f"Query: {ticket.get('query', 'No query')[:100]}..."
|
|
2393
|
+
)
|
|
2394
|
+
ticket_list.append("")
|
|
2395
|
+
|
|
2396
|
+
return "\n".join(ticket_list)
|
|
2397
|
+
|
|
2398
|
+
# View a specific ticket
|
|
2399
|
+
elif action == "view" and len(parts) > 2:
|
|
2400
|
+
ticket_id = parts[2]
|
|
2401
|
+
ticket = self.tickets.find_one(
|
|
2402
|
+
{"_id": {"$regex": f"^{ticket_id}.*"}, "assigned_to": user_id}
|
|
2403
|
+
)
|
|
2404
|
+
|
|
2405
|
+
if not ticket:
|
|
2406
|
+
return f"⚠️ No ticket found with ID starting with '{ticket_id}'"
|
|
2407
|
+
|
|
2408
|
+
context = ticket.get("context", "No previous context")
|
|
2409
|
+
query = ticket.get("query", "No query")
|
|
2410
|
+
created = ticket.get(
|
|
2411
|
+
"created_at", datetime.datetime.now(datetime.timezone.utc)
|
|
2412
|
+
)
|
|
2413
|
+
time_ago = self._format_time_ago(created)
|
|
2414
|
+
|
|
2415
|
+
return f"""## Ticket Details ({ticket['_id']})
|
|
2416
|
+
|
|
2417
|
+
Status: {ticket.get('status', 'pending')}
|
|
2418
|
+
Created: {time_ago}
|
|
2419
|
+
|
|
2420
|
+
### User Query
|
|
2421
|
+
{query}
|
|
2422
|
+
|
|
2423
|
+
### Conversation Context
|
|
2424
|
+
{context}
|
|
2425
|
+
"""
|
|
2426
|
+
|
|
2427
|
+
# Respond to a ticket
|
|
2428
|
+
elif action == "respond" and len(parts) > 2:
|
|
2429
|
+
# Format: !ticket respond ticket_id response text here
|
|
2430
|
+
response_parts = parts[2].split(" ", 1)
|
|
2431
|
+
if len(response_parts) < 2:
|
|
2432
|
+
return "⚠️ Format: !ticket respond ticket_id your response text"
|
|
2433
|
+
|
|
2434
|
+
ticket_id = response_parts[0]
|
|
2435
|
+
response_text = response_parts[1]
|
|
2436
|
+
|
|
2437
|
+
# Find the ticket
|
|
2438
|
+
ticket = self.tickets.find_one(
|
|
2439
|
+
{"_id": {"$regex": f"^{ticket_id}.*"}, "assigned_to": user_id}
|
|
2440
|
+
)
|
|
2441
|
+
if not ticket:
|
|
2442
|
+
return f"⚠️ No ticket found with ID starting with '{ticket_id}'"
|
|
2443
|
+
|
|
2444
|
+
# Process the response
|
|
2445
|
+
response_result = await human_agent.respond(
|
|
2446
|
+
ticket["_id"], response_text
|
|
2447
|
+
)
|
|
2448
|
+
|
|
2449
|
+
# Check if response was successful
|
|
2450
|
+
if response_result.get("status") != "success":
|
|
2451
|
+
return f"⚠️ Failed to respond to ticket: {response_result.get('message', 'Unknown error')}"
|
|
2452
|
+
|
|
2453
|
+
# Update ticket and save response
|
|
2454
|
+
self.tickets.update_one(
|
|
2455
|
+
{"_id": ticket["_id"]},
|
|
2456
|
+
{
|
|
2457
|
+
"$set": {
|
|
2458
|
+
"status": "resolved",
|
|
2459
|
+
"human_response": response_text,
|
|
2460
|
+
"resolved_at": datetime.datetime.now(datetime.timezone.utc),
|
|
2461
|
+
}
|
|
2462
|
+
},
|
|
2463
|
+
)
|
|
2464
|
+
|
|
2465
|
+
# Also record in messages for continuity
|
|
2466
|
+
self.database.save_message(
|
|
2467
|
+
ticket["user_id"],
|
|
2468
|
+
{
|
|
2469
|
+
"message": ticket["query"],
|
|
2470
|
+
"response": response_text,
|
|
2471
|
+
"human_agent": user_id,
|
|
2472
|
+
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
|
2473
|
+
},
|
|
2474
|
+
)
|
|
2475
|
+
|
|
2476
|
+
return f"✅ Response recorded for ticket {ticket_id}. The ticket has been marked as resolved."
|
|
2477
|
+
|
|
2478
|
+
# Transfer a ticket
|
|
2479
|
+
elif action == "transfer" and len(parts) > 2:
|
|
2480
|
+
# Format: !ticket transfer ticket_id target_agent [reason]
|
|
2481
|
+
transfer_parts = parts[2].split(" ", 2)
|
|
2482
|
+
if len(transfer_parts) < 2:
|
|
2483
|
+
return "⚠️ Format: !ticket transfer ticket_id target_agent [reason]"
|
|
2484
|
+
|
|
2485
|
+
ticket_id = transfer_parts[0]
|
|
2486
|
+
target_agent = transfer_parts[1]
|
|
2487
|
+
reason = (
|
|
2488
|
+
transfer_parts[2]
|
|
2489
|
+
if len(transfer_parts) > 2
|
|
2490
|
+
else "Human agent transfer"
|
|
2491
|
+
)
|
|
2492
|
+
|
|
2493
|
+
# Find the ticket
|
|
2494
|
+
ticket = self.tickets.find_one(
|
|
2495
|
+
{"_id": {"$regex": f"^{ticket_id}.*"}, "assigned_to": user_id}
|
|
2496
|
+
)
|
|
2497
|
+
if not ticket:
|
|
2498
|
+
return f"⚠️ No ticket found with ID starting with '{ticket_id}'"
|
|
2499
|
+
|
|
2500
|
+
# Handle transfer logic
|
|
2501
|
+
# Determine if target is human or AI
|
|
2502
|
+
is_human_target = (
|
|
2503
|
+
hasattr(
|
|
2504
|
+
self, "human_agents") and target_agent in self.human_agents
|
|
2505
|
+
)
|
|
2506
|
+
|
|
2507
|
+
is_ai_target = target_agent in self.agents
|
|
2508
|
+
|
|
2509
|
+
if not (is_human_target or is_ai_target):
|
|
2510
|
+
return f"⚠️ Invalid transfer target '{target_agent}'. Must be a valid agent name."
|
|
2511
|
+
|
|
2512
|
+
# Record the handoff
|
|
2513
|
+
self.handoffs.insert_one(
|
|
2514
|
+
{
|
|
2515
|
+
"ticket_id": ticket["_id"],
|
|
2516
|
+
"user_id": ticket["user_id"],
|
|
2517
|
+
"from_agent": user_id,
|
|
2518
|
+
"to_agent": target_agent,
|
|
2519
|
+
"reason": reason,
|
|
2520
|
+
"query": ticket["query"],
|
|
2521
|
+
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
|
2522
|
+
}
|
|
2523
|
+
)
|
|
2524
|
+
|
|
2525
|
+
# Update ticket status in database
|
|
2526
|
+
self.tickets.update_one(
|
|
2527
|
+
{"_id": ticket["_id"]},
|
|
2528
|
+
{
|
|
2529
|
+
"$set": {
|
|
2530
|
+
"assigned_to": target_agent,
|
|
2531
|
+
"status": "transferred",
|
|
2532
|
+
"handoff_reason": reason,
|
|
2533
|
+
"updated_at": datetime.datetime.now(datetime.timezone.utc),
|
|
2534
|
+
}
|
|
2535
|
+
},
|
|
2536
|
+
)
|
|
2537
|
+
|
|
2538
|
+
# Process based on target type
|
|
2539
|
+
if is_human_target:
|
|
2540
|
+
# Human-to-human handoff
|
|
2541
|
+
target_human = self.human_agents[target_agent]
|
|
2542
|
+
|
|
2543
|
+
# Get updated context including the current human's notes
|
|
2544
|
+
context = (
|
|
2545
|
+
ticket.get("context", "")
|
|
2546
|
+
+ f"\n\nHuman agent {human_agent.name}: Transferring with note: {reason}"
|
|
2547
|
+
)
|
|
2548
|
+
|
|
2549
|
+
# Try to hand off to the human agent
|
|
2550
|
+
accepted = await target_human.receive_handoff(
|
|
2551
|
+
ticket_id=ticket["_id"],
|
|
2552
|
+
user_id=ticket["user_id"],
|
|
2553
|
+
query=ticket["query"],
|
|
2554
|
+
context=context,
|
|
2555
|
+
)
|
|
2556
|
+
|
|
2557
|
+
if accepted:
|
|
2558
|
+
return (
|
|
2559
|
+
f"✅ Ticket transferred to human agent {target_human.name}"
|
|
2560
|
+
)
|
|
2561
|
+
else:
|
|
2562
|
+
return f"⚠️ Human agent {target_human.name} is unavailable. Ticket is still transferred but pending their acceptance."
|
|
2563
|
+
else:
|
|
2564
|
+
# Human-to-AI handoff
|
|
2565
|
+
return f"✅ Ticket transferred to AI agent {target_agent}. The AI will handle this in the user's next interaction."
|
|
2566
|
+
|
|
2567
|
+
# Help command or invalid format
|
|
2568
|
+
help_text = """
|
|
2569
|
+
## Ticket Commands
|
|
2570
|
+
|
|
2571
|
+
- `!ticket list` - Show your pending tickets
|
|
2572
|
+
- `!ticket view [ticket_id]` - View details of a specific ticket
|
|
2573
|
+
- `!ticket respond [ticket_id] [response]` - Respond to a ticket
|
|
2574
|
+
- `!ticket transfer [ticket_id] [target_agent] [reason]` - Transfer ticket to another agent
|
|
2575
|
+
"""
|
|
2576
|
+
return help_text.strip()
|
|
2577
|
+
|
|
2578
|
+
def _format_time_ago(self, timestamp):
|
|
2579
|
+
"""Format a timestamp as a human-readable time ago string."""
|
|
2580
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
2581
|
+
diff = now - timestamp
|
|
2582
|
+
|
|
2583
|
+
if diff.days > 0:
|
|
2584
|
+
return f"{diff.days} days ago"
|
|
2585
|
+
|
|
2586
|
+
hours, remainder = divmod(diff.seconds, 3600)
|
|
2587
|
+
minutes, seconds = divmod(remainder, 60)
|
|
2588
|
+
|
|
2589
|
+
if hours > 0:
|
|
2590
|
+
return f"{hours} hours ago"
|
|
2591
|
+
if minutes > 0:
|
|
2592
|
+
return f"{minutes} minutes ago"
|
|
2593
|
+
return "just now"
|
|
2594
|
+
|
|
1821
2595
|
async def _stream_response(
|
|
1822
2596
|
self, user_id, user_text, current_agent, timezone=None
|
|
1823
2597
|
) -> AsyncGenerator[str, None]:
|
|
1824
|
-
"""Stream response from an agent, handling potential handoffs."""
|
|
2598
|
+
"""Stream response from an agent, handling potential handoffs to AI or human agents."""
|
|
1825
2599
|
handoff_detected = False
|
|
1826
2600
|
response_started = False
|
|
1827
2601
|
full_response = ""
|
|
2602
|
+
agent_name = None # For the agent's name
|
|
2603
|
+
|
|
2604
|
+
# Get agent name for recording purposes
|
|
2605
|
+
for name, agent in self.agents.items():
|
|
2606
|
+
if agent == current_agent:
|
|
2607
|
+
agent_name = name
|
|
2608
|
+
break
|
|
1828
2609
|
|
|
1829
2610
|
# Get recent feedback for this agent to improve the response
|
|
1830
|
-
agent_name = current_agent.__class__.__name__
|
|
1831
2611
|
recent_feedback = []
|
|
1832
2612
|
|
|
1833
2613
|
if self.enable_critic and hasattr(self, "critic"):
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
.limit(3)
|
|
1841
|
-
)
|
|
1842
|
-
|
|
1843
|
-
if feedback_records:
|
|
1844
|
-
# Extract specific improvement suggestions
|
|
1845
|
-
for record in feedback_records:
|
|
1846
|
-
recent_feedback.append(
|
|
1847
|
-
f"- Improve {record.get('improvement_area')}: {record.get('recommendation')}"
|
|
1848
|
-
)
|
|
1849
|
-
except Exception as e:
|
|
1850
|
-
print(f"Error getting recent feedback: {e}")
|
|
2614
|
+
# Get recent feedback for this specific agent
|
|
2615
|
+
recent_feedback = self.critic.get_agent_feedback(
|
|
2616
|
+
agent_name, limit=3)
|
|
2617
|
+
print(
|
|
2618
|
+
f"Retrieved {len(recent_feedback)} feedback items for agent {agent_name}"
|
|
2619
|
+
)
|
|
1851
2620
|
|
|
1852
2621
|
# Augment user text with feedback instructions if available
|
|
1853
2622
|
augmented_instruction = user_text
|
|
1854
2623
|
if recent_feedback:
|
|
1855
|
-
|
|
2624
|
+
# Create targeted improvement instructions based on past feedback
|
|
2625
|
+
feedback_summary = ""
|
|
2626
|
+
for feedback in recent_feedback:
|
|
2627
|
+
area = feedback.get("improvement_area", "Unknown")
|
|
2628
|
+
recommendation = feedback.get(
|
|
2629
|
+
"recommendation", "No specific recommendation"
|
|
2630
|
+
)
|
|
2631
|
+
feedback_summary += f"- {area}: {recommendation}\n"
|
|
2632
|
+
|
|
2633
|
+
# Add as hidden instructions to the agent
|
|
1856
2634
|
augmented_instruction = f"""
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
{
|
|
2635
|
+
{user_text}
|
|
2636
|
+
|
|
2637
|
+
[SYSTEM NOTE: Apply these improvements from recent feedback:
|
|
2638
|
+
{feedback_summary}
|
|
2639
|
+
The user will not see these instructions.]
|
|
1861
2640
|
"""
|
|
1862
|
-
print(
|
|
2641
|
+
print("Added feedback-based improvement instructions to prompt")
|
|
1863
2642
|
|
|
1864
2643
|
async for chunk in current_agent.text(
|
|
1865
2644
|
user_id, augmented_instruction, timezone, user_text
|
|
@@ -1871,13 +2650,19 @@ class Swarm:
|
|
|
1871
2650
|
if current_agent._handoff_info and not handoff_detected:
|
|
1872
2651
|
handoff_detected = True
|
|
1873
2652
|
target_name = current_agent._handoff_info["target"]
|
|
1874
|
-
target_agent = self.agents[target_name]
|
|
1875
2653
|
reason = current_agent._handoff_info["reason"]
|
|
2654
|
+
is_human_target = current_agent._handoff_info.get(
|
|
2655
|
+
"is_human_target", False
|
|
2656
|
+
)
|
|
1876
2657
|
|
|
1877
|
-
# Record handoff without waiting
|
|
2658
|
+
# Record the handoff without waiting
|
|
1878
2659
|
asyncio.create_task(
|
|
1879
2660
|
self._record_handoff(
|
|
1880
|
-
user_id,
|
|
2661
|
+
user_id,
|
|
2662
|
+
agent_name or "unknown_agent",
|
|
2663
|
+
target_name,
|
|
2664
|
+
reason,
|
|
2665
|
+
user_text,
|
|
1881
2666
|
)
|
|
1882
2667
|
)
|
|
1883
2668
|
|
|
@@ -1885,23 +2670,104 @@ class Swarm:
|
|
|
1885
2670
|
if response_started:
|
|
1886
2671
|
yield "\n\n---\n\n"
|
|
1887
2672
|
|
|
1888
|
-
#
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2673
|
+
# Handle differently based on target type (AI vs human)
|
|
2674
|
+
if is_human_target and hasattr(self, "human_agents"):
|
|
2675
|
+
# Create a ticket in the database
|
|
2676
|
+
ticket_id = str(uuid.uuid4())
|
|
2677
|
+
|
|
2678
|
+
# Get conversation history
|
|
2679
|
+
context = ""
|
|
2680
|
+
if hasattr(current_agent, "get_memory_context"):
|
|
2681
|
+
context = current_agent.get_memory_context(user_id)
|
|
2682
|
+
|
|
2683
|
+
# Store ticket in database
|
|
2684
|
+
self.tickets.insert_one(
|
|
2685
|
+
{
|
|
2686
|
+
"_id": ticket_id,
|
|
2687
|
+
"user_id": user_id,
|
|
2688
|
+
"query": user_text,
|
|
2689
|
+
"context": context,
|
|
2690
|
+
"ai_response_before_handoff": full_response,
|
|
2691
|
+
"assigned_to": target_name,
|
|
2692
|
+
"status": "pending",
|
|
2693
|
+
"created_at": datetime.datetime.now(datetime.timezone.utc),
|
|
2694
|
+
}
|
|
2695
|
+
)
|
|
2696
|
+
|
|
2697
|
+
# Get the human agent
|
|
2698
|
+
human_agent = self.human_agents.get(target_name)
|
|
2699
|
+
|
|
2700
|
+
if human_agent:
|
|
2701
|
+
# Try to hand off to the human agent
|
|
2702
|
+
accepted = await human_agent.receive_handoff(
|
|
2703
|
+
ticket_id=ticket_id,
|
|
2704
|
+
user_id=user_id,
|
|
2705
|
+
query=user_text,
|
|
2706
|
+
context=context,
|
|
2707
|
+
)
|
|
2708
|
+
|
|
2709
|
+
if accepted:
|
|
2710
|
+
human_availability = {
|
|
2711
|
+
"available": "available now",
|
|
2712
|
+
"busy": "busy but will respond soon",
|
|
2713
|
+
"offline": "currently offline but will respond when back",
|
|
2714
|
+
}.get(
|
|
2715
|
+
human_agent.availability_status,
|
|
2716
|
+
"will respond when available",
|
|
2717
|
+
)
|
|
2718
|
+
|
|
2719
|
+
# Provide a friendly handoff message to the user
|
|
2720
|
+
handoff_message = f"""
|
|
2721
|
+
I've transferred your question to {human_agent.name}, who specializes in {human_agent.specialization}.
|
|
2722
|
+
|
|
2723
|
+
A human specialist will provide a more tailored response. They are {human_availability}.
|
|
2724
|
+
|
|
2725
|
+
Your ticket ID is: {ticket_id}
|
|
2726
|
+
"""
|
|
2727
|
+
yield handoff_message.strip()
|
|
2728
|
+
else:
|
|
2729
|
+
# Human agent couldn't accept - fall back to an AI agent
|
|
2730
|
+
yield "I tried to transfer your question to a human specialist, but they're unavailable at the moment. Let me help you instead.\n\n"
|
|
2731
|
+
|
|
2732
|
+
# Get the first AI agent
|
|
2733
|
+
fallback_agent = next(iter(self.agents.values()))
|
|
2734
|
+
|
|
2735
|
+
# Stream from fallback AI agent
|
|
2736
|
+
async for new_chunk in fallback_agent.text(
|
|
2737
|
+
user_id, user_text
|
|
2738
|
+
):
|
|
2739
|
+
yield new_chunk
|
|
2740
|
+
# Force immediate delivery
|
|
2741
|
+
await asyncio.sleep(0)
|
|
2742
|
+
else:
|
|
2743
|
+
yield "I tried to transfer your question to a human specialist, but there was an error. Let me help you instead.\n\n"
|
|
2744
|
+
|
|
2745
|
+
# Fallback to first AI agent
|
|
2746
|
+
fallback_agent = next(iter(self.agents.values()))
|
|
2747
|
+
async for new_chunk in fallback_agent.text(user_id, user_text):
|
|
2748
|
+
yield new_chunk
|
|
2749
|
+
await asyncio.sleep(0)
|
|
2750
|
+
else:
|
|
2751
|
+
# Standard AI-to-AI handoff
|
|
2752
|
+
target_agent = self.agents[target_name]
|
|
2753
|
+
|
|
2754
|
+
# Pass to target agent with comprehensive instructions
|
|
2755
|
+
handoff_query = f"""
|
|
2756
|
+
Answer this ENTIRE question completely from scratch:
|
|
2757
|
+
{user_text}
|
|
2758
|
+
|
|
2759
|
+
IMPORTANT INSTRUCTIONS:
|
|
2760
|
+
1. Address ALL aspects of the question comprehensively
|
|
2761
|
+
2. Include both explanations AND implementations as needed
|
|
2762
|
+
3. Do not mention any handoff or that you're continuing from another agent
|
|
2763
|
+
4. Consider any relevant context from previous conversation
|
|
2764
|
+
"""
|
|
2765
|
+
|
|
2766
|
+
# Stream from target agent
|
|
2767
|
+
async for new_chunk in target_agent.text(user_id, handoff_query):
|
|
2768
|
+
yield new_chunk
|
|
2769
|
+
await asyncio.sleep(0) # Force immediate delivery
|
|
2770
|
+
return
|
|
1905
2771
|
|
|
1906
2772
|
# Regular response if no handoff detected
|
|
1907
2773
|
if not handoff_detected:
|
|
@@ -1910,15 +2776,16 @@ class Swarm:
|
|
|
1910
2776
|
await asyncio.sleep(0) # Force immediate delivery
|
|
1911
2777
|
|
|
1912
2778
|
# After full response is delivered, invoke critic (if enabled)
|
|
1913
|
-
if self.enable_critic and hasattr(self, "critic"):
|
|
1914
|
-
#
|
|
2779
|
+
if self.enable_critic and hasattr(self, "critic") and agent_name:
|
|
2780
|
+
# Schedule async analysis without blocking
|
|
1915
2781
|
asyncio.create_task(
|
|
1916
2782
|
self.critic.analyze_interaction(
|
|
1917
|
-
agent_name=
|
|
2783
|
+
agent_name=agent_name,
|
|
1918
2784
|
user_query=user_text,
|
|
1919
2785
|
response=full_response,
|
|
1920
2786
|
)
|
|
1921
2787
|
)
|
|
2788
|
+
print(f"Scheduled critic analysis for {agent_name} response")
|
|
1922
2789
|
|
|
1923
2790
|
async def _run_post_processing_tasks(self, tasks):
|
|
1924
2791
|
"""Run multiple post-processing tasks concurrently."""
|
|
@@ -1931,22 +2798,22 @@ class Swarm:
|
|
|
1931
2798
|
"""Get routing decision in parallel to reduce latency."""
|
|
1932
2799
|
enhanced_prompt = f"""
|
|
1933
2800
|
Analyze this user query carefully to determine the MOST APPROPRIATE specialist.
|
|
1934
|
-
|
|
2801
|
+
|
|
1935
2802
|
User query: "{user_text}"
|
|
1936
|
-
|
|
2803
|
+
|
|
1937
2804
|
Available specialists:
|
|
1938
2805
|
{json.dumps(self.specializations, indent=2)}
|
|
1939
|
-
|
|
2806
|
+
|
|
1940
2807
|
CRITICAL ROUTING INSTRUCTIONS:
|
|
1941
2808
|
1. For compound questions with multiple aspects spanning different domains,
|
|
1942
2809
|
choose the specialist who should address the CONCEPTUAL or EDUCATIONAL aspects first.
|
|
1943
|
-
|
|
2810
|
+
|
|
1944
2811
|
2. Choose implementation specialists (technical, development, coding) only when
|
|
1945
2812
|
the query is PURELY about implementation with no conceptual explanation needed.
|
|
1946
|
-
|
|
2813
|
+
|
|
1947
2814
|
3. When a query involves a SEQUENCE (like "explain X and then do Y"),
|
|
1948
2815
|
prioritize the specialist handling the FIRST part of the sequence.
|
|
1949
|
-
|
|
2816
|
+
|
|
1950
2817
|
Return ONLY the name of the single most appropriate specialist.
|
|
1951
2818
|
"""
|
|
1952
2819
|
|
|
@@ -2031,11 +2898,11 @@ class Critic:
|
|
|
2031
2898
|
|
|
2032
2899
|
prompt = f"""
|
|
2033
2900
|
Analyze this agent interaction to identify specific improvements.
|
|
2034
|
-
|
|
2901
|
+
|
|
2035
2902
|
INTERACTION:
|
|
2036
2903
|
User query: {user_query}
|
|
2037
2904
|
Agent response: {response}
|
|
2038
|
-
|
|
2905
|
+
|
|
2039
2906
|
Provide feedback on accuracy, completeness, clarity, efficiency, and tone.
|
|
2040
2907
|
"""
|
|
2041
2908
|
|