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,241 @@
|
|
|
1
|
+
"""Main router class for LLM model selection."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, Type, Union
|
|
4
|
+
|
|
5
|
+
from noesium.core.llm import get_llm_client
|
|
6
|
+
from noesium.core.llm.base import BaseLLMClient
|
|
7
|
+
from noesium.core.utils.logging import get_logger
|
|
8
|
+
|
|
9
|
+
from .base import BaseRoutingStrategy
|
|
10
|
+
from .strategies import DynamicComplexityStrategy, SelfAssessmentStrategy
|
|
11
|
+
from .types import ModelTier, RoutingResult
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ModelRouter:
|
|
17
|
+
"""
|
|
18
|
+
Main router class for determining appropriate model tier for queries.
|
|
19
|
+
|
|
20
|
+
This router uses pluggable strategies to analyze query complexity
|
|
21
|
+
and recommend the most suitable model tier (lite/fast/power).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Registry of available strategies
|
|
25
|
+
STRATEGIES = {
|
|
26
|
+
"self_assessment": SelfAssessmentStrategy,
|
|
27
|
+
"dynamic_complexity": DynamicComplexityStrategy,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
strategy: Union[str, BaseRoutingStrategy] = "dynamic_complexity",
|
|
33
|
+
lite_client: Optional[BaseLLMClient] = None,
|
|
34
|
+
lite_client_config: Optional[Dict[str, Any]] = None,
|
|
35
|
+
strategy_config: Optional[Dict[str, Any]] = None,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialize the model router.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
strategy: Routing strategy name or instance
|
|
42
|
+
lite_client: Pre-configured lite model client
|
|
43
|
+
lite_client_config: Configuration for lite client creation
|
|
44
|
+
strategy_config: Strategy-specific configuration
|
|
45
|
+
"""
|
|
46
|
+
self.strategy_config = (strategy_config or {}).copy() # Make a copy to avoid modifying original
|
|
47
|
+
|
|
48
|
+
# Setup lite client if needed
|
|
49
|
+
self.lite_client = self._setup_lite_client(lite_client, lite_client_config)
|
|
50
|
+
|
|
51
|
+
# Initialize routing strategy
|
|
52
|
+
self.strategy = self._setup_strategy(strategy)
|
|
53
|
+
|
|
54
|
+
logger.info(f"Initialized ModelRouter with strategy: {self.strategy.get_strategy_name()}")
|
|
55
|
+
|
|
56
|
+
def route(self, query: str) -> RoutingResult:
|
|
57
|
+
"""
|
|
58
|
+
Route a query to the appropriate model tier.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
query: Input query to analyze
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
RoutingResult with tier recommendation and analysis
|
|
65
|
+
"""
|
|
66
|
+
if not query or not query.strip():
|
|
67
|
+
# Handle empty queries
|
|
68
|
+
logger.warning("Empty query provided, defaulting to LITE tier")
|
|
69
|
+
from .types import ComplexityScore
|
|
70
|
+
|
|
71
|
+
return RoutingResult(
|
|
72
|
+
tier=ModelTier.LITE,
|
|
73
|
+
confidence=0.5,
|
|
74
|
+
complexity_score=ComplexityScore(total=0.0),
|
|
75
|
+
strategy=self.strategy.get_strategy_name(),
|
|
76
|
+
metadata={"empty_query": True},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
return self.strategy.route(query.strip())
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Error in routing: {e}")
|
|
83
|
+
# Fallback to FAST tier on error
|
|
84
|
+
from .types import ComplexityScore
|
|
85
|
+
|
|
86
|
+
return RoutingResult(
|
|
87
|
+
tier=ModelTier.FAST,
|
|
88
|
+
confidence=0.0,
|
|
89
|
+
complexity_score=ComplexityScore(total=0.5),
|
|
90
|
+
strategy=self.strategy.get_strategy_name(),
|
|
91
|
+
metadata={"error": str(e), "fallback": True},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def get_recommended_model_params(
|
|
95
|
+
self, routing_result: RoutingResult, model_configs: Optional[Dict[ModelTier, Dict[str, Any]]] = None
|
|
96
|
+
) -> Dict[str, Any]:
|
|
97
|
+
"""
|
|
98
|
+
Get recommended model parameters based on routing result.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
routing_result: Result from route() call
|
|
102
|
+
model_configs: Optional mapping of tiers to model configurations
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dictionary of model parameters for the recommended tier
|
|
106
|
+
"""
|
|
107
|
+
tier = routing_result.tier
|
|
108
|
+
|
|
109
|
+
if model_configs and tier in model_configs:
|
|
110
|
+
return model_configs[tier].copy()
|
|
111
|
+
|
|
112
|
+
# Default configurations for each tier
|
|
113
|
+
default_configs = {
|
|
114
|
+
ModelTier.LITE: {
|
|
115
|
+
"provider": "llamacpp", # Fast local inference
|
|
116
|
+
"temperature": 0.3,
|
|
117
|
+
"max_tokens": 512,
|
|
118
|
+
},
|
|
119
|
+
ModelTier.FAST: {
|
|
120
|
+
"provider": "openai",
|
|
121
|
+
"chat_model": "gpt-4o-mini",
|
|
122
|
+
"temperature": 0.7,
|
|
123
|
+
"max_tokens": 1024,
|
|
124
|
+
},
|
|
125
|
+
ModelTier.POWER: {
|
|
126
|
+
"provider": "openai",
|
|
127
|
+
"chat_model": "gpt-4o",
|
|
128
|
+
"temperature": 0.7,
|
|
129
|
+
"max_tokens": 2048,
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return default_configs.get(tier, default_configs[ModelTier.FAST])
|
|
134
|
+
|
|
135
|
+
def route_and_configure(
|
|
136
|
+
self, query: str, model_configs: Optional[Dict[ModelTier, Dict[str, Any]]] = None
|
|
137
|
+
) -> tuple[RoutingResult, Dict[str, Any]]:
|
|
138
|
+
"""
|
|
139
|
+
Route query and return both result and recommended model configuration.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
query: Input query to analyze
|
|
143
|
+
model_configs: Optional mapping of tiers to model configurations
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Tuple of (RoutingResult, model_config_dict)
|
|
147
|
+
"""
|
|
148
|
+
routing_result = self.route(query)
|
|
149
|
+
model_config = self.get_recommended_model_params(routing_result, model_configs)
|
|
150
|
+
|
|
151
|
+
return routing_result, model_config
|
|
152
|
+
|
|
153
|
+
def update_strategy_config(self, config: Dict[str, Any]) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Update strategy configuration.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
config: New configuration parameters
|
|
159
|
+
"""
|
|
160
|
+
self.strategy_config.update(config)
|
|
161
|
+
|
|
162
|
+
# Reinitialize strategy with new config
|
|
163
|
+
strategy_name = self.strategy.get_strategy_name()
|
|
164
|
+
self.strategy = self._setup_strategy(strategy_name)
|
|
165
|
+
|
|
166
|
+
logger.info(f"Updated strategy configuration for {strategy_name}")
|
|
167
|
+
|
|
168
|
+
def get_strategy_info(self) -> Dict[str, Any]:
|
|
169
|
+
"""
|
|
170
|
+
Get information about the current routing strategy.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dictionary with strategy information
|
|
174
|
+
"""
|
|
175
|
+
return {
|
|
176
|
+
"name": self.strategy.get_strategy_name(),
|
|
177
|
+
"config": self.strategy_config.copy(),
|
|
178
|
+
"requires_lite_client": self.lite_client is not None,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def get_available_strategies(cls) -> list[str]:
|
|
183
|
+
"""Get list of available strategy names."""
|
|
184
|
+
return list(cls.STRATEGIES.keys())
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def register_strategy(cls, name: str, strategy_class: Type[BaseRoutingStrategy]) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Register a new routing strategy.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
name: Strategy name
|
|
193
|
+
strategy_class: Strategy class
|
|
194
|
+
"""
|
|
195
|
+
if not issubclass(strategy_class, BaseRoutingStrategy):
|
|
196
|
+
raise ValueError("Strategy class must inherit from BaseRoutingStrategy")
|
|
197
|
+
|
|
198
|
+
cls.STRATEGIES[name] = strategy_class
|
|
199
|
+
logger.info(f"Registered new routing strategy: {name}")
|
|
200
|
+
|
|
201
|
+
def _setup_lite_client(
|
|
202
|
+
self, lite_client: Optional[BaseLLMClient], lite_client_config: Optional[Dict[str, Any]]
|
|
203
|
+
) -> Optional[BaseLLMClient]:
|
|
204
|
+
"""Setup lite model client if needed."""
|
|
205
|
+
if lite_client:
|
|
206
|
+
return lite_client
|
|
207
|
+
|
|
208
|
+
if lite_client_config:
|
|
209
|
+
try:
|
|
210
|
+
return get_llm_client(**lite_client_config)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.warning(f"Failed to create lite client: {e}")
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
# Try to create a default lite client
|
|
216
|
+
try:
|
|
217
|
+
# Try llamacpp first (fastest for lite operations)
|
|
218
|
+
return get_llm_client(provider="llamacpp")
|
|
219
|
+
except Exception:
|
|
220
|
+
try:
|
|
221
|
+
# Fallback to OpenAI with a fast model
|
|
222
|
+
return get_llm_client(provider="openai", chat_model="gpt-4o-mini")
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.warning(f"Could not create default lite client: {e}")
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def _setup_strategy(self, strategy: Union[str, BaseRoutingStrategy]) -> BaseRoutingStrategy:
|
|
228
|
+
"""Setup the routing strategy."""
|
|
229
|
+
if isinstance(strategy, BaseRoutingStrategy):
|
|
230
|
+
return strategy
|
|
231
|
+
|
|
232
|
+
if isinstance(strategy, str):
|
|
233
|
+
if strategy not in self.STRATEGIES:
|
|
234
|
+
raise ValueError(
|
|
235
|
+
f"Unknown strategy: {strategy}. " f"Available strategies: {list(self.STRATEGIES.keys())}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
strategy_class = self.STRATEGIES[strategy]
|
|
239
|
+
return strategy_class(lite_client=self.lite_client, config=self.strategy_config)
|
|
240
|
+
|
|
241
|
+
raise ValueError(f"Invalid strategy type: {type(strategy)}")
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Dynamic complexity routing strategy implementation."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from noesium.core.routing.base import BaseRoutingStrategy
|
|
7
|
+
from noesium.core.routing.types import ComplexityScore, ModelTier, RoutingResult
|
|
8
|
+
from noesium.core.utils.logging import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DynamicComplexityStrategy(BaseRoutingStrategy):
|
|
14
|
+
"""
|
|
15
|
+
Routing strategy based on dynamic complexity index calculation.
|
|
16
|
+
|
|
17
|
+
This strategy computes a complexity score from multiple signals:
|
|
18
|
+
- Linguistic complexity (sentence structure, vocabulary)
|
|
19
|
+
- Reasoning depth (assessed by lite model)
|
|
20
|
+
- Knowledge uncertainty (perplexity/confidence analysis)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, lite_client=None, config=None):
|
|
24
|
+
"""
|
|
25
|
+
Initialize the dynamic complexity strategy.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
lite_client: LLM client for the lite model
|
|
29
|
+
config: Configuration dict with optional parameters:
|
|
30
|
+
- alpha: Weight for linguistic score (default: 0.4)
|
|
31
|
+
- beta: Weight for reasoning score (default: 0.4)
|
|
32
|
+
- gamma: Weight for uncertainty score (default: 0.2)
|
|
33
|
+
- lite_threshold: Max score for lite routing (default: 0.3)
|
|
34
|
+
- fast_threshold: Max score for fast routing (default: 0.65)
|
|
35
|
+
- temperature: Temperature for reasoning assessment (default: 0.1)
|
|
36
|
+
- reasoning_max_tokens: Max tokens for reasoning assessment (default: 3)
|
|
37
|
+
- uncertainty_max_tokens: Max tokens for uncertainty analysis (default: 64)
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(lite_client, config)
|
|
40
|
+
|
|
41
|
+
# Weighting parameters
|
|
42
|
+
self.alpha = self.config.get("alpha", 0.4) # Linguistic weight
|
|
43
|
+
self.beta = self.config.get("beta", 0.4) # Reasoning weight
|
|
44
|
+
self.gamma = self.config.get("gamma", 0.2) # Uncertainty weight
|
|
45
|
+
|
|
46
|
+
# Threshold parameters
|
|
47
|
+
self.lite_threshold = self.config.get("lite_threshold", 0.3)
|
|
48
|
+
self.fast_threshold = self.config.get("fast_threshold", 0.65)
|
|
49
|
+
|
|
50
|
+
# LLM parameters
|
|
51
|
+
self.temperature = self.config.get("temperature", 0.1)
|
|
52
|
+
self.reasoning_max_tokens = self.config.get("reasoning_max_tokens", 3)
|
|
53
|
+
self.uncertainty_max_tokens = self.config.get("uncertainty_max_tokens", 64)
|
|
54
|
+
|
|
55
|
+
# Validate weights sum to 1.0
|
|
56
|
+
total_weight = self.alpha + self.beta + self.gamma
|
|
57
|
+
if abs(total_weight - 1.0) > 0.01:
|
|
58
|
+
logger.warning(f"Weights don't sum to 1.0 (sum={total_weight}). Normalizing...")
|
|
59
|
+
self.alpha /= total_weight
|
|
60
|
+
self.beta /= total_weight
|
|
61
|
+
self.gamma /= total_weight
|
|
62
|
+
|
|
63
|
+
def route(self, query: str) -> RoutingResult:
|
|
64
|
+
"""
|
|
65
|
+
Route query based on dynamic complexity index.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
query: Input query to assess
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
RoutingResult with tier recommendation and detailed analysis
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
# Calculate individual complexity components
|
|
75
|
+
linguistic_score = self._calculate_linguistic_score(query)
|
|
76
|
+
reasoning_score = self._calculate_reasoning_score(query)
|
|
77
|
+
uncertainty_score = self._calculate_uncertainty_score(query)
|
|
78
|
+
|
|
79
|
+
# Compute weighted complexity index
|
|
80
|
+
complexity_index = (
|
|
81
|
+
self.alpha * linguistic_score + self.beta * reasoning_score + self.gamma * uncertainty_score
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Determine tier based on complexity index
|
|
85
|
+
tier = self._index_to_tier(complexity_index)
|
|
86
|
+
|
|
87
|
+
# Calculate confidence based on component consistency
|
|
88
|
+
confidence = self._calculate_confidence(linguistic_score, reasoning_score, uncertainty_score)
|
|
89
|
+
|
|
90
|
+
# Create detailed complexity score
|
|
91
|
+
complexity_score_obj = ComplexityScore(
|
|
92
|
+
total=complexity_index,
|
|
93
|
+
linguistic=linguistic_score,
|
|
94
|
+
reasoning=reasoning_score,
|
|
95
|
+
uncertainty=uncertainty_score,
|
|
96
|
+
metadata={
|
|
97
|
+
"weights": {"alpha": self.alpha, "beta": self.beta, "gamma": self.gamma},
|
|
98
|
+
"components": {
|
|
99
|
+
"linguistic": linguistic_score,
|
|
100
|
+
"reasoning": reasoning_score,
|
|
101
|
+
"uncertainty": uncertainty_score,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return self._create_result(
|
|
107
|
+
tier=tier,
|
|
108
|
+
confidence=confidence,
|
|
109
|
+
complexity_score=complexity_score_obj,
|
|
110
|
+
metadata={"thresholds": {"lite": self.lite_threshold, "fast": self.fast_threshold}},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Error in dynamic complexity routing: {e}")
|
|
115
|
+
# Fallback to fast tier on error
|
|
116
|
+
return self._create_result(
|
|
117
|
+
tier=ModelTier.FAST,
|
|
118
|
+
confidence=0.0,
|
|
119
|
+
complexity_score=ComplexityScore(total=0.5),
|
|
120
|
+
metadata={"error": str(e), "fallback": True},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def get_strategy_name(self) -> str:
|
|
124
|
+
"""Return the strategy name."""
|
|
125
|
+
return "dynamic_complexity"
|
|
126
|
+
|
|
127
|
+
def _calculate_linguistic_score(self, query: str) -> float:
|
|
128
|
+
"""
|
|
129
|
+
Calculate linguistic complexity based on sentence structure and vocabulary.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
query: Input query
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Linguistic complexity score (0.0-1.0)
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
# Handle empty query case
|
|
139
|
+
if not query.strip():
|
|
140
|
+
return 0.0
|
|
141
|
+
|
|
142
|
+
# Count tokens (approximate)
|
|
143
|
+
tokens = re.findall(r"\w+", query)
|
|
144
|
+
token_count = len(tokens)
|
|
145
|
+
|
|
146
|
+
# Count structural complexity indicators
|
|
147
|
+
clauses = (
|
|
148
|
+
query.count(",")
|
|
149
|
+
+ query.count(";")
|
|
150
|
+
+ query.count(" and ")
|
|
151
|
+
+ query.count(" or ")
|
|
152
|
+
+ query.count(" but ")
|
|
153
|
+
+ query.count(" because ")
|
|
154
|
+
+ query.count(" if ")
|
|
155
|
+
+ query.count(" when ")
|
|
156
|
+
+ query.count(" while ")
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Count sentences
|
|
160
|
+
sentences = len(re.split(r"[.!?]+", query.strip()))
|
|
161
|
+
|
|
162
|
+
# Calculate complexity factors
|
|
163
|
+
token_factor = min(1.0, token_count / 50.0) # Normalize around 50 tokens
|
|
164
|
+
clause_factor = min(1.0, clauses / 5.0) # Normalize around 5 clauses
|
|
165
|
+
sentence_factor = min(1.0, sentences / 3.0) # Normalize around 3 sentences
|
|
166
|
+
|
|
167
|
+
# Count complex words (>6 characters as simple heuristic)
|
|
168
|
+
complex_words = sum(1 for token in tokens if len(token) > 6)
|
|
169
|
+
vocab_factor = min(1.0, complex_words / max(1, token_count) * 2)
|
|
170
|
+
|
|
171
|
+
# Weighted combination
|
|
172
|
+
linguistic_score = 0.3 * token_factor + 0.3 * clause_factor + 0.2 * sentence_factor + 0.2 * vocab_factor
|
|
173
|
+
|
|
174
|
+
return min(1.0, max(0.0, linguistic_score))
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.warning(f"Error calculating linguistic score: {e}")
|
|
178
|
+
return 0.5
|
|
179
|
+
|
|
180
|
+
def _calculate_reasoning_score(self, query: str) -> float:
|
|
181
|
+
"""
|
|
182
|
+
Calculate reasoning depth using lite model assessment.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
query: Input query
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Reasoning complexity score (0.0-1.0)
|
|
189
|
+
"""
|
|
190
|
+
if not self.lite_client:
|
|
191
|
+
# Fallback: simple keyword-based reasoning detection
|
|
192
|
+
return self._fallback_reasoning_score(query)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
prompt = f"""Classify reasoning depth of request:
|
|
196
|
+
- 0 = factual recall
|
|
197
|
+
- 1 = some reasoning/planning
|
|
198
|
+
- 2 = multi-step or abstract reasoning
|
|
199
|
+
|
|
200
|
+
Request: "{query}"
|
|
201
|
+
Output: number only"""
|
|
202
|
+
|
|
203
|
+
messages = [{"role": "user", "content": prompt}]
|
|
204
|
+
response = self.lite_client.completion(
|
|
205
|
+
messages=messages, temperature=self.temperature, max_tokens=self.reasoning_max_tokens
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Parse response
|
|
209
|
+
response_clean = response.strip()
|
|
210
|
+
for char in response_clean:
|
|
211
|
+
if char.isdigit():
|
|
212
|
+
score = int(char)
|
|
213
|
+
if 0 <= score <= 2:
|
|
214
|
+
return score / 2.0 # Normalize to 0-1 range
|
|
215
|
+
|
|
216
|
+
# Fallback if parsing fails
|
|
217
|
+
logger.warning(f"Could not parse reasoning response: {response}")
|
|
218
|
+
return self._fallback_reasoning_score(query)
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.warning(f"Error calculating reasoning score with LLM: {e}")
|
|
222
|
+
return self._fallback_reasoning_score(query)
|
|
223
|
+
|
|
224
|
+
def _fallback_reasoning_score(self, query: str) -> float:
|
|
225
|
+
"""Fallback reasoning score based on keywords."""
|
|
226
|
+
reasoning_keywords = [
|
|
227
|
+
"analyze",
|
|
228
|
+
"compare",
|
|
229
|
+
"evaluate",
|
|
230
|
+
"explain",
|
|
231
|
+
"why",
|
|
232
|
+
"how",
|
|
233
|
+
"cause",
|
|
234
|
+
"effect",
|
|
235
|
+
"relationship",
|
|
236
|
+
"implication",
|
|
237
|
+
"conclusion",
|
|
238
|
+
"strategy",
|
|
239
|
+
"plan",
|
|
240
|
+
"design",
|
|
241
|
+
"create",
|
|
242
|
+
"develop",
|
|
243
|
+
"solve",
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
query_lower = query.lower()
|
|
247
|
+
keyword_count = sum(1 for keyword in reasoning_keywords if keyword in query_lower)
|
|
248
|
+
|
|
249
|
+
return min(1.0, keyword_count / 3.0) # Normalize around 3 keywords
|
|
250
|
+
|
|
251
|
+
def _calculate_uncertainty_score(self, query: str) -> float:
|
|
252
|
+
"""
|
|
253
|
+
Calculate knowledge uncertainty using perplexity analysis.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
query: Input query
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Uncertainty score (0.0-1.0)
|
|
260
|
+
"""
|
|
261
|
+
if not self.lite_client:
|
|
262
|
+
# Fallback: domain-based uncertainty estimation
|
|
263
|
+
return self._fallback_uncertainty_score(query)
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
# Check if we can get logprobs (depends on the LLM client implementation)
|
|
267
|
+
messages = [{"role": "user", "content": query}]
|
|
268
|
+
|
|
269
|
+
# Try to get response with some generation to assess uncertainty
|
|
270
|
+
response = self.lite_client.completion(
|
|
271
|
+
messages=messages,
|
|
272
|
+
temperature=0.1, # Low temperature for consistency
|
|
273
|
+
max_tokens=self.uncertainty_max_tokens,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# For now, use response length and coherence as uncertainty proxy
|
|
277
|
+
# A very short or very long response might indicate uncertainty
|
|
278
|
+
response_tokens = len(response.split())
|
|
279
|
+
|
|
280
|
+
if response_tokens < 5: # Very short response
|
|
281
|
+
uncertainty = 0.7
|
|
282
|
+
elif response_tokens > 40: # Very long response
|
|
283
|
+
uncertainty = 0.6
|
|
284
|
+
else:
|
|
285
|
+
uncertainty = 0.3 # Normal length suggests confidence
|
|
286
|
+
|
|
287
|
+
# Adjust based on hedging words
|
|
288
|
+
hedging_words = ["maybe", "perhaps", "possibly", "might", "could", "uncertain"]
|
|
289
|
+
hedging_count = sum(1 for word in hedging_words if word in response.lower())
|
|
290
|
+
uncertainty += min(0.3, hedging_count * 0.1)
|
|
291
|
+
|
|
292
|
+
return min(1.0, max(0.0, uncertainty))
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.warning(f"Error calculating uncertainty score with LLM: {e}")
|
|
296
|
+
return self._fallback_uncertainty_score(query)
|
|
297
|
+
|
|
298
|
+
def _fallback_uncertainty_score(self, query: str) -> float:
|
|
299
|
+
"""Fallback uncertainty score based on domain heuristics."""
|
|
300
|
+
# Questions tend to have higher uncertainty
|
|
301
|
+
question_count = query.count("?")
|
|
302
|
+
|
|
303
|
+
# Specific vs general queries
|
|
304
|
+
specific_indicators = ["specific", "exact", "precise", "particular"]
|
|
305
|
+
general_indicators = ["general", "overview", "broad", "overall"]
|
|
306
|
+
|
|
307
|
+
query_lower = query.lower()
|
|
308
|
+
specific_score = sum(1 for word in specific_indicators if word in query_lower)
|
|
309
|
+
general_score = sum(1 for word in general_indicators if word in query_lower)
|
|
310
|
+
|
|
311
|
+
base_uncertainty = 0.4
|
|
312
|
+
uncertainty_adjustment = question_count * 0.1 + general_score * 0.1 - specific_score * 0.1
|
|
313
|
+
|
|
314
|
+
return min(1.0, max(0.0, base_uncertainty + uncertainty_adjustment))
|
|
315
|
+
|
|
316
|
+
def _index_to_tier(self, complexity_index: float) -> ModelTier:
|
|
317
|
+
"""
|
|
318
|
+
Convert complexity index to model tier.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
complexity_index: Overall complexity score (0.0-1.0)
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Appropriate ModelTier
|
|
325
|
+
"""
|
|
326
|
+
if complexity_index < self.lite_threshold:
|
|
327
|
+
return ModelTier.LITE
|
|
328
|
+
elif complexity_index < self.fast_threshold:
|
|
329
|
+
return ModelTier.FAST
|
|
330
|
+
else:
|
|
331
|
+
return ModelTier.POWER
|
|
332
|
+
|
|
333
|
+
def _calculate_confidence(self, linguistic: float, reasoning: float, uncertainty: float) -> float:
|
|
334
|
+
"""
|
|
335
|
+
Calculate confidence based on consistency of component scores.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
linguistic: Linguistic complexity score
|
|
339
|
+
reasoning: Reasoning complexity score
|
|
340
|
+
uncertainty: Uncertainty score
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Confidence score (0.0-1.0)
|
|
344
|
+
"""
|
|
345
|
+
scores = [linguistic, reasoning, uncertainty]
|
|
346
|
+
|
|
347
|
+
# Calculate standard deviation as measure of consistency
|
|
348
|
+
mean_score = sum(scores) / len(scores)
|
|
349
|
+
variance = sum((score - mean_score) ** 2 for score in scores) / len(scores)
|
|
350
|
+
std_dev = math.sqrt(variance)
|
|
351
|
+
|
|
352
|
+
# Higher consistency (lower std_dev) = higher confidence
|
|
353
|
+
# Max std_dev is ~0.58 (when scores are maximally spread, e.g., 0, 1, 0.5)
|
|
354
|
+
# Make it more sensitive to inconsistency
|
|
355
|
+
consistency = 1.0 - min(1.0, std_dev * 2.5)
|
|
356
|
+
|
|
357
|
+
# Base confidence adjusted by consistency
|
|
358
|
+
base_confidence = 0.6
|
|
359
|
+
confidence = base_confidence + (consistency * 0.4)
|
|
360
|
+
|
|
361
|
+
return min(1.0, max(0.0, confidence))
|