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,4 @@
1
+ from noesium.core.utils.logging import setup_logging
2
+
3
+ # Enable colorful logging by default for cogents
4
+ setup_logging(level="INFO", enable_colors=True)
@@ -0,0 +1,14 @@
1
+ from dotenv import load_dotenv
2
+
3
+ from .base import BaseAgent, BaseConversationAgent, BaseGraphicAgent, BaseResearcher, ResearchOutput
4
+
5
+ # Load environment variables
6
+ load_dotenv()
7
+
8
+ __all__ = [
9
+ "BaseAgent",
10
+ "BaseGraphicAgent",
11
+ "BaseResearcher",
12
+ "BaseConversationAgent",
13
+ "ResearchOutput",
14
+ ]
@@ -0,0 +1,227 @@
1
+ import os
2
+ from abc import ABC, abstractmethod
3
+ from datetime import datetime
4
+ from typing import Any, Dict, List, Optional, Type
5
+
6
+ from langchain_core.runnables import RunnableConfig
7
+ from langgraph.graph import StateGraph
8
+ from pydantic import BaseModel, Field
9
+
10
+ from noesium.core.llm import get_llm_client
11
+ from noesium.core.tracing import get_token_tracker
12
+ from noesium.core.utils.logging import get_logger
13
+ from noesium.core.utils.typing import override
14
+
15
+
16
+ class BaseAgent(ABC):
17
+ """
18
+ Base class for all agents with common functionality.
19
+
20
+ Provides:
21
+ - LLM client management with instructor support
22
+ - Token usage tracking
23
+ - Logging capabilities
24
+ - Configuration management
25
+ - Error handling patterns
26
+ """
27
+
28
+ def __init__(self, llm_provider: str = "openrouter", model_name: Optional[str] = None):
29
+ """Initialize base agent with LLM client and logging."""
30
+ self.logger = get_logger(self.__class__.__name__)
31
+ self.llm_provider = llm_provider
32
+ self.model_name = model_name
33
+
34
+ # Initialize LLM client with instructor support
35
+ self.llm = get_llm_client(provider=llm_provider, chat_model=model_name)
36
+
37
+ self.logger.info(f"Initialized {self.__class__.__name__} with {llm_provider} provider")
38
+
39
+ @abstractmethod
40
+ def run(self, user_message: str, context: Dict[str, Any] = None, config: Optional[RunnableConfig] = None) -> Any:
41
+ """Run the agent with a user message and context."""
42
+
43
+ def get_token_usage_stats(self) -> Dict[str, Any]:
44
+ """Get comprehensive token usage statistics."""
45
+ return get_token_tracker().get_stats()
46
+
47
+ def print_token_usage_summary(self):
48
+ """Print a brief structured token usage summary."""
49
+ stats = self.get_token_usage_stats()
50
+ if stats["total_tokens"] > 0:
51
+ print(
52
+ f"FINAL_SUMMARY: {stats['total_tokens']} total | {stats['total_calls']} calls | P:{stats['total_prompt_tokens']} C:{stats['total_completion_tokens']}"
53
+ )
54
+ else:
55
+ print("FINAL_SUMMARY: 0 total | 0 calls | P:0 C:0")
56
+
57
+
58
+ class BaseGraphicAgent(BaseAgent):
59
+ """
60
+ Base class for agents using LangGraph.
61
+
62
+ Provides:
63
+ - LangGraph state management
64
+ - Graph building abstractions
65
+ - Graph export functionality
66
+ - Common graph patterns
67
+ """
68
+
69
+ def __init__(self, llm_provider: str = "openrouter", model_name: Optional[str] = None):
70
+ """Initialize graphic agent with graph support."""
71
+ super().__init__(llm_provider, model_name)
72
+ self.graph: Optional[StateGraph] = None
73
+
74
+ @abstractmethod
75
+ def get_state_class(self) -> Type:
76
+ """Get the state class for this agent's graph."""
77
+
78
+ @abstractmethod
79
+ def _build_graph(self) -> StateGraph:
80
+ """Build the agent's graph. Must be implemented by subclasses."""
81
+
82
+ def export_graph(self, output_path: Optional[str] = None, format: str = "png"):
83
+ """
84
+ Export the agent graph visualization to file.
85
+
86
+ Args:
87
+ output_path: Optional path for output file. If None, uses default naming.
88
+ format: Export format ('png' or 'mermaid')
89
+ """
90
+ if not self.graph:
91
+ self.logger.warning("No graph to export. Build graph first.")
92
+ return
93
+
94
+ if not output_path:
95
+ class_name = self.__class__.__name__.lower()
96
+ output_path = os.path.join(os.path.dirname(__file__), f"{class_name}_graph.{format}")
97
+
98
+ try:
99
+ graph_structure = self.graph.get_graph()
100
+
101
+ if format == "png":
102
+ try:
103
+ graph_structure.draw_png(output_path)
104
+ self.logger.info(f"Graph exported successfully to {output_path}")
105
+ except ImportError:
106
+ self.logger.warning("pygraphviz not installed, trying mermaid fallback")
107
+ graph_structure.draw_mermaid_png(output_path)
108
+ self.logger.info(f"Graph exported with mermaid to {output_path}")
109
+ elif format == "mermaid":
110
+ # For mermaid, we might want to save the mermaid code itself
111
+ mermaid_code = graph_structure.draw_mermaid()
112
+ with open(output_path.replace(".png", ".md"), "w") as f:
113
+ f.write(f"```mermaid\n{mermaid_code}\n```")
114
+ self.logger.info(f"Mermaid graph exported to {output_path.replace('.png', '.md')}")
115
+ else:
116
+ self.logger.error(f"Unsupported export format: {format}")
117
+
118
+ except Exception as e:
119
+ self.logger.error(f"Failed to export graph: {e}")
120
+
121
+ def _create_error_response(self, error_message: str, **kwargs) -> Dict[str, Any]:
122
+ """Create a standardized error response."""
123
+ self.logger.error(f"Agent error: {error_message}")
124
+ return {"error": error_message, "success": False, "timestamp": self._now_iso(), **kwargs}
125
+
126
+ def _now_iso(self) -> str:
127
+ """Get current time in ISO format."""
128
+ from datetime import datetime, timezone
129
+
130
+ return datetime.now(timezone.utc).isoformat()
131
+
132
+
133
+ class BaseConversationAgent(BaseGraphicAgent):
134
+ """
135
+ Abstract base class for conversation-style agents like AskuraAgent.
136
+
137
+ Provides:
138
+ - Session management patterns
139
+ - Message handling abstractions
140
+ - Conversation state management
141
+ - Response generation patterns
142
+ """
143
+
144
+ def __init__(self, llm_provider: str = "openrouter", model_name: Optional[str] = None):
145
+ """Initialize conversation agent."""
146
+ super().__init__(llm_provider, model_name)
147
+ self._session_states: Dict[str, Any] = {}
148
+
149
+ @abstractmethod
150
+ def start_conversation(self, user_id: str, initial_message: Optional[str] = None) -> Any:
151
+ """Start a new conversation with a user."""
152
+
153
+ @abstractmethod
154
+ def process_user_message(self, user_id: str, session_id: str, message: str) -> Any:
155
+ """Process a user message and return the agent's response."""
156
+
157
+ def get_session_state(self, session_id: str) -> Optional[Any]:
158
+ """Get the state for a specific session."""
159
+ return self._session_states.get(session_id)
160
+
161
+ def list_sessions(self) -> list[str]:
162
+ """List all active session IDs."""
163
+ return list(self._session_states.keys())
164
+
165
+ def clear_session(self, session_id: str) -> bool:
166
+ """Clear a specific session."""
167
+ if session_id in self._session_states:
168
+ del self._session_states[session_id]
169
+ self.logger.info(f"Cleared session {session_id}")
170
+ return True
171
+ return False
172
+
173
+ def clear_all_sessions(self):
174
+ """Clear all sessions."""
175
+ session_count = len(self._session_states)
176
+ self._session_states.clear()
177
+ self.logger.info(f"Cleared {session_count} sessions")
178
+
179
+ @override
180
+ def run(self, user_message: str, context: Dict[str, Any] = None, config: Optional[RunnableConfig] = None) -> str:
181
+ """Run the agent with a user message and context. Required by BaseAgent."""
182
+ # Create a temporary user ID for standalone run
183
+ response = self.start_conversation("standalone_user", user_message)
184
+ return response.message
185
+
186
+
187
+ class ResearchOutput(BaseModel):
188
+ """Output from research process."""
189
+
190
+ content: str = Field(default="")
191
+ sources: List[Dict[str, Any]] = Field(default_factory=list)
192
+ summary: str = Field(default="")
193
+ timestamp: datetime = Field(default_factory=datetime.now)
194
+
195
+
196
+ class BaseResearcher(BaseGraphicAgent):
197
+ """
198
+ Abstract base class for research-style agents like SeekraAgent.
199
+
200
+ Provides:
201
+ - Research workflow patterns
202
+ - Source management
203
+ - Query generation abstractions
204
+ - Result compilation patterns
205
+ """
206
+
207
+ def __init__(self, llm_provider: str = "openrouter", model_name: Optional[str] = None):
208
+ """Initialize researcher agent."""
209
+ super().__init__(llm_provider, model_name)
210
+
211
+ @abstractmethod
212
+ def research(
213
+ self,
214
+ user_message: str,
215
+ context: Dict[str, Any] = None,
216
+ config: Optional[RunnableConfig] = None,
217
+ ) -> ResearchOutput:
218
+ """Research a topic and return structured results."""
219
+
220
+ @override
221
+ def run(self, user_message: str, context: Dict[str, Any] = None, config: Optional[RunnableConfig] = None) -> str:
222
+ """
223
+ Default implementation of run() for researchers.
224
+ Calls research() and returns the content.
225
+ """
226
+ result = self.research(user_message, context, config)
227
+ return result.content if result else "Research failed to produce results."
noesium/core/consts.py ADDED
@@ -0,0 +1,6 @@
1
+ # Gemini models
2
+ GEMINI_PRO = "google/gemini-2.5-pro"
3
+ GEMINI_FLASH = "google/gemini-2.5-flash"
4
+
5
+ # Default embedding dimensions
6
+ DEFAULT_EMBEDDING_DIMS = 768
@@ -0,0 +1,104 @@
1
+ from abc import ABC, abstractmethod
2
+ from datetime import datetime, timezone
3
+ from enum import Enum
4
+ from typing import Any, Dict, List, Optional
5
+ from uuid import uuid4
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class ConflictType(str, Enum):
11
+ """Types of conflicts that can occur."""
12
+
13
+ CYCLE_DETECTED = "cycle_detected"
14
+ CONCURRENT_UPDATE = "concurrent_update"
15
+ STATUS_INCONSISTENCY = "status_inconsistency"
16
+ PRIORITY_CONFLICT = "priority_conflict"
17
+ DEPENDENCY_VIOLATION = "dependency_violation"
18
+ RESOURCE_CONFLICT = "resource_conflict"
19
+ SEMANTIC_INCONSISTENCY = "semantic_inconsistency"
20
+
21
+
22
+ class Conflict(BaseModel):
23
+ """
24
+ Represents a conflict in the system.
25
+ """
26
+
27
+ # Core identification
28
+ id: str = Field(default_factory=lambda: str(uuid4()))
29
+ conflict_type: ConflictType = Field(..., description="Type of conflict")
30
+
31
+ # Conflict details
32
+ affected_nodes: List[str] = Field(..., description="IDs of affected nodes")
33
+ description: str = Field(default="", description="Human-readable description")
34
+ context: Dict[str, Any] = Field(default_factory=dict, description="Additional conflict context")
35
+ detected_at: datetime = Field(
36
+ default_factory=lambda: datetime.now(timezone.utc), description="When the conflict was detected"
37
+ )
38
+
39
+ # Resolution status
40
+ resolved: bool = Field(default=False, description="Whether the conflict has been resolved")
41
+ resolution_strategy: Optional[str] = Field(default=None, description="Strategy used to resolve the conflict")
42
+ resolution_action: Optional[Dict[str, Any]] = Field(
43
+ default=None, description="Resolution action returned by resolver"
44
+ )
45
+ resolved_at: Optional[datetime] = Field(default=None, description="When the conflict was resolved")
46
+
47
+ class Config:
48
+ """Pydantic configuration."""
49
+
50
+ use_enum_values = True
51
+
52
+ def __eq__(self, other: object) -> bool:
53
+ """
54
+ Compare two Conflicts for equality based on meaningful content.
55
+
56
+ Excludes timestamp fields as they are automatically generated.
57
+ """
58
+ if not isinstance(other, Conflict):
59
+ return False
60
+
61
+ return (
62
+ self.id == other.id
63
+ and self.conflict_type == other.conflict_type
64
+ and self.affected_nodes == other.affected_nodes
65
+ and self.description == other.description
66
+ and self.context == other.context
67
+ and self.resolved == other.resolved
68
+ and self.resolution_strategy == other.resolution_strategy
69
+ and self.resolution_action == other.resolution_action
70
+ )
71
+
72
+ def to_dict(self) -> Dict[str, Any]:
73
+ """Convert to dictionary for serialization."""
74
+ return {
75
+ "id": self.id,
76
+ "conflict_type": str(self.conflict_type),
77
+ "affected_nodes": self.affected_nodes,
78
+ "description": self.description,
79
+ "context": self.context,
80
+ "detected_at": self.detected_at.isoformat() if self.detected_at else None,
81
+ "resolved": self.resolved,
82
+ "resolution_strategy": self.resolution_strategy,
83
+ "resolved_at": self.resolved_at.isoformat() if self.resolved_at else None,
84
+ }
85
+
86
+
87
+ class ConflictResolver(ABC):
88
+ """
89
+ Abstract interface for conflict resolvers.
90
+
91
+ Can be implemented by LLM-based reasoning, human input, or rule-based systems.
92
+ """
93
+
94
+ @abstractmethod
95
+ def resolve(self, conflict: Conflict) -> Optional[Dict[str, Any]]:
96
+ """
97
+ Resolve a conflict.
98
+
99
+ Args:
100
+ conflict: The conflict to resolve
101
+
102
+ Returns:
103
+ Resolution action description, or None if cannot resolve
104
+ """
@@ -0,0 +1,53 @@
1
+ from typing import Any, Dict, List
2
+
3
+ from noesium.core.goalith.goalgraph.graph import GoalGraph
4
+
5
+ from .conflict import Conflict
6
+
7
+
8
+ class ConflictDetector:
9
+ """
10
+ Detects conflicts in the graph.
11
+
12
+ Watches for illegal states, cycles, and semantic inconsistencies.
13
+ """
14
+
15
+ def __init__(self, goal_graph: GoalGraph):
16
+ """
17
+ Initialize conflict detector.
18
+
19
+ Args:
20
+ goal_graph: The graph to monitor
21
+ """
22
+ self._goal_graph = goal_graph
23
+ self._detection_stats = {
24
+ "total_checked": 0,
25
+ "conflicts_found": 0,
26
+ "by_type": {},
27
+ }
28
+
29
+ @property
30
+ def goal_graph(self):
31
+ """Get the graph."""
32
+ return self._goal_graph
33
+
34
+ def detect_conflicts(self) -> List[Conflict]:
35
+ """
36
+ Detect conflicts in the graph.
37
+
38
+ Returns:
39
+ List of detected conflicts
40
+ """
41
+ self._detection_stats["total_checked"] += 1
42
+ conflicts = []
43
+ # TODO: Implement conflict detection
44
+ return conflicts
45
+
46
+ def get_detection_stats(self) -> Dict[str, Any]:
47
+ """
48
+ Get conflict detection statistics.
49
+
50
+ Returns:
51
+ Detection statistics
52
+ """
53
+ return self._detection_stats.copy()
@@ -0,0 +1,6 @@
1
+ from .base import GoalDecomposer
2
+ from .callable_decomposer import CallableDecomposer
3
+ from .llm_decomposer import LLMDecomposer
4
+ from .simple_decomposer import SimpleListDecomposer
5
+
6
+ __all__ = ["GoalDecomposer", "SimpleListDecomposer", "LLMDecomposer", "CallableDecomposer"]
@@ -0,0 +1,46 @@
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
+ from abc import ABC, abstractmethod
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from noesium.core.goalith.goalgraph.node import GoalNode
12
+
13
+
14
+ class GoalDecomposer(ABC):
15
+ """
16
+ Abstract interface for goal decomposers.
17
+
18
+ Decomposers can be humans, AI agents (LLMs), symbolic planners,
19
+ or any callable entity that can break down goals into subgoals/tasks.
20
+ """
21
+
22
+ @abstractmethod
23
+ def decompose(self, goal_node: GoalNode, context: Optional[Dict[str, Any]] = None) -> List[GoalNode]:
24
+ """
25
+ Decompose a goal into subgoals or tasks.
26
+
27
+ Args:
28
+ goal_node: The goal node to decompose
29
+ context: Optional context for decomposition
30
+
31
+ Returns:
32
+ List of subgoal/task nodes
33
+
34
+ Raises:
35
+ DecompositionError: If decomposition fails
36
+ """
37
+
38
+ @property
39
+ @abstractmethod
40
+ def name(self) -> str:
41
+ """Get the name of this decomposer."""
42
+
43
+ @property
44
+ def description(self) -> str:
45
+ """Get the description of this decomposer."""
46
+ return f"Decomposer: {self.name}"
@@ -0,0 +1,65 @@
1
+ from typing import Any, Callable, Dict, List, Optional
2
+
3
+ from noesium.core.goalith.errors import DecompositionError
4
+ from noesium.core.goalith.goalgraph.node import GoalNode
5
+
6
+ from .base import GoalDecomposer
7
+
8
+
9
+ class CallableDecomposer(GoalDecomposer):
10
+ """
11
+ Wrapper for callable decomposition functions.
12
+
13
+ Allows any function that matches the decomposition signature
14
+ to be used as a decomposer.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ callable_func: Callable[[GoalNode, Optional[Dict[str, Any]]], List[GoalNode]],
20
+ name: Optional[str] = None,
21
+ description: Optional[str] = None,
22
+ ):
23
+ """
24
+ Initialize with a callable function.
25
+
26
+ Args:
27
+ callable_func: Function that performs decomposition
28
+ name: Name of this decomposer (auto-detected from function if None)
29
+ description: Optional description
30
+ """
31
+ self._callable = callable_func
32
+ self._name = name or getattr(callable_func, "__name__", "callable_decomposer")
33
+ self._description = description or f"Callable decomposer: {self._name}"
34
+
35
+ @property
36
+ def name(self) -> str:
37
+ """Get the name of this decomposer."""
38
+ return self._name
39
+
40
+ @property
41
+ def description(self) -> str:
42
+ """Get the description of this decomposer."""
43
+ return self._description
44
+
45
+ def decompose(self, goal_node: GoalNode, context: Optional[Dict[str, Any]] = None) -> List[GoalNode]:
46
+ """
47
+ Decompose using the callable function.
48
+
49
+ Args:
50
+ goal_node: The goal node to decompose
51
+ context: Optional context
52
+
53
+ Returns:
54
+ List of subgoal/task nodes
55
+
56
+ Raises:
57
+ DecompositionError: If callable raises an exception
58
+ """
59
+ try:
60
+ return self._callable(goal_node, context)
61
+ except ValueError:
62
+ # Let ValueError propagate as-is for test compatibility
63
+ raise
64
+ except Exception as e:
65
+ raise DecompositionError(f"Decomposition failed: {e}") from e