dacp 0.3.2__py3-none-any.whl → 0.3.4__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.
dacp/workflow.py ADDED
@@ -0,0 +1,414 @@
1
+ """
2
+ DACP Workflow Management - Agent-to-agent communication and task routing.
3
+
4
+ This module provides workflow orchestration capabilities for multi-agent systems,
5
+ including task boards, message routing, and automated agent collaboration.
6
+ """
7
+
8
+ import logging
9
+ import time
10
+ import uuid
11
+ from typing import Dict, Any, List, Optional, Callable
12
+ from dataclasses import dataclass, field
13
+ from enum import Enum
14
+
15
+ logger = logging.getLogger("dacp.workflow")
16
+
17
+
18
+ class TaskStatus(Enum):
19
+ """Task status enumeration."""
20
+
21
+ PENDING = "pending"
22
+ ASSIGNED = "assigned"
23
+ IN_PROGRESS = "in_progress"
24
+ COMPLETED = "completed"
25
+ FAILED = "failed"
26
+ CANCELLED = "cancelled"
27
+
28
+
29
+ class TaskPriority(Enum):
30
+ """Task priority enumeration."""
31
+
32
+ LOW = 1
33
+ NORMAL = 2
34
+ HIGH = 3
35
+ URGENT = 4
36
+
37
+
38
+ @dataclass
39
+ class Task:
40
+ """Represents a task in the workflow system."""
41
+
42
+ id: str
43
+ type: str
44
+ data: Dict[str, Any]
45
+ source_agent: str
46
+ target_agent: Optional[str] = None
47
+ status: TaskStatus = TaskStatus.PENDING
48
+ priority: TaskPriority = TaskPriority.NORMAL
49
+ created_at: float = field(default_factory=time.time)
50
+ assigned_at: Optional[float] = None
51
+ completed_at: Optional[float] = None
52
+ result: Optional[Dict[str, Any]] = None
53
+ error: Optional[str] = None
54
+ dependencies: List[str] = field(default_factory=list)
55
+ metadata: Dict[str, Any] = field(default_factory=dict)
56
+
57
+ def to_dict(self) -> Dict[str, Any]:
58
+ """Convert task to dictionary representation."""
59
+ return {
60
+ "id": self.id,
61
+ "type": self.type,
62
+ "data": self.data,
63
+ "source_agent": self.source_agent,
64
+ "target_agent": self.target_agent,
65
+ "status": self.status.value,
66
+ "priority": self.priority.value,
67
+ "created_at": self.created_at,
68
+ "assigned_at": self.assigned_at,
69
+ "completed_at": self.completed_at,
70
+ "result": self.result,
71
+ "error": self.error,
72
+ "dependencies": self.dependencies,
73
+ "metadata": self.metadata,
74
+ }
75
+
76
+
77
+ @dataclass
78
+ class WorkflowRule:
79
+ """Defines routing rules for agent-to-agent communication."""
80
+
81
+ source_task_type: str
82
+ target_agent: str
83
+ target_task_type: str
84
+ condition: Optional[Callable[[Task], bool]] = None
85
+ transform_data: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None
86
+ priority: TaskPriority = TaskPriority.NORMAL
87
+
88
+
89
+ class TaskBoard:
90
+ """Central task board for managing agent-to-agent tasks."""
91
+
92
+ def __init__(self) -> None:
93
+ self.tasks: Dict[str, Task] = {}
94
+ self.agent_queues: Dict[str, List[str]] = {}
95
+ self.completed_tasks: List[str] = []
96
+ self.workflow_rules: List[WorkflowRule] = []
97
+
98
+ def add_task(
99
+ self,
100
+ task_type: str,
101
+ data: Dict[str, Any],
102
+ source_agent: str,
103
+ target_agent: Optional[str] = None,
104
+ priority: TaskPriority = TaskPriority.NORMAL,
105
+ dependencies: Optional[List[str]] = None,
106
+ ) -> str:
107
+ """Add a new task to the board."""
108
+ task_id = str(uuid.uuid4())
109
+
110
+ task = Task(
111
+ id=task_id,
112
+ type=task_type,
113
+ data=data,
114
+ source_agent=source_agent,
115
+ target_agent=target_agent,
116
+ priority=priority,
117
+ dependencies=dependencies or [],
118
+ )
119
+
120
+ self.tasks[task_id] = task
121
+
122
+ # Add to appropriate agent queue
123
+ if target_agent:
124
+ if target_agent not in self.agent_queues:
125
+ self.agent_queues[target_agent] = []
126
+ self.agent_queues[target_agent].append(task_id)
127
+ task.status = TaskStatus.ASSIGNED
128
+ task.assigned_at = time.time()
129
+
130
+ logger.info(f"📋 Task '{task_id}' added: {task_type} from {source_agent} to {target_agent}")
131
+ return task_id
132
+
133
+ def get_next_task(self, agent_name: str) -> Optional[Task]:
134
+ """Get the next task for an agent."""
135
+ if agent_name not in self.agent_queues or not self.agent_queues[agent_name]:
136
+ return None
137
+
138
+ # Sort by priority and creation time
139
+ queue = self.agent_queues[agent_name]
140
+ available_tasks = []
141
+
142
+ for task_id in queue:
143
+ task = self.tasks[task_id]
144
+ if task.status == TaskStatus.ASSIGNED and self._dependencies_satisfied(task):
145
+ available_tasks.append(task)
146
+
147
+ if not available_tasks:
148
+ return None
149
+
150
+ # Sort by priority (higher first) then by creation time (older first)
151
+ available_tasks.sort(key=lambda t: (-t.priority.value, t.created_at))
152
+
153
+ next_task = available_tasks[0]
154
+ next_task.status = TaskStatus.IN_PROGRESS
155
+
156
+ logger.info(f"📤 Task '{next_task.id}' assigned to agent '{agent_name}'")
157
+ return next_task
158
+
159
+ def complete_task(
160
+ self, task_id: str, result: Dict[str, Any], trigger_rules: bool = True
161
+ ) -> None:
162
+ """Mark a task as completed and trigger workflow rules."""
163
+ if task_id not in self.tasks:
164
+ logger.error(f"❌ Task '{task_id}' not found")
165
+ return
166
+
167
+ task = self.tasks[task_id]
168
+ task.status = TaskStatus.COMPLETED
169
+ task.completed_at = time.time()
170
+ task.result = result
171
+
172
+ # Remove from agent queue
173
+ if task.target_agent and task.target_agent in self.agent_queues:
174
+ if task_id in self.agent_queues[task.target_agent]:
175
+ self.agent_queues[task.target_agent].remove(task_id)
176
+
177
+ self.completed_tasks.append(task_id)
178
+
179
+ logger.info(f"✅ Task '{task_id}' completed by agent '{task.target_agent}'")
180
+
181
+ # Trigger workflow rules if enabled
182
+ if trigger_rules:
183
+ self._trigger_workflow_rules(task)
184
+
185
+ def fail_task(self, task_id: str, error: str) -> None:
186
+ """Mark a task as failed."""
187
+ if task_id not in self.tasks:
188
+ logger.error(f"❌ Task '{task_id}' not found")
189
+ return
190
+
191
+ task = self.tasks[task_id]
192
+ task.status = TaskStatus.FAILED
193
+ task.completed_at = time.time()
194
+ task.error = error
195
+
196
+ # Remove from agent queue
197
+ if task.target_agent and task.target_agent in self.agent_queues:
198
+ if task_id in self.agent_queues[task.target_agent]:
199
+ self.agent_queues[task.target_agent].remove(task_id)
200
+
201
+ logger.error(f"❌ Task '{task_id}' failed: {error}")
202
+
203
+ def add_workflow_rule(self, rule: WorkflowRule) -> None:
204
+ """Add a workflow rule for automatic task routing."""
205
+ self.workflow_rules.append(rule)
206
+ logger.info(
207
+ f"🔄 Workflow rule added: {rule.source_task_type} → "
208
+ f"{rule.target_agent} ({rule.target_task_type})"
209
+ )
210
+
211
+ def _dependencies_satisfied(self, task: Task) -> bool:
212
+ """Check if all task dependencies are satisfied."""
213
+ for dep_id in task.dependencies:
214
+ if dep_id not in self.tasks:
215
+ return False
216
+ dep_task = self.tasks[dep_id]
217
+ if dep_task.status != TaskStatus.COMPLETED:
218
+ return False
219
+ return True
220
+
221
+ def _trigger_workflow_rules(self, completed_task: Task) -> None:
222
+ """Trigger workflow rules based on completed task."""
223
+ for rule in self.workflow_rules:
224
+ if rule.source_task_type == completed_task.type:
225
+ # Check condition if specified
226
+ if rule.condition and not rule.condition(completed_task):
227
+ continue
228
+
229
+ # Transform data if specified
230
+ if rule.transform_data and completed_task.result:
231
+ new_data = rule.transform_data(completed_task.result)
232
+ else:
233
+ new_data = completed_task.result or {}
234
+
235
+ # Create new task
236
+ new_task_id = self.add_task(
237
+ task_type=rule.target_task_type,
238
+ data=new_data,
239
+ source_agent=completed_task.target_agent or completed_task.source_agent,
240
+ target_agent=rule.target_agent,
241
+ priority=rule.priority,
242
+ )
243
+
244
+ logger.info(f"🔄 Workflow rule triggered: {completed_task.id} → {new_task_id}")
245
+
246
+ def get_task_status(self, task_id: str) -> Optional[Dict[str, Any]]:
247
+ """Get task status and details."""
248
+ if task_id not in self.tasks:
249
+ return None
250
+ return self.tasks[task_id].to_dict()
251
+
252
+ def get_agent_queue_status(self, agent_name: str) -> Dict[str, Any]:
253
+ """Get status of an agent's task queue."""
254
+ if agent_name not in self.agent_queues:
255
+ return {"agent": agent_name, "queue_length": 0, "tasks": []}
256
+
257
+ queue = self.agent_queues[agent_name]
258
+ task_details = []
259
+
260
+ for task_id in queue:
261
+ if task_id in self.tasks:
262
+ task = self.tasks[task_id]
263
+ task_details.append(
264
+ {
265
+ "id": task_id,
266
+ "type": task.type,
267
+ "status": task.status.value,
268
+ "priority": task.priority.value,
269
+ "created_at": task.created_at,
270
+ }
271
+ )
272
+
273
+ return {
274
+ "agent": agent_name,
275
+ "queue_length": len(queue),
276
+ "tasks": task_details,
277
+ }
278
+
279
+ def get_workflow_summary(self) -> Dict[str, Any]:
280
+ """Get overall workflow summary."""
281
+ status_counts: Dict[str, int] = {}
282
+ for task in self.tasks.values():
283
+ status = task.status.value
284
+ status_counts[status] = status_counts.get(status, 0) + 1
285
+
286
+ return {
287
+ "total_tasks": len(self.tasks),
288
+ "status_counts": status_counts,
289
+ "agent_queues": {agent: len(queue) for agent, queue in self.agent_queues.items()},
290
+ "completed_tasks": len(self.completed_tasks),
291
+ "workflow_rules": len(self.workflow_rules),
292
+ }
293
+
294
+
295
+ class WorkflowOrchestrator:
296
+ """Enhanced orchestrator with workflow and agent-to-agent communication."""
297
+
298
+ def __init__(self, orchestrator: Any) -> None:
299
+ """Initialize with a base orchestrator."""
300
+ self.orchestrator = orchestrator
301
+ self.task_board = TaskBoard()
302
+ self.auto_processing = False
303
+ self._processing_interval = 1.0 # seconds
304
+
305
+ def enable_auto_processing(self, interval: float = 1.0) -> None:
306
+ """Enable automatic task processing."""
307
+ self.auto_processing = True
308
+ self._processing_interval = interval
309
+ logger.info(f"🤖 Auto-processing enabled (interval: {interval}s)")
310
+
311
+ def disable_auto_processing(self) -> None:
312
+ """Disable automatic task processing."""
313
+ self.auto_processing = False
314
+ logger.info("⏸️ Auto-processing disabled")
315
+
316
+ def submit_task_for_agent(
317
+ self,
318
+ source_agent: str,
319
+ target_agent: str,
320
+ task_type: str,
321
+ task_data: Dict[str, Any],
322
+ priority: TaskPriority = TaskPriority.NORMAL,
323
+ ) -> str:
324
+ """Submit a task from one agent to another."""
325
+ return self.task_board.add_task(
326
+ task_type=task_type,
327
+ data=task_data,
328
+ source_agent=source_agent,
329
+ target_agent=target_agent,
330
+ priority=priority,
331
+ )
332
+
333
+ def process_agent_tasks(self, agent_name: str, max_tasks: int = 1) -> List[Dict[str, Any]]:
334
+ """Process available tasks for an agent."""
335
+ if agent_name not in self.orchestrator.agents:
336
+ logger.error(f"❌ Agent '{agent_name}' not registered")
337
+ return []
338
+
339
+ results = []
340
+ tasks_processed = 0
341
+
342
+ while tasks_processed < max_tasks:
343
+ task = self.task_board.get_next_task(agent_name)
344
+ if not task:
345
+ break
346
+
347
+ try:
348
+ # Convert task to agent message format
349
+ # Only include the task type and the actual task data
350
+ message = {
351
+ "task": task.type,
352
+ **task.data,
353
+ }
354
+
355
+ # Send to agent
356
+ response = self.orchestrator.send_message(agent_name, message)
357
+
358
+ if "error" in response:
359
+ self.task_board.fail_task(task.id, response["error"])
360
+ results.append(
361
+ {
362
+ "task_id": task.id,
363
+ "status": "failed",
364
+ "error": response["error"],
365
+ }
366
+ )
367
+ else:
368
+ self.task_board.complete_task(task.id, response)
369
+ results.append({"task_id": task.id, "status": "completed", "result": response})
370
+
371
+ tasks_processed += 1
372
+
373
+ except Exception as e:
374
+ error_msg = f"Task processing failed: {e}"
375
+ self.task_board.fail_task(task.id, error_msg)
376
+ results.append({"task_id": task.id, "status": "failed", "error": error_msg})
377
+ tasks_processed += 1
378
+
379
+ return results
380
+
381
+ def add_workflow_rule(
382
+ self,
383
+ source_task_type: str,
384
+ target_agent: str,
385
+ target_task_type: str,
386
+ condition: Optional[Callable[[Task], bool]] = None,
387
+ transform_data: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None,
388
+ priority: TaskPriority = TaskPriority.NORMAL,
389
+ ) -> None:
390
+ """Add a workflow rule for automatic task chaining."""
391
+ rule = WorkflowRule(
392
+ source_task_type=source_task_type,
393
+ target_agent=target_agent,
394
+ target_task_type=target_task_type,
395
+ condition=condition,
396
+ transform_data=transform_data,
397
+ priority=priority,
398
+ )
399
+ self.task_board.add_workflow_rule(rule)
400
+
401
+ def get_workflow_status(self) -> Dict[str, Any]:
402
+ """Get comprehensive workflow status."""
403
+ return {
404
+ "orchestrator": {
405
+ "session_id": self.orchestrator.session_id,
406
+ "registered_agents": list(self.orchestrator.agents.keys()),
407
+ "auto_processing": self.auto_processing,
408
+ },
409
+ "task_board": self.task_board.get_workflow_summary(),
410
+ "agent_queues": {
411
+ agent: self.task_board.get_agent_queue_status(agent)
412
+ for agent in self.orchestrator.agents.keys()
413
+ },
414
+ }