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 CHANGED
@@ -300,22 +300,31 @@ class AgentType(str, Enum):
300
300
 
301
301
 
302
302
  class Ticket(BaseModel):
303
- """Represents a user support ticket."""
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
- context: Optional[str] = None
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
- updated_at: Optional[datetime.datetime] = None
317
- resolved_at: Optional[datetime.datetime] = None
318
- handoff_reason: Optional[str] = None
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
- """Represents a subtask breakdown of a complex task."""
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
- assignee: Optional[str] = None
466
- status: TicketStatus = TicketStatus.PLANNING
467
- sequence: int
468
- dependencies: List[str] = Field(default_factory=list)
469
- estimated_minutes: int = 30
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
- elif command == "!plan" and args:
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.generate_subtasks(
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
- f" - Estimated time: {subtask.estimated_minutes} minutes\n"
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
- f" - Dependencies: {len(subtask.dependencies)} subtasks\n"
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.2.0
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,,