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/__init__.py +42 -1
- dacp/api.py +365 -0
- dacp/cli.py +249 -0
- dacp/intelligence.py +10 -28
- dacp/json_parser.py +230 -0
- dacp/logging_config.py +1 -3
- dacp/orchestrator.py +20 -26
- dacp/tools.py +1 -3
- dacp/workflow.py +414 -0
- dacp/workflow_runtime.py +546 -0
- {dacp-0.3.2.dist-info → dacp-0.3.4.dist-info}/METADATA +8 -3
- dacp-0.3.4.dist-info/RECORD +21 -0
- dacp-0.3.4.dist-info/entry_points.txt +2 -0
- dacp-0.3.2.dist-info/RECORD +0 -15
- {dacp-0.3.2.dist-info → dacp-0.3.4.dist-info}/WHEEL +0 -0
- {dacp-0.3.2.dist-info → dacp-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {dacp-0.3.2.dist-info → dacp-0.3.4.dist-info}/top_level.txt +0 -0
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
|
+
}
|