Skip to content

Commit bd4395c

Browse files
committed
fix: improve task assignment and equipment dispatch validation
- Make worker_id optional in assign_task to allow queued tasks - Fix parameter validation to allow None for optional parameters - Update business rules validation to skip None values - Improve error handling for equipment dispatch workflow - Update documentation page guardrails section with accurate info
1 parent 63d7887 commit bd4395c

5 files changed

Lines changed: 150 additions & 66 deletions

File tree

src/api/agents/operations/action_tools.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,21 +316,34 @@ async def create_task(
316316
async def assign_task(
317317
self,
318318
task_id: str,
319-
worker_id: str,
319+
worker_id: Optional[str] = None,
320320
assignment_type: str = "manual",
321321
) -> Dict[str, Any]:
322322
"""
323323
Assign a task to a worker.
324324
325325
Args:
326326
task_id: Task ID to assign
327-
worker_id: Worker ID to assign task to
327+
worker_id: Worker ID to assign task to (optional - if None, task remains unassigned)
328328
assignment_type: Type of assignment (manual, automatic)
329329
330330
Returns:
331331
Dict with assignment result
332332
"""
333333
try:
334+
# If no worker_id provided, skip assignment but return success
335+
# Task will remain in 'queued' status until manually assigned
336+
if not worker_id:
337+
logger.info(f"Task {task_id} created but not assigned (no worker_id provided). Task is queued and ready for assignment.")
338+
return {
339+
"success": True,
340+
"task_id": task_id,
341+
"worker_id": None,
342+
"assignment_type": assignment_type,
343+
"status": "queued",
344+
"message": "Task created successfully but not assigned. Please assign a worker manually or specify worker_id.",
345+
}
346+
334347
if not self.wms_service:
335348
await self.initialize()
336349

@@ -349,11 +362,14 @@ async def assign_task(
349362
"status": "assigned",
350363
}
351364
else:
365+
error_msg = result.get("error", "Failed to update work queue entry") if result else "Failed to update work queue entry"
366+
logger.warning(f"Failed to assign task {task_id} to worker {worker_id}: {error_msg}")
352367
return {
353368
"success": False,
354369
"task_id": task_id,
355370
"worker_id": worker_id,
356-
"error": "Failed to update work queue entry",
371+
"error": error_msg,
372+
"status": "queued", # Task remains queued if assignment fails
357373
}
358374
except Exception as e:
359375
logger.error(f"Failed to assign task: {e}")
@@ -362,6 +378,7 @@ async def assign_task(
362378
"task_id": task_id,
363379
"worker_id": worker_id,
364380
"error": str(e),
381+
"status": "queued", # Task remains queued if assignment fails
365382
}
366383

367384
async def get_task_status(

src/api/agents/operations/mcp_operations_agent.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,41 @@ async def execute_single_tool(step: Dict[str, Any], previous_results: Dict[str,
857857
if dep_result:
858858
break # Found the dependency, no need to check others
859859

860+
# Skip tool execution if required parameters are missing
861+
if tool_name == "assign_task" and (arguments.get("task_id") is None or arguments.get("task_id") == "None"):
862+
logger.warning(f"Skipping {tool_name} - task_id is required but not provided")
863+
result_dict = {
864+
"tool_name": tool_name,
865+
"success": False,
866+
"error": "task_id is required but not provided",
867+
"execution_time": datetime.utcnow().isoformat(),
868+
}
869+
return (tool_id, result_dict)
870+
871+
if tool_name == "assign_task" and (arguments.get("worker_id") is None or arguments.get("worker_id") == "None"):
872+
logger.info(f"Executing {tool_name} without worker_id - task will remain queued")
873+
# Continue execution - the tool will handle None worker_id gracefully
874+
875+
if tool_name in ["assign_equipment", "dispatch_equipment"]:
876+
if arguments.get("task_id") is None or arguments.get("task_id") == "None":
877+
logger.warning(f"Skipping {tool_name} - task_id is required but not provided")
878+
result_dict = {
879+
"tool_name": tool_name,
880+
"success": False,
881+
"error": "task_id is required but not provided (should come from create_task result)",
882+
"execution_time": datetime.utcnow().isoformat(),
883+
}
884+
return (tool_id, result_dict)
885+
if arguments.get("asset_id") is None or arguments.get("asset_id") == "None":
886+
logger.warning(f"Skipping {tool_name} - asset_id is required but not provided (should come from get_equipment_status result)")
887+
result_dict = {
888+
"tool_name": tool_name,
889+
"success": False,
890+
"error": "asset_id is required but not provided (should come from get_equipment_status result)",
891+
"execution_time": datetime.utcnow().isoformat(),
892+
}
893+
return (tool_id, result_dict)
894+
860895
try:
861896
logger.info(
862897
f"Executing MCP tool: {tool_name} with arguments: {arguments}"

src/api/services/mcp/adapters/operations_adapter.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,22 +137,22 @@ async def _register_tools(self) -> None:
137137
# Register assign_task tool
138138
self.tools["assign_task"] = MCPTool(
139139
name="assign_task",
140-
description="Assign a task to a worker",
140+
description="Assign a task to a worker. If worker_id is not provided, task will remain queued for manual assignment.",
141141
tool_type=MCPToolType.FUNCTION,
142142
parameters={
143143
"type": "object",
144144
"properties": {
145145
"task_id": {"type": "string", "description": "Task ID to assign"},
146146
"worker_id": {
147147
"type": "string",
148-
"description": "Worker ID to assign task to",
148+
"description": "Worker ID to assign task to (optional - if not provided, task remains queued)",
149149
},
150150
"assignment_type": {
151151
"type": "string",
152152
"description": "Type of assignment (manual, automatic)",
153153
},
154154
},
155-
"required": ["task_id", "worker_id"],
155+
"required": ["task_id"], # worker_id is now optional
156156
},
157157
handler=self._handle_assign_task,
158158
)
@@ -231,9 +231,10 @@ async def _handle_create_task(self, arguments: Dict[str, Any]) -> Dict[str, Any]
231231
async def _handle_assign_task(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
232232
"""Handle assign_task tool execution."""
233233
try:
234+
# worker_id is now optional - if not provided, task will remain queued
234235
result = await self.operations_tools.assign_task(
235236
task_id=arguments["task_id"],
236-
worker_id=arguments["worker_id"],
237+
worker_id=arguments.get("worker_id"), # Optional - can be None
237238
assignment_type=arguments.get("assignment_type", "manual"),
238239
)
239240
return result

src/api/services/mcp/parameter_validator.py

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,16 @@ async def validate_tool_parameters(
179179
for param_name, param_value in arguments.items():
180180
if param_name in properties:
181181
param_schema = properties[param_name]
182+
is_required = param_name in required_params
183+
184+
# Skip validation for None values of optional parameters
185+
# This allows tools like get_equipment_status to work with asset_id=None
186+
if param_value is None and not is_required:
187+
validated_arguments[param_name] = None
188+
continue
189+
182190
validation_result = await self._validate_parameter(
183-
param_name, param_value, param_schema, tool_name
191+
param_name, param_value, param_schema, tool_name, is_required
184192
)
185193

186194
if validation_result["valid"]:
@@ -247,12 +255,18 @@ async def _validate_parameter(
247255
param_value: Any,
248256
param_schema: Dict[str, Any],
249257
tool_name: str,
258+
is_required: bool = False,
250259
) -> Dict[str, Any]:
251260
"""Validate a single parameter."""
252261
try:
253262
# Get parameter type
254263
param_type = param_schema.get("type", "string")
255264

265+
# Allow None for optional parameters (not required)
266+
# This allows tools to work with optional parameters like asset_id in get_equipment_status
267+
if param_value is None and not is_required:
268+
return {"valid": True, "value": None, "issue": None}
269+
256270
# Type validation
257271
if not self._validate_type(param_value, param_type):
258272
return {
@@ -268,32 +282,36 @@ async def _validate_parameter(
268282
),
269283
}
270284

271-
# Format validation
272-
if param_type == "string":
285+
# Skip format/length/range validation for None values (already handled above)
286+
if param_value is None:
287+
return {"valid": True, "value": None, "issue": None}
288+
289+
# Format validation (skip if None - already handled above)
290+
if param_type == "string" and param_value is not None:
273291
format_validation = self._validate_string_format(
274292
param_name, param_value, param_schema
275293
)
276294
if not format_validation["valid"]:
277295
return format_validation
278296

279-
# Range validation
280-
if param_type in ["integer", "number"]:
297+
# Range validation (skip if None - already handled above)
298+
if param_type in ["integer", "number"] and param_value is not None:
281299
range_validation = self._validate_range(
282300
param_name, param_value, param_schema
283301
)
284302
if not range_validation["valid"]:
285303
return range_validation
286304

287-
# Length validation
288-
if param_type == "string":
305+
# Length validation (skip if None - already handled above)
306+
if param_type == "string" and param_value is not None:
289307
length_validation = self._validate_length(
290308
param_name, param_value, param_schema
291309
)
292310
if not length_validation["valid"]:
293311
return length_validation
294312

295-
# Enum validation
296-
if "enum" in param_schema:
313+
# Enum validation (skip if None - already handled above)
314+
if "enum" in param_schema and param_value is not None:
297315
enum_validation = self._validate_enum(
298316
param_name, param_value, param_schema
299317
)
@@ -496,8 +514,8 @@ async def _validate_equipment_business_rules(
496514
"""Validate equipment-specific business rules."""
497515
issues = []
498516

499-
# Equipment ID format validation
500-
if "asset_id" in arguments:
517+
# Equipment ID format validation (skip if None - it's optional)
518+
if "asset_id" in arguments and arguments["asset_id"] is not None:
501519
asset_id = arguments["asset_id"]
502520
if not self.validation_patterns[ParameterType.EQUIPMENT_ID.value].match(
503521
asset_id
@@ -512,8 +530,8 @@ async def _validate_equipment_business_rules(
512530
)
513531
)
514532

515-
# Equipment status validation
516-
if "status" in arguments:
533+
# Equipment status validation (skip if None - it's optional)
534+
if "status" in arguments and arguments["status"] is not None:
517535
status = arguments["status"]
518536
valid_statuses = self.business_rules["equipment_status"]["valid_values"]
519537
if status not in valid_statuses:
@@ -526,6 +544,22 @@ async def _validate_equipment_business_rules(
526544
provided_value=status,
527545
)
528546
)
547+
548+
# Equipment type validation (skip if None - it's optional, but if provided should be valid)
549+
if "equipment_type" in arguments and arguments["equipment_type"] is not None:
550+
equipment_type = arguments["equipment_type"]
551+
if "equipment_type" in self.business_rules:
552+
valid_types = self.business_rules["equipment_type"]["valid_values"]
553+
if equipment_type not in valid_types:
554+
issues.append(
555+
ValidationIssue(
556+
parameter="equipment_type",
557+
level=ValidationLevel.WARNING,
558+
message=f"Equipment type '{equipment_type}' is not in standard list",
559+
suggestion=f"Valid types are: {', '.join(valid_types)}",
560+
provided_value=equipment_type,
561+
)
562+
)
529563

530564
return issues
531565

@@ -535,8 +569,8 @@ async def _validate_task_business_rules(
535569
"""Validate task-specific business rules."""
536570
issues = []
537571

538-
# Task ID format validation
539-
if "task_id" in arguments:
572+
# Task ID format validation (skip if None - it's optional)
573+
if "task_id" in arguments and arguments["task_id"] is not None:
540574
task_id = arguments["task_id"]
541575
if not self.validation_patterns[ParameterType.TASK_ID.value].match(task_id):
542576
issues.append(

0 commit comments

Comments
 (0)