massgen 0.1.4__py3-none-any.whl → 0.1.5__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.
Potentially problematic release.
This version of massgen might be problematic. Click here for more details.
- massgen/__init__.py +1 -1
- massgen/chat_agent.py +340 -20
- massgen/cli.py +326 -19
- massgen/configs/README.md +52 -10
- massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
- massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
- massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
- massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
- massgen/configs/memory/single_agent_compression_test.yaml +64 -0
- massgen/configs/tools/custom_tools/multimodal_tools/playwright_with_img_understanding.yaml +98 -0
- massgen/configs/tools/custom_tools/multimodal_tools/understand_video_example.yaml +54 -0
- massgen/memory/README.md +277 -0
- massgen/memory/__init__.py +26 -0
- massgen/memory/_base.py +193 -0
- massgen/memory/_compression.py +237 -0
- massgen/memory/_context_monitor.py +211 -0
- massgen/memory/_conversation.py +255 -0
- massgen/memory/_fact_extraction_prompts.py +333 -0
- massgen/memory/_mem0_adapters.py +257 -0
- massgen/memory/_persistent.py +687 -0
- massgen/memory/docker-compose.qdrant.yml +36 -0
- massgen/memory/docs/DESIGN.md +388 -0
- massgen/memory/docs/QUICKSTART.md +409 -0
- massgen/memory/docs/SUMMARY.md +319 -0
- massgen/memory/docs/agent_use_memory.md +408 -0
- massgen/memory/docs/orchestrator_use_memory.md +586 -0
- massgen/memory/examples.py +237 -0
- massgen/orchestrator.py +207 -7
- massgen/tests/memory/test_agent_compression.py +174 -0
- massgen/tests/memory/test_context_window_management.py +286 -0
- massgen/tests/memory/test_force_compression.py +154 -0
- massgen/tests/memory/test_simple_compression.py +147 -0
- massgen/tests/test_agent_memory.py +534 -0
- massgen/tests/test_conversation_memory.py +382 -0
- massgen/tests/test_orchestrator_memory.py +620 -0
- massgen/tests/test_persistent_memory.py +435 -0
- massgen/token_manager/token_manager.py +6 -0
- massgen/tools/__init__.py +8 -0
- massgen/tools/_planning_mcp_server.py +520 -0
- massgen/tools/planning_dataclasses.py +434 -0
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/METADATA +109 -76
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/RECORD +46 -12
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/WHEEL +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Task Planning Data Structures for MassGen
|
|
4
|
+
|
|
5
|
+
Provides dataclasses for managing agent task plans with dependency tracking,
|
|
6
|
+
status management, and validation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Task:
|
|
17
|
+
"""
|
|
18
|
+
Represents a single task in an agent's plan.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
id: Unique task identifier (UUID or custom string)
|
|
22
|
+
description: Human-readable task description
|
|
23
|
+
status: Current task status (pending/in_progress/completed/blocked)
|
|
24
|
+
created_at: Timestamp when task was created
|
|
25
|
+
completed_at: Timestamp when task was completed (if applicable)
|
|
26
|
+
dependencies: List of task IDs this task depends on
|
|
27
|
+
metadata: Additional task-specific metadata
|
|
28
|
+
"""
|
|
29
|
+
id: str
|
|
30
|
+
description: str
|
|
31
|
+
status: Literal["pending", "in_progress", "completed", "blocked"] = "pending"
|
|
32
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
33
|
+
completed_at: Optional[datetime] = None
|
|
34
|
+
dependencies: List[str] = field(default_factory=list)
|
|
35
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
38
|
+
"""Convert task to dictionary for serialization."""
|
|
39
|
+
return {
|
|
40
|
+
"id": self.id,
|
|
41
|
+
"description": self.description,
|
|
42
|
+
"status": self.status,
|
|
43
|
+
"created_at": self.created_at.isoformat(),
|
|
44
|
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
45
|
+
"dependencies": self.dependencies.copy(),
|
|
46
|
+
"metadata": self.metadata.copy(),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Task":
|
|
51
|
+
"""Create task from dictionary."""
|
|
52
|
+
return cls(
|
|
53
|
+
id=data["id"],
|
|
54
|
+
description=data["description"],
|
|
55
|
+
status=data["status"],
|
|
56
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
57
|
+
completed_at=datetime.fromisoformat(data["completed_at"]) if data.get("completed_at") else None,
|
|
58
|
+
dependencies=data.get("dependencies", []),
|
|
59
|
+
metadata=data.get("metadata", {}),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class TaskPlan:
|
|
65
|
+
"""
|
|
66
|
+
Manages an agent's task plan with dependency tracking.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
agent_id: ID of the agent who owns this plan
|
|
70
|
+
tasks: List of tasks in the plan
|
|
71
|
+
created_at: Timestamp when plan was created
|
|
72
|
+
updated_at: Timestamp when plan was last updated
|
|
73
|
+
"""
|
|
74
|
+
agent_id: str
|
|
75
|
+
tasks: List[Task] = field(default_factory=list)
|
|
76
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
77
|
+
updated_at: datetime = field(default_factory=datetime.now)
|
|
78
|
+
|
|
79
|
+
def __post_init__(self):
|
|
80
|
+
"""Initialize task index for fast lookups."""
|
|
81
|
+
self._task_index: Dict[str, Task] = {task.id: task for task in self.tasks}
|
|
82
|
+
|
|
83
|
+
def get_task(self, task_id: str) -> Optional[Task]:
|
|
84
|
+
"""
|
|
85
|
+
Get a task by ID.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
task_id: Task identifier
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Task if found, None otherwise
|
|
92
|
+
"""
|
|
93
|
+
return self._task_index.get(task_id)
|
|
94
|
+
|
|
95
|
+
def can_start_task(self, task_id: str) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Check if a task can be started (all dependencies completed).
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
task_id: Task to check
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if all dependencies are completed, False otherwise
|
|
104
|
+
"""
|
|
105
|
+
task = self.get_task(task_id)
|
|
106
|
+
if not task:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
for dep_id in task.dependencies:
|
|
110
|
+
dep_task = self.get_task(dep_id)
|
|
111
|
+
if not dep_task or dep_task.status != "completed":
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
def get_ready_tasks(self) -> List[Task]:
|
|
117
|
+
"""
|
|
118
|
+
Get all tasks ready to start (pending with satisfied dependencies).
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of tasks with status='pending' and all dependencies completed
|
|
122
|
+
"""
|
|
123
|
+
return [
|
|
124
|
+
task for task in self.tasks
|
|
125
|
+
if task.status == "pending" and self.can_start_task(task.id)
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
def get_blocked_tasks(self) -> List[Task]:
|
|
129
|
+
"""
|
|
130
|
+
Get all tasks blocked by dependencies.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of tasks with status='pending' but dependencies not completed,
|
|
134
|
+
including information about what each task is waiting on
|
|
135
|
+
"""
|
|
136
|
+
blocked = []
|
|
137
|
+
for task in self.tasks:
|
|
138
|
+
if task.status == "pending" and not self.can_start_task(task.id):
|
|
139
|
+
blocked.append(task)
|
|
140
|
+
return blocked
|
|
141
|
+
|
|
142
|
+
def get_blocking_tasks(self, task_id: str) -> List[str]:
|
|
143
|
+
"""
|
|
144
|
+
Get list of incomplete dependency task IDs blocking a task.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
task_id: Task to check
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
List of task IDs that are blocking this task
|
|
151
|
+
"""
|
|
152
|
+
task = self.get_task(task_id)
|
|
153
|
+
if not task:
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
blocking = []
|
|
157
|
+
for dep_id in task.dependencies:
|
|
158
|
+
dep_task = self.get_task(dep_id)
|
|
159
|
+
if dep_task and dep_task.status != "completed":
|
|
160
|
+
blocking.append(dep_id)
|
|
161
|
+
|
|
162
|
+
return blocking
|
|
163
|
+
|
|
164
|
+
def add_task(
|
|
165
|
+
self,
|
|
166
|
+
description: str,
|
|
167
|
+
task_id: Optional[str] = None,
|
|
168
|
+
after_task_id: Optional[str] = None,
|
|
169
|
+
depends_on: Optional[List[str]] = None
|
|
170
|
+
) -> Task:
|
|
171
|
+
"""
|
|
172
|
+
Add a new task to the plan.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
description: Task description
|
|
176
|
+
task_id: Optional custom task ID (generates UUID if not provided)
|
|
177
|
+
after_task_id: Optional ID to insert after (otherwise appends)
|
|
178
|
+
depends_on: Optional list of task IDs this task depends on
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
The newly created task
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
ValueError: If dependencies are invalid or circular
|
|
185
|
+
"""
|
|
186
|
+
# Generate ID if not provided
|
|
187
|
+
if not task_id:
|
|
188
|
+
task_id = str(uuid.uuid4())
|
|
189
|
+
|
|
190
|
+
# Validate task ID is unique
|
|
191
|
+
if task_id in self._task_index:
|
|
192
|
+
raise ValueError(f"Task ID already exists: {task_id}")
|
|
193
|
+
|
|
194
|
+
# Validate dependencies exist
|
|
195
|
+
if depends_on:
|
|
196
|
+
for dep_id in depends_on:
|
|
197
|
+
if dep_id not in self._task_index:
|
|
198
|
+
raise ValueError(f"Dependency task does not exist: {dep_id}")
|
|
199
|
+
|
|
200
|
+
# Create task
|
|
201
|
+
task = Task(
|
|
202
|
+
id=task_id,
|
|
203
|
+
description=description,
|
|
204
|
+
dependencies=depends_on or [],
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Check for circular dependencies before adding
|
|
208
|
+
temp_tasks = self.tasks + [task]
|
|
209
|
+
if self._has_circular_dependency(task_id, temp_tasks):
|
|
210
|
+
raise ValueError(f"Circular dependency detected for task: {task_id}")
|
|
211
|
+
|
|
212
|
+
# Add to plan
|
|
213
|
+
if after_task_id:
|
|
214
|
+
# Find position and insert
|
|
215
|
+
for i, t in enumerate(self.tasks):
|
|
216
|
+
if t.id == after_task_id:
|
|
217
|
+
self.tasks.insert(i + 1, task)
|
|
218
|
+
break
|
|
219
|
+
else:
|
|
220
|
+
raise ValueError(f"after_task_id not found: {after_task_id}")
|
|
221
|
+
else:
|
|
222
|
+
# Append to end
|
|
223
|
+
self.tasks.append(task)
|
|
224
|
+
|
|
225
|
+
# Update index and timestamp
|
|
226
|
+
self._task_index[task_id] = task
|
|
227
|
+
self.updated_at = datetime.now()
|
|
228
|
+
|
|
229
|
+
return task
|
|
230
|
+
|
|
231
|
+
def update_task_status(
|
|
232
|
+
self,
|
|
233
|
+
task_id: str,
|
|
234
|
+
status: Literal["pending", "in_progress", "completed", "blocked"]
|
|
235
|
+
) -> Dict[str, Any]:
|
|
236
|
+
"""
|
|
237
|
+
Update task status and detect newly unblocked tasks.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
task_id: ID of task to update
|
|
241
|
+
status: New status
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Dictionary with updated task and newly_ready_tasks
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
ValueError: If task not found
|
|
248
|
+
"""
|
|
249
|
+
task = self.get_task(task_id)
|
|
250
|
+
if not task:
|
|
251
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
252
|
+
|
|
253
|
+
task.status = status
|
|
254
|
+
self.updated_at = datetime.now()
|
|
255
|
+
|
|
256
|
+
if status == "completed":
|
|
257
|
+
task.completed_at = datetime.now()
|
|
258
|
+
|
|
259
|
+
# Find newly ready tasks
|
|
260
|
+
newly_ready = []
|
|
261
|
+
for other_task in self.tasks:
|
|
262
|
+
if (other_task.status == "pending" and
|
|
263
|
+
task_id in other_task.dependencies and
|
|
264
|
+
self.can_start_task(other_task.id)):
|
|
265
|
+
newly_ready.append(other_task)
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"task": task.to_dict(),
|
|
269
|
+
"newly_ready_tasks": [t.to_dict() for t in newly_ready]
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {"task": task.to_dict()}
|
|
273
|
+
|
|
274
|
+
def edit_task(self, task_id: str, description: Optional[str] = None) -> Task:
|
|
275
|
+
"""
|
|
276
|
+
Edit a task's description.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
task_id: ID of task to edit
|
|
280
|
+
description: New description (if provided)
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Updated task
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
ValueError: If task not found
|
|
287
|
+
"""
|
|
288
|
+
task = self.get_task(task_id)
|
|
289
|
+
if not task:
|
|
290
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
291
|
+
|
|
292
|
+
if description is not None:
|
|
293
|
+
task.description = description
|
|
294
|
+
|
|
295
|
+
self.updated_at = datetime.now()
|
|
296
|
+
return task
|
|
297
|
+
|
|
298
|
+
def delete_task(self, task_id: str) -> None:
|
|
299
|
+
"""
|
|
300
|
+
Remove a task from the plan.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
task_id: ID of task to delete
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
ValueError: If task not found or other tasks depend on it
|
|
307
|
+
"""
|
|
308
|
+
task = self.get_task(task_id)
|
|
309
|
+
if not task:
|
|
310
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
311
|
+
|
|
312
|
+
# Check if any tasks depend on this one
|
|
313
|
+
for other_task in self.tasks:
|
|
314
|
+
if task_id in other_task.dependencies:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
f"Cannot delete task {task_id}: task {other_task.id} depends on it"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Remove from list and index
|
|
320
|
+
self.tasks.remove(task)
|
|
321
|
+
del self._task_index[task_id]
|
|
322
|
+
self.updated_at = datetime.now()
|
|
323
|
+
|
|
324
|
+
def validate_dependencies(self, task_list: List[Dict[str, Any]]) -> None:
|
|
325
|
+
"""
|
|
326
|
+
Validate dependencies when creating a task plan.
|
|
327
|
+
|
|
328
|
+
Checks:
|
|
329
|
+
- Dependencies reference valid tasks (earlier in list or by valid ID)
|
|
330
|
+
- No circular dependencies
|
|
331
|
+
- No self-references
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
task_list: List of task dictionaries with potential dependencies
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
ValueError: If validation fails
|
|
338
|
+
"""
|
|
339
|
+
# Build ID mapping
|
|
340
|
+
task_ids = set()
|
|
341
|
+
for i, task_spec in enumerate(task_list):
|
|
342
|
+
if isinstance(task_spec, dict) and "id" in task_spec:
|
|
343
|
+
task_id = task_spec["id"]
|
|
344
|
+
else:
|
|
345
|
+
task_id = f"task_{i}"
|
|
346
|
+
task_ids.add(task_id)
|
|
347
|
+
|
|
348
|
+
# Validate each task's dependencies
|
|
349
|
+
for i, task_spec in enumerate(task_list):
|
|
350
|
+
if isinstance(task_spec, dict):
|
|
351
|
+
task_id = task_spec.get("id", f"task_{i}")
|
|
352
|
+
depends_on = task_spec.get("depends_on", [])
|
|
353
|
+
|
|
354
|
+
if depends_on:
|
|
355
|
+
for dep in depends_on:
|
|
356
|
+
# Handle index-based dependency
|
|
357
|
+
if isinstance(dep, int):
|
|
358
|
+
if dep < 0 or dep >= len(task_list):
|
|
359
|
+
raise ValueError(
|
|
360
|
+
f"Task {task_id}: Invalid dependency index {dep}"
|
|
361
|
+
)
|
|
362
|
+
if dep >= i:
|
|
363
|
+
raise ValueError(
|
|
364
|
+
f"Task {task_id}: Dependencies must reference earlier tasks (index {dep} >= {i})"
|
|
365
|
+
)
|
|
366
|
+
# Handle ID-based dependency
|
|
367
|
+
else:
|
|
368
|
+
if dep not in task_ids:
|
|
369
|
+
raise ValueError(
|
|
370
|
+
f"Task {task_id}: Dependency {dep} not found in task list"
|
|
371
|
+
)
|
|
372
|
+
if dep == task_id:
|
|
373
|
+
raise ValueError(
|
|
374
|
+
f"Task {task_id}: Self-dependency detected"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def _has_circular_dependency(self, task_id: str, all_tasks: List[Task]) -> bool:
|
|
378
|
+
"""
|
|
379
|
+
Check if adding a task would create a circular dependency.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
task_id: ID of task to check
|
|
383
|
+
all_tasks: All tasks including the new one
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
True if circular dependency detected, False otherwise
|
|
387
|
+
"""
|
|
388
|
+
# Build dependency graph
|
|
389
|
+
task_map = {t.id: t for t in all_tasks}
|
|
390
|
+
|
|
391
|
+
# DFS to detect cycles
|
|
392
|
+
visited = set()
|
|
393
|
+
rec_stack = set()
|
|
394
|
+
|
|
395
|
+
def has_cycle(tid: str) -> bool:
|
|
396
|
+
if tid in rec_stack:
|
|
397
|
+
return True
|
|
398
|
+
if tid in visited:
|
|
399
|
+
return False
|
|
400
|
+
|
|
401
|
+
visited.add(tid)
|
|
402
|
+
rec_stack.add(tid)
|
|
403
|
+
|
|
404
|
+
task = task_map.get(tid)
|
|
405
|
+
if task:
|
|
406
|
+
for dep_id in task.dependencies:
|
|
407
|
+
if has_cycle(dep_id):
|
|
408
|
+
return True
|
|
409
|
+
|
|
410
|
+
rec_stack.remove(tid)
|
|
411
|
+
return False
|
|
412
|
+
|
|
413
|
+
return has_cycle(task_id)
|
|
414
|
+
|
|
415
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
416
|
+
"""Convert plan to dictionary for serialization."""
|
|
417
|
+
return {
|
|
418
|
+
"agent_id": self.agent_id,
|
|
419
|
+
"tasks": [task.to_dict() for task in self.tasks],
|
|
420
|
+
"created_at": self.created_at.isoformat(),
|
|
421
|
+
"updated_at": self.updated_at.isoformat(),
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
@classmethod
|
|
425
|
+
def from_dict(cls, data: Dict[str, Any]) -> "TaskPlan":
|
|
426
|
+
"""Create plan from dictionary."""
|
|
427
|
+
tasks = [Task.from_dict(t) for t in data.get("tasks", [])]
|
|
428
|
+
plan = cls(
|
|
429
|
+
agent_id=data["agent_id"],
|
|
430
|
+
tasks=tasks,
|
|
431
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
432
|
+
updated_at=datetime.fromisoformat(data["updated_at"]),
|
|
433
|
+
)
|
|
434
|
+
return plan
|