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,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,9 @@
1
+ """Routing strategies module."""
2
+
3
+ from .dynamic_complexity import DynamicComplexityStrategy
4
+ from .self_assessment import SelfAssessmentStrategy
5
+
6
+ __all__ = [
7
+ "SelfAssessmentStrategy",
8
+ "DynamicComplexityStrategy",
9
+ ]
@@ -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))