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