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.
- noesium/core/__init__.py +4 -0
- noesium/core/agent/__init__.py +14 -0
- noesium/core/agent/base.py +227 -0
- noesium/core/consts.py +6 -0
- noesium/core/goalith/conflict/conflict.py +104 -0
- noesium/core/goalith/conflict/detector.py +53 -0
- noesium/core/goalith/decomposer/__init__.py +6 -0
- noesium/core/goalith/decomposer/base.py +46 -0
- noesium/core/goalith/decomposer/callable_decomposer.py +65 -0
- noesium/core/goalith/decomposer/llm_decomposer.py +326 -0
- noesium/core/goalith/decomposer/prompts.py +140 -0
- noesium/core/goalith/decomposer/simple_decomposer.py +61 -0
- noesium/core/goalith/errors.py +22 -0
- noesium/core/goalith/goalgraph/graph.py +526 -0
- noesium/core/goalith/goalgraph/node.py +179 -0
- noesium/core/goalith/replanner/base.py +31 -0
- noesium/core/goalith/replanner/replanner.py +36 -0
- noesium/core/goalith/service.py +26 -0
- noesium/core/llm/__init__.py +154 -0
- noesium/core/llm/base.py +152 -0
- noesium/core/llm/litellm.py +528 -0
- noesium/core/llm/llamacpp.py +487 -0
- noesium/core/llm/message.py +184 -0
- noesium/core/llm/ollama.py +459 -0
- noesium/core/llm/openai.py +520 -0
- noesium/core/llm/openrouter.py +89 -0
- noesium/core/llm/prompt.py +551 -0
- noesium/core/memory/__init__.py +11 -0
- noesium/core/memory/base.py +464 -0
- noesium/core/memory/memu/__init__.py +24 -0
- noesium/core/memory/memu/config/__init__.py +26 -0
- noesium/core/memory/memu/config/activity/config.py +46 -0
- noesium/core/memory/memu/config/event/config.py +46 -0
- noesium/core/memory/memu/config/markdown_config.py +241 -0
- noesium/core/memory/memu/config/profile/config.py +48 -0
- noesium/core/memory/memu/llm_adapter.py +129 -0
- noesium/core/memory/memu/memory/__init__.py +31 -0
- noesium/core/memory/memu/memory/actions/__init__.py +40 -0
- noesium/core/memory/memu/memory/actions/add_activity_memory.py +299 -0
- noesium/core/memory/memu/memory/actions/base_action.py +342 -0
- noesium/core/memory/memu/memory/actions/cluster_memories.py +262 -0
- noesium/core/memory/memu/memory/actions/generate_suggestions.py +198 -0
- noesium/core/memory/memu/memory/actions/get_available_categories.py +66 -0
- noesium/core/memory/memu/memory/actions/link_related_memories.py +515 -0
- noesium/core/memory/memu/memory/actions/run_theory_of_mind.py +254 -0
- noesium/core/memory/memu/memory/actions/update_memory_with_suggestions.py +514 -0
- noesium/core/memory/memu/memory/embeddings.py +130 -0
- noesium/core/memory/memu/memory/file_manager.py +306 -0
- noesium/core/memory/memu/memory/memory_agent.py +578 -0
- noesium/core/memory/memu/memory/recall_agent.py +376 -0
- noesium/core/memory/memu/memory_store.py +628 -0
- noesium/core/memory/models.py +149 -0
- noesium/core/msgbus/__init__.py +12 -0
- noesium/core/msgbus/base.py +395 -0
- noesium/core/orchestrix/__init__.py +0 -0
- noesium/core/py.typed +0 -0
- noesium/core/routing/__init__.py +20 -0
- noesium/core/routing/base.py +66 -0
- noesium/core/routing/router.py +241 -0
- noesium/core/routing/strategies/__init__.py +9 -0
- noesium/core/routing/strategies/dynamic_complexity.py +361 -0
- noesium/core/routing/strategies/self_assessment.py +147 -0
- noesium/core/routing/types.py +38 -0
- noesium/core/toolify/__init__.py +39 -0
- noesium/core/toolify/base.py +360 -0
- noesium/core/toolify/config.py +138 -0
- noesium/core/toolify/mcp_integration.py +275 -0
- noesium/core/toolify/registry.py +214 -0
- noesium/core/toolify/toolkits/__init__.py +1 -0
- noesium/core/tracing/__init__.py +37 -0
- noesium/core/tracing/langgraph_hooks.py +308 -0
- noesium/core/tracing/opik_tracing.py +144 -0
- noesium/core/tracing/token_tracker.py +166 -0
- noesium/core/utils/__init__.py +10 -0
- noesium/core/utils/logging.py +172 -0
- noesium/core/utils/statistics.py +12 -0
- noesium/core/utils/typing.py +17 -0
- noesium/core/vector_store/__init__.py +79 -0
- noesium/core/vector_store/base.py +94 -0
- noesium/core/vector_store/pgvector.py +304 -0
- noesium/core/vector_store/weaviate.py +383 -0
- noesium-0.1.0.dist-info/METADATA +525 -0
- noesium-0.1.0.dist-info/RECORD +86 -0
- noesium-0.1.0.dist-info/WHEEL +5 -0
- noesium-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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."""
|