solana-agent 11.1.1__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
@@ -15,6 +15,7 @@ import datetime
15
15
  import json
16
16
  import re
17
17
  import traceback
18
+ from unittest.mock import AsyncMock
18
19
  import uuid
19
20
  from enum import Enum
20
21
  from typing import (
@@ -299,22 +300,31 @@ class AgentType(str, Enum):
299
300
 
300
301
 
301
302
  class Ticket(BaseModel):
302
- """Represents a user support ticket."""
303
-
304
- id: str
303
+ """Model for a support ticket."""
304
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
305
305
  user_id: str
306
306
  query: str
307
- status: TicketStatus
308
- assigned_to: str
309
- 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
310
316
  complexity: Optional[Dict[str, Any]] = None
311
- context: Optional[str] = None
317
+ agent_context: Optional[Dict[str, Any]] = None
312
318
  is_parent: bool = False
313
319
  is_subtask: bool = False
314
320
  parent_id: Optional[str] = None
315
- updated_at: Optional[datetime.datetime] = None
316
- resolved_at: Optional[datetime.datetime] = None
317
- 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]] = []
318
328
 
319
329
 
320
330
  class Handoff(BaseModel):
@@ -455,17 +465,22 @@ class PlanStatus(BaseModel):
455
465
 
456
466
 
457
467
  class SubtaskModel(BaseModel):
458
- """Represents a subtask breakdown of a complex task."""
468
+ """Model for a subtask in a complex task breakdown."""
459
469
 
460
470
  id: str = Field(default_factory=lambda: str(uuid.uuid4()))
461
- parent_id: str
471
+ parent_id: Optional[str] = None
462
472
  title: str
463
473
  description: str
464
- assignee: Optional[str] = None
465
- status: TicketStatus = TicketStatus.PLANNING
466
- sequence: int
467
- dependencies: List[str] = Field(default_factory=list)
468
- 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]] = []
469
484
 
470
485
 
471
486
  class WorkCapacity(BaseModel):
@@ -482,10 +497,117 @@ class WorkCapacity(BaseModel):
482
497
  )
483
498
 
484
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
+
485
606
  #############################################
486
607
  # INTERFACES
487
608
  #############################################
488
609
 
610
+
489
611
  class LLMProvider(Protocol):
490
612
  """Interface for language model providers."""
491
613
 
@@ -1025,6 +1147,217 @@ class PineconeAdapter:
1025
1147
  # IMPLEMENTATIONS - REPOSITORIES
1026
1148
  #############################################
1027
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
+
1028
1361
  class MongoMemoryProvider:
1029
1362
  """MongoDB implementation of MemoryProvider."""
1030
1363
 
@@ -1355,6 +1688,15 @@ class MongoTicketRepository:
1355
1688
  """Count tickets matching query."""
1356
1689
  return self.db.count_documents(self.collection, query)
1357
1690
 
1691
+ def find_stalled_tickets(self, cutoff_time, statuses):
1692
+ """Find tickets that haven't been updated since the cutoff time."""
1693
+ query = {
1694
+ "status": {"$in": [status.value if isinstance(status, Enum) else status for status in statuses]},
1695
+ "updated_at": {"$lt": cutoff_time}
1696
+ }
1697
+ tickets = self.db_adapter.find("tickets", query)
1698
+ return [Ticket(**ticket) for ticket in tickets]
1699
+
1358
1700
 
1359
1701
  class MongoHandoffRepository:
1360
1702
  """MongoDB implementation of HandoffRepository."""
@@ -2644,6 +2986,143 @@ class AgentService:
2644
2986
  return self.ai_agents
2645
2987
 
2646
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
+
2647
3126
  class TaskPlanningService:
2648
3127
  """Service for managing complex task planning and breakdown."""
2649
3128
 
@@ -2998,6 +3477,181 @@ class TaskPlanningService:
2998
3477
  "domain_knowledge": 5,
2999
3478
  }
3000
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
+
3001
3655
 
3002
3656
  class ProjectApprovalService:
3003
3657
  """Service for managing human approval of new projects."""
@@ -3018,27 +3672,6 @@ class ProjectApprovalService:
3018
3672
  if agent_id in self.human_agent_registry.get_all_human_agents():
3019
3673
  self.approvers.append(agent_id)
3020
3674
 
3021
- async def submit_for_approval(self, ticket: Ticket) -> None:
3022
- """Submit a project for human approval."""
3023
- # Update ticket status
3024
- self.ticket_repository.update(
3025
- ticket.id,
3026
- {
3027
- "status": TicketStatus.PENDING,
3028
- "approval_status": "awaiting_approval",
3029
- "updated_at": datetime.datetime.now(datetime.timezone.utc),
3030
- },
3031
- )
3032
-
3033
- # Notify approvers
3034
- if self.notification_service:
3035
- for approver_id in self.approvers:
3036
- self.notification_service.send_notification(
3037
- approver_id,
3038
- f"New project requires approval: {ticket.query}",
3039
- {"ticket_id": ticket.id, "type": "approval_request"},
3040
- )
3041
-
3042
3675
  async def process_approval(
3043
3676
  self, ticket_id: str, approver_id: str, approved: bool, comments: str = ""
3044
3677
  ) -> None:
@@ -3073,6 +3706,27 @@ class ProjectApprovalService:
3073
3706
  },
3074
3707
  )
3075
3708
 
3709
+ async def submit_for_approval(self, ticket: Ticket) -> None:
3710
+ """Submit a project for human approval."""
3711
+ # Update ticket status
3712
+ self.ticket_repository.update(
3713
+ ticket.id,
3714
+ {
3715
+ "status": TicketStatus.PENDING,
3716
+ "approval_status": "awaiting_approval",
3717
+ "updated_at": datetime.datetime.now(datetime.timezone.utc),
3718
+ },
3719
+ )
3720
+
3721
+ # Notify approvers
3722
+ if self.notification_service and self.approvers:
3723
+ for approver_id in self.approvers:
3724
+ await self.notification_service.send_notification(
3725
+ approver_id,
3726
+ f"New project requires approval: {ticket.query}",
3727
+ {"ticket_id": ticket.id, "type": "approval_request"},
3728
+ )
3729
+
3076
3730
 
3077
3731
  class ProjectSimulationService:
3078
3732
  """Service for simulating project feasibility and requirements using historical data."""
@@ -3767,6 +4421,77 @@ class SchedulingService:
3767
4421
 
3768
4422
  return task
3769
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
+
3770
4495
  async def optimize_schedule(self) -> Dict[str, Any]:
3771
4496
  """Optimize the entire schedule to maximize efficiency."""
3772
4497
  # Get all pending and scheduled tasks
@@ -4421,6 +5146,7 @@ class SchedulingService:
4421
5146
 
4422
5147
  return formatted_requests
4423
5148
 
5149
+
4424
5150
  #############################################
4425
5151
  # MAIN AGENT PROCESSOR
4426
5152
  #############################################
@@ -4446,6 +5172,7 @@ class QueryProcessor:
4446
5172
  project_simulation_service: Optional[ProjectSimulationService] = None,
4447
5173
  require_human_approval: bool = False,
4448
5174
  scheduling_service: Optional[SchedulingService] = None,
5175
+ stalled_ticket_timeout: Optional[int] = 60,
4449
5176
  ):
4450
5177
  self.agent_service = agent_service
4451
5178
  self.routing_service = routing_service
@@ -4463,11 +5190,37 @@ class QueryProcessor:
4463
5190
  self.require_human_approval = require_human_approval
4464
5191
  self._shutdown_event = asyncio.Event()
4465
5192
  self.scheduling_service = scheduling_service
5193
+ self.stalled_ticket_timeout = stalled_ticket_timeout
5194
+
5195
+ self._stalled_ticket_task = None
5196
+
5197
+ # Start background task for stalled ticket detection if not already running
5198
+ if self.stalled_ticket_timeout is not None and self._stalled_ticket_task is None:
5199
+ try:
5200
+ self._stalled_ticket_task = asyncio.create_task(
5201
+ self._run_stalled_ticket_checks())
5202
+ except RuntimeError:
5203
+ # No running event loop - likely in test environment
5204
+ pass
4466
5205
 
4467
5206
  async def process(
4468
5207
  self, user_id: str, user_text: str, timezone: str = None
4469
5208
  ) -> AsyncGenerator[str, None]:
4470
5209
  """Process the user request with appropriate agent and handle ticket management."""
5210
+ # Start background task for stalled ticket detection if not already running
5211
+ if self.stalled_ticket_timeout is not None and self._stalled_ticket_task is None:
5212
+ try:
5213
+ loop = asyncio.get_running_loop()
5214
+ self._stalled_ticket_task = loop.create_task(
5215
+ self._run_stalled_ticket_checks())
5216
+ except RuntimeError:
5217
+ # No running event loop - likely in test environment
5218
+ # Instead of just passing, log a message for clarity
5219
+ import logging
5220
+ logging.warning(
5221
+ "No running event loop available for stalled ticket checker.")
5222
+ # Don't try to create the task - this prevents the coroutine warning
5223
+
4471
5224
  try:
4472
5225
  # Handle human agent messages differently
4473
5226
  if await self._is_human_agent(user_id):
@@ -4515,7 +5268,9 @@ class QueryProcessor:
4515
5268
  except Exception as e:
4516
5269
  print(f"Error in request processing: {str(e)}")
4517
5270
  print(traceback.format_exc())
4518
- yield "\n\nI apologize for the technical difficulty.\n\n"
5271
+ # Use yield instead of direct function calling to avoid coroutine warning
5272
+ error_msg = "\n\nI apologize for the technical difficulty.\n\n"
5273
+ yield error_msg
4519
5274
 
4520
5275
  async def _is_human_agent(self, user_id: str) -> bool:
4521
5276
  """Check if the user is a registered human agent."""
@@ -4583,6 +5338,69 @@ class QueryProcessor:
4583
5338
 
4584
5339
  return response
4585
5340
 
5341
+ async def shutdown(self):
5342
+ """Clean shutdown of the query processor."""
5343
+ self._shutdown_event.set()
5344
+
5345
+ # Cancel the stalled ticket task if running
5346
+ if hasattr(self, '_stalled_ticket_task') and self._stalled_ticket_task is not None:
5347
+ self._stalled_ticket_task.cancel()
5348
+ try:
5349
+ await self._stalled_ticket_task
5350
+ except (asyncio.CancelledError, TypeError):
5351
+ # Either properly cancelled coroutine or a mock that can't be awaited
5352
+ pass
5353
+
5354
+ async def _run_stalled_ticket_checks(self):
5355
+ """Run periodic checks for stalled tickets."""
5356
+ try:
5357
+ while not self._shutdown_event.is_set():
5358
+ await self._check_for_stalled_tickets()
5359
+ # Check every 5 minutes or half the timeout period, whichever is smaller
5360
+ check_interval = min(
5361
+ 300, self.stalled_ticket_timeout * 30) if self.stalled_ticket_timeout else 300
5362
+ await asyncio.sleep(check_interval)
5363
+ except asyncio.CancelledError:
5364
+ # Task was cancelled, clean exit
5365
+ pass
5366
+ except Exception as e:
5367
+ print(f"Error in stalled ticket check: {e}")
5368
+
5369
+ async def _check_for_stalled_tickets(self):
5370
+ """Check for tickets that haven't been updated in a while and reassign them."""
5371
+ # If stalled ticket detection is disabled, exit early
5372
+ if self.stalled_ticket_timeout is None:
5373
+ return
5374
+
5375
+ # Find tickets that haven't been updated in the configured time
5376
+ stalled_cutoff = datetime.datetime.now(
5377
+ datetime.timezone.utc) - datetime.timedelta(minutes=self.stalled_ticket_timeout)
5378
+
5379
+ # Query for stalled tickets using the find_stalled_tickets method
5380
+ stalled_tickets = await self.ticket_service.ticket_repository.find_stalled_tickets(
5381
+ stalled_cutoff, [TicketStatus.ACTIVE, TicketStatus.TRANSFERRED]
5382
+ )
5383
+
5384
+ for ticket in stalled_tickets:
5385
+ # Re-route using routing service to find the optimal agent
5386
+ new_agent = await self.routing_service.route_query(ticket.query)
5387
+
5388
+ # Skip if the routing didn't change
5389
+ if new_agent == ticket.assigned_to:
5390
+ continue
5391
+
5392
+ # Process as handoff
5393
+ await self.handoff_service.process_handoff(
5394
+ ticket.id,
5395
+ ticket.assigned_to or "unassigned",
5396
+ new_agent,
5397
+ f"Automatically reassigned after {self.stalled_ticket_timeout} minutes of inactivity"
5398
+ )
5399
+
5400
+ # Log the reassignment
5401
+ print(
5402
+ f"Stalled ticket {ticket.id} reassigned from {ticket.assigned_to or 'unassigned'} to {new_agent}")
5403
+
4586
5404
  async def _process_system_commands(
4587
5405
  self, user_id: str, user_text: str
4588
5406
  ) -> Optional[str]:
@@ -4607,7 +5425,7 @@ class QueryProcessor:
4607
5425
 
4608
5426
  return response
4609
5427
 
4610
- elif command == "!plan" and args:
5428
+ if command == "!plan" and args:
4611
5429
  # Create a new plan from the task description
4612
5430
  if not self.task_planning_service:
4613
5431
  return "Task planning service is not available."
@@ -4619,8 +5437,8 @@ class QueryProcessor:
4619
5437
  user_id, args, complexity
4620
5438
  )
4621
5439
 
4622
- # Generate subtasks
4623
- 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(
4624
5442
  ticket.id, args
4625
5443
  )
4626
5444
 
@@ -4634,17 +5452,44 @@ class QueryProcessor:
4634
5452
  for i, subtask in enumerate(subtasks, 1):
4635
5453
  response += f"{i}. **{subtask.title}**\n"
4636
5454
  response += f" - Description: {subtask.description}\n"
4637
- response += (
4638
- f" - Estimated time: {subtask.estimated_minutes} minutes\n"
4639
- )
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
+
4640
5466
  if subtask.dependencies:
4641
- response += (
4642
- f" - Dependencies: {len(subtask.dependencies)} subtasks\n"
4643
- )
5467
+ response += f" - Dependencies: {len(subtask.dependencies)} subtasks\n"
5468
+
4644
5469
  response += "\n"
4645
5470
 
4646
5471
  return response
4647
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
+
4648
5493
  elif command == "!status" and args:
4649
5494
  # Show status of a specific plan
4650
5495
  if not self.task_planning_service:
@@ -4739,154 +5584,530 @@ class QueryProcessor:
4739
5584
  )
4740
5585
  return f"Project {ticket_id} has been {'approved' if approved else 'rejected'}."
4741
5586
 
4742
- elif command == "!schedule" and args and self.scheduling_service:
4743
- # Format: !schedule task_id [agent_id] [YYYY-MM-DD HH:MM]
4744
- parts = args.strip().split(" ", 2)
4745
- if len(parts) < 1:
4746
- return "Usage: !schedule task_id [agent_id] [YYYY-MM-DD HH:MM]"
4747
-
4748
- task_id = parts[0]
4749
- agent_id = parts[1] if len(parts) > 1 else None
4750
- time_str = parts[2] if len(parts) > 2 else None
4751
-
4752
- # Fetch the task from ticket repository
4753
- ticket = self.ticket_service.ticket_repository.get_by_id(task_id)
4754
- if not ticket:
4755
- return f"Task {task_id} not found."
4756
-
4757
- # Convert ticket to scheduled task
4758
- scheduled_task = ScheduledTask(
4759
- task_id=task_id,
4760
- title=ticket.query[:50] +
4761
- "..." if len(ticket.query) > 50 else ticket.query,
4762
- description=ticket.query,
4763
- estimated_minutes=ticket.complexity.get(
4764
- "estimated_minutes", 30) if ticket.complexity else 30,
4765
- priority=5, # Default priority
4766
- assigned_to=agent_id or ticket.assigned_to,
4767
- # Use current agent as a specialization tag
4768
- specialization_tags=[ticket.assigned_to],
4769
- )
5587
+ elif command == "!schedule" and args and self.scheduling_service:
5588
+ # Format: !schedule task_id [agent_id] [YYYY-MM-DD HH:MM]
5589
+ parts = args.strip().split(" ", 2)
5590
+ if len(parts) < 1:
5591
+ return "Usage: !schedule task_id [agent_id] [YYYY-MM-DD HH:MM]"
5592
+
5593
+ task_id = parts[0]
5594
+ agent_id = parts[1] if len(parts) > 1 else None
5595
+ time_str = parts[2] if len(parts) > 2 else None
5596
+
5597
+ # Fetch the task from ticket repository
5598
+ ticket = self.ticket_service.ticket_repository.get_by_id(
5599
+ task_id)
5600
+ if not ticket:
5601
+ return f"Task {task_id} not found."
5602
+
5603
+ # Convert ticket to scheduled task
5604
+ scheduled_task = ScheduledTask(
5605
+ task_id=task_id,
5606
+ title=ticket.query[:50] +
5607
+ "..." if len(ticket.query) > 50 else ticket.query,
5608
+ description=ticket.query,
5609
+ estimated_minutes=ticket.complexity.get(
5610
+ "estimated_minutes", 30) if ticket.complexity else 30,
5611
+ priority=5, # Default priority
5612
+ assigned_to=agent_id or ticket.assigned_to,
5613
+ # Use current agent as a specialization tag
5614
+ specialization_tags=[ticket.assigned_to],
5615
+ )
4770
5616
 
4771
- # Set scheduled time if provided
4772
- if time_str:
4773
- try:
4774
- scheduled_time = datetime.datetime.fromisoformat(time_str)
4775
- scheduled_task.scheduled_start = scheduled_time
4776
- scheduled_task.scheduled_end = scheduled_time + datetime.timedelta(
4777
- minutes=scheduled_task.estimated_minutes
4778
- )
4779
- except ValueError:
4780
- return "Invalid date format. Use YYYY-MM-DD HH:MM."
5617
+ # Set scheduled time if provided
5618
+ if time_str:
5619
+ try:
5620
+ scheduled_time = datetime.datetime.fromisoformat(
5621
+ time_str)
5622
+ scheduled_task.scheduled_start = scheduled_time
5623
+ scheduled_task.scheduled_end = scheduled_time + datetime.timedelta(
5624
+ minutes=scheduled_task.estimated_minutes
5625
+ )
5626
+ except ValueError:
5627
+ return "Invalid date format. Use YYYY-MM-DD HH:MM."
4781
5628
 
4782
- # Schedule the task
4783
- result = await self.scheduling_service.schedule_task(
4784
- scheduled_task, preferred_agent_id=agent_id
4785
- )
5629
+ # Schedule the task
5630
+ result = await self.scheduling_service.schedule_task(
5631
+ scheduled_task, preferred_agent_id=agent_id
5632
+ )
4786
5633
 
4787
- # Update ticket with scheduling info
4788
- self.ticket_service.update_ticket_status(
4789
- task_id,
4790
- ticket.status,
4791
- scheduled_start=result.scheduled_start,
4792
- scheduled_agent=result.assigned_to
4793
- )
5634
+ # Update ticket with scheduling info
5635
+ self.ticket_service.update_ticket_status(
5636
+ task_id,
5637
+ ticket.status,
5638
+ scheduled_start=result.scheduled_start,
5639
+ scheduled_agent=result.assigned_to
5640
+ )
4794
5641
 
4795
- # Format response
4796
- response = "# Task Scheduled\n\n"
4797
- response += f"**Task:** {scheduled_task.title}\n"
4798
- response += f"**Assigned to:** {result.assigned_to}\n"
4799
- response += f"**Scheduled start:** {result.scheduled_start.strftime('%Y-%m-%d %H:%M')}\n"
4800
- response += f"**Estimated duration:** {result.estimated_minutes} minutes"
5642
+ # Format response
5643
+ response = "# Task Scheduled\n\n"
5644
+ response += f"**Task:** {scheduled_task.title}\n"
5645
+ response += f"**Assigned to:** {result.assigned_to}\n"
5646
+ response += f"**Scheduled start:** {result.scheduled_start.strftime('%Y-%m-%d %H:%M')}\n"
5647
+ response += f"**Estimated duration:** {result.estimated_minutes} minutes"
4801
5648
 
4802
- return response
5649
+ return response
4803
5650
 
4804
- elif command == "!timeoff" and args and self.scheduling_service:
4805
- # Format: !timeoff request YYYY-MM-DD HH:MM YYYY-MM-DD HH:MM reason
4806
- # or: !timeoff cancel request_id
4807
- parts = args.strip().split(" ", 1)
4808
- if len(parts) < 2:
4809
- return "Usage: \n- !timeoff request START_DATE END_DATE reason\n- !timeoff cancel request_id"
5651
+ elif command == "!timeoff" and args and self.scheduling_service:
5652
+ # Format: !timeoff request YYYY-MM-DD HH:MM YYYY-MM-DD HH:MM reason
5653
+ # or: !timeoff cancel request_id
5654
+ parts = args.strip().split(" ", 1)
5655
+ if len(parts) < 2:
5656
+ return "Usage: \n- !timeoff request START_DATE END_DATE reason\n- !timeoff cancel request_id"
4810
5657
 
4811
- action = parts[0].lower()
4812
- action_args = parts[1]
5658
+ action = parts[0].lower()
5659
+ action_args = parts[1]
4813
5660
 
4814
- if action == "request":
4815
- # Parse request args
4816
- request_parts = action_args.split(" ", 2)
4817
- if len(request_parts) < 3:
4818
- return "Usage: !timeoff request YYYY-MM-DD YYYY-MM-DD reason"
5661
+ if action == "request":
5662
+ # Parse request args
5663
+ request_parts = action_args.split(" ", 2)
5664
+ if len(request_parts) < 3:
5665
+ return "Usage: !timeoff request YYYY-MM-DD YYYY-MM-DD reason"
4819
5666
 
4820
- start_str = request_parts[0]
4821
- end_str = request_parts[1]
4822
- reason = request_parts[2]
5667
+ start_str = request_parts[0]
5668
+ end_str = request_parts[1]
5669
+ reason = request_parts[2]
4823
5670
 
4824
- try:
4825
- start_time = datetime.datetime.fromisoformat(start_str)
4826
- end_time = datetime.datetime.fromisoformat(end_str)
4827
- except ValueError:
4828
- return "Invalid date format. Use YYYY-MM-DD HH:MM."
4829
-
4830
- # Submit time off request
4831
- success, status, request_id = await self.scheduling_service.request_time_off(
4832
- user_id, start_time, end_time, reason
4833
- )
5671
+ try:
5672
+ start_time = datetime.datetime.fromisoformat(start_str)
5673
+ end_time = datetime.datetime.fromisoformat(end_str)
5674
+ except ValueError:
5675
+ return "Invalid date format. Use YYYY-MM-DD HH:MM."
5676
+
5677
+ # Submit time off request
5678
+ success, status, request_id = await self.scheduling_service.request_time_off(
5679
+ user_id, start_time, end_time, reason
5680
+ )
4834
5681
 
4835
- if success:
4836
- return f"Time off request submitted and automatically approved. Request ID: {request_id}"
4837
- else:
4838
- return f"Time off request {status}. Request ID: {request_id}"
5682
+ if success:
5683
+ return f"Time off request submitted and automatically approved. Request ID: {request_id}"
5684
+ else:
5685
+ return f"Time off request {status}. Request ID: {request_id}"
5686
+
5687
+ elif action == "cancel":
5688
+ request_id = action_args.strip()
5689
+ success, status = await self.scheduling_service.cancel_time_off_request(
5690
+ user_id, request_id
5691
+ )
5692
+
5693
+ if success:
5694
+ return f"Time off request {request_id} cancelled successfully."
5695
+ else:
5696
+ return f"Failed to cancel request: {status}"
5697
+
5698
+ return "Unknown timeoff action. Use 'request' or 'cancel'."
5699
+
5700
+ elif command == "!schedule-view" and self.scheduling_service:
5701
+ # View agent's schedule for the next week
5702
+ # Default to current user if no agent specified
5703
+ agent_id = args.strip() if args else user_id
5704
+
5705
+ start_time = datetime.datetime.now(datetime.timezone.utc)
5706
+ end_time = start_time + datetime.timedelta(days=7)
4839
5707
 
4840
- elif action == "cancel":
4841
- request_id = action_args.strip()
4842
- success, status = await self.scheduling_service.cancel_time_off_request(
4843
- user_id, request_id
5708
+ # Get tasks for the specified time period
5709
+ tasks = await self.scheduling_service.get_agent_tasks(
5710
+ agent_id, start_time, end_time, include_completed=False
4844
5711
  )
4845
5712
 
4846
- if success:
4847
- return f"Time off request {request_id} cancelled successfully."
4848
- else:
4849
- return f"Failed to cancel request: {status}"
5713
+ if not tasks:
5714
+ return f"No scheduled tasks found for {agent_id} in the next 7 days."
4850
5715
 
4851
- return "Unknown timeoff action. Use 'request' or 'cancel'."
5716
+ # Sort by start time
5717
+ tasks.sort(
5718
+ key=lambda t: t.scheduled_start or datetime.datetime.max)
4852
5719
 
4853
- elif command == "!schedule-view" and self.scheduling_service:
4854
- # View agent's schedule for the next week
4855
- # Default to current user if no agent specified
4856
- agent_id = args.strip() if args else user_id
5720
+ # Format response
5721
+ response = f"# Schedule for {agent_id}\n\n"
4857
5722
 
4858
- start_time = datetime.datetime.now(datetime.timezone.utc)
4859
- end_time = start_time + datetime.timedelta(days=7)
5723
+ current_day = None
5724
+ for task in tasks:
5725
+ # Group by day
5726
+ task_day = task.scheduled_start.strftime(
5727
+ "%Y-%m-%d") if task.scheduled_start else "Unscheduled"
4860
5728
 
4861
- # Get tasks for the specified time period
4862
- tasks = await self.scheduling_service.get_agent_tasks(
4863
- agent_id, start_time, end_time, include_completed=False
4864
- )
5729
+ if task_day != current_day:
5730
+ response += f"\n## {task_day}\n\n"
5731
+ current_day = task_day
4865
5732
 
4866
- if not tasks:
4867
- return f"No scheduled tasks found for {agent_id} in the next 7 days."
5733
+ start_time = task.scheduled_start.strftime(
5734
+ "%H:%M") if task.scheduled_start else "TBD"
5735
+ response += f"- **{start_time}** ({task.estimated_minutes} min): {task.title}\n"
4868
5736
 
4869
- # Sort by start time
4870
- tasks.sort(key=lambda t: t.scheduled_start or datetime.datetime.max)
5737
+ return response
4871
5738
 
4872
- # Format response
4873
- response = f"# Schedule for {agent_id}\n\n"
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"
4874
5784
 
4875
- current_day = None
4876
- for task in tasks:
4877
- # Group by day
4878
- task_day = task.scheduled_start.strftime(
4879
- "%Y-%m-%d") if task.scheduled_start else "Unscheduled"
5785
+ return response
4880
5786
 
4881
- if task_day != current_day:
4882
- response += f"\n## {task_day}\n\n"
4883
- current_day = task_day
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"
4884
5845
 
4885
- start_time = task.scheduled_start.strftime(
4886
- "%H:%M") if task.scheduled_start else "TBD"
4887
- response += f"- **{start_time}** ({task.estimated_minutes} min): {task.title}\n"
5846
+ return response
4888
5847
 
4889
- return response
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
4890
6111
 
4891
6112
  return None
4892
6113
 
@@ -5589,7 +6810,8 @@ class SolanaAgentFactory:
5589
6810
  project_approval_service=project_approval_service,
5590
6811
  project_simulation_service=project_simulation_service,
5591
6812
  require_human_approval=config.get("require_human_approval", False),
5592
- scheduling_service=scheduling_service
6813
+ scheduling_service=scheduling_service,
6814
+ stalled_ticket_timeout=config.get("stalled_ticket_timeout", 60),
5593
6815
  )
5594
6816
 
5595
6817
  return query_processor
@@ -5876,6 +7098,22 @@ class TenantManager:
5876
7098
  llm_provider, task_planning_service, ticket_repo
5877
7099
  )
5878
7100
 
7101
+ # Create scheduling repository and service
7102
+ tenant_db_adapter = self._create_tenant_db_adapter(
7103
+ tenant) # Get the DB adapter properly
7104
+ scheduling_repository = SchedulingRepository(
7105
+ tenant_db_adapter) # Use the correct adapter
7106
+
7107
+ scheduling_service = SchedulingService(
7108
+ scheduling_repository=scheduling_repository,
7109
+ task_planning_service=task_planning_service,
7110
+ agent_service=agent_service
7111
+ )
7112
+
7113
+ # Update task_planning_service with scheduling_service if needed
7114
+ if task_planning_service:
7115
+ task_planning_service.scheduling_service = scheduling_service
7116
+
5879
7117
  # Create query processor
5880
7118
  return QueryProcessor(
5881
7119
  agent_service=agent_service,
@@ -5895,6 +7133,9 @@ class TenantManager:
5895
7133
  require_human_approval=tenant.get_config_value(
5896
7134
  "require_human_approval", False
5897
7135
  ),
7136
+ scheduling_service=scheduling_service,
7137
+ stalled_ticket_timeout=tenant.get_config_value(
7138
+ "stalled_ticket_timeout"),
5898
7139
  )
5899
7140
 
5900
7141
  def _deep_merge(self, target: Dict, source: Dict) -> None: