solana-agent 11.2.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 +2132 -591
- {solana_agent-11.2.0.dist-info → solana_agent-12.0.0.dist-info}/METADATA +28 -57
- solana_agent-12.0.0.dist-info/RECORD +6 -0
- solana_agent-11.2.0.dist-info/RECORD +0 -6
- {solana_agent-11.2.0.dist-info → solana_agent-12.0.0.dist-info}/LICENSE +0 -0
- {solana_agent-11.2.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"
|
@@ -300,22 +368,31 @@ class AgentType(str, Enum):
|
|
300
368
|
|
301
369
|
|
302
370
|
class Ticket(BaseModel):
|
303
|
-
"""
|
304
|
-
|
305
|
-
id: str
|
371
|
+
"""Model for a support ticket."""
|
372
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
306
373
|
user_id: str
|
307
374
|
query: str
|
308
|
-
status: TicketStatus
|
309
|
-
assigned_to: str
|
310
|
-
created_at: datetime.datetime
|
375
|
+
status: TicketStatus = TicketStatus.NEW
|
376
|
+
assigned_to: str = ""
|
377
|
+
created_at: datetime.datetime = Field(
|
378
|
+
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc))
|
379
|
+
updated_at: Optional[datetime.datetime] = None
|
380
|
+
resolved_at: Optional[datetime.datetime] = None
|
381
|
+
resolution_confidence: Optional[float] = None
|
382
|
+
resolution_reasoning: Optional[str] = None
|
383
|
+
handoff_reason: Optional[str] = None
|
311
384
|
complexity: Optional[Dict[str, Any]] = None
|
312
|
-
|
385
|
+
agent_context: Optional[Dict[str, Any]] = None
|
313
386
|
is_parent: bool = False
|
314
387
|
is_subtask: bool = False
|
315
388
|
parent_id: Optional[str] = None
|
316
|
-
|
317
|
-
|
318
|
-
|
389
|
+
|
390
|
+
# Add fields for resource integration
|
391
|
+
description: Optional[str] = None
|
392
|
+
scheduled_start: Optional[datetime.datetime] = None
|
393
|
+
scheduled_end: Optional[datetime.datetime] = None
|
394
|
+
required_resources: List[Dict[str, Any]] = []
|
395
|
+
resource_assignments: List[Dict[str, Any]] = []
|
319
396
|
|
320
397
|
|
321
398
|
class Handoff(BaseModel):
|
@@ -456,17 +533,22 @@ class PlanStatus(BaseModel):
|
|
456
533
|
|
457
534
|
|
458
535
|
class SubtaskModel(BaseModel):
|
459
|
-
"""
|
536
|
+
"""Model for a subtask in a complex task breakdown."""
|
460
537
|
|
461
538
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
462
|
-
parent_id: str
|
539
|
+
parent_id: Optional[str] = None
|
463
540
|
title: str
|
464
541
|
description: str
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
542
|
+
estimated_minutes: int
|
543
|
+
dependencies: List[str] = []
|
544
|
+
status: str = "pending"
|
545
|
+
priority: Optional[int] = None
|
546
|
+
assigned_to: Optional[str] = None
|
547
|
+
scheduled_start: Optional[datetime.datetime] = None
|
548
|
+
specialization_tags: List[str] = []
|
549
|
+
sequence: int = 0 # Added missing sequence field
|
550
|
+
required_resources: List[Dict[str, Any]] = []
|
551
|
+
resource_assignments: List[Dict[str, Any]] = []
|
470
552
|
|
471
553
|
|
472
554
|
class WorkCapacity(BaseModel):
|
@@ -483,10 +565,117 @@ class WorkCapacity(BaseModel):
|
|
483
565
|
)
|
484
566
|
|
485
567
|
|
568
|
+
class ResourceType(str, Enum):
|
569
|
+
"""Types of resources that can be booked."""
|
570
|
+
ROOM = "room"
|
571
|
+
VEHICLE = "vehicle"
|
572
|
+
EQUIPMENT = "equipment"
|
573
|
+
SEAT = "seat"
|
574
|
+
DESK = "desk"
|
575
|
+
OTHER = "other"
|
576
|
+
|
577
|
+
|
578
|
+
class ResourceStatus(str, Enum):
|
579
|
+
"""Status of a resource."""
|
580
|
+
AVAILABLE = "available"
|
581
|
+
IN_USE = "in_use"
|
582
|
+
MAINTENANCE = "maintenance"
|
583
|
+
UNAVAILABLE = "unavailable"
|
584
|
+
|
585
|
+
|
586
|
+
class ResourceLocation(BaseModel):
|
587
|
+
"""Physical location of a resource."""
|
588
|
+
address: Optional[str] = None
|
589
|
+
building: Optional[str] = None
|
590
|
+
floor: Optional[int] = None
|
591
|
+
room: Optional[str] = None
|
592
|
+
coordinates: Optional[Dict[str, float]] = None # Lat/Long if applicable
|
593
|
+
|
594
|
+
|
595
|
+
class TimeWindow(BaseModel):
|
596
|
+
"""Time window model for availability and exceptions."""
|
597
|
+
start: datetime.datetime
|
598
|
+
end: datetime.datetime
|
599
|
+
|
600
|
+
def overlaps_with(self, other: 'TimeWindow') -> bool:
|
601
|
+
"""Check if this window overlaps with another one."""
|
602
|
+
return self.start < other.end and self.end > other.start
|
603
|
+
|
604
|
+
|
605
|
+
class ResourceAvailabilityWindow(BaseModel):
|
606
|
+
"""Availability window for a resource with recurring pattern options."""
|
607
|
+
day_of_week: Optional[List[int]] = None # 0 = Monday, 6 = Sunday
|
608
|
+
start_time: str # Format: "HH:MM", 24h format
|
609
|
+
end_time: str # Format: "HH:MM", 24h format
|
610
|
+
timezone: str = "UTC"
|
611
|
+
|
612
|
+
|
613
|
+
class Resource(BaseModel):
|
614
|
+
"""Model for a bookable resource."""
|
615
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
616
|
+
name: str
|
617
|
+
description: Optional[str] = None
|
618
|
+
resource_type: ResourceType
|
619
|
+
status: ResourceStatus = ResourceStatus.AVAILABLE
|
620
|
+
location: Optional[ResourceLocation] = None
|
621
|
+
capacity: Optional[int] = None # For rooms/vehicles
|
622
|
+
tags: List[str] = []
|
623
|
+
attributes: Dict[str, str] = {} # Custom attributes
|
624
|
+
availability_schedule: List[ResourceAvailabilityWindow] = []
|
625
|
+
# Overrides for maintenance, holidays
|
626
|
+
availability_exceptions: List[TimeWindow] = []
|
627
|
+
created_at: datetime.datetime = Field(
|
628
|
+
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc))
|
629
|
+
updated_at: Optional[datetime.datetime] = None
|
630
|
+
|
631
|
+
def is_available_at(self, time_window: TimeWindow) -> bool:
|
632
|
+
"""Check if resource is available during the specified time window."""
|
633
|
+
# Check if resource is generally available
|
634
|
+
if self.status != ResourceStatus.AVAILABLE:
|
635
|
+
return False
|
636
|
+
|
637
|
+
# Check against exceptions (maintenance, holidays)
|
638
|
+
for exception in self.availability_exceptions:
|
639
|
+
if exception.overlaps_with(time_window):
|
640
|
+
return False
|
641
|
+
|
642
|
+
# Check if the requested time falls within regular availability
|
643
|
+
day_of_week = time_window.start.weekday()
|
644
|
+
start_time = time_window.start.strftime("%H:%M")
|
645
|
+
end_time = time_window.end.strftime("%H:%M")
|
646
|
+
|
647
|
+
for window in self.availability_schedule:
|
648
|
+
if window.day_of_week is None or day_of_week in window.day_of_week:
|
649
|
+
if window.start_time <= start_time and window.end_time >= end_time:
|
650
|
+
return True
|
651
|
+
|
652
|
+
# Default available if no schedule defined
|
653
|
+
return len(self.availability_schedule) == 0
|
654
|
+
|
655
|
+
|
656
|
+
class ResourceBooking(BaseModel):
|
657
|
+
"""Model for a resource booking."""
|
658
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
659
|
+
resource_id: str
|
660
|
+
user_id: str
|
661
|
+
title: str
|
662
|
+
description: Optional[str] = None
|
663
|
+
start_time: datetime.datetime
|
664
|
+
end_time: datetime.datetime
|
665
|
+
status: str = "confirmed" # confirmed, cancelled, completed
|
666
|
+
booking_reference: Optional[str] = None
|
667
|
+
payment_status: Optional[str] = None
|
668
|
+
payment_amount: Optional[float] = None
|
669
|
+
notes: Optional[str] = None
|
670
|
+
created_at: datetime.datetime = Field(
|
671
|
+
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc))
|
672
|
+
updated_at: Optional[datetime.datetime] = None
|
673
|
+
|
486
674
|
#############################################
|
487
675
|
# INTERFACES
|
488
676
|
#############################################
|
489
677
|
|
678
|
+
|
490
679
|
class LLMProvider(Protocol):
|
491
680
|
"""Interface for language model providers."""
|
492
681
|
|
@@ -621,8 +810,13 @@ class MemoryRepository(Protocol):
|
|
621
810
|
class AgentRegistry(Protocol):
|
622
811
|
"""Interface for agent management."""
|
623
812
|
|
624
|
-
def register_ai_agent(
|
625
|
-
|
813
|
+
def register_ai_agent(
|
814
|
+
self,
|
815
|
+
name: str,
|
816
|
+
instructions: str,
|
817
|
+
specialization: str,
|
818
|
+
model: str = "gpt-4o-mini",
|
819
|
+
) -> None: ...
|
626
820
|
|
627
821
|
def register_human_agent(
|
628
822
|
self,
|
@@ -889,6 +1083,7 @@ class OpenAIAdapter:
|
|
889
1083
|
messages.append({"role": "user", "content": prompt})
|
890
1084
|
|
891
1085
|
try:
|
1086
|
+
# First try the beta parsing API
|
892
1087
|
completion = self.client.beta.chat.completions.parse(
|
893
1088
|
model=kwargs.get("model", self.model),
|
894
1089
|
messages=messages,
|
@@ -897,7 +1092,25 @@ class OpenAIAdapter:
|
|
897
1092
|
)
|
898
1093
|
return completion.choices[0].message.parsed
|
899
1094
|
except Exception as e:
|
900
|
-
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
|
+
|
901
1114
|
# Return default instance as fallback
|
902
1115
|
return model_class()
|
903
1116
|
|
@@ -1026,6 +1239,217 @@ class PineconeAdapter:
|
|
1026
1239
|
# IMPLEMENTATIONS - REPOSITORIES
|
1027
1240
|
#############################################
|
1028
1241
|
|
1242
|
+
class ResourceRepository:
|
1243
|
+
"""Repository for managing resources."""
|
1244
|
+
|
1245
|
+
def __init__(self, db_provider):
|
1246
|
+
"""Initialize with database provider."""
|
1247
|
+
self.db = db_provider
|
1248
|
+
self.resources_collection = "resources"
|
1249
|
+
self.bookings_collection = "resource_bookings"
|
1250
|
+
|
1251
|
+
# Ensure collections exist
|
1252
|
+
self.db.create_collection(self.resources_collection)
|
1253
|
+
self.db.create_collection(self.bookings_collection)
|
1254
|
+
|
1255
|
+
# Create indexes
|
1256
|
+
self.db.create_index(self.resources_collection, [("resource_type", 1)])
|
1257
|
+
self.db.create_index(self.resources_collection, [("status", 1)])
|
1258
|
+
self.db.create_index(self.resources_collection, [("tags", 1)])
|
1259
|
+
|
1260
|
+
self.db.create_index(self.bookings_collection, [("resource_id", 1)])
|
1261
|
+
self.db.create_index(self.bookings_collection, [("user_id", 1)])
|
1262
|
+
self.db.create_index(self.bookings_collection, [("start_time", 1)])
|
1263
|
+
self.db.create_index(self.bookings_collection, [("end_time", 1)])
|
1264
|
+
self.db.create_index(self.bookings_collection, [("status", 1)])
|
1265
|
+
|
1266
|
+
# Resource CRUD operations
|
1267
|
+
def create_resource(self, resource: Resource) -> str:
|
1268
|
+
"""Create a new resource."""
|
1269
|
+
resource_dict = resource.model_dump(mode="json")
|
1270
|
+
return self.db.insert_one(self.resources_collection, resource_dict)
|
1271
|
+
|
1272
|
+
def get_resource(self, resource_id: str) -> Optional[Resource]:
|
1273
|
+
"""Get a resource by ID."""
|
1274
|
+
data = self.db.find_one(self.resources_collection, {"id": resource_id})
|
1275
|
+
return Resource(**data) if data else None
|
1276
|
+
|
1277
|
+
def update_resource(self, resource: Resource) -> bool:
|
1278
|
+
"""Update a resource."""
|
1279
|
+
resource.updated_at = datetime.datetime.now(datetime.timezone.utc)
|
1280
|
+
resource_dict = resource.model_dump(mode="json")
|
1281
|
+
return self.db.update_one(
|
1282
|
+
self.resources_collection,
|
1283
|
+
{"id": resource.id},
|
1284
|
+
{"$set": resource_dict}
|
1285
|
+
)
|
1286
|
+
|
1287
|
+
def delete_resource(self, resource_id: str) -> bool:
|
1288
|
+
"""Delete a resource."""
|
1289
|
+
return self.db.delete_one(self.resources_collection, {"id": resource_id})
|
1290
|
+
|
1291
|
+
def find_resources(
|
1292
|
+
self,
|
1293
|
+
query: Dict[str, Any],
|
1294
|
+
sort_by: Optional[str] = None,
|
1295
|
+
limit: int = 0
|
1296
|
+
) -> List[Resource]:
|
1297
|
+
"""Find resources matching query."""
|
1298
|
+
sort_params = [(sort_by, 1)] if sort_by else [("name", 1)]
|
1299
|
+
data = self.db.find(self.resources_collection,
|
1300
|
+
query, sort_params, limit)
|
1301
|
+
return [Resource(**item) for item in data]
|
1302
|
+
|
1303
|
+
def find_available_resources(
|
1304
|
+
self,
|
1305
|
+
resource_type: Optional[str] = None,
|
1306
|
+
tags: Optional[List[str]] = None,
|
1307
|
+
start_time: Optional[datetime.datetime] = None,
|
1308
|
+
end_time: Optional[datetime.datetime] = None,
|
1309
|
+
capacity: Optional[int] = None
|
1310
|
+
) -> List[Resource]:
|
1311
|
+
"""Find available resources matching the criteria."""
|
1312
|
+
# Build base query for available resources
|
1313
|
+
query = {"status": ResourceStatus.AVAILABLE}
|
1314
|
+
|
1315
|
+
if resource_type:
|
1316
|
+
query["resource_type"] = resource_type
|
1317
|
+
|
1318
|
+
if tags:
|
1319
|
+
query["tags"] = {"$all": tags}
|
1320
|
+
|
1321
|
+
if capacity:
|
1322
|
+
query["capacity"] = {"$gte": capacity}
|
1323
|
+
|
1324
|
+
# First get resources that match base criteria
|
1325
|
+
resources = self.find_resources(query)
|
1326
|
+
|
1327
|
+
# If no time range specified, return all matching resources
|
1328
|
+
if not start_time or not end_time:
|
1329
|
+
return resources
|
1330
|
+
|
1331
|
+
# Filter by time availability (check bookings and exceptions)
|
1332
|
+
time_window = TimeWindow(start=start_time, end=end_time)
|
1333
|
+
|
1334
|
+
# Check each resource's availability
|
1335
|
+
available_resources = []
|
1336
|
+
for resource in resources:
|
1337
|
+
if resource.is_available_at(time_window):
|
1338
|
+
# Check existing bookings
|
1339
|
+
if not self._has_conflicting_bookings(resource.id, start_time, end_time):
|
1340
|
+
available_resources.append(resource)
|
1341
|
+
|
1342
|
+
return available_resources
|
1343
|
+
|
1344
|
+
# Booking CRUD operations
|
1345
|
+
def create_booking(self, booking: ResourceBooking) -> str:
|
1346
|
+
"""Create a new booking."""
|
1347
|
+
booking_dict = booking.model_dump(mode="json")
|
1348
|
+
return self.db.insert_one(self.bookings_collection, booking_dict)
|
1349
|
+
|
1350
|
+
def get_booking(self, booking_id: str) -> Optional[ResourceBooking]:
|
1351
|
+
"""Get a booking by ID."""
|
1352
|
+
data = self.db.find_one(self.bookings_collection, {"id": booking_id})
|
1353
|
+
return ResourceBooking(**data) if data else None
|
1354
|
+
|
1355
|
+
def update_booking(self, booking: ResourceBooking) -> bool:
|
1356
|
+
"""Update a booking."""
|
1357
|
+
booking.updated_at = datetime.datetime.now(datetime.timezone.utc)
|
1358
|
+
booking_dict = booking.model_dump(mode="json")
|
1359
|
+
return self.db.update_one(
|
1360
|
+
self.bookings_collection,
|
1361
|
+
{"id": booking.id},
|
1362
|
+
{"$set": booking_dict}
|
1363
|
+
)
|
1364
|
+
|
1365
|
+
def cancel_booking(self, booking_id: str) -> bool:
|
1366
|
+
"""Cancel a booking."""
|
1367
|
+
return self.db.update_one(
|
1368
|
+
self.bookings_collection,
|
1369
|
+
{"id": booking_id},
|
1370
|
+
{
|
1371
|
+
"$set": {
|
1372
|
+
"status": "cancelled",
|
1373
|
+
"updated_at": datetime.datetime.now(datetime.timezone.utc)
|
1374
|
+
}
|
1375
|
+
}
|
1376
|
+
)
|
1377
|
+
|
1378
|
+
def get_resource_bookings(
|
1379
|
+
self,
|
1380
|
+
resource_id: str,
|
1381
|
+
start_time: Optional[datetime.datetime] = None,
|
1382
|
+
end_time: Optional[datetime.datetime] = None,
|
1383
|
+
include_cancelled: bool = False
|
1384
|
+
) -> List[ResourceBooking]:
|
1385
|
+
"""Get all bookings for a resource within a time range."""
|
1386
|
+
query = {"resource_id": resource_id}
|
1387
|
+
|
1388
|
+
if not include_cancelled:
|
1389
|
+
query["status"] = {"$ne": "cancelled"}
|
1390
|
+
|
1391
|
+
if start_time or end_time:
|
1392
|
+
time_query = {}
|
1393
|
+
if start_time:
|
1394
|
+
time_query["$lte"] = end_time
|
1395
|
+
if end_time:
|
1396
|
+
time_query["$gte"] = start_time
|
1397
|
+
if time_query:
|
1398
|
+
query["$or"] = [
|
1399
|
+
{"start_time": time_query},
|
1400
|
+
{"end_time": time_query},
|
1401
|
+
{
|
1402
|
+
"$and": [
|
1403
|
+
{"start_time": {"$lte": start_time}},
|
1404
|
+
{"end_time": {"$gte": end_time}}
|
1405
|
+
]
|
1406
|
+
}
|
1407
|
+
]
|
1408
|
+
|
1409
|
+
data = self.db.find(self.bookings_collection,
|
1410
|
+
query, sort=[("start_time", 1)])
|
1411
|
+
return [ResourceBooking(**item) for item in data]
|
1412
|
+
|
1413
|
+
def get_user_bookings(
|
1414
|
+
self,
|
1415
|
+
user_id: str,
|
1416
|
+
include_cancelled: bool = False
|
1417
|
+
) -> List[ResourceBooking]:
|
1418
|
+
"""Get all bookings for a user."""
|
1419
|
+
query = {"user_id": user_id}
|
1420
|
+
|
1421
|
+
if not include_cancelled:
|
1422
|
+
query["status"] = {"$ne": "cancelled"}
|
1423
|
+
|
1424
|
+
data = self.db.find(self.bookings_collection,
|
1425
|
+
query, sort=[("start_time", 1)])
|
1426
|
+
return [ResourceBooking(**item) for item in data]
|
1427
|
+
|
1428
|
+
def _has_conflicting_bookings(
|
1429
|
+
self,
|
1430
|
+
resource_id: str,
|
1431
|
+
start_time: datetime.datetime,
|
1432
|
+
end_time: datetime.datetime
|
1433
|
+
) -> bool:
|
1434
|
+
"""Check if there are any conflicting bookings."""
|
1435
|
+
query = {
|
1436
|
+
"resource_id": resource_id,
|
1437
|
+
"status": {"$ne": "cancelled"},
|
1438
|
+
"$or": [
|
1439
|
+
{"start_time": {"$lt": end_time, "$gte": start_time}},
|
1440
|
+
{"end_time": {"$gt": start_time, "$lte": end_time}},
|
1441
|
+
{
|
1442
|
+
"$and": [
|
1443
|
+
{"start_time": {"$lte": start_time}},
|
1444
|
+
{"end_time": {"$gte": end_time}}
|
1445
|
+
]
|
1446
|
+
}
|
1447
|
+
]
|
1448
|
+
}
|
1449
|
+
|
1450
|
+
return self.db.count_documents(self.bookings_collection, query) > 0
|
1451
|
+
|
1452
|
+
|
1029
1453
|
class MongoMemoryProvider:
|
1030
1454
|
"""MongoDB implementation of MemoryProvider."""
|
1031
1455
|
|
@@ -1160,16 +1584,26 @@ class MongoAIAgentRegistry:
|
|
1160
1584
|
}
|
1161
1585
|
|
1162
1586
|
def delete_agent(self, name: str) -> bool:
|
1163
|
-
"""Delete an AI agent
|
1587
|
+
"""Delete an AI agent from the registry."""
|
1588
|
+
# First check if agent exists
|
1164
1589
|
if name not in self.ai_agents_cache:
|
1165
1590
|
return False
|
1166
1591
|
|
1167
|
-
#
|
1168
|
-
|
1592
|
+
# Delete from database
|
1593
|
+
result = self.db.delete_one(
|
1594
|
+
self.collection,
|
1595
|
+
{"name": name}
|
1596
|
+
)
|
1169
1597
|
|
1170
|
-
#
|
1171
|
-
|
1172
|
-
|
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
|
1173
1607
|
|
1174
1608
|
|
1175
1609
|
class MongoHumanAgentRegistry:
|
@@ -1356,13 +1790,13 @@ class MongoTicketRepository:
|
|
1356
1790
|
"""Count tickets matching query."""
|
1357
1791
|
return self.db.count_documents(self.collection, query)
|
1358
1792
|
|
1359
|
-
def find_stalled_tickets(self, cutoff_time, statuses):
|
1793
|
+
async def find_stalled_tickets(self, cutoff_time, statuses):
|
1360
1794
|
"""Find tickets that haven't been updated since the cutoff time."""
|
1361
1795
|
query = {
|
1362
1796
|
"status": {"$in": [status.value if isinstance(status, Enum) else status for status in statuses]},
|
1363
1797
|
"updated_at": {"$lt": cutoff_time}
|
1364
1798
|
}
|
1365
|
-
tickets = self.
|
1799
|
+
tickets = self.db.find("tickets", query)
|
1366
1800
|
return [Ticket(**ticket) for ticket in tickets]
|
1367
1801
|
|
1368
1802
|
|
@@ -1947,79 +2381,91 @@ class RoutingService:
|
|
1947
2381
|
self.router_model = router_model
|
1948
2382
|
|
1949
2383
|
async def route_query(self, query: str) -> str:
|
1950
|
-
"""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 = []
|
1951
2394
|
specializations = self.agent_registry.get_specializations()
|
1952
|
-
# Get AI-only specializations
|
1953
|
-
ai_specialists = {
|
1954
|
-
k: v
|
1955
|
-
for k, v in specializations.items()
|
1956
|
-
if k in self.agent_registry.get_all_ai_agents()
|
1957
|
-
}
|
1958
2395
|
|
1959
|
-
|
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
|
1960
2403
|
prompt = f"""
|
1961
|
-
|
2404
|
+
You are a router that determines which AI agent should handle a user query.
|
1962
2405
|
|
1963
2406
|
User query: "{query}"
|
1964
2407
|
|
1965
|
-
Available
|
1966
|
-
{
|
2408
|
+
Available agents:
|
2409
|
+
{agent_info}
|
1967
2410
|
|
1968
|
-
|
1969
|
-
|
1970
|
-
2. Return EXACTLY ONE specialist name from the available list.
|
1971
|
-
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.
|
1972
2413
|
"""
|
1973
2414
|
|
1974
|
-
|
1975
|
-
response = ""
|
1976
|
-
async for chunk in self.llm_provider.generate_text(
|
1977
|
-
"router",
|
1978
|
-
prompt,
|
1979
|
-
system_prompt="You are a routing system that matches queries to the best specialist.",
|
1980
|
-
stream=False,
|
1981
|
-
model=self.router_model,
|
1982
|
-
temperature=0.2,
|
1983
|
-
response_format={"type": "json_object"},
|
1984
|
-
):
|
1985
|
-
response += chunk
|
1986
|
-
|
2415
|
+
response_text = ""
|
1987
2416
|
try:
|
1988
|
-
|
1989
|
-
|
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
|
1990
2425
|
|
1991
|
-
#
|
1992
|
-
|
1993
|
-
|
1994
|
-
|
1995
|
-
|
1996
|
-
|
1997
|
-
|
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)
|
1998
2440
|
|
1999
|
-
return agent_name
|
2000
2441
|
except Exception as e:
|
2001
|
-
print(f"Error
|
2002
|
-
#
|
2003
|
-
return
|
2442
|
+
print(f"Error in routing: {e}")
|
2443
|
+
# Default to the first agent if there's an error
|
2444
|
+
return agent_names[0]
|
2004
2445
|
|
2005
2446
|
def _match_agent_name(self, response: str, agent_names: List[str]) -> str:
|
2006
|
-
"""Match
|
2007
|
-
#
|
2008
|
-
if response in
|
2009
|
-
|
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
|
2451
|
+
|
2452
|
+
# Convert to string and clean it up
|
2453
|
+
clean_response = str(response).strip().lower()
|
2010
2454
|
|
2011
|
-
#
|
2455
|
+
# Direct match first
|
2012
2456
|
for name in agent_names:
|
2013
|
-
if name.lower() ==
|
2457
|
+
if name.lower() == clean_response:
|
2014
2458
|
return name
|
2015
2459
|
|
2016
|
-
#
|
2460
|
+
# Check for partial matches
|
2017
2461
|
for name in agent_names:
|
2018
|
-
if name.lower() in
|
2462
|
+
if name.lower() in clean_response or clean_response in name.lower():
|
2019
2463
|
return name
|
2020
2464
|
|
2021
|
-
#
|
2022
|
-
|
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]
|
2023
2469
|
|
2024
2470
|
|
2025
2471
|
class TicketService:
|
@@ -2340,21 +2786,14 @@ class CriticService:
|
|
2340
2786
|
class NotificationService:
|
2341
2787
|
"""Service for sending notifications to human agents or users using notification plugins."""
|
2342
2788
|
|
2343
|
-
def __init__(self, human_agent_registry: MongoHumanAgentRegistry):
|
2789
|
+
def __init__(self, human_agent_registry: MongoHumanAgentRegistry, tool_registry=None):
|
2344
2790
|
"""Initialize the notification service with a human agent registry."""
|
2345
2791
|
self.human_agent_registry = human_agent_registry
|
2792
|
+
self.tool_registry = tool_registry
|
2346
2793
|
|
2347
2794
|
def send_notification(self, recipient_id: str, message: str, metadata: Dict[str, Any] = None) -> bool:
|
2348
2795
|
"""
|
2349
2796
|
Send a notification to a human agent using configured notification channels or legacy handler.
|
2350
|
-
|
2351
|
-
Args:
|
2352
|
-
recipient_id: ID of the human agent to notify
|
2353
|
-
message: Notification message content
|
2354
|
-
metadata: Additional data related to the notification (e.g., ticket_id)
|
2355
|
-
|
2356
|
-
Returns:
|
2357
|
-
True if notification was sent, False otherwise
|
2358
2797
|
"""
|
2359
2798
|
# Get human agent information
|
2360
2799
|
agent = self.human_agent_registry.get_human_agent(recipient_id)
|
@@ -2380,6 +2819,11 @@ class NotificationService:
|
|
2380
2819
|
f"No notification channels configured for agent {recipient_id}")
|
2381
2820
|
return False
|
2382
2821
|
|
2822
|
+
# No tool registry available
|
2823
|
+
if not self.tool_registry:
|
2824
|
+
print("No tool registry available for notifications")
|
2825
|
+
return False
|
2826
|
+
|
2383
2827
|
# Try each notification channel until one succeeds
|
2384
2828
|
success = False
|
2385
2829
|
for channel in notification_channels:
|
@@ -2396,9 +2840,9 @@ class NotificationService:
|
|
2396
2840
|
if metadata:
|
2397
2841
|
tool_params["metadata"] = metadata
|
2398
2842
|
|
2399
|
-
|
2400
|
-
if
|
2401
|
-
response =
|
2843
|
+
tool = self.tool_registry.get_tool(f"notify_{channel_type}")
|
2844
|
+
if tool:
|
2845
|
+
response = tool.execute(**tool_params)
|
2402
2846
|
if response.get("status") == "success":
|
2403
2847
|
success = True
|
2404
2848
|
break
|
@@ -2431,11 +2875,15 @@ class AgentService:
|
|
2431
2875
|
human_agent_registry: Optional[MongoHumanAgentRegistry] = None,
|
2432
2876
|
ai_agent_registry: Optional[MongoAIAgentRegistry] = None,
|
2433
2877
|
organization_mission: Optional[OrganizationMission] = None,
|
2878
|
+
config: Optional[Dict[str, Any]] = None
|
2434
2879
|
):
|
2880
|
+
"""Initialize the agent service with LLM provider and optional registries."""
|
2435
2881
|
self.llm_provider = llm_provider
|
2436
2882
|
self.human_agent_registry = human_agent_registry
|
2437
2883
|
self.ai_agent_registry = ai_agent_registry
|
2438
2884
|
self.organization_mission = organization_mission
|
2885
|
+
self.config = config or {}
|
2886
|
+
self._last_handoff = None
|
2439
2887
|
|
2440
2888
|
# For backward compatibility
|
2441
2889
|
self.ai_agents = {}
|
@@ -2444,9 +2892,22 @@ class AgentService:
|
|
2444
2892
|
|
2445
2893
|
self.specializations = {}
|
2446
2894
|
|
2447
|
-
#
|
2448
|
-
self.
|
2449
|
-
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()}")
|
2450
2911
|
|
2451
2912
|
# If human agent registry is provided, initialize specializations from it
|
2452
2913
|
if self.human_agent_registry:
|
@@ -2462,6 +2923,205 @@ class AgentService:
|
|
2462
2923
|
if not self.human_agent_registry:
|
2463
2924
|
self.human_agents = {}
|
2464
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
|
+
|
2465
3125
|
def register_ai_agent(
|
2466
3126
|
self,
|
2467
3127
|
name: str,
|
@@ -2487,7 +3147,7 @@ class AgentService:
|
|
2487
3147
|
# Use registry if available
|
2488
3148
|
if self.ai_agent_registry:
|
2489
3149
|
self.ai_agent_registry.register_ai_agent(
|
2490
|
-
name, full_instructions, specialization, model
|
3150
|
+
name, full_instructions, specialization, model,
|
2491
3151
|
)
|
2492
3152
|
# Update local cache for backward compatibility
|
2493
3153
|
self.ai_agents = self.ai_agent_registry.get_all_ai_agents()
|
@@ -2507,32 +3167,37 @@ class AgentService:
|
|
2507
3167
|
return merged
|
2508
3168
|
return self.specializations
|
2509
3169
|
|
2510
|
-
def
|
2511
|
-
"""
|
2512
|
-
|
2513
|
-
|
2514
|
-
|
2515
|
-
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}")
|
2516
3174
|
|
2517
|
-
|
2518
|
-
|
2519
|
-
|
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"}
|
2520
3181
|
|
2521
|
-
|
2522
|
-
self
|
2523
|
-
|
2524
|
-
"""Execute a tool on behalf of an agent."""
|
2525
|
-
# Check if agent has access to this tool
|
2526
|
-
agent_tools = tool_registry.get_agent_tools(agent_name)
|
2527
|
-
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]
|
2528
3185
|
|
2529
3186
|
if tool_name not in tool_names:
|
2530
|
-
|
2531
|
-
f"Agent {agent_name} does not have access to tool {tool_name}"
|
2532
|
-
|
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}"}
|
2533
3190
|
|
2534
|
-
# Execute the tool
|
2535
|
-
|
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)}"}
|
2536
3201
|
|
2537
3202
|
def register_human_agent(
|
2538
3203
|
self,
|
@@ -2589,69 +3254,246 @@ class AgentService:
|
|
2589
3254
|
|
2590
3255
|
agent_config = self.ai_agents[agent_name]
|
2591
3256
|
|
2592
|
-
# Get
|
2593
|
-
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
|
2594
3261
|
if memory_context:
|
2595
3262
|
instructions += f"\n\nUser context and history:\n{memory_context}"
|
2596
3263
|
|
2597
|
-
# Add
|
2598
|
-
tools
|
2599
|
-
if tools and "tools" not in kwargs:
|
2600
|
-
kwargs["tools"] = tools
|
2601
|
-
|
2602
|
-
# Add specific instruction for simple queries to prevent handoff format leakage
|
2603
|
-
if len(query.strip()) < 10:
|
2604
|
-
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."
|
2605
3266
|
|
2606
3267
|
# Generate response
|
2607
|
-
|
2608
|
-
|
2609
|
-
user_id=user_id,
|
2610
|
-
prompt=query,
|
2611
|
-
system_prompt=instructions,
|
2612
|
-
model=agent_config["model"],
|
2613
|
-
**kwargs,
|
2614
|
-
):
|
2615
|
-
# Filter out raw handoff JSON before yielding to user
|
2616
|
-
if not response_text and chunk.strip().startswith('{"handoff":'):
|
2617
|
-
# If we're starting with a handoff JSON, replace with a proper response
|
2618
|
-
yield "Hello! I'm here to help. What can I assist you with today?"
|
2619
|
-
response_text += chunk # Still store it for processing later
|
2620
|
-
else:
|
2621
|
-
yield chunk
|
2622
|
-
response_text += chunk
|
3268
|
+
tool_json_found = False
|
3269
|
+
full_response = ""
|
2623
3270
|
|
2624
|
-
# Process handoffs after yielding response (unchanged code)
|
2625
3271
|
try:
|
2626
|
-
|
2627
|
-
|
2628
|
-
|
2629
|
-
|
2630
|
-
|
2631
|
-
|
2632
|
-
|
2633
|
-
|
2634
|
-
|
2635
|
-
|
2636
|
-
|
2637
|
-
|
2638
|
-
|
2639
|
-
|
2640
|
-
|
2641
|
-
|
2642
|
-
except Exception:
|
2643
|
-
params = {}
|
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
|
2644
3288
|
|
2645
|
-
|
2646
|
-
|
2647
|
-
|
2648
|
-
except Exception:
|
2649
|
-
# If it's not JSON or doesn't have tool_calls, we've already yielded the response
|
2650
|
-
pass
|
3289
|
+
# If not JSON, yield the chunk
|
3290
|
+
if not tool_json_found:
|
3291
|
+
yield chunk
|
2651
3292
|
|
2652
|
-
|
2653
|
-
|
2654
|
-
|
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}")
|
3310
|
+
|
3311
|
+
if tool_name:
|
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
|
3339
|
+
|
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)}"
|
3360
|
+
|
3361
|
+
|
3362
|
+
class ResourceService:
|
3363
|
+
"""Service for managing resources and bookings."""
|
3364
|
+
|
3365
|
+
def __init__(self, resource_repository: ResourceRepository):
|
3366
|
+
"""Initialize with resource repository."""
|
3367
|
+
self.repository = resource_repository
|
3368
|
+
|
3369
|
+
async def create_resource(self, resource_data, resource_type):
|
3370
|
+
"""Create a new resource from dictionary data."""
|
3371
|
+
# Generate UUID for ID since it can't be None
|
3372
|
+
resource_id = str(uuid.uuid4())
|
3373
|
+
|
3374
|
+
resource = Resource(
|
3375
|
+
id=resource_id,
|
3376
|
+
name=resource_data["name"],
|
3377
|
+
resource_type=resource_type,
|
3378
|
+
description=resource_data.get("description"),
|
3379
|
+
location=resource_data.get("location"),
|
3380
|
+
capacity=resource_data.get("capacity"),
|
3381
|
+
tags=resource_data.get("tags", []),
|
3382
|
+
attributes=resource_data.get("attributes", {}),
|
3383
|
+
availability_schedule=resource_data.get(
|
3384
|
+
"availability_schedule", [])
|
3385
|
+
)
|
3386
|
+
|
3387
|
+
# Don't use await when calling repository methods
|
3388
|
+
return self.repository.create_resource(resource)
|
3389
|
+
|
3390
|
+
async def get_resource(self, resource_id):
|
3391
|
+
"""Get a resource by ID."""
|
3392
|
+
# Don't use await
|
3393
|
+
return self.repository.get_resource(resource_id)
|
3394
|
+
|
3395
|
+
async def update_resource(self, resource_id, updates):
|
3396
|
+
"""Update a resource."""
|
3397
|
+
resource = self.repository.get_resource(resource_id)
|
3398
|
+
if not resource:
|
3399
|
+
return False
|
3400
|
+
|
3401
|
+
# Apply updates
|
3402
|
+
for key, value in updates.items():
|
3403
|
+
if hasattr(resource, key):
|
3404
|
+
setattr(resource, key, value)
|
3405
|
+
|
3406
|
+
# Don't use await
|
3407
|
+
return self.repository.update_resource(resource)
|
3408
|
+
|
3409
|
+
async def list_resources(self, resource_type=None):
|
3410
|
+
"""List all resources, optionally filtered by type."""
|
3411
|
+
# Don't use await
|
3412
|
+
return self.repository.list_resources(resource_type)
|
3413
|
+
|
3414
|
+
async def find_available_resources(self, start_time, end_time, capacity=None, tags=None, resource_type=None):
|
3415
|
+
"""Find available resources for a time period."""
|
3416
|
+
# Don't use await
|
3417
|
+
resources = self.repository.find_resources(
|
3418
|
+
resource_type, capacity, tags)
|
3419
|
+
|
3420
|
+
# Filter by availability
|
3421
|
+
available = []
|
3422
|
+
for resource in resources:
|
3423
|
+
time_window = TimeWindow(start=start_time, end=end_time)
|
3424
|
+
if resource.is_available_at(time_window):
|
3425
|
+
if not self.repository._has_conflicting_bookings(resource.id, start_time, end_time):
|
3426
|
+
available.append(resource)
|
3427
|
+
|
3428
|
+
return available
|
3429
|
+
|
3430
|
+
async def create_booking(self, resource_id, user_id, title, start_time, end_time, description=None, notes=None):
|
3431
|
+
"""Create a booking for a resource."""
|
3432
|
+
# Check if resource exists
|
3433
|
+
resource = self.repository.get_resource(resource_id)
|
3434
|
+
if not resource:
|
3435
|
+
return False, None, "Resource not found"
|
3436
|
+
|
3437
|
+
# Check for conflicts
|
3438
|
+
if self.repository._has_conflicting_bookings(resource_id, start_time, end_time):
|
3439
|
+
return False, None, "Resource is already booked during the requested time"
|
3440
|
+
|
3441
|
+
# Create booking
|
3442
|
+
booking_data = ResourceBooking(
|
3443
|
+
id=str(uuid.uuid4()),
|
3444
|
+
resource_id=resource_id,
|
3445
|
+
user_id=user_id,
|
3446
|
+
title=title,
|
3447
|
+
description=description,
|
3448
|
+
status="confirmed",
|
3449
|
+
start_time=start_time,
|
3450
|
+
end_time=end_time,
|
3451
|
+
notes=notes,
|
3452
|
+
created_at=datetime.datetime.now(datetime.timezone.utc)
|
3453
|
+
)
|
3454
|
+
|
3455
|
+
booking_id = self.repository.create_booking(booking_data)
|
3456
|
+
|
3457
|
+
# Return (success, booking_id, error)
|
3458
|
+
return True, booking_id, None
|
3459
|
+
|
3460
|
+
async def cancel_booking(self, booking_id, user_id):
|
3461
|
+
"""Cancel a booking."""
|
3462
|
+
# Verify booking exists
|
3463
|
+
booking = self.repository.get_booking(booking_id)
|
3464
|
+
if not booking:
|
3465
|
+
return False, "Booking not found"
|
3466
|
+
|
3467
|
+
# Verify user owns the booking
|
3468
|
+
if booking.user_id != user_id:
|
3469
|
+
return False, "Not authorized to cancel this booking"
|
3470
|
+
|
3471
|
+
# Cancel booking
|
3472
|
+
result = self.repository.cancel_booking(booking_id)
|
3473
|
+
if result:
|
3474
|
+
return True, None
|
3475
|
+
return False, "Failed to cancel booking"
|
3476
|
+
|
3477
|
+
async def get_resource_schedule(self, resource_id, start_date, end_date):
|
3478
|
+
"""Get a resource's schedule for a date range."""
|
3479
|
+
return self.repository.get_resource_schedule(resource_id, start_date, end_date)
|
3480
|
+
|
3481
|
+
async def get_user_bookings(self, user_id, include_cancelled=False):
|
3482
|
+
"""Get all bookings for a user with resource details."""
|
3483
|
+
bookings = self.repository.get_user_bookings(
|
3484
|
+
user_id,
|
3485
|
+
include_cancelled
|
3486
|
+
)
|
3487
|
+
|
3488
|
+
result = []
|
3489
|
+
for booking in bookings:
|
3490
|
+
resource = self.repository.get_resource(booking.resource_id)
|
3491
|
+
result.append({
|
3492
|
+
"booking": booking.model_dump(),
|
3493
|
+
"resource": resource.model_dump() if resource else None
|
3494
|
+
})
|
3495
|
+
|
3496
|
+
return result
|
2655
3497
|
|
2656
3498
|
|
2657
3499
|
class TaskPlanningService:
|
@@ -3008,6 +3850,181 @@ class TaskPlanningService:
|
|
3008
3850
|
"domain_knowledge": 5,
|
3009
3851
|
}
|
3010
3852
|
|
3853
|
+
async def generate_subtasks_with_resources(
|
3854
|
+
self, ticket_id: str, task_description: str
|
3855
|
+
) -> List[SubtaskModel]:
|
3856
|
+
"""Generate subtasks for a complex task with resource requirements."""
|
3857
|
+
# Fetch ticket to verify it exists
|
3858
|
+
ticket = self.ticket_repository.get_by_id(ticket_id)
|
3859
|
+
if not ticket:
|
3860
|
+
raise ValueError(f"Ticket {ticket_id} not found")
|
3861
|
+
|
3862
|
+
# Mark parent ticket as a parent
|
3863
|
+
self.ticket_repository.update(
|
3864
|
+
ticket_id, {"is_parent": True, "status": TicketStatus.PLANNING}
|
3865
|
+
)
|
3866
|
+
|
3867
|
+
# Generate subtasks using LLM
|
3868
|
+
agent_name = next(iter(self.agent_service.get_all_ai_agents().keys()))
|
3869
|
+
agent_config = self.agent_service.get_all_ai_agents()[agent_name]
|
3870
|
+
model = agent_config.get("model", "gpt-4o-mini")
|
3871
|
+
|
3872
|
+
prompt = f"""
|
3873
|
+
Break down the following complex task into logical subtasks with resource requirements:
|
3874
|
+
|
3875
|
+
TASK: {task_description}
|
3876
|
+
|
3877
|
+
For each subtask, provide:
|
3878
|
+
1. A brief title
|
3879
|
+
2. A clear description of what needs to be done
|
3880
|
+
3. An estimate of time required in minutes
|
3881
|
+
4. Any dependencies (which subtasks must be completed first)
|
3882
|
+
5. Required resources with these details:
|
3883
|
+
- Resource type (room, equipment, etc.)
|
3884
|
+
- Quantity needed
|
3885
|
+
- Specific requirements (e.g., "room with projector", "laptop with design software")
|
3886
|
+
|
3887
|
+
Format as a JSON array of objects with these fields:
|
3888
|
+
- title: string
|
3889
|
+
- description: string
|
3890
|
+
- estimated_minutes: number
|
3891
|
+
- dependencies: array of previous subtask titles that must be completed first
|
3892
|
+
- required_resources: array of objects with fields:
|
3893
|
+
- resource_type: string
|
3894
|
+
- quantity: number
|
3895
|
+
- requirements: string (specific features needed)
|
3896
|
+
|
3897
|
+
The subtasks should be in a logical sequence. Keep dependencies minimal and avoid circular dependencies.
|
3898
|
+
"""
|
3899
|
+
|
3900
|
+
response_text = ""
|
3901
|
+
async for chunk in self.llm_provider.generate_text(
|
3902
|
+
ticket.user_id,
|
3903
|
+
prompt,
|
3904
|
+
system_prompt="You are an expert project planner who breaks down complex tasks efficiently and identifies required resources.",
|
3905
|
+
stream=False,
|
3906
|
+
model=model,
|
3907
|
+
temperature=0.2,
|
3908
|
+
response_format={"type": "json_object"},
|
3909
|
+
):
|
3910
|
+
response_text += chunk
|
3911
|
+
|
3912
|
+
try:
|
3913
|
+
data = json.loads(response_text)
|
3914
|
+
subtasks_data = data.get("subtasks", [])
|
3915
|
+
|
3916
|
+
# Create subtask objects
|
3917
|
+
subtasks = []
|
3918
|
+
for i, task_data in enumerate(subtasks_data):
|
3919
|
+
subtask = SubtaskModel(
|
3920
|
+
parent_id=ticket_id,
|
3921
|
+
title=task_data.get("title", f"Subtask {i+1}"),
|
3922
|
+
description=task_data.get("description", ""),
|
3923
|
+
estimated_minutes=task_data.get("estimated_minutes", 30),
|
3924
|
+
dependencies=[], # We'll fill this after all subtasks are created
|
3925
|
+
status="planning",
|
3926
|
+
required_resources=task_data.get("required_resources", []),
|
3927
|
+
is_subtask=True,
|
3928
|
+
created_at=datetime.datetime.now(datetime.timezone.utc)
|
3929
|
+
)
|
3930
|
+
subtasks.append(subtask)
|
3931
|
+
|
3932
|
+
# Process dependencies (convert title references to IDs)
|
3933
|
+
title_to_id = {task.title: task.id for task in subtasks}
|
3934
|
+
for i, task_data in enumerate(subtasks_data):
|
3935
|
+
dependency_titles = task_data.get("dependencies", [])
|
3936
|
+
for title in dependency_titles:
|
3937
|
+
if title in title_to_id:
|
3938
|
+
subtasks[i].dependencies.append(title_to_id[title])
|
3939
|
+
|
3940
|
+
# Store subtasks in database
|
3941
|
+
for subtask in subtasks:
|
3942
|
+
self.ticket_repository.create(subtask)
|
3943
|
+
|
3944
|
+
return subtasks
|
3945
|
+
|
3946
|
+
except Exception as e:
|
3947
|
+
print(f"Error generating subtasks with resources: {e}")
|
3948
|
+
return []
|
3949
|
+
|
3950
|
+
async def allocate_resources(
|
3951
|
+
self, subtask_id: str, resource_service: ResourceService
|
3952
|
+
) -> Tuple[bool, str]:
|
3953
|
+
"""Allocate resources to a subtask."""
|
3954
|
+
# Get the subtask
|
3955
|
+
subtask = self.ticket_repository.get_by_id(subtask_id)
|
3956
|
+
if not subtask or not subtask.is_subtask:
|
3957
|
+
return False, "Subtask not found"
|
3958
|
+
|
3959
|
+
if not subtask.required_resources:
|
3960
|
+
return True, "No resources required"
|
3961
|
+
|
3962
|
+
if not subtask.scheduled_start or not subtask.scheduled_end:
|
3963
|
+
return False, "Subtask must be scheduled before resources can be allocated"
|
3964
|
+
|
3965
|
+
# For each required resource
|
3966
|
+
resource_assignments = []
|
3967
|
+
for resource_req in subtask.required_resources:
|
3968
|
+
resource_type = resource_req.get("resource_type")
|
3969
|
+
requirements = resource_req.get("requirements", "")
|
3970
|
+
quantity = resource_req.get("quantity", 1)
|
3971
|
+
|
3972
|
+
# Find available resources matching the requirements
|
3973
|
+
resources = await resource_service.find_available_resources(
|
3974
|
+
start_time=subtask.scheduled_start,
|
3975
|
+
end_time=subtask.scheduled_end,
|
3976
|
+
resource_type=resource_type,
|
3977
|
+
tags=requirements.split() if requirements else None,
|
3978
|
+
capacity=None # Could use quantity here if it represents capacity
|
3979
|
+
)
|
3980
|
+
|
3981
|
+
if not resources or len(resources) < quantity:
|
3982
|
+
return False, f"Insufficient {resource_type} resources available"
|
3983
|
+
|
3984
|
+
# Allocate the resources by creating bookings
|
3985
|
+
allocated_resources = []
|
3986
|
+
for i in range(quantity):
|
3987
|
+
if i >= len(resources):
|
3988
|
+
break
|
3989
|
+
|
3990
|
+
resource = resources[i]
|
3991
|
+
success, booking_id, error = await resource_service.create_booking(
|
3992
|
+
resource_id=resource.id,
|
3993
|
+
user_id=subtask.assigned_to or "system",
|
3994
|
+
# Use query instead of title
|
3995
|
+
title=f"Task: {subtask.query}",
|
3996
|
+
start_time=subtask.scheduled_start,
|
3997
|
+
end_time=subtask.scheduled_end,
|
3998
|
+
description=subtask.description
|
3999
|
+
)
|
4000
|
+
|
4001
|
+
if success:
|
4002
|
+
allocated_resources.append({
|
4003
|
+
"resource_id": resource.id,
|
4004
|
+
"resource_name": resource.name,
|
4005
|
+
"booking_id": booking_id,
|
4006
|
+
"resource_type": resource.resource_type
|
4007
|
+
})
|
4008
|
+
else:
|
4009
|
+
# Clean up any allocations already made
|
4010
|
+
for alloc in allocated_resources:
|
4011
|
+
await resource_service.cancel_booking(alloc["booking_id"], subtask.assigned_to or "system")
|
4012
|
+
return False, f"Failed to book resource: {error}"
|
4013
|
+
|
4014
|
+
resource_assignments.append({
|
4015
|
+
"requirement": resource_req,
|
4016
|
+
"allocated": allocated_resources
|
4017
|
+
})
|
4018
|
+
|
4019
|
+
# Update the subtask with resource assignments
|
4020
|
+
subtask.resource_assignments = resource_assignments
|
4021
|
+
self.ticket_repository.update(subtask_id, {
|
4022
|
+
"resource_assignments": resource_assignments,
|
4023
|
+
"updated_at": datetime.datetime.now(datetime.timezone.utc)
|
4024
|
+
})
|
4025
|
+
|
4026
|
+
return True, f"Successfully allocated {len(resource_assignments)} resource types"
|
4027
|
+
|
3011
4028
|
|
3012
4029
|
class ProjectApprovalService:
|
3013
4030
|
"""Service for managing human approval of new projects."""
|
@@ -3777,6 +4794,77 @@ class SchedulingService:
|
|
3777
4794
|
|
3778
4795
|
return task
|
3779
4796
|
|
4797
|
+
async def find_optimal_time_slot_with_resources(
|
4798
|
+
self,
|
4799
|
+
task: ScheduledTask,
|
4800
|
+
resource_service: ResourceService,
|
4801
|
+
agent_schedule: Optional[AgentSchedule] = None
|
4802
|
+
) -> Optional[TimeWindow]:
|
4803
|
+
"""Find the optimal time slot for a task based on both agent and resource availability."""
|
4804
|
+
if not task.assigned_to:
|
4805
|
+
return None
|
4806
|
+
|
4807
|
+
# First, find potential time slots based on agent availability
|
4808
|
+
agent_id = task.assigned_to
|
4809
|
+
duration = task.estimated_minutes or 30
|
4810
|
+
|
4811
|
+
# Start no earlier than now
|
4812
|
+
start_after = datetime.datetime.now(datetime.timezone.utc)
|
4813
|
+
|
4814
|
+
# Apply task constraints
|
4815
|
+
for constraint in task.constraints:
|
4816
|
+
if constraint.get("type") == "must_start_after" and constraint.get("time"):
|
4817
|
+
constraint_time = datetime.datetime.fromisoformat(
|
4818
|
+
constraint["time"])
|
4819
|
+
if constraint_time > start_after:
|
4820
|
+
start_after = constraint_time
|
4821
|
+
|
4822
|
+
# Get potential time slots for the agent
|
4823
|
+
agent_slots = await self.find_available_time_slots(
|
4824
|
+
agent_id,
|
4825
|
+
duration,
|
4826
|
+
start_after,
|
4827
|
+
count=3, # Get multiple slots to try with resources
|
4828
|
+
agent_schedule=agent_schedule
|
4829
|
+
)
|
4830
|
+
|
4831
|
+
if not agent_slots:
|
4832
|
+
return None
|
4833
|
+
|
4834
|
+
# Check if task has resource requirements
|
4835
|
+
required_resources = getattr(task, "required_resources", [])
|
4836
|
+
if not required_resources:
|
4837
|
+
# If no resources needed, return the first available agent slot
|
4838
|
+
return agent_slots[0]
|
4839
|
+
|
4840
|
+
# For each potential time slot, check resource availability
|
4841
|
+
for time_slot in agent_slots:
|
4842
|
+
all_resources_available = True
|
4843
|
+
|
4844
|
+
for resource_req in required_resources:
|
4845
|
+
resource_type = resource_req.get("resource_type")
|
4846
|
+
requirements = resource_req.get("requirements", "")
|
4847
|
+
quantity = resource_req.get("quantity", 1)
|
4848
|
+
|
4849
|
+
# Find available resources for this time slot
|
4850
|
+
resources = await resource_service.find_available_resources(
|
4851
|
+
start_time=time_slot.start,
|
4852
|
+
end_time=time_slot.end,
|
4853
|
+
resource_type=resource_type,
|
4854
|
+
tags=requirements.split() if requirements else None
|
4855
|
+
)
|
4856
|
+
|
4857
|
+
if len(resources) < quantity:
|
4858
|
+
all_resources_available = False
|
4859
|
+
break
|
4860
|
+
|
4861
|
+
# If all resources are available, use this time slot
|
4862
|
+
if all_resources_available:
|
4863
|
+
return time_slot
|
4864
|
+
|
4865
|
+
# If no time slot has all resources available, default to first slot
|
4866
|
+
return agent_slots[0]
|
4867
|
+
|
3780
4868
|
async def optimize_schedule(self) -> Dict[str, Any]:
|
3781
4869
|
"""Optimize the entire schedule to maximize efficiency."""
|
3782
4870
|
# Get all pending and scheduled tasks
|
@@ -4431,6 +5519,7 @@ class SchedulingService:
|
|
4431
5519
|
|
4432
5520
|
return formatted_requests
|
4433
5521
|
|
5522
|
+
|
4434
5523
|
#############################################
|
4435
5524
|
# MAIN AGENT PROCESSOR
|
4436
5525
|
#############################################
|
@@ -4498,28 +5587,18 @@ class QueryProcessor:
|
|
4498
5587
|
self._stalled_ticket_task = loop.create_task(
|
4499
5588
|
self._run_stalled_ticket_checks())
|
4500
5589
|
except RuntimeError:
|
4501
|
-
# No running event loop - likely in test environment
|
4502
|
-
# Instead of just passing, log a message for clarity
|
4503
5590
|
import logging
|
4504
5591
|
logging.warning(
|
4505
5592
|
"No running event loop available for stalled ticket checker.")
|
4506
|
-
# Don't try to create the task - this prevents the coroutine warning
|
4507
5593
|
|
4508
5594
|
try:
|
4509
|
-
#
|
4510
|
-
if
|
4511
|
-
|
4512
|
-
|
4513
|
-
|
4514
|
-
|
4515
|
-
|
4516
|
-
|
4517
|
-
# Handle simple greetings without full agent routing
|
4518
|
-
if await self._is_simple_greeting(user_text):
|
4519
|
-
greeting_response = await self._generate_greeting_response(
|
4520
|
-
user_id, user_text
|
4521
|
-
)
|
4522
|
-
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)
|
4523
5602
|
return
|
4524
5603
|
|
4525
5604
|
# Handle system commands
|
@@ -4528,100 +5607,66 @@ class QueryProcessor:
|
|
4528
5607
|
yield command_response
|
4529
5608
|
return
|
4530
5609
|
|
5610
|
+
# Route to appropriate agent
|
5611
|
+
agent_name = await self.routing_service.route_query(user_text)
|
5612
|
+
|
4531
5613
|
# Check for active ticket
|
4532
5614
|
active_ticket = self.ticket_service.ticket_repository.get_active_for_user(
|
4533
|
-
user_id
|
4534
|
-
)
|
5615
|
+
user_id)
|
4535
5616
|
|
4536
5617
|
if active_ticket:
|
4537
5618
|
# Process existing ticket
|
4538
|
-
|
4539
|
-
|
4540
|
-
|
4541
|
-
|
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
|
+
|
4542
5641
|
else:
|
4543
5642
|
# Create new ticket
|
4544
|
-
|
4545
|
-
|
4546
|
-
|
4547
|
-
|
4548
|
-
|
4549
|
-
|
4550
|
-
|
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)}"
|
4551
5660
|
|
4552
5661
|
except Exception as e:
|
4553
5662
|
print(f"Error in request processing: {str(e)}")
|
4554
5663
|
print(traceback.format_exc())
|
4555
|
-
|
4556
|
-
error_msg = "\n\nI apologize for the technical difficulty.\n\n"
|
4557
|
-
yield error_msg
|
5664
|
+
yield "I apologize for the technical difficulty.\n\n"
|
4558
5665
|
|
4559
5666
|
async def _is_human_agent(self, user_id: str) -> bool:
|
4560
5667
|
"""Check if the user is a registered human agent."""
|
4561
5668
|
return user_id in self.agent_service.get_all_human_agents()
|
4562
5669
|
|
4563
|
-
async def _is_simple_greeting(self, text: str) -> bool:
|
4564
|
-
"""Determine if the user message is a simple greeting."""
|
4565
|
-
text_lower = text.lower().strip()
|
4566
|
-
|
4567
|
-
# Common greetings list
|
4568
|
-
simple_greetings = [
|
4569
|
-
"hello",
|
4570
|
-
"hi",
|
4571
|
-
"hey",
|
4572
|
-
"greetings",
|
4573
|
-
"good morning",
|
4574
|
-
"good afternoon",
|
4575
|
-
"good evening",
|
4576
|
-
"what's up",
|
4577
|
-
"how are you",
|
4578
|
-
"how's it going",
|
4579
|
-
]
|
4580
|
-
|
4581
|
-
# Check if text starts with a greeting and is relatively short
|
4582
|
-
is_greeting = any(
|
4583
|
-
text_lower.startswith(greeting) for greeting in simple_greetings
|
4584
|
-
)
|
4585
|
-
# Arbitrary threshold for "simple" messages
|
4586
|
-
is_short = len(text.split()) < 7
|
4587
|
-
|
4588
|
-
return is_greeting and is_short
|
4589
|
-
|
4590
|
-
async def _generate_greeting_response(self, user_id: str, text: str) -> str:
|
4591
|
-
"""Generate a friendly response to a simple greeting."""
|
4592
|
-
# Get user context if available
|
4593
|
-
context = ""
|
4594
|
-
if self.memory_provider:
|
4595
|
-
context = await self.memory_provider.retrieve(user_id)
|
4596
|
-
|
4597
|
-
# Get first available AI agent for the greeting
|
4598
|
-
first_agent_name = next(
|
4599
|
-
iter(self.agent_service.get_all_ai_agents().keys()))
|
4600
|
-
|
4601
|
-
response = ""
|
4602
|
-
async for chunk in self.agent_service.generate_response(
|
4603
|
-
first_agent_name,
|
4604
|
-
user_id,
|
4605
|
-
text,
|
4606
|
-
context,
|
4607
|
-
temperature=0.7,
|
4608
|
-
max_tokens=100, # Keep it brief
|
4609
|
-
):
|
4610
|
-
response += chunk
|
4611
|
-
|
4612
|
-
# Store in memory if available
|
4613
|
-
if self.memory_provider:
|
4614
|
-
await self.memory_provider.store(
|
4615
|
-
user_id,
|
4616
|
-
[
|
4617
|
-
{"role": "user", "content": text},
|
4618
|
-
{"role": "assistant",
|
4619
|
-
"content": self._truncate(response, 2500)},
|
4620
|
-
],
|
4621
|
-
)
|
4622
|
-
|
4623
|
-
return response
|
4624
|
-
|
4625
5670
|
async def shutdown(self):
|
4626
5671
|
"""Clean shutdown of the query processor."""
|
4627
5672
|
self._shutdown_event.set()
|
@@ -4656,34 +5701,48 @@ class QueryProcessor:
|
|
4656
5701
|
if self.stalled_ticket_timeout is None:
|
4657
5702
|
return
|
4658
5703
|
|
4659
|
-
|
4660
|
-
|
4661
|
-
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
|
+
)
|
4662
5709
|
|
4663
|
-
|
4664
|
-
|
4665
|
-
|
4666
|
-
|
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
|
+
)
|
4667
5714
|
|
4668
|
-
|
4669
|
-
|
4670
|
-
|
5715
|
+
for ticket in stalled_tickets:
|
5716
|
+
print(
|
5717
|
+
f"Found stalled ticket: {ticket.id} (last updated: {ticket.updated_at})")
|
4671
5718
|
|
4672
|
-
|
4673
|
-
|
4674
|
-
|
5719
|
+
# Skip tickets without an assigned agent
|
5720
|
+
if not ticket.assigned_to:
|
5721
|
+
continue
|
4675
5722
|
|
4676
|
-
|
4677
|
-
|
4678
|
-
ticket.id,
|
4679
|
-
ticket.assigned_to or "unassigned",
|
4680
|
-
new_agent,
|
4681
|
-
f"Automatically reassigned after {self.stalled_ticket_timeout} minutes of inactivity"
|
4682
|
-
)
|
5723
|
+
# Re-route the query to see if a different agent is better
|
5724
|
+
new_agent = await self.routing_service.route_query(ticket.query)
|
4683
5725
|
|
4684
|
-
|
4685
|
-
|
4686
|
-
|
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())
|
4687
5746
|
|
4688
5747
|
async def _process_system_commands(
|
4689
5748
|
self, user_id: str, user_text: str
|
@@ -4709,7 +5768,7 @@ class QueryProcessor:
|
|
4709
5768
|
|
4710
5769
|
return response
|
4711
5770
|
|
4712
|
-
|
5771
|
+
if command == "!plan" and args:
|
4713
5772
|
# Create a new plan from the task description
|
4714
5773
|
if not self.task_planning_service:
|
4715
5774
|
return "Task planning service is not available."
|
@@ -4721,8 +5780,8 @@ class QueryProcessor:
|
|
4721
5780
|
user_id, args, complexity
|
4722
5781
|
)
|
4723
5782
|
|
4724
|
-
# Generate subtasks
|
4725
|
-
subtasks = await self.task_planning_service.
|
5783
|
+
# Generate subtasks with resource requirements
|
5784
|
+
subtasks = await self.task_planning_service.generate_subtasks_with_resources(
|
4726
5785
|
ticket.id, args
|
4727
5786
|
)
|
4728
5787
|
|
@@ -4736,17 +5795,44 @@ class QueryProcessor:
|
|
4736
5795
|
for i, subtask in enumerate(subtasks, 1):
|
4737
5796
|
response += f"{i}. **{subtask.title}**\n"
|
4738
5797
|
response += f" - Description: {subtask.description}\n"
|
4739
|
-
response +=
|
4740
|
-
|
4741
|
-
|
5798
|
+
response += f" - Estimated time: {subtask.estimated_minutes} minutes\n"
|
5799
|
+
|
5800
|
+
if subtask.required_resources:
|
5801
|
+
response += f" - Required resources:\n"
|
5802
|
+
for res in subtask.required_resources:
|
5803
|
+
res_type = res.get("resource_type", "unknown")
|
5804
|
+
quantity = res.get("quantity", 1)
|
5805
|
+
requirements = res.get("requirements", "")
|
5806
|
+
response += f" * {quantity} {res_type}" + \
|
5807
|
+
(f" ({requirements})" if requirements else "") + "\n"
|
5808
|
+
|
4742
5809
|
if subtask.dependencies:
|
4743
|
-
response += (
|
4744
|
-
|
4745
|
-
)
|
5810
|
+
response += f" - Dependencies: {len(subtask.dependencies)} subtasks\n"
|
5811
|
+
|
4746
5812
|
response += "\n"
|
4747
5813
|
|
4748
5814
|
return response
|
4749
5815
|
|
5816
|
+
# Add a new command for allocating resources to tasks
|
5817
|
+
elif command == "!allocate-resources" and args:
|
5818
|
+
parts = args.split()
|
5819
|
+
if len(parts) < 1:
|
5820
|
+
return "Usage: !allocate-resources [subtask_id]"
|
5821
|
+
|
5822
|
+
subtask_id = parts[0]
|
5823
|
+
|
5824
|
+
if not hasattr(self, "resource_service") or not self.resource_service:
|
5825
|
+
return "Resource service is not available."
|
5826
|
+
|
5827
|
+
success, message = await self.task_planning_service.allocate_resources(
|
5828
|
+
subtask_id, self.resource_service
|
5829
|
+
)
|
5830
|
+
|
5831
|
+
if success:
|
5832
|
+
return f"✅ Resources allocated successfully: {message}"
|
5833
|
+
else:
|
5834
|
+
return f"❌ Failed to allocate resources: {message}"
|
5835
|
+
|
4750
5836
|
elif command == "!status" and args:
|
4751
5837
|
# Show status of a specific plan
|
4752
5838
|
if not self.task_planning_service:
|
@@ -4993,117 +6079,482 @@ class QueryProcessor:
|
|
4993
6079
|
|
4994
6080
|
return response
|
4995
6081
|
|
6082
|
+
elif command == "!resources" and self.resource_service:
|
6083
|
+
# Format: !resources [list|find|show|create|update|delete]
|
6084
|
+
parts = args.strip().split(" ", 1)
|
6085
|
+
subcommand = parts[0] if parts else "list"
|
6086
|
+
subcmd_args = parts[1] if len(parts) > 1 else ""
|
6087
|
+
|
6088
|
+
if subcommand == "list":
|
6089
|
+
# List available resources, optionally filtered by type
|
6090
|
+
resource_type = subcmd_args if subcmd_args else None
|
6091
|
+
|
6092
|
+
query = {}
|
6093
|
+
if resource_type:
|
6094
|
+
query["resource_type"] = resource_type
|
6095
|
+
|
6096
|
+
resources = self.resource_service.repository.find_resources(
|
6097
|
+
query)
|
6098
|
+
|
6099
|
+
if not resources:
|
6100
|
+
return "No resources found."
|
6101
|
+
|
6102
|
+
response = "# Available Resources\n\n"
|
6103
|
+
|
6104
|
+
# Group by type
|
6105
|
+
resources_by_type = {}
|
6106
|
+
for resource in resources:
|
6107
|
+
r_type = resource.resource_type
|
6108
|
+
if r_type not in resources_by_type:
|
6109
|
+
resources_by_type[r_type] = []
|
6110
|
+
resources_by_type[r_type].append(resource)
|
6111
|
+
|
6112
|
+
for r_type, r_list in resources_by_type.items():
|
6113
|
+
response += f"## {r_type.capitalize()}\n\n"
|
6114
|
+
for resource in r_list:
|
6115
|
+
status_emoji = "🟢" if resource.status == "available" else "🔴"
|
6116
|
+
response += f"{status_emoji} **{resource.name}** (ID: {resource.id})\n"
|
6117
|
+
if resource.description:
|
6118
|
+
response += f" {resource.description}\n"
|
6119
|
+
if resource.location and resource.location.building:
|
6120
|
+
response += f" Location: {resource.location.building}"
|
6121
|
+
if resource.location.room:
|
6122
|
+
response += f", Room {resource.location.room}"
|
6123
|
+
response += "\n"
|
6124
|
+
if resource.capacity:
|
6125
|
+
response += f" Capacity: {resource.capacity}\n"
|
6126
|
+
response += "\n"
|
6127
|
+
|
6128
|
+
return response
|
6129
|
+
|
6130
|
+
elif subcommand == "show" and subcmd_args:
|
6131
|
+
# Show details for a specific resource
|
6132
|
+
resource_id = subcmd_args.strip()
|
6133
|
+
resource = await self.resource_service.get_resource(resource_id)
|
6134
|
+
|
6135
|
+
if not resource:
|
6136
|
+
return f"Resource with ID {resource_id} not found."
|
6137
|
+
|
6138
|
+
response = f"# Resource: {resource.name}\n\n"
|
6139
|
+
response += f"**ID**: {resource.id}\n"
|
6140
|
+
response += f"**Type**: {resource.resource_type}\n"
|
6141
|
+
response += f"**Status**: {resource.status}\n"
|
6142
|
+
|
6143
|
+
if resource.description:
|
6144
|
+
response += f"\n**Description**: {resource.description}\n"
|
6145
|
+
|
6146
|
+
if resource.location:
|
6147
|
+
response += "\n**Location**:\n"
|
6148
|
+
if resource.location.address:
|
6149
|
+
response += f"- Address: {resource.location.address}\n"
|
6150
|
+
if resource.location.building:
|
6151
|
+
response += f"- Building: {resource.location.building}\n"
|
6152
|
+
if resource.location.floor is not None:
|
6153
|
+
response += f"- Floor: {resource.location.floor}\n"
|
6154
|
+
if resource.location.room:
|
6155
|
+
response += f"- Room: {resource.location.room}\n"
|
6156
|
+
|
6157
|
+
if resource.capacity:
|
6158
|
+
response += f"\n**Capacity**: {resource.capacity}\n"
|
6159
|
+
|
6160
|
+
if resource.tags:
|
6161
|
+
response += f"\n**Tags**: {', '.join(resource.tags)}\n"
|
6162
|
+
|
6163
|
+
# Show availability schedule
|
6164
|
+
if resource.availability_schedule:
|
6165
|
+
response += "\n**Regular Availability**:\n"
|
6166
|
+
for window in resource.availability_schedule:
|
6167
|
+
days = "Every day"
|
6168
|
+
if window.day_of_week:
|
6169
|
+
day_names = [
|
6170
|
+
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
6171
|
+
days = ", ".join([day_names[d]
|
6172
|
+
for d in window.day_of_week])
|
6173
|
+
response += f"- {days}: {window.start_time} - {window.end_time} ({window.timezone})\n"
|
6174
|
+
|
6175
|
+
# Show upcoming bookings
|
6176
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
6177
|
+
next_month = now + datetime.timedelta(days=30)
|
6178
|
+
bookings = self.resource_service.repository.get_resource_bookings(
|
6179
|
+
resource_id, now, next_month)
|
6180
|
+
|
6181
|
+
if bookings:
|
6182
|
+
response += "\n**Upcoming Bookings**:\n"
|
6183
|
+
for booking in bookings:
|
6184
|
+
start_str = booking.start_time.strftime(
|
6185
|
+
"%Y-%m-%d %H:%M")
|
6186
|
+
end_str = booking.end_time.strftime("%H:%M")
|
6187
|
+
response += f"- {start_str} - {end_str}: {booking.title}\n"
|
6188
|
+
|
6189
|
+
return response
|
6190
|
+
|
6191
|
+
elif subcommand == "find":
|
6192
|
+
# Find available resources for a time period
|
6193
|
+
# Format: !resources find room 2023-03-15 14:00 16:00
|
6194
|
+
parts = subcmd_args.split()
|
6195
|
+
if len(parts) < 4:
|
6196
|
+
return "Usage: !resources find [type] [date] [start_time] [end_time] [capacity]"
|
6197
|
+
|
6198
|
+
resource_type = parts[0]
|
6199
|
+
date_str = parts[1]
|
6200
|
+
start_time_str = parts[2]
|
6201
|
+
end_time_str = parts[3]
|
6202
|
+
capacity = int(parts[4]) if len(parts) > 4 else None
|
6203
|
+
|
6204
|
+
try:
|
6205
|
+
# Parse date and times
|
6206
|
+
date_obj = datetime.datetime.strptime(
|
6207
|
+
date_str, "%Y-%m-%d").date()
|
6208
|
+
start_time = datetime.datetime.combine(
|
6209
|
+
date_obj,
|
6210
|
+
datetime.datetime.strptime(
|
6211
|
+
start_time_str, "%H:%M").time(),
|
6212
|
+
tzinfo=datetime.timezone.utc
|
6213
|
+
)
|
6214
|
+
end_time = datetime.datetime.combine(
|
6215
|
+
date_obj,
|
6216
|
+
datetime.datetime.strptime(
|
6217
|
+
end_time_str, "%H:%M").time(),
|
6218
|
+
tzinfo=datetime.timezone.utc
|
6219
|
+
)
|
6220
|
+
|
6221
|
+
# Find available resources
|
6222
|
+
resources = await self.resource_service.find_available_resources(
|
6223
|
+
resource_type=resource_type,
|
6224
|
+
start_time=start_time,
|
6225
|
+
end_time=end_time,
|
6226
|
+
capacity=capacity
|
6227
|
+
)
|
6228
|
+
|
6229
|
+
if not resources:
|
6230
|
+
return f"No {resource_type}s available for the requested time period."
|
6231
|
+
|
6232
|
+
response = f"# Available {resource_type.capitalize()}s\n\n"
|
6233
|
+
response += f"**Date**: {date_str}\n"
|
6234
|
+
response += f"**Time**: {start_time_str} - {end_time_str}\n"
|
6235
|
+
if capacity:
|
6236
|
+
response += f"**Minimum Capacity**: {capacity}\n"
|
6237
|
+
response += "\n"
|
6238
|
+
|
6239
|
+
for resource in resources:
|
6240
|
+
response += f"- **{resource.name}** (ID: {resource.id})\n"
|
6241
|
+
if resource.description:
|
6242
|
+
response += f" {resource.description}\n"
|
6243
|
+
if resource.capacity:
|
6244
|
+
response += f" Capacity: {resource.capacity}\n"
|
6245
|
+
if resource.location and resource.location.building:
|
6246
|
+
response += f" Location: {resource.location.building}"
|
6247
|
+
if resource.location.room:
|
6248
|
+
response += f", Room {resource.location.room}"
|
6249
|
+
response += "\n"
|
6250
|
+
response += "\n"
|
6251
|
+
|
6252
|
+
return response
|
6253
|
+
|
6254
|
+
except ValueError as e:
|
6255
|
+
return f"Error parsing date/time: {e}"
|
6256
|
+
|
6257
|
+
elif subcommand == "book":
|
6258
|
+
# Book a resource
|
6259
|
+
# Format: !resources book [resource_id] [date] [start_time] [end_time] [title]
|
6260
|
+
parts = subcmd_args.split(" ", 5)
|
6261
|
+
if len(parts) < 5:
|
6262
|
+
return "Usage: !resources book [resource_id] [date] [start_time] [end_time] [title]"
|
6263
|
+
|
6264
|
+
resource_id = parts[0]
|
6265
|
+
date_str = parts[1]
|
6266
|
+
start_time_str = parts[2]
|
6267
|
+
end_time_str = parts[3]
|
6268
|
+
title = parts[4] if len(parts) > 4 else "Booking"
|
6269
|
+
|
6270
|
+
try:
|
6271
|
+
# Parse date and times
|
6272
|
+
date_obj = datetime.datetime.strptime(
|
6273
|
+
date_str, "%Y-%m-%d").date()
|
6274
|
+
start_time = datetime.datetime.combine(
|
6275
|
+
date_obj,
|
6276
|
+
datetime.datetime.strptime(
|
6277
|
+
start_time_str, "%H:%M").time(),
|
6278
|
+
tzinfo=datetime.timezone.utc
|
6279
|
+
)
|
6280
|
+
end_time = datetime.datetime.combine(
|
6281
|
+
date_obj,
|
6282
|
+
datetime.datetime.strptime(
|
6283
|
+
end_time_str, "%H:%M").time(),
|
6284
|
+
tzinfo=datetime.timezone.utc
|
6285
|
+
)
|
6286
|
+
|
6287
|
+
# Create booking
|
6288
|
+
success, booking, error = await self.resource_service.create_booking(
|
6289
|
+
resource_id=resource_id,
|
6290
|
+
user_id=user_id,
|
6291
|
+
title=title,
|
6292
|
+
start_time=start_time,
|
6293
|
+
end_time=end_time
|
6294
|
+
)
|
6295
|
+
|
6296
|
+
if not success:
|
6297
|
+
return f"Failed to book resource: {error}"
|
6298
|
+
|
6299
|
+
# Get resource details
|
6300
|
+
resource = await self.resource_service.get_resource(resource_id)
|
6301
|
+
resource_name = resource.name if resource else resource_id
|
6302
|
+
|
6303
|
+
response = "# Booking Confirmed\n\n"
|
6304
|
+
response += f"**Resource**: {resource_name}\n"
|
6305
|
+
response += f"**Date**: {date_str}\n"
|
6306
|
+
response += f"**Time**: {start_time_str} - {end_time_str}\n"
|
6307
|
+
response += f"**Title**: {title}\n"
|
6308
|
+
response += f"**Booking ID**: {booking.id}\n\n"
|
6309
|
+
response += "Your booking has been confirmed and added to your schedule."
|
6310
|
+
|
6311
|
+
return response
|
6312
|
+
|
6313
|
+
except ValueError as e:
|
6314
|
+
return f"Error parsing date/time: {e}"
|
6315
|
+
|
6316
|
+
elif subcommand == "bookings":
|
6317
|
+
# View all bookings for the current user
|
6318
|
+
include_cancelled = "all" in subcmd_args.lower()
|
6319
|
+
|
6320
|
+
bookings = await self.resource_service.get_user_bookings(user_id, include_cancelled)
|
6321
|
+
|
6322
|
+
if not bookings:
|
6323
|
+
return "You don't have any bookings." + (
|
6324
|
+
" (Use 'bookings all' to include cancelled bookings)" if not include_cancelled else ""
|
6325
|
+
)
|
6326
|
+
|
6327
|
+
response = "# Your Bookings\n\n"
|
6328
|
+
|
6329
|
+
# Group bookings by date
|
6330
|
+
bookings_by_date = {}
|
6331
|
+
for booking_data in bookings:
|
6332
|
+
booking = booking_data["booking"]
|
6333
|
+
resource = booking_data["resource"]
|
6334
|
+
|
6335
|
+
date_str = booking["start_time"].strftime("%Y-%m-%d")
|
6336
|
+
if date_str not in bookings_by_date:
|
6337
|
+
bookings_by_date[date_str] = []
|
6338
|
+
|
6339
|
+
bookings_by_date[date_str].append((booking, resource))
|
6340
|
+
|
6341
|
+
# Sort dates
|
6342
|
+
for date_str in sorted(bookings_by_date.keys()):
|
6343
|
+
response += f"## {date_str}\n\n"
|
6344
|
+
|
6345
|
+
for booking, resource in bookings_by_date[date_str]:
|
6346
|
+
start_time = booking["start_time"].strftime(
|
6347
|
+
"%H:%M")
|
6348
|
+
end_time = booking["end_time"].strftime("%H:%M")
|
6349
|
+
resource_name = resource["name"] if resource else "Unknown Resource"
|
6350
|
+
|
6351
|
+
status_emoji = "🟢" if booking["status"] == "confirmed" else "🔴"
|
6352
|
+
response += f"{status_emoji} **{start_time}-{end_time}**: {booking['title']}\n"
|
6353
|
+
response += f" Resource: {resource_name}\n"
|
6354
|
+
response += f" Booking ID: {booking['id']}\n\n"
|
6355
|
+
|
6356
|
+
return response
|
6357
|
+
|
6358
|
+
elif subcommand == "cancel" and subcmd_args:
|
6359
|
+
# Cancel a booking
|
6360
|
+
booking_id = subcmd_args.strip()
|
6361
|
+
|
6362
|
+
success, error = await self.resource_service.cancel_booking(booking_id, user_id)
|
6363
|
+
|
6364
|
+
if success:
|
6365
|
+
return "✅ Your booking has been successfully cancelled."
|
6366
|
+
else:
|
6367
|
+
return f"❌ Failed to cancel booking: {error}"
|
6368
|
+
|
6369
|
+
elif subcommand == "schedule" and subcmd_args:
|
6370
|
+
# View resource schedule
|
6371
|
+
# Format: !resources schedule resource_id [YYYY-MM-DD] [days]
|
6372
|
+
parts = subcmd_args.split()
|
6373
|
+
if len(parts) < 1:
|
6374
|
+
return "Usage: !resources schedule resource_id [YYYY-MM-DD] [days]"
|
6375
|
+
|
6376
|
+
resource_id = parts[0]
|
6377
|
+
|
6378
|
+
# Default to today and 7 days
|
6379
|
+
start_date = datetime.datetime.now(datetime.timezone.utc)
|
6380
|
+
days = 7
|
6381
|
+
|
6382
|
+
if len(parts) > 1:
|
6383
|
+
try:
|
6384
|
+
start_date = datetime.datetime.strptime(
|
6385
|
+
parts[1], "%Y-%m-%d"
|
6386
|
+
).replace(tzinfo=datetime.timezone.utc)
|
6387
|
+
except ValueError:
|
6388
|
+
return "Invalid date format. Use YYYY-MM-DD."
|
6389
|
+
|
6390
|
+
if len(parts) > 2:
|
6391
|
+
try:
|
6392
|
+
days = min(int(parts[2]), 31) # Limit to 31 days
|
6393
|
+
except ValueError:
|
6394
|
+
return "Days must be a number."
|
6395
|
+
|
6396
|
+
end_date = start_date + datetime.timedelta(days=days)
|
6397
|
+
|
6398
|
+
# Get the resource
|
6399
|
+
resource = await self.resource_service.get_resource(resource_id)
|
6400
|
+
if not resource:
|
6401
|
+
return f"Resource with ID {resource_id} not found."
|
6402
|
+
|
6403
|
+
# Get schedule
|
6404
|
+
schedule = await self.resource_service.get_resource_schedule(
|
6405
|
+
resource_id, start_date, end_date
|
6406
|
+
)
|
6407
|
+
|
6408
|
+
# Create calendar visualization
|
6409
|
+
response = f"# Schedule for {resource.name}\n\n"
|
6410
|
+
response += f"Period: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}\n\n"
|
6411
|
+
|
6412
|
+
# Group by date
|
6413
|
+
schedule_by_date = {}
|
6414
|
+
current_date = start_date
|
6415
|
+
while current_date < end_date:
|
6416
|
+
date_str = current_date.strftime("%Y-%m-%d")
|
6417
|
+
schedule_by_date[date_str] = []
|
6418
|
+
current_date += datetime.timedelta(days=1)
|
6419
|
+
|
6420
|
+
# Add entries to appropriate dates
|
6421
|
+
for entry in schedule:
|
6422
|
+
date_str = entry["start_time"].strftime("%Y-%m-%d")
|
6423
|
+
if date_str in schedule_by_date:
|
6424
|
+
schedule_by_date[date_str].append(entry)
|
6425
|
+
|
6426
|
+
# Generate calendar view
|
6427
|
+
for date_str, entries in schedule_by_date.items():
|
6428
|
+
# Convert to datetime for day of week
|
6429
|
+
entry_date = datetime.datetime.strptime(
|
6430
|
+
date_str, "%Y-%m-%d")
|
6431
|
+
day_of_week = entry_date.strftime("%A")
|
6432
|
+
|
6433
|
+
response += f"## {date_str} ({day_of_week})\n\n"
|
6434
|
+
|
6435
|
+
if not entries:
|
6436
|
+
response += "No bookings or exceptions\n\n"
|
6437
|
+
continue
|
6438
|
+
|
6439
|
+
# Sort by start time
|
6440
|
+
entries.sort(key=lambda x: x["start_time"])
|
6441
|
+
|
6442
|
+
for entry in entries:
|
6443
|
+
start_time = entry["start_time"].strftime("%H:%M")
|
6444
|
+
end_time = entry["end_time"].strftime("%H:%M")
|
6445
|
+
|
6446
|
+
if entry["type"] == "booking":
|
6447
|
+
response += f"- **{start_time}-{end_time}**: {entry['title']} (by {entry['user_id']})\n"
|
6448
|
+
else: # exception
|
6449
|
+
response += f"- **{start_time}-{end_time}**: {entry['status']} (Unavailable)\n"
|
6450
|
+
|
6451
|
+
response += "\n"
|
6452
|
+
|
6453
|
+
return response
|
6454
|
+
|
4996
6455
|
return None
|
4997
6456
|
|
4998
6457
|
async def _process_existing_ticket(
|
4999
6458
|
self, user_id: str, user_text: str, ticket: Ticket, timezone: str = None
|
5000
6459
|
) -> AsyncGenerator[str, None]:
|
5001
|
-
"""
|
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
|
+
"""
|
5002
6465
|
# Get assigned agent or re-route if needed
|
5003
6466
|
agent_name = ticket.assigned_to
|
5004
|
-
|
5005
|
-
# If no valid assignment, route to appropriate agent
|
5006
|
-
if not agent_name or agent_name not in self.agent_service.get_all_ai_agents():
|
6467
|
+
if not agent_name:
|
5007
6468
|
agent_name = await self.routing_service.route_query(user_text)
|
5008
|
-
# Update ticket with new assignment
|
5009
6469
|
self.ticket_service.update_ticket_status(
|
5010
|
-
ticket.id, TicketStatus.
|
6470
|
+
ticket.id, TicketStatus.IN_PROGRESS, assigned_to=agent_name
|
5011
6471
|
)
|
5012
6472
|
|
5013
|
-
# Update ticket status
|
5014
|
-
self.ticket_service.update_ticket_status(
|
5015
|
-
ticket.id, TicketStatus.ACTIVE)
|
5016
|
-
|
5017
6473
|
# Get memory context if available
|
5018
6474
|
memory_context = ""
|
5019
6475
|
if self.memory_provider:
|
5020
6476
|
memory_context = await self.memory_provider.retrieve(user_id)
|
5021
6477
|
|
5022
|
-
#
|
6478
|
+
# Try to generate response
|
5023
6479
|
full_response = ""
|
5024
6480
|
handoff_info = None
|
6481
|
+
handoff_detected = False
|
5025
6482
|
|
5026
|
-
async for chunk in self.agent_service.generate_response(
|
5027
|
-
agent_name, user_id, user_text, memory_context, temperature=0.7
|
5028
|
-
):
|
5029
|
-
yield chunk
|
5030
|
-
full_response += chunk
|
5031
|
-
|
5032
|
-
# Check for handoff in structured format
|
5033
6483
|
try:
|
5034
|
-
#
|
5035
|
-
|
5036
|
-
|
5037
|
-
|
5038
|
-
|
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}
|
5039
6510
|
target_agent = handoff_data["handoff"].get("target_agent")
|
5040
6511
|
reason = handoff_data["handoff"].get("reason")
|
5041
|
-
|
6512
|
+
|
6513
|
+
if target_agent:
|
5042
6514
|
handoff_info = {
|
5043
6515
|
"target": target_agent, "reason": reason}
|
5044
|
-
except Exception as e:
|
5045
|
-
print(f"Error parsing handoff data: {e}")
|
5046
|
-
|
5047
|
-
# Fall back to old method if structured parsing fails
|
5048
|
-
if "HANDOFF:" in chunk and not handoff_info:
|
5049
|
-
handoff_pattern = r"HANDOFF:\s*([A-Za-z0-9_]+)\s*REASON:\s*(.+)"
|
5050
|
-
match = re.search(handoff_pattern, full_response)
|
5051
|
-
if match:
|
5052
|
-
target_agent = match.group(1)
|
5053
|
-
reason = match.group(2)
|
5054
|
-
handoff_info = {"target": target_agent, "reason": reason}
|
5055
|
-
|
5056
|
-
# Store conversation in memory if available
|
5057
|
-
if self.memory_provider:
|
5058
|
-
await self.memory_provider.store(
|
5059
|
-
user_id,
|
5060
|
-
[
|
5061
|
-
{"role": "user", "content": user_text},
|
5062
|
-
{
|
5063
|
-
"role": "assistant",
|
5064
|
-
"content": self._truncate(full_response, 2500),
|
5065
|
-
},
|
5066
|
-
],
|
5067
|
-
)
|
5068
6516
|
|
5069
|
-
|
5070
|
-
|
5071
|
-
|
5072
|
-
|
5073
|
-
|
5074
|
-
|
5075
|
-
handoff_info["target"],
|
5076
|
-
handoff_info["reason"],
|
5077
|
-
)
|
5078
|
-
except ValueError as e:
|
5079
|
-
# If handoff fails, just continue with current agent
|
5080
|
-
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
|
+
)
|
5081
6523
|
|
5082
|
-
|
5083
|
-
|
5084
|
-
|
5085
|
-
|
5086
|
-
|
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
|
5087
6535
|
|
5088
|
-
|
5089
|
-
self.ticket_service.mark_ticket_resolved(
|
5090
|
-
ticket.id,
|
5091
|
-
{
|
5092
|
-
"confidence": resolution.confidence,
|
5093
|
-
"reasoning": resolution.reasoning,
|
5094
|
-
},
|
5095
|
-
)
|
6536
|
+
full_response = new_response_buffer
|
5096
6537
|
|
5097
|
-
|
5098
|
-
self.nps_service.create_survey(user_id, ticket.id, agent_name)
|
6538
|
+
self.agent_service._last_handoff = None
|
5099
6539
|
|
5100
|
-
|
5101
|
-
|
5102
|
-
|
5103
|
-
|
5104
|
-
|
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
|
+
],
|
5105
6551
|
)
|
5106
|
-
|
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)}"
|
5107
6558
|
|
5108
6559
|
async def _process_new_ticket(
|
5109
6560
|
self,
|
@@ -5195,106 +6646,113 @@ class QueryProcessor:
|
|
5195
6646
|
ticket.id, TicketStatus.ACTIVE, assigned_to=agent_name
|
5196
6647
|
)
|
5197
6648
|
|
5198
|
-
# Generate response with streaming
|
6649
|
+
# Generate initial response with streaming
|
5199
6650
|
full_response = ""
|
5200
|
-
|
6651
|
+
handoff_detected = False
|
5201
6652
|
|
5202
|
-
|
5203
|
-
|
5204
|
-
|
5205
|
-
|
5206
|
-
|
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
|
5207
6663
|
|
5208
|
-
|
5209
|
-
|
5210
|
-
|
5211
|
-
|
5212
|
-
|
5213
|
-
|
5214
|
-
|
5215
|
-
|
5216
|
-
|
5217
|
-
|
5218
|
-
|
5219
|
-
|
5220
|
-
|
5221
|
-
|
5222
|
-
|
5223
|
-
|
5224
|
-
|
5225
|
-
|
5226
|
-
|
5227
|
-
|
5228
|
-
|
5229
|
-
|
5230
|
-
|
5231
|
-
|
5232
|
-
|
5233
|
-
|
5234
|
-
|
5235
|
-
|
5236
|
-
|
5237
|
-
|
5238
|
-
|
5239
|
-
|
5240
|
-
|
5241
|
-
|
5242
|
-
|
5243
|
-
|
5244
|
-
|
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
|
5245
6717
|
)
|
5246
6718
|
|
5247
|
-
|
5248
|
-
|
5249
|
-
try:
|
5250
|
-
await self.handoff_service.process_handoff(
|
6719
|
+
if resolution.status == "resolved" and resolution.confidence >= 0.7:
|
6720
|
+
self.ticket_service.mark_ticket_resolved(
|
5251
6721
|
ticket.id,
|
5252
|
-
|
5253
|
-
|
5254
|
-
|
6722
|
+
{
|
6723
|
+
"confidence": resolution.confidence,
|
6724
|
+
"reasoning": resolution.reasoning,
|
6725
|
+
},
|
5255
6726
|
)
|
5256
|
-
except ValueError as e:
|
5257
|
-
print(f"Handoff failed: {e}")
|
5258
6727
|
|
5259
|
-
#
|
5260
|
-
|
5261
|
-
|
5262
|
-
await self.handoff_service.process_handoff(
|
5263
|
-
ticket.id,
|
5264
|
-
agent_name,
|
5265
|
-
handoff_info["target"],
|
5266
|
-
handoff_info["reason"],
|
5267
|
-
)
|
5268
|
-
except ValueError as e:
|
5269
|
-
print(f"Handoff failed: {e}")
|
5270
|
-
|
5271
|
-
# Check if ticket can be considered resolved
|
5272
|
-
if not handoff_info:
|
5273
|
-
resolution = await self._check_ticket_resolution(
|
5274
|
-
user_id, full_response, user_text
|
5275
|
-
)
|
5276
|
-
|
5277
|
-
if resolution.status == "resolved" and resolution.confidence >= 0.7:
|
5278
|
-
self.ticket_service.mark_ticket_resolved(
|
5279
|
-
ticket.id,
|
5280
|
-
{
|
5281
|
-
"confidence": resolution.confidence,
|
5282
|
-
"reasoning": resolution.reasoning,
|
5283
|
-
},
|
5284
|
-
)
|
6728
|
+
# Create NPS survey
|
6729
|
+
self.nps_service.create_survey(
|
6730
|
+
user_id, ticket.id, agent_name)
|
5285
6731
|
|
5286
|
-
|
5287
|
-
|
5288
|
-
|
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
|
+
)
|
5289
6742
|
|
5290
|
-
|
5291
|
-
|
5292
|
-
|
5293
|
-
|
5294
|
-
|
5295
|
-
|
5296
|
-
)
|
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}
|
5297
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)}"
|
5298
6756
|
|
5299
6757
|
async def _process_human_agent_message(
|
5300
6758
|
self, user_id: str, user_text: str
|
@@ -5398,7 +6856,7 @@ class QueryProcessor:
|
|
5398
6856
|
return result
|
5399
6857
|
|
5400
6858
|
async def _check_ticket_resolution(
|
5401
|
-
self,
|
6859
|
+
self, response: str, query: str
|
5402
6860
|
) -> TicketResolution:
|
5403
6861
|
"""Determine if a ticket can be considered resolved based on the response."""
|
5404
6862
|
# Get first AI agent for analysis
|
@@ -5416,39 +6874,48 @@ class QueryProcessor:
|
|
5416
6874
|
2. "needs_followup" - The assistant couldn't fully address the issue or more information is needed
|
5417
6875
|
3. "cannot_determine" - Cannot tell if the issue is resolved
|
5418
6876
|
|
5419
|
-
Return a
|
6877
|
+
Return a structured output with:
|
5420
6878
|
- "status": One of the above values
|
5421
6879
|
- "confidence": A score from 0.0 to 1.0 indicating confidence in this assessment
|
5422
6880
|
- "reasoning": Brief explanation for your decision
|
5423
6881
|
- "suggested_actions": Array of recommended next steps (if any)
|
5424
6882
|
"""
|
5425
6883
|
|
5426
|
-
# Generate resolution assessment
|
5427
|
-
resolution_text = ""
|
5428
|
-
async for chunk in self.agent_service.generate_response(
|
5429
|
-
first_agent,
|
5430
|
-
"resolution_checker",
|
5431
|
-
prompt,
|
5432
|
-
"", # No memory context needed
|
5433
|
-
stream=False,
|
5434
|
-
temperature=0.2,
|
5435
|
-
response_format={"type": "json_object"},
|
5436
|
-
):
|
5437
|
-
resolution_text += chunk
|
5438
|
-
|
5439
6884
|
try:
|
5440
|
-
|
5441
|
-
|
5442
|
-
|
5443
|
-
|
5444
|
-
|
5445
|
-
|
5446
|
-
|
5447
|
-
|
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,
|
5448
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
|
+
)
|
5449
6905
|
|
5450
6906
|
async def _assess_task_complexity(self, query: str) -> Dict[str, Any]:
|
5451
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
|
+
|
5452
6919
|
# Get first AI agent for analysis
|
5453
6920
|
first_agent = next(iter(self.agent_service.get_all_ai_agents().keys()))
|
5454
6921
|
|
@@ -5478,16 +6945,28 @@ class QueryProcessor:
|
|
5478
6945
|
):
|
5479
6946
|
response_text += chunk
|
5480
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
|
+
|
5481
6958
|
complexity_data = json.loads(response_text)
|
6959
|
+
print(f"Successfully parsed complexity: {complexity_data}")
|
5482
6960
|
return complexity_data
|
5483
6961
|
except Exception as e:
|
5484
6962
|
print(f"Error assessing complexity: {e}")
|
6963
|
+
print(f"Failed response text: '{response_text}'")
|
5485
6964
|
return {
|
5486
|
-
"t_shirt_size": "
|
5487
|
-
"story_points":
|
5488
|
-
"estimated_minutes":
|
5489
|
-
"technical_complexity":
|
5490
|
-
"domain_knowledge":
|
6965
|
+
"t_shirt_size": "S",
|
6966
|
+
"story_points": 2,
|
6967
|
+
"estimated_minutes": 15,
|
6968
|
+
"technical_complexity": 3,
|
6969
|
+
"domain_knowledge": 2,
|
5491
6970
|
}
|
5492
6971
|
|
5493
6972
|
async def _extract_and_store_insights(
|
@@ -5522,6 +7001,20 @@ class QueryProcessor:
|
|
5522
7001
|
|
5523
7002
|
return truncated + "..."
|
5524
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
|
5525
7018
|
|
5526
7019
|
#############################################
|
5527
7020
|
# FACTORY AND DEPENDENCY INJECTION
|
@@ -5599,16 +7092,25 @@ class SolanaAgentFactory:
|
|
5599
7092
|
|
5600
7093
|
# Create services
|
5601
7094
|
agent_service = AgentService(
|
5602
|
-
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
|
+
|
5603
7101
|
routing_service = RoutingService(
|
5604
7102
|
llm_adapter,
|
5605
7103
|
agent_service,
|
5606
7104
|
router_model=config.get("router_model", "gpt-4o-mini"),
|
5607
7105
|
)
|
7106
|
+
|
5608
7107
|
ticket_service = TicketService(ticket_repo)
|
7108
|
+
|
5609
7109
|
handoff_service = HandoffService(
|
5610
7110
|
handoff_repo, ticket_repo, agent_service)
|
7111
|
+
|
5611
7112
|
memory_service = MemoryService(memory_repo, llm_adapter)
|
7113
|
+
|
5612
7114
|
nps_service = NPSService(nps_repo, ticket_repo)
|
5613
7115
|
|
5614
7116
|
# Create critic service if enabled
|
@@ -5621,7 +7123,11 @@ class SolanaAgentFactory:
|
|
5621
7123
|
ticket_repo, llm_adapter, agent_service
|
5622
7124
|
)
|
5623
7125
|
|
5624
|
-
notification_service = NotificationService(
|
7126
|
+
notification_service = NotificationService(
|
7127
|
+
human_agent_registry=human_agent_repo,
|
7128
|
+
tool_registry=agent_service.tool_registry
|
7129
|
+
)
|
7130
|
+
|
5625
7131
|
project_approval_service = ProjectApprovalService(
|
5626
7132
|
ticket_repo, human_agent_repo, notification_service
|
5627
7133
|
)
|
@@ -5647,6 +7153,27 @@ class SolanaAgentFactory:
|
|
5647
7153
|
loaded_plugins = agent_service.plugin_manager.load_all_plugins()
|
5648
7154
|
print(f"Loaded {loaded_plugins} plugins")
|
5649
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
|
+
|
5650
7177
|
# Register predefined agents if any
|
5651
7178
|
for agent_config in config.get("ai_agents", []):
|
5652
7179
|
agent_service.register_ai_agent(
|
@@ -5659,10 +7186,15 @@ class SolanaAgentFactory:
|
|
5659
7186
|
# Register tools for this agent if specified
|
5660
7187
|
if "tools" in agent_config:
|
5661
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()}")
|
5662
7192
|
try:
|
5663
7193
|
agent_service.register_tool_for_agent(
|
5664
7194
|
agent_config["name"], tool_name
|
5665
7195
|
)
|
7196
|
+
print(
|
7197
|
+
f"Successfully registered {tool_name} for agent {agent_config['name']}")
|
5666
7198
|
except ValueError as e:
|
5667
7199
|
print(
|
5668
7200
|
f"Error registering tool {tool_name} for agent {agent_config['name']}: {e}"
|
@@ -6340,51 +7872,57 @@ class SolanaAgent:
|
|
6340
7872
|
# PLUGIN SYSTEM
|
6341
7873
|
#############################################
|
6342
7874
|
|
7875
|
+
class AutoTool:
|
7876
|
+
"""Base class for tools that automatically register with the system."""
|
6343
7877
|
|
6344
|
-
|
6345
|
-
|
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 = {}
|
6346
7883
|
|
6347
|
-
|
6348
|
-
|
6349
|
-
|
6350
|
-
"""Unique name of the tool."""
|
6351
|
-
pass
|
7884
|
+
# Register with the provided registry if given
|
7885
|
+
if registry is not None:
|
7886
|
+
registry.register_tool(self)
|
6352
7887
|
|
6353
|
-
|
6354
|
-
|
6355
|
-
|
6356
|
-
"""Human-readable description of what the tool does."""
|
6357
|
-
pass
|
7888
|
+
def configure(self, config: Dict[str, Any]) -> None:
|
7889
|
+
"""Configure the tool with settings from config."""
|
7890
|
+
self._config = config
|
6358
7891
|
|
6359
|
-
|
6360
|
-
|
6361
|
-
|
6362
|
-
|
6363
|
-
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 {}
|
6364
7896
|
|
6365
|
-
|
6366
|
-
|
6367
|
-
|
6368
|
-
|
7897
|
+
def execute(self, **params) -> Dict[str, Any]:
|
7898
|
+
"""Execute the tool with the provided parameters."""
|
7899
|
+
# Override in subclasses
|
7900
|
+
raise NotImplementedError()
|
6369
7901
|
|
6370
7902
|
|
6371
7903
|
class ToolRegistry:
|
6372
|
-
"""
|
7904
|
+
"""Instance-based registry that manages tools and their access permissions."""
|
6373
7905
|
|
6374
7906
|
def __init__(self):
|
6375
|
-
|
6376
|
-
#
|
6377
|
-
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
|
6378
7916
|
|
6379
|
-
def
|
6380
|
-
"""
|
6381
|
-
|
6382
|
-
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)
|
6383
7920
|
|
6384
|
-
def assign_tool_to_agent(self, agent_name: str, tool_name: str) ->
|
6385
|
-
"""
|
7921
|
+
def assign_tool_to_agent(self, agent_name: str, tool_name: str) -> bool:
|
7922
|
+
"""Give an agent access to a specific tool."""
|
6386
7923
|
if tool_name not in self._tools:
|
6387
|
-
|
7924
|
+
print(f"Error: Tool {tool_name} is not registered")
|
7925
|
+
return False
|
6388
7926
|
|
6389
7927
|
if agent_name not in self._agent_tools:
|
6390
7928
|
self._agent_tools[agent_name] = []
|
@@ -6392,83 +7930,86 @@ class ToolRegistry:
|
|
6392
7930
|
if tool_name not in self._agent_tools[agent_name]:
|
6393
7931
|
self._agent_tools[agent_name].append(tool_name)
|
6394
7932
|
|
7933
|
+
return True
|
7934
|
+
|
6395
7935
|
def get_agent_tools(self, agent_name: str) -> List[Dict[str, Any]]:
|
6396
|
-
"""Get all tools available to
|
7936
|
+
"""Get all tools available to an agent."""
|
6397
7937
|
tool_names = self._agent_tools.get(agent_name, [])
|
6398
|
-
|
6399
|
-
|
6400
|
-
|
6401
|
-
|
6402
|
-
|
6403
|
-
|
6404
|
-
|
6405
|
-
|
6406
|
-
"description": tool_instance.description,
|
6407
|
-
"parameters": tool_instance.parameters_schema,
|
6408
|
-
}
|
6409
|
-
)
|
6410
|
-
|
6411
|
-
return tool_defs
|
6412
|
-
|
6413
|
-
def get_tool(self, tool_name: str) -> Optional[Tool]:
|
6414
|
-
"""Get a tool by name."""
|
6415
|
-
tool_class = self._tools.get(tool_name)
|
6416
|
-
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
|
+
]
|
6417
7946
|
|
6418
7947
|
def list_all_tools(self) -> List[str]:
|
6419
|
-
"""List all registered
|
7948
|
+
"""List all registered tools."""
|
6420
7949
|
return list(self._tools.keys())
|
6421
7950
|
|
6422
|
-
|
6423
|
-
|
6424
|
-
|
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)
|
6425
7955
|
|
6426
7956
|
|
6427
7957
|
class PluginManager:
|
6428
|
-
"""
|
7958
|
+
"""Manager for discovering and loading plugins."""
|
6429
7959
|
|
6430
|
-
|
6431
|
-
|
7960
|
+
# Class variable to track loaded entry points
|
7961
|
+
_loaded_entry_points = set()
|
6432
7962
|
|
6433
|
-
def
|
6434
|
-
"""
|
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()
|
6435
7967
|
|
6436
|
-
|
6437
|
-
"""
|
6438
|
-
|
7968
|
+
def load_all_plugins(self) -> int:
|
7969
|
+
"""Load all plugins using entry points and apply configuration."""
|
7970
|
+
loaded_count = 0
|
7971
|
+
plugins = []
|
6439
7972
|
|
6440
|
-
|
6441
|
-
# Discover plugins registered via entry_points
|
7973
|
+
# Discover plugins through entry points
|
6442
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
|
+
|
6443
7981
|
try:
|
6444
|
-
|
6445
|
-
|
6446
|
-
|
6447
|
-
|
6448
|
-
|
6449
|
-
|
6450
|
-
|
6451
|
-
|
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
|
6452
7995
|
except Exception as e:
|
6453
7996
|
print(f"Error loading plugin {entry_point.name}: {e}")
|
6454
7997
|
|
6455
|
-
|
6456
|
-
|
6457
|
-
|
6458
|
-
|
6459
|
-
|
6460
|
-
|
6461
|
-
|
6462
|
-
|
6463
|
-
|
6464
|
-
|
6465
|
-
|
6466
|
-
|
6467
|
-
|
6468
|
-
|
6469
|
-
|
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}")
|
6470
8014
|
|
6471
|
-
|
6472
|
-
return tool.execute(**kwargs)
|
6473
|
-
except Exception as e:
|
6474
|
-
return {"error": str(e), "status": "error"}
|
8015
|
+
return loaded_count
|