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
noesium/core/__init__.py
ADDED
|
@@ -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,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
|