solana-agent 7.0.0__py3-none-any.whl → 8.0.1__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 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", "relevance": "Important for understanding Z"}}
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
- # Update the extract_and_store_insights method in Swarm class
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
- try:
1835
- # Get the most recent feedback for this agent
1836
- feedback_records = list(
1837
- self.critic.feedback_collection.find(
1838
- {"agent_name": agent_name})
1839
- .sort("timestamp", -1)
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
- feedback_text = "\n".join(recent_feedback)
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
- Answer this question: {user_text}
1858
-
1859
- IMPORTANT - Apply these specific improvements from previous feedback:
1860
- {feedback_text}
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(f"Applying feedback to improve response: {feedback_text}")
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, current_agent, target_name, reason, user_text
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
- # Pass to target agent with comprehensive instructions
1889
- handoff_query = f"""
1890
- Answer this ENTIRE question completely from scratch:
1891
- {user_text}
1892
-
1893
- IMPORTANT INSTRUCTIONS:
1894
- 1. Address ALL aspects of the question comprehensively
1895
- 2. Include both explanations AND implementations as needed
1896
- 3. Do not mention any handoff or that you're continuing from another agent
1897
- 4. Consider any relevant context from previous conversation
1898
- """
1899
-
1900
- # Stream from target agent
1901
- async for new_chunk in target_agent.text(user_id, handoff_query):
1902
- yield new_chunk
1903
- await asyncio.sleep(0) # Force immediate delivery
1904
- return
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
- # Don't block - run asynchronously
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=current_agent.__class__.__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