noesium 0.1.0__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.
Files changed (86) hide show
  1. noesium/core/__init__.py +4 -0
  2. noesium/core/agent/__init__.py +14 -0
  3. noesium/core/agent/base.py +227 -0
  4. noesium/core/consts.py +6 -0
  5. noesium/core/goalith/conflict/conflict.py +104 -0
  6. noesium/core/goalith/conflict/detector.py +53 -0
  7. noesium/core/goalith/decomposer/__init__.py +6 -0
  8. noesium/core/goalith/decomposer/base.py +46 -0
  9. noesium/core/goalith/decomposer/callable_decomposer.py +65 -0
  10. noesium/core/goalith/decomposer/llm_decomposer.py +326 -0
  11. noesium/core/goalith/decomposer/prompts.py +140 -0
  12. noesium/core/goalith/decomposer/simple_decomposer.py +61 -0
  13. noesium/core/goalith/errors.py +22 -0
  14. noesium/core/goalith/goalgraph/graph.py +526 -0
  15. noesium/core/goalith/goalgraph/node.py +179 -0
  16. noesium/core/goalith/replanner/base.py +31 -0
  17. noesium/core/goalith/replanner/replanner.py +36 -0
  18. noesium/core/goalith/service.py +26 -0
  19. noesium/core/llm/__init__.py +154 -0
  20. noesium/core/llm/base.py +152 -0
  21. noesium/core/llm/litellm.py +528 -0
  22. noesium/core/llm/llamacpp.py +487 -0
  23. noesium/core/llm/message.py +184 -0
  24. noesium/core/llm/ollama.py +459 -0
  25. noesium/core/llm/openai.py +520 -0
  26. noesium/core/llm/openrouter.py +89 -0
  27. noesium/core/llm/prompt.py +551 -0
  28. noesium/core/memory/__init__.py +11 -0
  29. noesium/core/memory/base.py +464 -0
  30. noesium/core/memory/memu/__init__.py +24 -0
  31. noesium/core/memory/memu/config/__init__.py +26 -0
  32. noesium/core/memory/memu/config/activity/config.py +46 -0
  33. noesium/core/memory/memu/config/event/config.py +46 -0
  34. noesium/core/memory/memu/config/markdown_config.py +241 -0
  35. noesium/core/memory/memu/config/profile/config.py +48 -0
  36. noesium/core/memory/memu/llm_adapter.py +129 -0
  37. noesium/core/memory/memu/memory/__init__.py +31 -0
  38. noesium/core/memory/memu/memory/actions/__init__.py +40 -0
  39. noesium/core/memory/memu/memory/actions/add_activity_memory.py +299 -0
  40. noesium/core/memory/memu/memory/actions/base_action.py +342 -0
  41. noesium/core/memory/memu/memory/actions/cluster_memories.py +262 -0
  42. noesium/core/memory/memu/memory/actions/generate_suggestions.py +198 -0
  43. noesium/core/memory/memu/memory/actions/get_available_categories.py +66 -0
  44. noesium/core/memory/memu/memory/actions/link_related_memories.py +515 -0
  45. noesium/core/memory/memu/memory/actions/run_theory_of_mind.py +254 -0
  46. noesium/core/memory/memu/memory/actions/update_memory_with_suggestions.py +514 -0
  47. noesium/core/memory/memu/memory/embeddings.py +130 -0
  48. noesium/core/memory/memu/memory/file_manager.py +306 -0
  49. noesium/core/memory/memu/memory/memory_agent.py +578 -0
  50. noesium/core/memory/memu/memory/recall_agent.py +376 -0
  51. noesium/core/memory/memu/memory_store.py +628 -0
  52. noesium/core/memory/models.py +149 -0
  53. noesium/core/msgbus/__init__.py +12 -0
  54. noesium/core/msgbus/base.py +395 -0
  55. noesium/core/orchestrix/__init__.py +0 -0
  56. noesium/core/py.typed +0 -0
  57. noesium/core/routing/__init__.py +20 -0
  58. noesium/core/routing/base.py +66 -0
  59. noesium/core/routing/router.py +241 -0
  60. noesium/core/routing/strategies/__init__.py +9 -0
  61. noesium/core/routing/strategies/dynamic_complexity.py +361 -0
  62. noesium/core/routing/strategies/self_assessment.py +147 -0
  63. noesium/core/routing/types.py +38 -0
  64. noesium/core/toolify/__init__.py +39 -0
  65. noesium/core/toolify/base.py +360 -0
  66. noesium/core/toolify/config.py +138 -0
  67. noesium/core/toolify/mcp_integration.py +275 -0
  68. noesium/core/toolify/registry.py +214 -0
  69. noesium/core/toolify/toolkits/__init__.py +1 -0
  70. noesium/core/tracing/__init__.py +37 -0
  71. noesium/core/tracing/langgraph_hooks.py +308 -0
  72. noesium/core/tracing/opik_tracing.py +144 -0
  73. noesium/core/tracing/token_tracker.py +166 -0
  74. noesium/core/utils/__init__.py +10 -0
  75. noesium/core/utils/logging.py +172 -0
  76. noesium/core/utils/statistics.py +12 -0
  77. noesium/core/utils/typing.py +17 -0
  78. noesium/core/vector_store/__init__.py +79 -0
  79. noesium/core/vector_store/base.py +94 -0
  80. noesium/core/vector_store/pgvector.py +304 -0
  81. noesium/core/vector_store/weaviate.py +383 -0
  82. noesium-0.1.0.dist-info/METADATA +525 -0
  83. noesium-0.1.0.dist-info/RECORD +86 -0
  84. noesium-0.1.0.dist-info/WHEEL +5 -0
  85. noesium-0.1.0.dist-info/licenses/LICENSE +21 -0
  86. noesium-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,326 @@
1
+ """
2
+ LLM-based goal decomposer for the GoalithService.
3
+
4
+ Provides structured goal decomposition using LLM clients with instructor integration.
5
+ """
6
+
7
+ import os
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ from noesium.core.goalith.errors import DecompositionError
13
+ from noesium.core.goalith.goalgraph.node import GoalNode
14
+ from noesium.core.llm import get_llm_client
15
+ from noesium.core.utils.logging import get_logger
16
+
17
+ from .base import GoalDecomposer
18
+ from .prompts import get_decomposition_system_prompt, get_decomposition_user_prompt, get_fallback_prompt
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ class SubgoalSpec(BaseModel):
24
+ """Specification for a single subgoal or task."""
25
+
26
+ description: str = Field(description="Clear, actionable description of the subgoal or task")
27
+ context: Optional[str] = Field(description="Additional notes or context about this subgoal", default=None)
28
+ priority: float = Field(
29
+ description="Priority score between 0.0 and 10.0 (higher = more important)",
30
+ ge=0.0,
31
+ le=10.0,
32
+ )
33
+ estimated_effort: Optional[str] = Field(
34
+ description="Estimated effort or duration (e.g., '2 hours', '3 days', 'low', 'medium', 'high')",
35
+ default=None,
36
+ )
37
+ dependencies: List[str] = Field(
38
+ description="List of descriptions of other subgoals this depends on (will be matched by description)",
39
+ default_factory=list,
40
+ )
41
+ tags: List[str] = Field(
42
+ description="Tags for categorization (e.g., 'research', 'planning', 'execution')",
43
+ default_factory=list,
44
+ )
45
+
46
+
47
+ class GoalDecomposition(BaseModel):
48
+ """Complete decomposition of a goal into subgoals and tasks."""
49
+
50
+ reasoning: str = Field(description="Explanation of the decomposition approach and rationale")
51
+ subgoals: List[SubgoalSpec] = Field(
52
+ description="List of subgoals and tasks, in logical order (maximum 6 items for focus and manageability)",
53
+ min_length=1,
54
+ max_length=6,
55
+ )
56
+ success_criteria: List[str] = Field(
57
+ description="Criteria that indicate successful completion of the overall goal",
58
+ default_factory=list,
59
+ )
60
+ potential_risks: List[str] = Field(
61
+ description="Potential risks or challenges in executing this plan",
62
+ default_factory=list,
63
+ )
64
+ estimated_timeline: Optional[str] = Field(
65
+ description="Overall estimated timeline for goal completion", default=None
66
+ )
67
+ confidence: float = Field(description="Confidence in this decomposition (0.0 to 1.0)", ge=0.0, le=1.0)
68
+
69
+
70
+ class LLMDecomposer(GoalDecomposer):
71
+ """
72
+ Enhanced LLM-based goal decomposer using structured completion.
73
+
74
+ Uses the project's LLM infrastructure with instructor for structured output
75
+ to decompose goals into subgoals.
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ provider: str = os.getenv("COGENTS_LLM_PROVIDER", "openrouter"),
81
+ model_name: Optional[str] = None,
82
+ temperature: float = 0.3,
83
+ max_tokens: int = 2000,
84
+ name: str = "llm_decomposer",
85
+ ):
86
+ """
87
+ Initialize LLM decomposer.
88
+
89
+ Args:
90
+ model_name: LLM model to use (uses default if None)
91
+ temperature: Sampling temperature for LLM
92
+ max_tokens: Maximum tokens for response
93
+ name: Name of this decomposer
94
+ """
95
+ self._provider = provider
96
+ self._model_name = model_name
97
+ self._temperature = temperature
98
+ self._max_tokens = max_tokens
99
+ self._name = name
100
+ self._llm_client = None # Lazy initialization
101
+
102
+ @property
103
+ def name(self) -> str:
104
+ """Get the name of this decomposer."""
105
+ return self._name
106
+
107
+ @property
108
+ def llm_client(self):
109
+ """Lazily initialize and return the LLM client."""
110
+ if self._llm_client is None:
111
+ self._llm_client = get_llm_client(provider=self._provider, chat_model=self._model_name)
112
+ return self._llm_client
113
+
114
+ def decompose(self, goal_node: GoalNode, context: Optional[Dict[str, Any]] = None) -> List[GoalNode]:
115
+ """
116
+ Decompose a goal using LLM structured completion.
117
+
118
+ Args:
119
+ goal_node: The goal node to decompose
120
+ context: Optional context for decomposition
121
+
122
+ Returns:
123
+ List of subgoal nodes
124
+
125
+ Raises:
126
+ DecompositionError: If decomposition fails
127
+ """
128
+ try:
129
+ logger.info(f"Decomposing goal: {goal_node.description}")
130
+
131
+ # Build the decomposition prompts using the new structure
132
+ system_prompt = get_decomposition_system_prompt()
133
+ user_prompt = get_decomposition_user_prompt(
134
+ goal_node=goal_node,
135
+ context=context,
136
+ )
137
+
138
+ # Get structured decomposition from LLM with system and user messages
139
+ messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}]
140
+
141
+ decomposition: GoalDecomposition = self.llm_client.structured_completion(
142
+ messages=messages,
143
+ response_model=GoalDecomposition,
144
+ temperature=self._temperature,
145
+ max_tokens=self._max_tokens,
146
+ )
147
+
148
+ logger.info(f"LLM decomposition completed with {len(decomposition.subgoals)} subgoals")
149
+ logger.debug(f"Decomposition reasoning: {decomposition.reasoning}")
150
+
151
+ # Convert to GoalNode objects
152
+ subgoal_nodes = self._convert_to_goal_nodes(decomposition, goal_node, context)
153
+
154
+ # Set up dependencies
155
+ self._setup_dependencies(subgoal_nodes, decomposition.subgoals)
156
+
157
+ # Add decomposition metadata to parent goal
158
+ goal_node.update_context(
159
+ "llm_decomposition",
160
+ {
161
+ "reasoning": decomposition.reasoning,
162
+ "strategy": decomposition.decomposition_strategy,
163
+ "success_criteria": decomposition.success_criteria,
164
+ "potential_risks": decomposition.potential_risks,
165
+ "estimated_timeline": decomposition.estimated_timeline,
166
+ "confidence": decomposition.confidence,
167
+ "subgoal_count": len(decomposition.subgoals),
168
+ },
169
+ )
170
+
171
+ return subgoal_nodes
172
+
173
+ except Exception as e:
174
+ logger.warning(f"Structured LLM decomposition failed for goal {goal_node.id}: {e}")
175
+ logger.info("Attempting fallback decomposition...")
176
+
177
+ # Try fallback decomposition with simpler prompt
178
+ try:
179
+ return self._fallback_decomposition(goal_node, context)
180
+ except Exception as fallback_error:
181
+ logger.error(f"Fallback decomposition also failed: {fallback_error}")
182
+ raise DecompositionError(f"LLM decomposition failed: {e}")
183
+
184
+ def _fallback_decomposition(self, goal_node: GoalNode, context: Optional[Dict[str, Any]] = None) -> List[GoalNode]:
185
+ """
186
+ Fallback decomposition method when structured completion fails.
187
+ Uses a simpler prompt and attempts to parse the response manually.
188
+ """
189
+ fallback_prompt = get_fallback_prompt()
190
+ user_prompt = get_decomposition_user_prompt(
191
+ goal_node=goal_node,
192
+ context=context,
193
+ )
194
+
195
+ # Combine prompts for fallback
196
+ combined_prompt = f"{fallback_prompt}\n\n{user_prompt}"
197
+
198
+ # Get simple text response
199
+ messages = [{"role": "user", "content": combined_prompt}]
200
+ response = self.llm_client.completion(
201
+ messages=messages,
202
+ temperature=self._temperature,
203
+ max_tokens=self._max_tokens,
204
+ )
205
+
206
+ # Create simplified subgoals from the response
207
+ # This is a basic implementation - in practice, you might want more sophisticated parsing
208
+ lines = response.split("\n")
209
+ subgoals = []
210
+
211
+ for i, line in enumerate(lines[:6]): # Limit to 6 items max
212
+ line = line.strip()
213
+ if line and len(line) > 10: # Basic filtering
214
+ # Create a simple subgoal
215
+ node = GoalNode(
216
+ description=line.lstrip("1234567890.-• "), # Remove numbering
217
+ priority=5.0, # Default priority
218
+ parent=goal_node.id,
219
+ context={
220
+ "llm_generated": True,
221
+ "fallback_mode": True,
222
+ "parent_goal_id": goal_node.id,
223
+ },
224
+ decomposer_name=self.name,
225
+ )
226
+ subgoals.append(node)
227
+
228
+ logger.info(f"Fallback decomposition created {len(subgoals)} subgoals")
229
+ return subgoals
230
+
231
+ def _convert_to_goal_nodes(
232
+ self,
233
+ decomposition: GoalDecomposition,
234
+ parent_goal: GoalNode,
235
+ context: Optional[Dict[str, Any]] = None,
236
+ ) -> List[GoalNode]:
237
+ """Convert LLM decomposition to GoalNode objects."""
238
+
239
+ nodes = []
240
+
241
+ for spec in decomposition.subgoals:
242
+ # Create node context
243
+ node_context = {}
244
+ if context:
245
+ node_context.update(context)
246
+
247
+ # Add LLM-specific context
248
+ node_context.update(
249
+ {
250
+ "llm_generated": True,
251
+ "decomposition_strategy": decomposition.decomposition_strategy,
252
+ "estimated_effort": spec.estimated_effort,
253
+ "context": spec.context,
254
+ "parent_goal_id": parent_goal.id,
255
+ }
256
+ )
257
+
258
+ # Create the node
259
+ node = GoalNode(
260
+ description=spec.description,
261
+ priority=spec.priority,
262
+ parent=parent_goal.id,
263
+ context=node_context,
264
+ tags=set(spec.tags),
265
+ decomposer_name=self.name,
266
+ )
267
+
268
+ # Copy deadline from parent if not specified and it's a task
269
+ if parent_goal.deadline:
270
+ node.deadline = parent_goal.deadline
271
+
272
+ nodes.append(node)
273
+
274
+ return nodes
275
+
276
+ def _setup_dependencies(self, nodes: List[GoalNode], specs: List[SubgoalSpec]) -> None:
277
+ """Set up dependencies between nodes based on LLM specifications."""
278
+
279
+ # Create a mapping from description to node ID
280
+ desc_to_id = {node.description: node.id for node in nodes}
281
+
282
+ for i, spec in enumerate(specs):
283
+ if not spec.dependencies:
284
+ continue
285
+
286
+ current_node = nodes[i]
287
+
288
+ for dep_desc in spec.dependencies:
289
+ # Find the dependency by description (fuzzy matching)
290
+ dep_node_id = self._find_dependency_by_description(dep_desc, desc_to_id)
291
+
292
+ if dep_node_id:
293
+ current_node.add_dependency(dep_node_id)
294
+ logger.debug(f"Added dependency: {dep_node_id} -> {current_node.id}")
295
+ else:
296
+ logger.warning(f"Could not find dependency '{dep_desc}' for node '{current_node.description}'")
297
+
298
+ def _find_dependency_by_description(self, dep_desc: str, desc_to_id: Dict[str, str]) -> Optional[str]:
299
+ """Find a dependency node by description with fuzzy matching."""
300
+
301
+ dep_desc_lower = dep_desc.lower().strip()
302
+
303
+ # Exact match first
304
+ for desc, node_id in desc_to_id.items():
305
+ if desc.lower().strip() == dep_desc_lower:
306
+ return node_id
307
+
308
+ # Partial match (dependency description is contained in node description)
309
+ for desc, node_id in desc_to_id.items():
310
+ if dep_desc_lower in desc.lower() or desc.lower() in dep_desc_lower:
311
+ return node_id
312
+
313
+ # Word overlap matching
314
+ dep_words = set(dep_desc_lower.split())
315
+ best_match = None
316
+ best_overlap = 0
317
+
318
+ for desc, node_id in desc_to_id.items():
319
+ desc_words = set(desc.lower().split())
320
+ overlap = len(dep_words & desc_words)
321
+
322
+ if overlap > best_overlap and overlap >= 2: # At least 2 words overlap
323
+ best_match = node_id
324
+ best_overlap = overlap
325
+
326
+ return best_match
@@ -0,0 +1,140 @@
1
+ """
2
+ Prompts for the LLM-based goal decomposer.
3
+
4
+ Separated into system and user messages for easier maintenance and optimization.
5
+ """
6
+
7
+ from typing import Any, Dict, Optional
8
+
9
+ from noesium.core.goalith.goalgraph.node import GoalNode
10
+
11
+
12
+ def get_decomposition_system_prompt() -> str:
13
+ """
14
+ Get the system prompt for goal decomposition.
15
+
16
+ Returns:
17
+ System prompt that sets up the LLM as a goal decomposition expert
18
+ """
19
+ return """You are an expert goal decomposition assistant. Your role is to break down goals into actionable subgoals and tasks.
20
+
21
+ **KEY PRINCIPLES:**
22
+ 1. **Clarity**: Each subgoal should be clear, specific, and actionable
23
+ 2. **Limited Scope**: Create a maximum of 6 subgoals/tasks to maintain focus and avoid overwhelm
24
+ 3. **Appropriate Granularity**: Break down into manageable chunks (not too big, not too small)
25
+ 4. **Clear Dependencies**: Identify which subgoals depend on others
26
+ 5. **Realistic Effort**: Estimate effort accurately based on complexity
27
+
28
+ **DECOMPOSITION STRATEGIES:**
29
+ - **Sequential**: Tasks must be done in order (waterfall approach)
30
+ - **Parallel**: Tasks can be done simultaneously (parallel execution)
31
+ - **Hybrid**: Mix of sequential and parallel work streams
32
+ - **Milestone-based**: Organized around key milestones and deliverables
33
+
34
+ **PRIORITY SCORING (0.0-10.0):**
35
+ - 9-10: Critical/urgent - must be done first, blocks other work
36
+ - 7-8: High priority - important for success, should be done early
37
+ - 5-6: Medium priority - supports the goal, normal scheduling
38
+ - 3-4: Low priority - nice to have, can be done later
39
+ - 1-2: Optional - can be deferred or skipped if needed
40
+
41
+ **EFFORT ESTIMATION:**
42
+ Use specific terms like: "30 minutes", "2 hours", "half day", "1 day", "1 week"
43
+ Or qualitative terms: "low", "medium", "high", "very high"
44
+
45
+ **CONSTRAINTS:**
46
+ - Maximum 6 subgoals/tasks to ensure focus and manageability
47
+ - Each subgoal must be actionable and measurable
48
+ - Include realistic timelines and effort estimates
49
+ - Consider dependencies and sequencing
50
+ - Provide confidence score (0.0-1.0) for the decomposition
51
+
52
+ Your response must be a structured JSON following the GoalDecomposition schema."""
53
+
54
+
55
+ def get_decomposition_user_prompt(
56
+ goal_node: GoalNode,
57
+ context: Optional[Dict[str, Any]] = None,
58
+ ) -> str:
59
+ """
60
+ Generate the user prompt for goal decomposition.
61
+
62
+ Args:
63
+ goal_node: The goal node to decompose
64
+ context: Optional additional context for decomposition
65
+
66
+ Returns:
67
+ Formatted user prompt with goal details and context
68
+ """
69
+
70
+ prompt_parts = []
71
+
72
+ # Main goal information
73
+ prompt_parts.append("**GOAL TO DECOMPOSE:**")
74
+ prompt_parts.append(goal_node.description)
75
+ prompt_parts.append("")
76
+
77
+ # Goal details
78
+ prompt_parts.append("**GOAL DETAILS:**")
79
+ prompt_parts.append(f"- Priority: {goal_node.priority}")
80
+ prompt_parts.append(f"- Current Status: {goal_node.status}")
81
+
82
+ # Add deadline if present
83
+ if goal_node.deadline:
84
+ prompt_parts.append(f"- Deadline: {goal_node.deadline.isoformat()}")
85
+
86
+ # Add existing context
87
+ if goal_node.context:
88
+ prompt_parts.append("- Existing Context:")
89
+ for key, value in goal_node.context.items():
90
+ prompt_parts.append(f" - {key}: {value}")
91
+
92
+ # Add tags if present
93
+ if goal_node.tags:
94
+ prompt_parts.append(f"- Tags: {', '.join(goal_node.tags)}")
95
+
96
+ prompt_parts.append("")
97
+
98
+ # Add additional context
99
+ if context:
100
+ prompt_parts.append("**ADDITIONAL CONTEXT:**")
101
+ for key, value in context.items():
102
+ prompt_parts.append(f"- {key}: {value}")
103
+ prompt_parts.append("")
104
+
105
+ # Add specific requirements for this decomposition
106
+ prompt_parts.append("**DECOMPOSITION REQUIREMENTS:**")
107
+ prompt_parts.append("- Break down into maximum 6 subgoals/tasks (this is critical for focus)")
108
+ prompt_parts.append("- Each subgoal must be specific and actionable")
109
+ prompt_parts.append("- Assign realistic priorities and effort estimates")
110
+ prompt_parts.append("- Identify clear dependencies between subgoals")
111
+ prompt_parts.append("- Choose the most appropriate decomposition strategy")
112
+ prompt_parts.append("- Provide success criteria for the overall goal")
113
+ prompt_parts.append("- List potential risks and mitigation strategies")
114
+ prompt_parts.append("- Estimate realistic timeline for completion")
115
+ prompt_parts.append("- Include your confidence level (0.0-1.0) in this decomposition")
116
+ prompt_parts.append("")
117
+
118
+ prompt_parts.append(
119
+ "**IMPORTANT:** Limit your decomposition to a maximum of 6 subgoals/tasks. Quality over quantity - focus on the most essential steps needed to achieve the goal."
120
+ )
121
+
122
+ return "\n".join(prompt_parts)
123
+
124
+
125
+ def get_fallback_prompt() -> str:
126
+ """
127
+ Get a fallback prompt when structured generation fails.
128
+
129
+ Returns:
130
+ Simple fallback prompt for basic decomposition
131
+ """
132
+ return """Please break down the given goal into a maximum of 6 actionable subgoals or tasks.
133
+
134
+ For each item, provide:
135
+ 1. Clear description of what needs to be done
136
+ 2. Priority (1-10 scale)
137
+ 3. Estimated effort
138
+ 4. Any dependencies
139
+
140
+ Focus on the most essential steps needed to achieve the goal. Keep it practical and actionable."""
@@ -0,0 +1,61 @@
1
+ from typing import Any, Dict, List, Optional
2
+
3
+ from noesium.core.goalith.goalgraph.node import GoalNode
4
+
5
+ from .base import GoalDecomposer
6
+
7
+
8
+ class SimpleListDecomposer(GoalDecomposer):
9
+ """
10
+ Simple decomposer that takes a list of subtask descriptions.
11
+
12
+ Useful for manual decomposition or simple cases.
13
+ """
14
+
15
+ def __init__(self, subtasks: List[str], name: str = "simple_list"):
16
+ """
17
+ Initialize with a list of subtask descriptions.
18
+
19
+ Args:
20
+ subtasks: List of subtask descriptions
21
+ name: Name of this decomposer instance
22
+ """
23
+ self._subtasks = subtasks
24
+ self._name = name
25
+
26
+ @property
27
+ def name(self) -> str:
28
+ """Get the name of this decomposer."""
29
+ return self._name
30
+
31
+ def decompose(self, goal_node: GoalNode, context: Optional[Dict[str, Any]] = None) -> List[GoalNode]:
32
+ """
33
+ Decompose goal into the predefined subtasks.
34
+
35
+ Args:
36
+ goal_node: The goal node to decompose
37
+ context: Optional context (unused)
38
+
39
+ Returns:
40
+ List of task nodes
41
+ """
42
+ import copy
43
+
44
+ nodes = []
45
+ for i, subtask_desc in enumerate(self._subtasks):
46
+ # Deep copy the context to avoid shared references
47
+ context_copy = copy.deepcopy(goal_node.context) if goal_node.context else {}
48
+
49
+ task_node = GoalNode(
50
+ description=subtask_desc,
51
+ parent=goal_node.id,
52
+ priority=goal_node.priority,
53
+ context=context_copy,
54
+ tags=goal_node.tags.copy() if goal_node.tags else [],
55
+ estimated_effort=goal_node.estimated_effort,
56
+ assigned_to=goal_node.assigned_to,
57
+ decomposer_name=self.name,
58
+ )
59
+ nodes.append(task_node)
60
+
61
+ return nodes
@@ -0,0 +1,22 @@
1
+ """
2
+ Base classes and exceptions for the GoalithService.
3
+
4
+ This module contains the foundational classes and exceptions used across
5
+ the goalith_service module to avoid circular imports.
6
+ """
7
+
8
+
9
+ class DecompositionError(Exception):
10
+ """Raised when decomposition fails."""
11
+
12
+
13
+ class CycleDetectedError(Exception):
14
+ """Raised when a cycle is detected in the DAG."""
15
+
16
+
17
+ class NodeNotFoundError(Exception):
18
+ """Raised when a node is not found in the graph."""
19
+
20
+
21
+ class SchedulingError(Exception):
22
+ """Raised when scheduling operations fail."""