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.

Files changed (46) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/chat_agent.py +340 -20
  3. massgen/cli.py +326 -19
  4. massgen/configs/README.md +52 -10
  5. massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
  6. massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
  7. massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
  8. massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
  9. massgen/configs/memory/single_agent_compression_test.yaml +64 -0
  10. massgen/configs/tools/custom_tools/multimodal_tools/playwright_with_img_understanding.yaml +98 -0
  11. massgen/configs/tools/custom_tools/multimodal_tools/understand_video_example.yaml +54 -0
  12. massgen/memory/README.md +277 -0
  13. massgen/memory/__init__.py +26 -0
  14. massgen/memory/_base.py +193 -0
  15. massgen/memory/_compression.py +237 -0
  16. massgen/memory/_context_monitor.py +211 -0
  17. massgen/memory/_conversation.py +255 -0
  18. massgen/memory/_fact_extraction_prompts.py +333 -0
  19. massgen/memory/_mem0_adapters.py +257 -0
  20. massgen/memory/_persistent.py +687 -0
  21. massgen/memory/docker-compose.qdrant.yml +36 -0
  22. massgen/memory/docs/DESIGN.md +388 -0
  23. massgen/memory/docs/QUICKSTART.md +409 -0
  24. massgen/memory/docs/SUMMARY.md +319 -0
  25. massgen/memory/docs/agent_use_memory.md +408 -0
  26. massgen/memory/docs/orchestrator_use_memory.md +586 -0
  27. massgen/memory/examples.py +237 -0
  28. massgen/orchestrator.py +207 -7
  29. massgen/tests/memory/test_agent_compression.py +174 -0
  30. massgen/tests/memory/test_context_window_management.py +286 -0
  31. massgen/tests/memory/test_force_compression.py +154 -0
  32. massgen/tests/memory/test_simple_compression.py +147 -0
  33. massgen/tests/test_agent_memory.py +534 -0
  34. massgen/tests/test_conversation_memory.py +382 -0
  35. massgen/tests/test_orchestrator_memory.py +620 -0
  36. massgen/tests/test_persistent_memory.py +435 -0
  37. massgen/token_manager/token_manager.py +6 -0
  38. massgen/tools/__init__.py +8 -0
  39. massgen/tools/_planning_mcp_server.py +520 -0
  40. massgen/tools/planning_dataclasses.py +434 -0
  41. {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/METADATA +109 -76
  42. {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/RECORD +46 -12
  43. {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/WHEEL +0 -0
  44. {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/entry_points.txt +0 -0
  45. {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/licenses/LICENSE +0 -0
  46. {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