solana-agent 11.2.0__py3-none-any.whl → 11.3.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 +1142 -26
- {solana_agent-11.2.0.dist-info → solana_agent-11.3.0.dist-info}/METADATA +21 -2
- solana_agent-11.3.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-11.3.0.dist-info}/LICENSE +0 -0
- {solana_agent-11.2.0.dist-info → solana_agent-11.3.0.dist-info}/WHEEL +0 -0
solana_agent/ai.py
CHANGED
@@ -300,22 +300,31 @@ class AgentType(str, Enum):
|
|
300
300
|
|
301
301
|
|
302
302
|
class Ticket(BaseModel):
|
303
|
-
"""
|
304
|
-
|
305
|
-
id: str
|
303
|
+
"""Model for a support ticket."""
|
304
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
306
305
|
user_id: str
|
307
306
|
query: str
|
308
|
-
status: TicketStatus
|
309
|
-
assigned_to: str
|
310
|
-
created_at: datetime.datetime
|
307
|
+
status: TicketStatus = TicketStatus.NEW
|
308
|
+
assigned_to: str = ""
|
309
|
+
created_at: datetime.datetime = Field(
|
310
|
+
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc))
|
311
|
+
updated_at: Optional[datetime.datetime] = None
|
312
|
+
resolved_at: Optional[datetime.datetime] = None
|
313
|
+
resolution_confidence: Optional[float] = None
|
314
|
+
resolution_reasoning: Optional[str] = None
|
315
|
+
handoff_reason: Optional[str] = None
|
311
316
|
complexity: Optional[Dict[str, Any]] = None
|
312
|
-
|
317
|
+
agent_context: Optional[Dict[str, Any]] = None
|
313
318
|
is_parent: bool = False
|
314
319
|
is_subtask: bool = False
|
315
320
|
parent_id: Optional[str] = None
|
316
|
-
|
317
|
-
|
318
|
-
|
321
|
+
|
322
|
+
# Add fields for resource integration
|
323
|
+
description: Optional[str] = None
|
324
|
+
scheduled_start: Optional[datetime.datetime] = None
|
325
|
+
scheduled_end: Optional[datetime.datetime] = None
|
326
|
+
required_resources: List[Dict[str, Any]] = []
|
327
|
+
resource_assignments: List[Dict[str, Any]] = []
|
319
328
|
|
320
329
|
|
321
330
|
class Handoff(BaseModel):
|
@@ -456,17 +465,22 @@ class PlanStatus(BaseModel):
|
|
456
465
|
|
457
466
|
|
458
467
|
class SubtaskModel(BaseModel):
|
459
|
-
"""
|
468
|
+
"""Model for a subtask in a complex task breakdown."""
|
460
469
|
|
461
470
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
462
|
-
parent_id: str
|
471
|
+
parent_id: Optional[str] = None
|
463
472
|
title: str
|
464
473
|
description: str
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
474
|
+
estimated_minutes: int
|
475
|
+
dependencies: List[str] = []
|
476
|
+
status: str = "pending"
|
477
|
+
priority: Optional[int] = None
|
478
|
+
assigned_to: Optional[str] = None
|
479
|
+
scheduled_start: Optional[datetime.datetime] = None
|
480
|
+
specialization_tags: List[str] = []
|
481
|
+
sequence: int = 0 # Added missing sequence field
|
482
|
+
required_resources: List[Dict[str, Any]] = []
|
483
|
+
resource_assignments: List[Dict[str, Any]] = []
|
470
484
|
|
471
485
|
|
472
486
|
class WorkCapacity(BaseModel):
|
@@ -483,10 +497,117 @@ class WorkCapacity(BaseModel):
|
|
483
497
|
)
|
484
498
|
|
485
499
|
|
500
|
+
class ResourceType(str, Enum):
|
501
|
+
"""Types of resources that can be booked."""
|
502
|
+
ROOM = "room"
|
503
|
+
VEHICLE = "vehicle"
|
504
|
+
EQUIPMENT = "equipment"
|
505
|
+
SEAT = "seat"
|
506
|
+
DESK = "desk"
|
507
|
+
OTHER = "other"
|
508
|
+
|
509
|
+
|
510
|
+
class ResourceStatus(str, Enum):
|
511
|
+
"""Status of a resource."""
|
512
|
+
AVAILABLE = "available"
|
513
|
+
IN_USE = "in_use"
|
514
|
+
MAINTENANCE = "maintenance"
|
515
|
+
UNAVAILABLE = "unavailable"
|
516
|
+
|
517
|
+
|
518
|
+
class ResourceLocation(BaseModel):
|
519
|
+
"""Physical location of a resource."""
|
520
|
+
address: Optional[str] = None
|
521
|
+
building: Optional[str] = None
|
522
|
+
floor: Optional[int] = None
|
523
|
+
room: Optional[str] = None
|
524
|
+
coordinates: Optional[Dict[str, float]] = None # Lat/Long if applicable
|
525
|
+
|
526
|
+
|
527
|
+
class TimeWindow(BaseModel):
|
528
|
+
"""Time window model for availability and exceptions."""
|
529
|
+
start: datetime.datetime
|
530
|
+
end: datetime.datetime
|
531
|
+
|
532
|
+
def overlaps_with(self, other: 'TimeWindow') -> bool:
|
533
|
+
"""Check if this window overlaps with another one."""
|
534
|
+
return self.start < other.end and self.end > other.start
|
535
|
+
|
536
|
+
|
537
|
+
class ResourceAvailabilityWindow(BaseModel):
|
538
|
+
"""Availability window for a resource with recurring pattern options."""
|
539
|
+
day_of_week: Optional[List[int]] = None # 0 = Monday, 6 = Sunday
|
540
|
+
start_time: str # Format: "HH:MM", 24h format
|
541
|
+
end_time: str # Format: "HH:MM", 24h format
|
542
|
+
timezone: str = "UTC"
|
543
|
+
|
544
|
+
|
545
|
+
class Resource(BaseModel):
|
546
|
+
"""Model for a bookable resource."""
|
547
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
548
|
+
name: str
|
549
|
+
description: Optional[str] = None
|
550
|
+
resource_type: ResourceType
|
551
|
+
status: ResourceStatus = ResourceStatus.AVAILABLE
|
552
|
+
location: Optional[ResourceLocation] = None
|
553
|
+
capacity: Optional[int] = None # For rooms/vehicles
|
554
|
+
tags: List[str] = []
|
555
|
+
attributes: Dict[str, str] = {} # Custom attributes
|
556
|
+
availability_schedule: List[ResourceAvailabilityWindow] = []
|
557
|
+
# Overrides for maintenance, holidays
|
558
|
+
availability_exceptions: List[TimeWindow] = []
|
559
|
+
created_at: datetime.datetime = Field(
|
560
|
+
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc))
|
561
|
+
updated_at: Optional[datetime.datetime] = None
|
562
|
+
|
563
|
+
def is_available_at(self, time_window: TimeWindow) -> bool:
|
564
|
+
"""Check if resource is available during the specified time window."""
|
565
|
+
# Check if resource is generally available
|
566
|
+
if self.status != ResourceStatus.AVAILABLE:
|
567
|
+
return False
|
568
|
+
|
569
|
+
# Check against exceptions (maintenance, holidays)
|
570
|
+
for exception in self.availability_exceptions:
|
571
|
+
if exception.overlaps_with(time_window):
|
572
|
+
return False
|
573
|
+
|
574
|
+
# Check if the requested time falls within regular availability
|
575
|
+
day_of_week = time_window.start.weekday()
|
576
|
+
start_time = time_window.start.strftime("%H:%M")
|
577
|
+
end_time = time_window.end.strftime("%H:%M")
|
578
|
+
|
579
|
+
for window in self.availability_schedule:
|
580
|
+
if window.day_of_week is None or day_of_week in window.day_of_week:
|
581
|
+
if window.start_time <= start_time and window.end_time >= end_time:
|
582
|
+
return True
|
583
|
+
|
584
|
+
# Default available if no schedule defined
|
585
|
+
return len(self.availability_schedule) == 0
|
586
|
+
|
587
|
+
|
588
|
+
class ResourceBooking(BaseModel):
|
589
|
+
"""Model for a resource booking."""
|
590
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
591
|
+
resource_id: str
|
592
|
+
user_id: str
|
593
|
+
title: str
|
594
|
+
description: Optional[str] = None
|
595
|
+
start_time: datetime.datetime
|
596
|
+
end_time: datetime.datetime
|
597
|
+
status: str = "confirmed" # confirmed, cancelled, completed
|
598
|
+
booking_reference: Optional[str] = None
|
599
|
+
payment_status: Optional[str] = None
|
600
|
+
payment_amount: Optional[float] = None
|
601
|
+
notes: Optional[str] = None
|
602
|
+
created_at: datetime.datetime = Field(
|
603
|
+
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc))
|
604
|
+
updated_at: Optional[datetime.datetime] = None
|
605
|
+
|
486
606
|
#############################################
|
487
607
|
# INTERFACES
|
488
608
|
#############################################
|
489
609
|
|
610
|
+
|
490
611
|
class LLMProvider(Protocol):
|
491
612
|
"""Interface for language model providers."""
|
492
613
|
|
@@ -1026,6 +1147,217 @@ class PineconeAdapter:
|
|
1026
1147
|
# IMPLEMENTATIONS - REPOSITORIES
|
1027
1148
|
#############################################
|
1028
1149
|
|
1150
|
+
class ResourceRepository:
|
1151
|
+
"""Repository for managing resources."""
|
1152
|
+
|
1153
|
+
def __init__(self, db_provider):
|
1154
|
+
"""Initialize with database provider."""
|
1155
|
+
self.db = db_provider
|
1156
|
+
self.resources_collection = "resources"
|
1157
|
+
self.bookings_collection = "resource_bookings"
|
1158
|
+
|
1159
|
+
# Ensure collections exist
|
1160
|
+
self.db.create_collection(self.resources_collection)
|
1161
|
+
self.db.create_collection(self.bookings_collection)
|
1162
|
+
|
1163
|
+
# Create indexes
|
1164
|
+
self.db.create_index(self.resources_collection, [("resource_type", 1)])
|
1165
|
+
self.db.create_index(self.resources_collection, [("status", 1)])
|
1166
|
+
self.db.create_index(self.resources_collection, [("tags", 1)])
|
1167
|
+
|
1168
|
+
self.db.create_index(self.bookings_collection, [("resource_id", 1)])
|
1169
|
+
self.db.create_index(self.bookings_collection, [("user_id", 1)])
|
1170
|
+
self.db.create_index(self.bookings_collection, [("start_time", 1)])
|
1171
|
+
self.db.create_index(self.bookings_collection, [("end_time", 1)])
|
1172
|
+
self.db.create_index(self.bookings_collection, [("status", 1)])
|
1173
|
+
|
1174
|
+
# Resource CRUD operations
|
1175
|
+
def create_resource(self, resource: Resource) -> str:
|
1176
|
+
"""Create a new resource."""
|
1177
|
+
resource_dict = resource.model_dump(mode="json")
|
1178
|
+
return self.db.insert_one(self.resources_collection, resource_dict)
|
1179
|
+
|
1180
|
+
def get_resource(self, resource_id: str) -> Optional[Resource]:
|
1181
|
+
"""Get a resource by ID."""
|
1182
|
+
data = self.db.find_one(self.resources_collection, {"id": resource_id})
|
1183
|
+
return Resource(**data) if data else None
|
1184
|
+
|
1185
|
+
def update_resource(self, resource: Resource) -> bool:
|
1186
|
+
"""Update a resource."""
|
1187
|
+
resource.updated_at = datetime.datetime.now(datetime.timezone.utc)
|
1188
|
+
resource_dict = resource.model_dump(mode="json")
|
1189
|
+
return self.db.update_one(
|
1190
|
+
self.resources_collection,
|
1191
|
+
{"id": resource.id},
|
1192
|
+
{"$set": resource_dict}
|
1193
|
+
)
|
1194
|
+
|
1195
|
+
def delete_resource(self, resource_id: str) -> bool:
|
1196
|
+
"""Delete a resource."""
|
1197
|
+
return self.db.delete_one(self.resources_collection, {"id": resource_id})
|
1198
|
+
|
1199
|
+
def find_resources(
|
1200
|
+
self,
|
1201
|
+
query: Dict[str, Any],
|
1202
|
+
sort_by: Optional[str] = None,
|
1203
|
+
limit: int = 0
|
1204
|
+
) -> List[Resource]:
|
1205
|
+
"""Find resources matching query."""
|
1206
|
+
sort_params = [(sort_by, 1)] if sort_by else [("name", 1)]
|
1207
|
+
data = self.db.find(self.resources_collection,
|
1208
|
+
query, sort_params, limit)
|
1209
|
+
return [Resource(**item) for item in data]
|
1210
|
+
|
1211
|
+
def find_available_resources(
|
1212
|
+
self,
|
1213
|
+
resource_type: Optional[str] = None,
|
1214
|
+
tags: Optional[List[str]] = None,
|
1215
|
+
start_time: Optional[datetime.datetime] = None,
|
1216
|
+
end_time: Optional[datetime.datetime] = None,
|
1217
|
+
capacity: Optional[int] = None
|
1218
|
+
) -> List[Resource]:
|
1219
|
+
"""Find available resources matching the criteria."""
|
1220
|
+
# Build base query for available resources
|
1221
|
+
query = {"status": ResourceStatus.AVAILABLE}
|
1222
|
+
|
1223
|
+
if resource_type:
|
1224
|
+
query["resource_type"] = resource_type
|
1225
|
+
|
1226
|
+
if tags:
|
1227
|
+
query["tags"] = {"$all": tags}
|
1228
|
+
|
1229
|
+
if capacity:
|
1230
|
+
query["capacity"] = {"$gte": capacity}
|
1231
|
+
|
1232
|
+
# First get resources that match base criteria
|
1233
|
+
resources = self.find_resources(query)
|
1234
|
+
|
1235
|
+
# If no time range specified, return all matching resources
|
1236
|
+
if not start_time or not end_time:
|
1237
|
+
return resources
|
1238
|
+
|
1239
|
+
# Filter by time availability (check bookings and exceptions)
|
1240
|
+
time_window = TimeWindow(start=start_time, end=end_time)
|
1241
|
+
|
1242
|
+
# Check each resource's availability
|
1243
|
+
available_resources = []
|
1244
|
+
for resource in resources:
|
1245
|
+
if resource.is_available_at(time_window):
|
1246
|
+
# Check existing bookings
|
1247
|
+
if not self._has_conflicting_bookings(resource.id, start_time, end_time):
|
1248
|
+
available_resources.append(resource)
|
1249
|
+
|
1250
|
+
return available_resources
|
1251
|
+
|
1252
|
+
# Booking CRUD operations
|
1253
|
+
def create_booking(self, booking: ResourceBooking) -> str:
|
1254
|
+
"""Create a new booking."""
|
1255
|
+
booking_dict = booking.model_dump(mode="json")
|
1256
|
+
return self.db.insert_one(self.bookings_collection, booking_dict)
|
1257
|
+
|
1258
|
+
def get_booking(self, booking_id: str) -> Optional[ResourceBooking]:
|
1259
|
+
"""Get a booking by ID."""
|
1260
|
+
data = self.db.find_one(self.bookings_collection, {"id": booking_id})
|
1261
|
+
return ResourceBooking(**data) if data else None
|
1262
|
+
|
1263
|
+
def update_booking(self, booking: ResourceBooking) -> bool:
|
1264
|
+
"""Update a booking."""
|
1265
|
+
booking.updated_at = datetime.datetime.now(datetime.timezone.utc)
|
1266
|
+
booking_dict = booking.model_dump(mode="json")
|
1267
|
+
return self.db.update_one(
|
1268
|
+
self.bookings_collection,
|
1269
|
+
{"id": booking.id},
|
1270
|
+
{"$set": booking_dict}
|
1271
|
+
)
|
1272
|
+
|
1273
|
+
def cancel_booking(self, booking_id: str) -> bool:
|
1274
|
+
"""Cancel a booking."""
|
1275
|
+
return self.db.update_one(
|
1276
|
+
self.bookings_collection,
|
1277
|
+
{"id": booking_id},
|
1278
|
+
{
|
1279
|
+
"$set": {
|
1280
|
+
"status": "cancelled",
|
1281
|
+
"updated_at": datetime.datetime.now(datetime.timezone.utc)
|
1282
|
+
}
|
1283
|
+
}
|
1284
|
+
)
|
1285
|
+
|
1286
|
+
def get_resource_bookings(
|
1287
|
+
self,
|
1288
|
+
resource_id: str,
|
1289
|
+
start_time: Optional[datetime.datetime] = None,
|
1290
|
+
end_time: Optional[datetime.datetime] = None,
|
1291
|
+
include_cancelled: bool = False
|
1292
|
+
) -> List[ResourceBooking]:
|
1293
|
+
"""Get all bookings for a resource within a time range."""
|
1294
|
+
query = {"resource_id": resource_id}
|
1295
|
+
|
1296
|
+
if not include_cancelled:
|
1297
|
+
query["status"] = {"$ne": "cancelled"}
|
1298
|
+
|
1299
|
+
if start_time or end_time:
|
1300
|
+
time_query = {}
|
1301
|
+
if start_time:
|
1302
|
+
time_query["$lte"] = end_time
|
1303
|
+
if end_time:
|
1304
|
+
time_query["$gte"] = start_time
|
1305
|
+
if time_query:
|
1306
|
+
query["$or"] = [
|
1307
|
+
{"start_time": time_query},
|
1308
|
+
{"end_time": time_query},
|
1309
|
+
{
|
1310
|
+
"$and": [
|
1311
|
+
{"start_time": {"$lte": start_time}},
|
1312
|
+
{"end_time": {"$gte": end_time}}
|
1313
|
+
]
|
1314
|
+
}
|
1315
|
+
]
|
1316
|
+
|
1317
|
+
data = self.db.find(self.bookings_collection,
|
1318
|
+
query, sort=[("start_time", 1)])
|
1319
|
+
return [ResourceBooking(**item) for item in data]
|
1320
|
+
|
1321
|
+
def get_user_bookings(
|
1322
|
+
self,
|
1323
|
+
user_id: str,
|
1324
|
+
include_cancelled: bool = False
|
1325
|
+
) -> List[ResourceBooking]:
|
1326
|
+
"""Get all bookings for a user."""
|
1327
|
+
query = {"user_id": user_id}
|
1328
|
+
|
1329
|
+
if not include_cancelled:
|
1330
|
+
query["status"] = {"$ne": "cancelled"}
|
1331
|
+
|
1332
|
+
data = self.db.find(self.bookings_collection,
|
1333
|
+
query, sort=[("start_time", 1)])
|
1334
|
+
return [ResourceBooking(**item) for item in data]
|
1335
|
+
|
1336
|
+
def _has_conflicting_bookings(
|
1337
|
+
self,
|
1338
|
+
resource_id: str,
|
1339
|
+
start_time: datetime.datetime,
|
1340
|
+
end_time: datetime.datetime
|
1341
|
+
) -> bool:
|
1342
|
+
"""Check if there are any conflicting bookings."""
|
1343
|
+
query = {
|
1344
|
+
"resource_id": resource_id,
|
1345
|
+
"status": {"$ne": "cancelled"},
|
1346
|
+
"$or": [
|
1347
|
+
{"start_time": {"$lt": end_time, "$gte": start_time}},
|
1348
|
+
{"end_time": {"$gt": start_time, "$lte": end_time}},
|
1349
|
+
{
|
1350
|
+
"$and": [
|
1351
|
+
{"start_time": {"$lte": start_time}},
|
1352
|
+
{"end_time": {"$gte": end_time}}
|
1353
|
+
]
|
1354
|
+
}
|
1355
|
+
]
|
1356
|
+
}
|
1357
|
+
|
1358
|
+
return self.db.count_documents(self.bookings_collection, query) > 0
|
1359
|
+
|
1360
|
+
|
1029
1361
|
class MongoMemoryProvider:
|
1030
1362
|
"""MongoDB implementation of MemoryProvider."""
|
1031
1363
|
|
@@ -2654,6 +2986,143 @@ class AgentService:
|
|
2654
2986
|
return self.ai_agents
|
2655
2987
|
|
2656
2988
|
|
2989
|
+
class ResourceService:
|
2990
|
+
"""Service for managing resources and bookings."""
|
2991
|
+
|
2992
|
+
def __init__(self, resource_repository: ResourceRepository):
|
2993
|
+
"""Initialize with resource repository."""
|
2994
|
+
self.repository = resource_repository
|
2995
|
+
|
2996
|
+
async def create_resource(self, resource_data, resource_type):
|
2997
|
+
"""Create a new resource from dictionary data."""
|
2998
|
+
# Generate UUID for ID since it can't be None
|
2999
|
+
resource_id = str(uuid.uuid4())
|
3000
|
+
|
3001
|
+
resource = Resource(
|
3002
|
+
id=resource_id,
|
3003
|
+
name=resource_data["name"],
|
3004
|
+
resource_type=resource_type,
|
3005
|
+
description=resource_data.get("description"),
|
3006
|
+
location=resource_data.get("location"),
|
3007
|
+
capacity=resource_data.get("capacity"),
|
3008
|
+
tags=resource_data.get("tags", []),
|
3009
|
+
attributes=resource_data.get("attributes", {}),
|
3010
|
+
availability_schedule=resource_data.get(
|
3011
|
+
"availability_schedule", [])
|
3012
|
+
)
|
3013
|
+
|
3014
|
+
# Don't use await when calling repository methods
|
3015
|
+
return self.repository.create_resource(resource)
|
3016
|
+
|
3017
|
+
async def get_resource(self, resource_id):
|
3018
|
+
"""Get a resource by ID."""
|
3019
|
+
# Don't use await
|
3020
|
+
return self.repository.get_resource(resource_id)
|
3021
|
+
|
3022
|
+
async def update_resource(self, resource_id, updates):
|
3023
|
+
"""Update a resource."""
|
3024
|
+
resource = self.repository.get_resource(resource_id)
|
3025
|
+
if not resource:
|
3026
|
+
return False
|
3027
|
+
|
3028
|
+
# Apply updates
|
3029
|
+
for key, value in updates.items():
|
3030
|
+
if hasattr(resource, key):
|
3031
|
+
setattr(resource, key, value)
|
3032
|
+
|
3033
|
+
# Don't use await
|
3034
|
+
return self.repository.update_resource(resource)
|
3035
|
+
|
3036
|
+
async def list_resources(self, resource_type=None):
|
3037
|
+
"""List all resources, optionally filtered by type."""
|
3038
|
+
# Don't use await
|
3039
|
+
return self.repository.list_resources(resource_type)
|
3040
|
+
|
3041
|
+
async def find_available_resources(self, start_time, end_time, capacity=None, tags=None, resource_type=None):
|
3042
|
+
"""Find available resources for a time period."""
|
3043
|
+
# Don't use await
|
3044
|
+
resources = self.repository.find_resources(
|
3045
|
+
resource_type, capacity, tags)
|
3046
|
+
|
3047
|
+
# Filter by availability
|
3048
|
+
available = []
|
3049
|
+
for resource in resources:
|
3050
|
+
time_window = TimeWindow(start=start_time, end=end_time)
|
3051
|
+
if resource.is_available_at(time_window):
|
3052
|
+
if not self.repository._has_conflicting_bookings(resource.id, start_time, end_time):
|
3053
|
+
available.append(resource)
|
3054
|
+
|
3055
|
+
return available
|
3056
|
+
|
3057
|
+
async def create_booking(self, resource_id, user_id, title, start_time, end_time, description=None, notes=None):
|
3058
|
+
"""Create a booking for a resource."""
|
3059
|
+
# Check if resource exists
|
3060
|
+
resource = self.repository.get_resource(resource_id)
|
3061
|
+
if not resource:
|
3062
|
+
return False, None, "Resource not found"
|
3063
|
+
|
3064
|
+
# Check for conflicts
|
3065
|
+
if self.repository._has_conflicting_bookings(resource_id, start_time, end_time):
|
3066
|
+
return False, None, "Resource is already booked during the requested time"
|
3067
|
+
|
3068
|
+
# Create booking
|
3069
|
+
booking_data = ResourceBooking(
|
3070
|
+
id=str(uuid.uuid4()),
|
3071
|
+
resource_id=resource_id,
|
3072
|
+
user_id=user_id,
|
3073
|
+
title=title,
|
3074
|
+
description=description,
|
3075
|
+
status="confirmed",
|
3076
|
+
start_time=start_time,
|
3077
|
+
end_time=end_time,
|
3078
|
+
notes=notes,
|
3079
|
+
created_at=datetime.datetime.now(datetime.timezone.utc)
|
3080
|
+
)
|
3081
|
+
|
3082
|
+
booking_id = self.repository.create_booking(booking_data)
|
3083
|
+
|
3084
|
+
# Return (success, booking_id, error)
|
3085
|
+
return True, booking_id, None
|
3086
|
+
|
3087
|
+
async def cancel_booking(self, booking_id, user_id):
|
3088
|
+
"""Cancel a booking."""
|
3089
|
+
# Verify booking exists
|
3090
|
+
booking = self.repository.get_booking(booking_id)
|
3091
|
+
if not booking:
|
3092
|
+
return False, "Booking not found"
|
3093
|
+
|
3094
|
+
# Verify user owns the booking
|
3095
|
+
if booking.user_id != user_id:
|
3096
|
+
return False, "Not authorized to cancel this booking"
|
3097
|
+
|
3098
|
+
# Cancel booking
|
3099
|
+
result = self.repository.cancel_booking(booking_id)
|
3100
|
+
if result:
|
3101
|
+
return True, None
|
3102
|
+
return False, "Failed to cancel booking"
|
3103
|
+
|
3104
|
+
async def get_resource_schedule(self, resource_id, start_date, end_date):
|
3105
|
+
"""Get a resource's schedule for a date range."""
|
3106
|
+
return self.repository.get_resource_schedule(resource_id, start_date, end_date)
|
3107
|
+
|
3108
|
+
async def get_user_bookings(self, user_id, include_cancelled=False):
|
3109
|
+
"""Get all bookings for a user with resource details."""
|
3110
|
+
bookings = self.repository.get_user_bookings(
|
3111
|
+
user_id,
|
3112
|
+
include_cancelled
|
3113
|
+
)
|
3114
|
+
|
3115
|
+
result = []
|
3116
|
+
for booking in bookings:
|
3117
|
+
resource = self.repository.get_resource(booking.resource_id)
|
3118
|
+
result.append({
|
3119
|
+
"booking": booking.model_dump(),
|
3120
|
+
"resource": resource.model_dump() if resource else None
|
3121
|
+
})
|
3122
|
+
|
3123
|
+
return result
|
3124
|
+
|
3125
|
+
|
2657
3126
|
class TaskPlanningService:
|
2658
3127
|
"""Service for managing complex task planning and breakdown."""
|
2659
3128
|
|
@@ -3008,6 +3477,181 @@ class TaskPlanningService:
|
|
3008
3477
|
"domain_knowledge": 5,
|
3009
3478
|
}
|
3010
3479
|
|
3480
|
+
async def generate_subtasks_with_resources(
|
3481
|
+
self, ticket_id: str, task_description: str
|
3482
|
+
) -> List[SubtaskModel]:
|
3483
|
+
"""Generate subtasks for a complex task with resource requirements."""
|
3484
|
+
# Fetch ticket to verify it exists
|
3485
|
+
ticket = self.ticket_repository.get_by_id(ticket_id)
|
3486
|
+
if not ticket:
|
3487
|
+
raise ValueError(f"Ticket {ticket_id} not found")
|
3488
|
+
|
3489
|
+
# Mark parent ticket as a parent
|
3490
|
+
self.ticket_repository.update(
|
3491
|
+
ticket_id, {"is_parent": True, "status": TicketStatus.PLANNING}
|
3492
|
+
)
|
3493
|
+
|
3494
|
+
# Generate subtasks using LLM
|
3495
|
+
agent_name = next(iter(self.agent_service.get_all_ai_agents().keys()))
|
3496
|
+
agent_config = self.agent_service.get_all_ai_agents()[agent_name]
|
3497
|
+
model = agent_config.get("model", "gpt-4o-mini")
|
3498
|
+
|
3499
|
+
prompt = f"""
|
3500
|
+
Break down the following complex task into logical subtasks with resource requirements:
|
3501
|
+
|
3502
|
+
TASK: {task_description}
|
3503
|
+
|
3504
|
+
For each subtask, provide:
|
3505
|
+
1. A brief title
|
3506
|
+
2. A clear description of what needs to be done
|
3507
|
+
3. An estimate of time required in minutes
|
3508
|
+
4. Any dependencies (which subtasks must be completed first)
|
3509
|
+
5. Required resources with these details:
|
3510
|
+
- Resource type (room, equipment, etc.)
|
3511
|
+
- Quantity needed
|
3512
|
+
- Specific requirements (e.g., "room with projector", "laptop with design software")
|
3513
|
+
|
3514
|
+
Format as a JSON array of objects with these fields:
|
3515
|
+
- title: string
|
3516
|
+
- description: string
|
3517
|
+
- estimated_minutes: number
|
3518
|
+
- dependencies: array of previous subtask titles that must be completed first
|
3519
|
+
- required_resources: array of objects with fields:
|
3520
|
+
- resource_type: string
|
3521
|
+
- quantity: number
|
3522
|
+
- requirements: string (specific features needed)
|
3523
|
+
|
3524
|
+
The subtasks should be in a logical sequence. Keep dependencies minimal and avoid circular dependencies.
|
3525
|
+
"""
|
3526
|
+
|
3527
|
+
response_text = ""
|
3528
|
+
async for chunk in self.llm_provider.generate_text(
|
3529
|
+
ticket.user_id,
|
3530
|
+
prompt,
|
3531
|
+
system_prompt="You are an expert project planner who breaks down complex tasks efficiently and identifies required resources.",
|
3532
|
+
stream=False,
|
3533
|
+
model=model,
|
3534
|
+
temperature=0.2,
|
3535
|
+
response_format={"type": "json_object"},
|
3536
|
+
):
|
3537
|
+
response_text += chunk
|
3538
|
+
|
3539
|
+
try:
|
3540
|
+
data = json.loads(response_text)
|
3541
|
+
subtasks_data = data.get("subtasks", [])
|
3542
|
+
|
3543
|
+
# Create subtask objects
|
3544
|
+
subtasks = []
|
3545
|
+
for i, task_data in enumerate(subtasks_data):
|
3546
|
+
subtask = SubtaskModel(
|
3547
|
+
parent_id=ticket_id,
|
3548
|
+
title=task_data.get("title", f"Subtask {i+1}"),
|
3549
|
+
description=task_data.get("description", ""),
|
3550
|
+
estimated_minutes=task_data.get("estimated_minutes", 30),
|
3551
|
+
dependencies=[], # We'll fill this after all subtasks are created
|
3552
|
+
status="planning",
|
3553
|
+
required_resources=task_data.get("required_resources", []),
|
3554
|
+
is_subtask=True,
|
3555
|
+
created_at=datetime.datetime.now(datetime.timezone.utc)
|
3556
|
+
)
|
3557
|
+
subtasks.append(subtask)
|
3558
|
+
|
3559
|
+
# Process dependencies (convert title references to IDs)
|
3560
|
+
title_to_id = {task.title: task.id for task in subtasks}
|
3561
|
+
for i, task_data in enumerate(subtasks_data):
|
3562
|
+
dependency_titles = task_data.get("dependencies", [])
|
3563
|
+
for title in dependency_titles:
|
3564
|
+
if title in title_to_id:
|
3565
|
+
subtasks[i].dependencies.append(title_to_id[title])
|
3566
|
+
|
3567
|
+
# Store subtasks in database
|
3568
|
+
for subtask in subtasks:
|
3569
|
+
self.ticket_repository.create(subtask)
|
3570
|
+
|
3571
|
+
return subtasks
|
3572
|
+
|
3573
|
+
except Exception as e:
|
3574
|
+
print(f"Error generating subtasks with resources: {e}")
|
3575
|
+
return []
|
3576
|
+
|
3577
|
+
async def allocate_resources(
|
3578
|
+
self, subtask_id: str, resource_service: ResourceService
|
3579
|
+
) -> Tuple[bool, str]:
|
3580
|
+
"""Allocate resources to a subtask."""
|
3581
|
+
# Get the subtask
|
3582
|
+
subtask = self.ticket_repository.get_by_id(subtask_id)
|
3583
|
+
if not subtask or not subtask.is_subtask:
|
3584
|
+
return False, "Subtask not found"
|
3585
|
+
|
3586
|
+
if not subtask.required_resources:
|
3587
|
+
return True, "No resources required"
|
3588
|
+
|
3589
|
+
if not subtask.scheduled_start or not subtask.scheduled_end:
|
3590
|
+
return False, "Subtask must be scheduled before resources can be allocated"
|
3591
|
+
|
3592
|
+
# For each required resource
|
3593
|
+
resource_assignments = []
|
3594
|
+
for resource_req in subtask.required_resources:
|
3595
|
+
resource_type = resource_req.get("resource_type")
|
3596
|
+
requirements = resource_req.get("requirements", "")
|
3597
|
+
quantity = resource_req.get("quantity", 1)
|
3598
|
+
|
3599
|
+
# Find available resources matching the requirements
|
3600
|
+
resources = await resource_service.find_available_resources(
|
3601
|
+
start_time=subtask.scheduled_start,
|
3602
|
+
end_time=subtask.scheduled_end,
|
3603
|
+
resource_type=resource_type,
|
3604
|
+
tags=requirements.split() if requirements else None,
|
3605
|
+
capacity=None # Could use quantity here if it represents capacity
|
3606
|
+
)
|
3607
|
+
|
3608
|
+
if not resources or len(resources) < quantity:
|
3609
|
+
return False, f"Insufficient {resource_type} resources available"
|
3610
|
+
|
3611
|
+
# Allocate the resources by creating bookings
|
3612
|
+
allocated_resources = []
|
3613
|
+
for i in range(quantity):
|
3614
|
+
if i >= len(resources):
|
3615
|
+
break
|
3616
|
+
|
3617
|
+
resource = resources[i]
|
3618
|
+
success, booking_id, error = await resource_service.create_booking(
|
3619
|
+
resource_id=resource.id,
|
3620
|
+
user_id=subtask.assigned_to or "system",
|
3621
|
+
# Use query instead of title
|
3622
|
+
title=f"Task: {subtask.query}",
|
3623
|
+
start_time=subtask.scheduled_start,
|
3624
|
+
end_time=subtask.scheduled_end,
|
3625
|
+
description=subtask.description
|
3626
|
+
)
|
3627
|
+
|
3628
|
+
if success:
|
3629
|
+
allocated_resources.append({
|
3630
|
+
"resource_id": resource.id,
|
3631
|
+
"resource_name": resource.name,
|
3632
|
+
"booking_id": booking_id,
|
3633
|
+
"resource_type": resource.resource_type
|
3634
|
+
})
|
3635
|
+
else:
|
3636
|
+
# Clean up any allocations already made
|
3637
|
+
for alloc in allocated_resources:
|
3638
|
+
await resource_service.cancel_booking(alloc["booking_id"], subtask.assigned_to or "system")
|
3639
|
+
return False, f"Failed to book resource: {error}"
|
3640
|
+
|
3641
|
+
resource_assignments.append({
|
3642
|
+
"requirement": resource_req,
|
3643
|
+
"allocated": allocated_resources
|
3644
|
+
})
|
3645
|
+
|
3646
|
+
# Update the subtask with resource assignments
|
3647
|
+
subtask.resource_assignments = resource_assignments
|
3648
|
+
self.ticket_repository.update(subtask_id, {
|
3649
|
+
"resource_assignments": resource_assignments,
|
3650
|
+
"updated_at": datetime.datetime.now(datetime.timezone.utc)
|
3651
|
+
})
|
3652
|
+
|
3653
|
+
return True, f"Successfully allocated {len(resource_assignments)} resource types"
|
3654
|
+
|
3011
3655
|
|
3012
3656
|
class ProjectApprovalService:
|
3013
3657
|
"""Service for managing human approval of new projects."""
|
@@ -3777,6 +4421,77 @@ class SchedulingService:
|
|
3777
4421
|
|
3778
4422
|
return task
|
3779
4423
|
|
4424
|
+
async def find_optimal_time_slot_with_resources(
|
4425
|
+
self,
|
4426
|
+
task: ScheduledTask,
|
4427
|
+
resource_service: ResourceService,
|
4428
|
+
agent_schedule: Optional[AgentSchedule] = None
|
4429
|
+
) -> Optional[TimeWindow]:
|
4430
|
+
"""Find the optimal time slot for a task based on both agent and resource availability."""
|
4431
|
+
if not task.assigned_to:
|
4432
|
+
return None
|
4433
|
+
|
4434
|
+
# First, find potential time slots based on agent availability
|
4435
|
+
agent_id = task.assigned_to
|
4436
|
+
duration = task.estimated_minutes or 30
|
4437
|
+
|
4438
|
+
# Start no earlier than now
|
4439
|
+
start_after = datetime.datetime.now(datetime.timezone.utc)
|
4440
|
+
|
4441
|
+
# Apply task constraints
|
4442
|
+
for constraint in task.constraints:
|
4443
|
+
if constraint.get("type") == "must_start_after" and constraint.get("time"):
|
4444
|
+
constraint_time = datetime.datetime.fromisoformat(
|
4445
|
+
constraint["time"])
|
4446
|
+
if constraint_time > start_after:
|
4447
|
+
start_after = constraint_time
|
4448
|
+
|
4449
|
+
# Get potential time slots for the agent
|
4450
|
+
agent_slots = await self.find_available_time_slots(
|
4451
|
+
agent_id,
|
4452
|
+
duration,
|
4453
|
+
start_after,
|
4454
|
+
count=3, # Get multiple slots to try with resources
|
4455
|
+
agent_schedule=agent_schedule
|
4456
|
+
)
|
4457
|
+
|
4458
|
+
if not agent_slots:
|
4459
|
+
return None
|
4460
|
+
|
4461
|
+
# Check if task has resource requirements
|
4462
|
+
required_resources = getattr(task, "required_resources", [])
|
4463
|
+
if not required_resources:
|
4464
|
+
# If no resources needed, return the first available agent slot
|
4465
|
+
return agent_slots[0]
|
4466
|
+
|
4467
|
+
# For each potential time slot, check resource availability
|
4468
|
+
for time_slot in agent_slots:
|
4469
|
+
all_resources_available = True
|
4470
|
+
|
4471
|
+
for resource_req in required_resources:
|
4472
|
+
resource_type = resource_req.get("resource_type")
|
4473
|
+
requirements = resource_req.get("requirements", "")
|
4474
|
+
quantity = resource_req.get("quantity", 1)
|
4475
|
+
|
4476
|
+
# Find available resources for this time slot
|
4477
|
+
resources = await resource_service.find_available_resources(
|
4478
|
+
start_time=time_slot.start,
|
4479
|
+
end_time=time_slot.end,
|
4480
|
+
resource_type=resource_type,
|
4481
|
+
tags=requirements.split() if requirements else None
|
4482
|
+
)
|
4483
|
+
|
4484
|
+
if len(resources) < quantity:
|
4485
|
+
all_resources_available = False
|
4486
|
+
break
|
4487
|
+
|
4488
|
+
# If all resources are available, use this time slot
|
4489
|
+
if all_resources_available:
|
4490
|
+
return time_slot
|
4491
|
+
|
4492
|
+
# If no time slot has all resources available, default to first slot
|
4493
|
+
return agent_slots[0]
|
4494
|
+
|
3780
4495
|
async def optimize_schedule(self) -> Dict[str, Any]:
|
3781
4496
|
"""Optimize the entire schedule to maximize efficiency."""
|
3782
4497
|
# Get all pending and scheduled tasks
|
@@ -4431,6 +5146,7 @@ class SchedulingService:
|
|
4431
5146
|
|
4432
5147
|
return formatted_requests
|
4433
5148
|
|
5149
|
+
|
4434
5150
|
#############################################
|
4435
5151
|
# MAIN AGENT PROCESSOR
|
4436
5152
|
#############################################
|
@@ -4709,7 +5425,7 @@ class QueryProcessor:
|
|
4709
5425
|
|
4710
5426
|
return response
|
4711
5427
|
|
4712
|
-
|
5428
|
+
if command == "!plan" and args:
|
4713
5429
|
# Create a new plan from the task description
|
4714
5430
|
if not self.task_planning_service:
|
4715
5431
|
return "Task planning service is not available."
|
@@ -4721,8 +5437,8 @@ class QueryProcessor:
|
|
4721
5437
|
user_id, args, complexity
|
4722
5438
|
)
|
4723
5439
|
|
4724
|
-
# Generate subtasks
|
4725
|
-
subtasks = await self.task_planning_service.
|
5440
|
+
# Generate subtasks with resource requirements
|
5441
|
+
subtasks = await self.task_planning_service.generate_subtasks_with_resources(
|
4726
5442
|
ticket.id, args
|
4727
5443
|
)
|
4728
5444
|
|
@@ -4736,17 +5452,44 @@ class QueryProcessor:
|
|
4736
5452
|
for i, subtask in enumerate(subtasks, 1):
|
4737
5453
|
response += f"{i}. **{subtask.title}**\n"
|
4738
5454
|
response += f" - Description: {subtask.description}\n"
|
4739
|
-
response +=
|
4740
|
-
|
4741
|
-
|
5455
|
+
response += f" - Estimated time: {subtask.estimated_minutes} minutes\n"
|
5456
|
+
|
5457
|
+
if subtask.required_resources:
|
5458
|
+
response += f" - Required resources:\n"
|
5459
|
+
for res in subtask.required_resources:
|
5460
|
+
res_type = res.get("resource_type", "unknown")
|
5461
|
+
quantity = res.get("quantity", 1)
|
5462
|
+
requirements = res.get("requirements", "")
|
5463
|
+
response += f" * {quantity} {res_type}" + \
|
5464
|
+
(f" ({requirements})" if requirements else "") + "\n"
|
5465
|
+
|
4742
5466
|
if subtask.dependencies:
|
4743
|
-
response += (
|
4744
|
-
|
4745
|
-
)
|
5467
|
+
response += f" - Dependencies: {len(subtask.dependencies)} subtasks\n"
|
5468
|
+
|
4746
5469
|
response += "\n"
|
4747
5470
|
|
4748
5471
|
return response
|
4749
5472
|
|
5473
|
+
# Add a new command for allocating resources to tasks
|
5474
|
+
elif command == "!allocate-resources" and args:
|
5475
|
+
parts = args.split()
|
5476
|
+
if len(parts) < 1:
|
5477
|
+
return "Usage: !allocate-resources [subtask_id]"
|
5478
|
+
|
5479
|
+
subtask_id = parts[0]
|
5480
|
+
|
5481
|
+
if not hasattr(self, "resource_service") or not self.resource_service:
|
5482
|
+
return "Resource service is not available."
|
5483
|
+
|
5484
|
+
success, message = await self.task_planning_service.allocate_resources(
|
5485
|
+
subtask_id, self.resource_service
|
5486
|
+
)
|
5487
|
+
|
5488
|
+
if success:
|
5489
|
+
return f"✅ Resources allocated successfully: {message}"
|
5490
|
+
else:
|
5491
|
+
return f"❌ Failed to allocate resources: {message}"
|
5492
|
+
|
4750
5493
|
elif command == "!status" and args:
|
4751
5494
|
# Show status of a specific plan
|
4752
5495
|
if not self.task_planning_service:
|
@@ -4993,6 +5736,379 @@ class QueryProcessor:
|
|
4993
5736
|
|
4994
5737
|
return response
|
4995
5738
|
|
5739
|
+
elif command == "!resources" and self.resource_service:
|
5740
|
+
# Format: !resources [list|find|show|create|update|delete]
|
5741
|
+
parts = args.strip().split(" ", 1)
|
5742
|
+
subcommand = parts[0] if parts else "list"
|
5743
|
+
subcmd_args = parts[1] if len(parts) > 1 else ""
|
5744
|
+
|
5745
|
+
if subcommand == "list":
|
5746
|
+
# List available resources, optionally filtered by type
|
5747
|
+
resource_type = subcmd_args if subcmd_args else None
|
5748
|
+
|
5749
|
+
query = {}
|
5750
|
+
if resource_type:
|
5751
|
+
query["resource_type"] = resource_type
|
5752
|
+
|
5753
|
+
resources = self.resource_service.repository.find_resources(
|
5754
|
+
query)
|
5755
|
+
|
5756
|
+
if not resources:
|
5757
|
+
return "No resources found."
|
5758
|
+
|
5759
|
+
response = "# Available Resources\n\n"
|
5760
|
+
|
5761
|
+
# Group by type
|
5762
|
+
resources_by_type = {}
|
5763
|
+
for resource in resources:
|
5764
|
+
r_type = resource.resource_type
|
5765
|
+
if r_type not in resources_by_type:
|
5766
|
+
resources_by_type[r_type] = []
|
5767
|
+
resources_by_type[r_type].append(resource)
|
5768
|
+
|
5769
|
+
for r_type, r_list in resources_by_type.items():
|
5770
|
+
response += f"## {r_type.capitalize()}\n\n"
|
5771
|
+
for resource in r_list:
|
5772
|
+
status_emoji = "🟢" if resource.status == "available" else "🔴"
|
5773
|
+
response += f"{status_emoji} **{resource.name}** (ID: {resource.id})\n"
|
5774
|
+
if resource.description:
|
5775
|
+
response += f" {resource.description}\n"
|
5776
|
+
if resource.location and resource.location.building:
|
5777
|
+
response += f" Location: {resource.location.building}"
|
5778
|
+
if resource.location.room:
|
5779
|
+
response += f", Room {resource.location.room}"
|
5780
|
+
response += "\n"
|
5781
|
+
if resource.capacity:
|
5782
|
+
response += f" Capacity: {resource.capacity}\n"
|
5783
|
+
response += "\n"
|
5784
|
+
|
5785
|
+
return response
|
5786
|
+
|
5787
|
+
elif subcommand == "show" and subcmd_args:
|
5788
|
+
# Show details for a specific resource
|
5789
|
+
resource_id = subcmd_args.strip()
|
5790
|
+
resource = await self.resource_service.get_resource(resource_id)
|
5791
|
+
|
5792
|
+
if not resource:
|
5793
|
+
return f"Resource with ID {resource_id} not found."
|
5794
|
+
|
5795
|
+
response = f"# Resource: {resource.name}\n\n"
|
5796
|
+
response += f"**ID**: {resource.id}\n"
|
5797
|
+
response += f"**Type**: {resource.resource_type}\n"
|
5798
|
+
response += f"**Status**: {resource.status}\n"
|
5799
|
+
|
5800
|
+
if resource.description:
|
5801
|
+
response += f"\n**Description**: {resource.description}\n"
|
5802
|
+
|
5803
|
+
if resource.location:
|
5804
|
+
response += "\n**Location**:\n"
|
5805
|
+
if resource.location.address:
|
5806
|
+
response += f"- Address: {resource.location.address}\n"
|
5807
|
+
if resource.location.building:
|
5808
|
+
response += f"- Building: {resource.location.building}\n"
|
5809
|
+
if resource.location.floor is not None:
|
5810
|
+
response += f"- Floor: {resource.location.floor}\n"
|
5811
|
+
if resource.location.room:
|
5812
|
+
response += f"- Room: {resource.location.room}\n"
|
5813
|
+
|
5814
|
+
if resource.capacity:
|
5815
|
+
response += f"\n**Capacity**: {resource.capacity}\n"
|
5816
|
+
|
5817
|
+
if resource.tags:
|
5818
|
+
response += f"\n**Tags**: {', '.join(resource.tags)}\n"
|
5819
|
+
|
5820
|
+
# Show availability schedule
|
5821
|
+
if resource.availability_schedule:
|
5822
|
+
response += "\n**Regular Availability**:\n"
|
5823
|
+
for window in resource.availability_schedule:
|
5824
|
+
days = "Every day"
|
5825
|
+
if window.day_of_week:
|
5826
|
+
day_names = [
|
5827
|
+
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
5828
|
+
days = ", ".join([day_names[d]
|
5829
|
+
for d in window.day_of_week])
|
5830
|
+
response += f"- {days}: {window.start_time} - {window.end_time} ({window.timezone})\n"
|
5831
|
+
|
5832
|
+
# Show upcoming bookings
|
5833
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
5834
|
+
next_month = now + datetime.timedelta(days=30)
|
5835
|
+
bookings = self.resource_service.repository.get_resource_bookings(
|
5836
|
+
resource_id, now, next_month)
|
5837
|
+
|
5838
|
+
if bookings:
|
5839
|
+
response += "\n**Upcoming Bookings**:\n"
|
5840
|
+
for booking in bookings:
|
5841
|
+
start_str = booking.start_time.strftime(
|
5842
|
+
"%Y-%m-%d %H:%M")
|
5843
|
+
end_str = booking.end_time.strftime("%H:%M")
|
5844
|
+
response += f"- {start_str} - {end_str}: {booking.title}\n"
|
5845
|
+
|
5846
|
+
return response
|
5847
|
+
|
5848
|
+
elif subcommand == "find":
|
5849
|
+
# Find available resources for a time period
|
5850
|
+
# Format: !resources find room 2023-03-15 14:00 16:00
|
5851
|
+
parts = subcmd_args.split()
|
5852
|
+
if len(parts) < 4:
|
5853
|
+
return "Usage: !resources find [type] [date] [start_time] [end_time] [capacity]"
|
5854
|
+
|
5855
|
+
resource_type = parts[0]
|
5856
|
+
date_str = parts[1]
|
5857
|
+
start_time_str = parts[2]
|
5858
|
+
end_time_str = parts[3]
|
5859
|
+
capacity = int(parts[4]) if len(parts) > 4 else None
|
5860
|
+
|
5861
|
+
try:
|
5862
|
+
# Parse date and times
|
5863
|
+
date_obj = datetime.datetime.strptime(
|
5864
|
+
date_str, "%Y-%m-%d").date()
|
5865
|
+
start_time = datetime.datetime.combine(
|
5866
|
+
date_obj,
|
5867
|
+
datetime.datetime.strptime(
|
5868
|
+
start_time_str, "%H:%M").time(),
|
5869
|
+
tzinfo=datetime.timezone.utc
|
5870
|
+
)
|
5871
|
+
end_time = datetime.datetime.combine(
|
5872
|
+
date_obj,
|
5873
|
+
datetime.datetime.strptime(
|
5874
|
+
end_time_str, "%H:%M").time(),
|
5875
|
+
tzinfo=datetime.timezone.utc
|
5876
|
+
)
|
5877
|
+
|
5878
|
+
# Find available resources
|
5879
|
+
resources = await self.resource_service.find_available_resources(
|
5880
|
+
resource_type=resource_type,
|
5881
|
+
start_time=start_time,
|
5882
|
+
end_time=end_time,
|
5883
|
+
capacity=capacity
|
5884
|
+
)
|
5885
|
+
|
5886
|
+
if not resources:
|
5887
|
+
return f"No {resource_type}s available for the requested time period."
|
5888
|
+
|
5889
|
+
response = f"# Available {resource_type.capitalize()}s\n\n"
|
5890
|
+
response += f"**Date**: {date_str}\n"
|
5891
|
+
response += f"**Time**: {start_time_str} - {end_time_str}\n"
|
5892
|
+
if capacity:
|
5893
|
+
response += f"**Minimum Capacity**: {capacity}\n"
|
5894
|
+
response += "\n"
|
5895
|
+
|
5896
|
+
for resource in resources:
|
5897
|
+
response += f"- **{resource.name}** (ID: {resource.id})\n"
|
5898
|
+
if resource.description:
|
5899
|
+
response += f" {resource.description}\n"
|
5900
|
+
if resource.capacity:
|
5901
|
+
response += f" Capacity: {resource.capacity}\n"
|
5902
|
+
if resource.location and resource.location.building:
|
5903
|
+
response += f" Location: {resource.location.building}"
|
5904
|
+
if resource.location.room:
|
5905
|
+
response += f", Room {resource.location.room}"
|
5906
|
+
response += "\n"
|
5907
|
+
response += "\n"
|
5908
|
+
|
5909
|
+
return response
|
5910
|
+
|
5911
|
+
except ValueError as e:
|
5912
|
+
return f"Error parsing date/time: {e}"
|
5913
|
+
|
5914
|
+
elif subcommand == "book":
|
5915
|
+
# Book a resource
|
5916
|
+
# Format: !resources book [resource_id] [date] [start_time] [end_time] [title]
|
5917
|
+
parts = subcmd_args.split(" ", 5)
|
5918
|
+
if len(parts) < 5:
|
5919
|
+
return "Usage: !resources book [resource_id] [date] [start_time] [end_time] [title]"
|
5920
|
+
|
5921
|
+
resource_id = parts[0]
|
5922
|
+
date_str = parts[1]
|
5923
|
+
start_time_str = parts[2]
|
5924
|
+
end_time_str = parts[3]
|
5925
|
+
title = parts[4] if len(parts) > 4 else "Booking"
|
5926
|
+
|
5927
|
+
try:
|
5928
|
+
# Parse date and times
|
5929
|
+
date_obj = datetime.datetime.strptime(
|
5930
|
+
date_str, "%Y-%m-%d").date()
|
5931
|
+
start_time = datetime.datetime.combine(
|
5932
|
+
date_obj,
|
5933
|
+
datetime.datetime.strptime(
|
5934
|
+
start_time_str, "%H:%M").time(),
|
5935
|
+
tzinfo=datetime.timezone.utc
|
5936
|
+
)
|
5937
|
+
end_time = datetime.datetime.combine(
|
5938
|
+
date_obj,
|
5939
|
+
datetime.datetime.strptime(
|
5940
|
+
end_time_str, "%H:%M").time(),
|
5941
|
+
tzinfo=datetime.timezone.utc
|
5942
|
+
)
|
5943
|
+
|
5944
|
+
# Create booking
|
5945
|
+
success, booking, error = await self.resource_service.create_booking(
|
5946
|
+
resource_id=resource_id,
|
5947
|
+
user_id=user_id,
|
5948
|
+
title=title,
|
5949
|
+
start_time=start_time,
|
5950
|
+
end_time=end_time
|
5951
|
+
)
|
5952
|
+
|
5953
|
+
if not success:
|
5954
|
+
return f"Failed to book resource: {error}"
|
5955
|
+
|
5956
|
+
# Get resource details
|
5957
|
+
resource = await self.resource_service.get_resource(resource_id)
|
5958
|
+
resource_name = resource.name if resource else resource_id
|
5959
|
+
|
5960
|
+
response = "# Booking Confirmed\n\n"
|
5961
|
+
response += f"**Resource**: {resource_name}\n"
|
5962
|
+
response += f"**Date**: {date_str}\n"
|
5963
|
+
response += f"**Time**: {start_time_str} - {end_time_str}\n"
|
5964
|
+
response += f"**Title**: {title}\n"
|
5965
|
+
response += f"**Booking ID**: {booking.id}\n\n"
|
5966
|
+
response += "Your booking has been confirmed and added to your schedule."
|
5967
|
+
|
5968
|
+
return response
|
5969
|
+
|
5970
|
+
except ValueError as e:
|
5971
|
+
return f"Error parsing date/time: {e}"
|
5972
|
+
|
5973
|
+
elif subcommand == "bookings":
|
5974
|
+
# View all bookings for the current user
|
5975
|
+
include_cancelled = "all" in subcmd_args.lower()
|
5976
|
+
|
5977
|
+
bookings = await self.resource_service.get_user_bookings(user_id, include_cancelled)
|
5978
|
+
|
5979
|
+
if not bookings:
|
5980
|
+
return "You don't have any bookings." + (
|
5981
|
+
" (Use 'bookings all' to include cancelled bookings)" if not include_cancelled else ""
|
5982
|
+
)
|
5983
|
+
|
5984
|
+
response = "# Your Bookings\n\n"
|
5985
|
+
|
5986
|
+
# Group bookings by date
|
5987
|
+
bookings_by_date = {}
|
5988
|
+
for booking_data in bookings:
|
5989
|
+
booking = booking_data["booking"]
|
5990
|
+
resource = booking_data["resource"]
|
5991
|
+
|
5992
|
+
date_str = booking["start_time"].strftime("%Y-%m-%d")
|
5993
|
+
if date_str not in bookings_by_date:
|
5994
|
+
bookings_by_date[date_str] = []
|
5995
|
+
|
5996
|
+
bookings_by_date[date_str].append((booking, resource))
|
5997
|
+
|
5998
|
+
# Sort dates
|
5999
|
+
for date_str in sorted(bookings_by_date.keys()):
|
6000
|
+
response += f"## {date_str}\n\n"
|
6001
|
+
|
6002
|
+
for booking, resource in bookings_by_date[date_str]:
|
6003
|
+
start_time = booking["start_time"].strftime(
|
6004
|
+
"%H:%M")
|
6005
|
+
end_time = booking["end_time"].strftime("%H:%M")
|
6006
|
+
resource_name = resource["name"] if resource else "Unknown Resource"
|
6007
|
+
|
6008
|
+
status_emoji = "🟢" if booking["status"] == "confirmed" else "🔴"
|
6009
|
+
response += f"{status_emoji} **{start_time}-{end_time}**: {booking['title']}\n"
|
6010
|
+
response += f" Resource: {resource_name}\n"
|
6011
|
+
response += f" Booking ID: {booking['id']}\n\n"
|
6012
|
+
|
6013
|
+
return response
|
6014
|
+
|
6015
|
+
elif subcommand == "cancel" and subcmd_args:
|
6016
|
+
# Cancel a booking
|
6017
|
+
booking_id = subcmd_args.strip()
|
6018
|
+
|
6019
|
+
success, error = await self.resource_service.cancel_booking(booking_id, user_id)
|
6020
|
+
|
6021
|
+
if success:
|
6022
|
+
return "✅ Your booking has been successfully cancelled."
|
6023
|
+
else:
|
6024
|
+
return f"❌ Failed to cancel booking: {error}"
|
6025
|
+
|
6026
|
+
elif subcommand == "schedule" and subcmd_args:
|
6027
|
+
# View resource schedule
|
6028
|
+
# Format: !resources schedule resource_id [YYYY-MM-DD] [days]
|
6029
|
+
parts = subcmd_args.split()
|
6030
|
+
if len(parts) < 1:
|
6031
|
+
return "Usage: !resources schedule resource_id [YYYY-MM-DD] [days]"
|
6032
|
+
|
6033
|
+
resource_id = parts[0]
|
6034
|
+
|
6035
|
+
# Default to today and 7 days
|
6036
|
+
start_date = datetime.datetime.now(datetime.timezone.utc)
|
6037
|
+
days = 7
|
6038
|
+
|
6039
|
+
if len(parts) > 1:
|
6040
|
+
try:
|
6041
|
+
start_date = datetime.datetime.strptime(
|
6042
|
+
parts[1], "%Y-%m-%d"
|
6043
|
+
).replace(tzinfo=datetime.timezone.utc)
|
6044
|
+
except ValueError:
|
6045
|
+
return "Invalid date format. Use YYYY-MM-DD."
|
6046
|
+
|
6047
|
+
if len(parts) > 2:
|
6048
|
+
try:
|
6049
|
+
days = min(int(parts[2]), 31) # Limit to 31 days
|
6050
|
+
except ValueError:
|
6051
|
+
return "Days must be a number."
|
6052
|
+
|
6053
|
+
end_date = start_date + datetime.timedelta(days=days)
|
6054
|
+
|
6055
|
+
# Get the resource
|
6056
|
+
resource = await self.resource_service.get_resource(resource_id)
|
6057
|
+
if not resource:
|
6058
|
+
return f"Resource with ID {resource_id} not found."
|
6059
|
+
|
6060
|
+
# Get schedule
|
6061
|
+
schedule = await self.resource_service.get_resource_schedule(
|
6062
|
+
resource_id, start_date, end_date
|
6063
|
+
)
|
6064
|
+
|
6065
|
+
# Create calendar visualization
|
6066
|
+
response = f"# Schedule for {resource.name}\n\n"
|
6067
|
+
response += f"Period: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}\n\n"
|
6068
|
+
|
6069
|
+
# Group by date
|
6070
|
+
schedule_by_date = {}
|
6071
|
+
current_date = start_date
|
6072
|
+
while current_date < end_date:
|
6073
|
+
date_str = current_date.strftime("%Y-%m-%d")
|
6074
|
+
schedule_by_date[date_str] = []
|
6075
|
+
current_date += datetime.timedelta(days=1)
|
6076
|
+
|
6077
|
+
# Add entries to appropriate dates
|
6078
|
+
for entry in schedule:
|
6079
|
+
date_str = entry["start_time"].strftime("%Y-%m-%d")
|
6080
|
+
if date_str in schedule_by_date:
|
6081
|
+
schedule_by_date[date_str].append(entry)
|
6082
|
+
|
6083
|
+
# Generate calendar view
|
6084
|
+
for date_str, entries in schedule_by_date.items():
|
6085
|
+
# Convert to datetime for day of week
|
6086
|
+
entry_date = datetime.datetime.strptime(
|
6087
|
+
date_str, "%Y-%m-%d")
|
6088
|
+
day_of_week = entry_date.strftime("%A")
|
6089
|
+
|
6090
|
+
response += f"## {date_str} ({day_of_week})\n\n"
|
6091
|
+
|
6092
|
+
if not entries:
|
6093
|
+
response += "No bookings or exceptions\n\n"
|
6094
|
+
continue
|
6095
|
+
|
6096
|
+
# Sort by start time
|
6097
|
+
entries.sort(key=lambda x: x["start_time"])
|
6098
|
+
|
6099
|
+
for entry in entries:
|
6100
|
+
start_time = entry["start_time"].strftime("%H:%M")
|
6101
|
+
end_time = entry["end_time"].strftime("%H:%M")
|
6102
|
+
|
6103
|
+
if entry["type"] == "booking":
|
6104
|
+
response += f"- **{start_time}-{end_time}**: {entry['title']} (by {entry['user_id']})\n"
|
6105
|
+
else: # exception
|
6106
|
+
response += f"- **{start_time}-{end_time}**: {entry['status']} (Unavailable)\n"
|
6107
|
+
|
6108
|
+
response += "\n"
|
6109
|
+
|
6110
|
+
return response
|
6111
|
+
|
4996
6112
|
return None
|
4997
6113
|
|
4998
6114
|
async def _process_existing_ticket(
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: solana-agent
|
3
|
-
Version: 11.
|
3
|
+
Version: 11.3.0
|
4
4
|
Summary: The Future of Work
|
5
5
|
License: MIT
|
6
6
|
Keywords: ai,openai,ai agents,agi
|
@@ -200,6 +200,19 @@ Solana Agent transforms organizations into living systems that continuously lear
|
|
200
200
|
Capacity-aware scheduling to prevent agent overloading.
|
201
201
|
Schedule analytics and efficiency metrics.
|
202
202
|
|
203
|
+
- **🏢 Comprehensive Resource Management System:**
|
204
|
+
Complete resource scheduling and booking infrastructure.
|
205
|
+
Resource categorization with customizable attributes and capabilities.
|
206
|
+
Intelligent conflict detection and prevention for bookings.
|
207
|
+
Location-aware resource tracking with hierarchical location model.
|
208
|
+
Availability management with customizable recurring schedules.
|
209
|
+
User-level booking authorization with permission controls.
|
210
|
+
Resource discovery by type, capacity, and available features.
|
211
|
+
Command-line interface for resource management.
|
212
|
+
Booking lifecycle management with confirmation and cancellation.
|
213
|
+
Time-aware resource scheduling across time zones.
|
214
|
+
Resource utilization analytics and optimization.
|
215
|
+
|
203
216
|
- **📊 Performance Optimization Framework:**
|
204
217
|
Integrated Net Promoter Score (NPS) system for interaction quality measurement.
|
205
218
|
Automatic satisfaction surveys after ticket resolution.
|
@@ -212,8 +225,14 @@ Solana Agent transforms organizations into living systems that continuously lear
|
|
212
225
|
Automated complexity assessment for incoming tasks.
|
213
226
|
Task decomposition into subtasks with dependencies.
|
214
227
|
Intelligent workload distribution based on agent capacity.
|
228
|
+
Resource requirements identification for each subtask.
|
229
|
+
Automatic resource allocation based on task needs.
|
230
|
+
Resource-aware scheduling to prevent conflicts.
|
231
|
+
Integrated resource and agent availability checking.
|
232
|
+
Command-line interface for resource allocation to tasks.
|
215
233
|
Visual progress tracking with completion estimates.
|
216
|
-
Prioritization based on urgency, importance, and dependencies.
|
234
|
+
Prioritization based on urgency, importance, and dependencies.
|
235
|
+
Resource utilization optimization across project timelines.
|
217
236
|
|
218
237
|
- **🛡️ Governance Framework:**
|
219
238
|
Define organization-wide values and operating principles in code.
|
@@ -0,0 +1,6 @@
|
|
1
|
+
solana_agent/__init__.py,sha256=zpfnWqANd3OHGWm7NCF5Y6m01BWG4NkNk8SK9Ex48nA,18
|
2
|
+
solana_agent/ai.py,sha256=W5h6hckGY_NAkqFFOQtuocdTbDfJnqNY9_Z7RYYy2K0,290852
|
3
|
+
solana_agent-11.3.0.dist-info/LICENSE,sha256=BnSRc-NSFuyF2s496l_4EyrwAP6YimvxWcjPiJ0J7g4,1057
|
4
|
+
solana_agent-11.3.0.dist-info/METADATA,sha256=H3juWeX6NQzKqC1mzSLysBURiykMRw_hM9gwfzE-cOE,19603
|
5
|
+
solana_agent-11.3.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
6
|
+
solana_agent-11.3.0.dist-info/RECORD,,
|
@@ -1,6 +0,0 @@
|
|
1
|
-
solana_agent/__init__.py,sha256=zpfnWqANd3OHGWm7NCF5Y6m01BWG4NkNk8SK9Ex48nA,18
|
2
|
-
solana_agent/ai.py,sha256=SPHPCQPqk1wuBymB3StNRYp84s9pAa7LOeROBSHiJfQ,243608
|
3
|
-
solana_agent-11.2.0.dist-info/LICENSE,sha256=BnSRc-NSFuyF2s496l_4EyrwAP6YimvxWcjPiJ0J7g4,1057
|
4
|
-
solana_agent-11.2.0.dist-info/METADATA,sha256=quIlcgag0-hHIAy7-ibsh-JWxpBmravXIeoCp4zXxDw,18468
|
5
|
-
solana_agent-11.2.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
6
|
-
solana_agent-11.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|