kailash 0.3.2__py3-none-any.whl → 0.4.1__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.
- kailash/__init__.py +33 -1
- kailash/access_control/__init__.py +129 -0
- kailash/access_control/managers.py +461 -0
- kailash/access_control/rule_evaluators.py +467 -0
- kailash/access_control_abac.py +825 -0
- kailash/config/__init__.py +27 -0
- kailash/config/database_config.py +359 -0
- kailash/database/__init__.py +28 -0
- kailash/database/execution_pipeline.py +499 -0
- kailash/middleware/__init__.py +306 -0
- kailash/middleware/auth/__init__.py +33 -0
- kailash/middleware/auth/access_control.py +436 -0
- kailash/middleware/auth/auth_manager.py +422 -0
- kailash/middleware/auth/jwt_auth.py +477 -0
- kailash/middleware/auth/kailash_jwt_auth.py +616 -0
- kailash/middleware/communication/__init__.py +37 -0
- kailash/middleware/communication/ai_chat.py +989 -0
- kailash/middleware/communication/api_gateway.py +802 -0
- kailash/middleware/communication/events.py +470 -0
- kailash/middleware/communication/realtime.py +710 -0
- kailash/middleware/core/__init__.py +21 -0
- kailash/middleware/core/agent_ui.py +890 -0
- kailash/middleware/core/schema.py +643 -0
- kailash/middleware/core/workflows.py +396 -0
- kailash/middleware/database/__init__.py +63 -0
- kailash/middleware/database/base.py +113 -0
- kailash/middleware/database/base_models.py +525 -0
- kailash/middleware/database/enums.py +106 -0
- kailash/middleware/database/migrations.py +12 -0
- kailash/{api/database.py → middleware/database/models.py} +183 -291
- kailash/middleware/database/repositories.py +685 -0
- kailash/middleware/database/session_manager.py +19 -0
- kailash/middleware/mcp/__init__.py +38 -0
- kailash/middleware/mcp/client_integration.py +585 -0
- kailash/middleware/mcp/enhanced_server.py +576 -0
- kailash/nodes/__init__.py +27 -3
- kailash/nodes/admin/__init__.py +42 -0
- kailash/nodes/admin/audit_log.py +794 -0
- kailash/nodes/admin/permission_check.py +864 -0
- kailash/nodes/admin/role_management.py +823 -0
- kailash/nodes/admin/security_event.py +1523 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +248 -40
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +436 -5
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/ai/vision_utils.py +148 -0
- kailash/nodes/alerts/__init__.py +26 -0
- kailash/nodes/alerts/base.py +234 -0
- kailash/nodes/alerts/discord.py +499 -0
- kailash/nodes/api/auth.py +287 -6
- kailash/nodes/api/rest.py +151 -0
- kailash/nodes/auth/__init__.py +17 -0
- kailash/nodes/auth/directory_integration.py +1228 -0
- kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
- kailash/nodes/auth/mfa.py +2338 -0
- kailash/nodes/auth/risk_assessment.py +872 -0
- kailash/nodes/auth/session_management.py +1093 -0
- kailash/nodes/auth/sso.py +1040 -0
- kailash/nodes/base.py +344 -13
- kailash/nodes/base_cycle_aware.py +4 -2
- kailash/nodes/base_with_acl.py +1 -1
- kailash/nodes/code/python.py +283 -10
- kailash/nodes/compliance/__init__.py +9 -0
- kailash/nodes/compliance/data_retention.py +1888 -0
- kailash/nodes/compliance/gdpr.py +2004 -0
- kailash/nodes/data/__init__.py +22 -2
- kailash/nodes/data/async_connection.py +469 -0
- kailash/nodes/data/async_sql.py +757 -0
- kailash/nodes/data/async_vector.py +598 -0
- kailash/nodes/data/readers.py +767 -0
- kailash/nodes/data/retrieval.py +360 -1
- kailash/nodes/data/sharepoint_graph.py +397 -21
- kailash/nodes/data/sql.py +94 -5
- kailash/nodes/data/streaming.py +68 -8
- kailash/nodes/data/vector_db.py +54 -4
- kailash/nodes/enterprise/__init__.py +13 -0
- kailash/nodes/enterprise/batch_processor.py +741 -0
- kailash/nodes/enterprise/data_lineage.py +497 -0
- kailash/nodes/logic/convergence.py +31 -9
- kailash/nodes/logic/operations.py +14 -3
- kailash/nodes/mixins/__init__.py +8 -0
- kailash/nodes/mixins/event_emitter.py +201 -0
- kailash/nodes/mixins/mcp.py +9 -4
- kailash/nodes/mixins/security.py +165 -0
- kailash/nodes/monitoring/__init__.py +7 -0
- kailash/nodes/monitoring/performance_benchmark.py +2497 -0
- kailash/nodes/rag/__init__.py +284 -0
- kailash/nodes/rag/advanced.py +1615 -0
- kailash/nodes/rag/agentic.py +773 -0
- kailash/nodes/rag/conversational.py +999 -0
- kailash/nodes/rag/evaluation.py +875 -0
- kailash/nodes/rag/federated.py +1188 -0
- kailash/nodes/rag/graph.py +721 -0
- kailash/nodes/rag/multimodal.py +671 -0
- kailash/nodes/rag/optimized.py +933 -0
- kailash/nodes/rag/privacy.py +1059 -0
- kailash/nodes/rag/query_processing.py +1335 -0
- kailash/nodes/rag/realtime.py +764 -0
- kailash/nodes/rag/registry.py +547 -0
- kailash/nodes/rag/router.py +837 -0
- kailash/nodes/rag/similarity.py +1854 -0
- kailash/nodes/rag/strategies.py +566 -0
- kailash/nodes/rag/workflows.py +575 -0
- kailash/nodes/security/__init__.py +19 -0
- kailash/nodes/security/abac_evaluator.py +1411 -0
- kailash/nodes/security/audit_log.py +103 -0
- kailash/nodes/security/behavior_analysis.py +1893 -0
- kailash/nodes/security/credential_manager.py +401 -0
- kailash/nodes/security/rotating_credentials.py +760 -0
- kailash/nodes/security/security_event.py +133 -0
- kailash/nodes/security/threat_detection.py +1103 -0
- kailash/nodes/testing/__init__.py +9 -0
- kailash/nodes/testing/credential_testing.py +499 -0
- kailash/nodes/transform/__init__.py +10 -2
- kailash/nodes/transform/chunkers.py +592 -1
- kailash/nodes/transform/processors.py +484 -14
- kailash/nodes/validation.py +321 -0
- kailash/runtime/access_controlled.py +1 -1
- kailash/runtime/async_local.py +41 -7
- kailash/runtime/docker.py +1 -1
- kailash/runtime/local.py +474 -55
- kailash/runtime/parallel.py +1 -1
- kailash/runtime/parallel_cyclic.py +1 -1
- kailash/runtime/testing.py +210 -2
- kailash/security.py +1 -1
- kailash/utils/migrations/__init__.py +25 -0
- kailash/utils/migrations/generator.py +433 -0
- kailash/utils/migrations/models.py +231 -0
- kailash/utils/migrations/runner.py +489 -0
- kailash/utils/secure_logging.py +342 -0
- kailash/workflow/__init__.py +16 -0
- kailash/workflow/cyclic_runner.py +3 -4
- kailash/workflow/graph.py +70 -2
- kailash/workflow/resilience.py +249 -0
- kailash/workflow/templates.py +726 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/METADATA +256 -20
- kailash-0.4.1.dist-info/RECORD +227 -0
- kailash/api/__init__.py +0 -17
- kailash/api/__main__.py +0 -6
- kailash/api/studio_secure.py +0 -893
- kailash/mcp/__main__.py +0 -13
- kailash/mcp/server_new.py +0 -336
- kailash/mcp/servers/__init__.py +0 -12
- kailash-0.3.2.dist-info/RECORD +0 -136
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/WHEEL +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,837 @@
|
|
1
|
+
"""
|
2
|
+
RAG Strategy Router and Analysis Nodes
|
3
|
+
|
4
|
+
Intelligent routing and analysis components for RAG strategies.
|
5
|
+
Includes LLM-powered strategy selection and performance monitoring.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
import time
|
10
|
+
from typing import Any, Dict, List, Optional
|
11
|
+
|
12
|
+
from ..ai.llm_agent import LLMAgentNode
|
13
|
+
from ..base import Node, NodeParameter, register_node
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
@register_node()
|
19
|
+
class RAGStrategyRouterNode(Node):
|
20
|
+
"""
|
21
|
+
RAG Strategy Router Node
|
22
|
+
|
23
|
+
LLM-powered intelligent routing that analyzes documents and queries
|
24
|
+
to automatically select the optimal RAG strategy for each use case.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
name: str = "rag_strategy_router",
|
30
|
+
llm_model: str = "gpt-4",
|
31
|
+
provider: str = "openai",
|
32
|
+
):
|
33
|
+
self.llm_model = llm_model
|
34
|
+
self.provider = provider
|
35
|
+
self.llm_agent = None
|
36
|
+
super().__init__(name)
|
37
|
+
|
38
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
39
|
+
return {
|
40
|
+
"documents": NodeParameter(
|
41
|
+
name="documents",
|
42
|
+
type=list,
|
43
|
+
required=True,
|
44
|
+
description="Documents to analyze for strategy selection",
|
45
|
+
),
|
46
|
+
"query": NodeParameter(
|
47
|
+
name="query",
|
48
|
+
type=str,
|
49
|
+
required=False,
|
50
|
+
description="Query context for strategy optimization",
|
51
|
+
),
|
52
|
+
"user_preferences": NodeParameter(
|
53
|
+
name="user_preferences",
|
54
|
+
type=dict,
|
55
|
+
required=False,
|
56
|
+
description="User preferences for strategy selection",
|
57
|
+
),
|
58
|
+
"performance_history": NodeParameter(
|
59
|
+
name="performance_history",
|
60
|
+
type=dict,
|
61
|
+
required=False,
|
62
|
+
description="Historical performance data for strategy optimization",
|
63
|
+
),
|
64
|
+
}
|
65
|
+
|
66
|
+
def run(self, **kwargs) -> Dict[str, Any]:
|
67
|
+
"""Analyze and route to optimal RAG strategy"""
|
68
|
+
documents = kwargs.get("documents", [])
|
69
|
+
query = kwargs.get("query", "")
|
70
|
+
user_preferences = kwargs.get("user_preferences", {})
|
71
|
+
performance_history = kwargs.get("performance_history", {})
|
72
|
+
|
73
|
+
# Initialize LLM agent if needed
|
74
|
+
if not self.llm_agent:
|
75
|
+
self.llm_agent = LLMAgentNode(
|
76
|
+
name=f"{self.name}_llm",
|
77
|
+
model=self.llm_model,
|
78
|
+
provider=self.provider,
|
79
|
+
system_prompt=self._get_strategy_selection_prompt(),
|
80
|
+
)
|
81
|
+
|
82
|
+
# Analyze documents
|
83
|
+
analysis = self._analyze_documents(documents, query)
|
84
|
+
|
85
|
+
# Get LLM recommendation
|
86
|
+
llm_input = self._format_llm_input(
|
87
|
+
analysis, query, user_preferences, performance_history
|
88
|
+
)
|
89
|
+
|
90
|
+
try:
|
91
|
+
llm_response = self.llm_agent.run(
|
92
|
+
messages=[{"role": "user", "content": llm_input}]
|
93
|
+
)
|
94
|
+
|
95
|
+
strategy_decision = self._parse_llm_response(llm_response)
|
96
|
+
|
97
|
+
except Exception as e:
|
98
|
+
logger.warning(f"LLM strategy selection failed: {e}, using fallback")
|
99
|
+
strategy_decision = self._fallback_strategy_selection(analysis)
|
100
|
+
|
101
|
+
# Combine analysis with decision
|
102
|
+
return {
|
103
|
+
"strategy": strategy_decision["recommended_strategy"],
|
104
|
+
"reasoning": strategy_decision["reasoning"],
|
105
|
+
"confidence": strategy_decision["confidence"],
|
106
|
+
"fallback_strategy": strategy_decision.get("fallback_strategy", "hybrid"),
|
107
|
+
"document_analysis": analysis,
|
108
|
+
"llm_model_used": self.llm_model,
|
109
|
+
"routing_metadata": {
|
110
|
+
"timestamp": time.time(),
|
111
|
+
"documents_count": len(documents),
|
112
|
+
"query_provided": bool(query),
|
113
|
+
"user_preferences_provided": bool(user_preferences),
|
114
|
+
},
|
115
|
+
}
|
116
|
+
|
117
|
+
def _get_strategy_selection_prompt(self) -> str:
|
118
|
+
"""Get system prompt for strategy selection"""
|
119
|
+
return """You are an expert RAG (Retrieval Augmented Generation) strategy advisor. Your job is to analyze documents and queries to recommend the optimal RAG approach.
|
120
|
+
|
121
|
+
Available RAG strategies:
|
122
|
+
|
123
|
+
1. **semantic**: Uses semantic chunking with dense embeddings
|
124
|
+
- Best for: Narrative content, general Q&A, conceptual queries
|
125
|
+
- Strengths: Excellent semantic similarity matching
|
126
|
+
- Use when: Documents have flowing text, user asks conceptual questions
|
127
|
+
|
128
|
+
2. **statistical**: Uses statistical chunking with sparse keyword matching
|
129
|
+
- Best for: Technical documentation, code, structured content
|
130
|
+
- Strengths: Precise keyword matching, handles technical terms well
|
131
|
+
- Use when: Documents are technical, contain code, or need exact term matching
|
132
|
+
|
133
|
+
3. **hybrid**: Combines semantic + statistical with result fusion
|
134
|
+
- Best for: Mixed content types, most general use cases
|
135
|
+
- Strengths: 20-30% better performance than single methods
|
136
|
+
- Use when: Unsure about content type or want maximum coverage
|
137
|
+
|
138
|
+
4. **hierarchical**: Multi-level processing preserving document structure
|
139
|
+
- Best for: Long documents, structured content with sections/headings
|
140
|
+
- Strengths: Maintains context relationships, handles complex documents
|
141
|
+
- Use when: Documents are long (>2000 chars) with clear structure
|
142
|
+
|
143
|
+
Performance considerations:
|
144
|
+
- semantic: Fast, good for most queries
|
145
|
+
- statistical: Fast, precise for technical content
|
146
|
+
- hybrid: Slower but more comprehensive
|
147
|
+
- hierarchical: Slowest but best for complex documents
|
148
|
+
|
149
|
+
Respond with ONLY a valid JSON object in this exact format:
|
150
|
+
{
|
151
|
+
"recommended_strategy": "semantic|statistical|hybrid|hierarchical",
|
152
|
+
"reasoning": "Brief explanation (max 100 words)",
|
153
|
+
"confidence": 0.0-1.0,
|
154
|
+
"fallback_strategy": "backup strategy if primary fails"
|
155
|
+
}"""
|
156
|
+
|
157
|
+
def _analyze_documents(self, documents: List[Dict], query: str) -> Dict[str, Any]:
|
158
|
+
"""Analyze documents for strategy selection"""
|
159
|
+
if not documents:
|
160
|
+
return {
|
161
|
+
"total_docs": 0,
|
162
|
+
"avg_length": 0,
|
163
|
+
"total_length": 0,
|
164
|
+
"has_structure": False,
|
165
|
+
"is_technical": False,
|
166
|
+
"content_types": [],
|
167
|
+
"complexity_score": 0.0,
|
168
|
+
}
|
169
|
+
|
170
|
+
# Basic statistics
|
171
|
+
total_length = sum(len(doc.get("content", "")) for doc in documents)
|
172
|
+
avg_length = total_length / len(documents)
|
173
|
+
|
174
|
+
# Structure detection
|
175
|
+
structure_indicators = [
|
176
|
+
"# ",
|
177
|
+
"## ",
|
178
|
+
"### ",
|
179
|
+
"heading",
|
180
|
+
"section",
|
181
|
+
"chapter",
|
182
|
+
"table of contents",
|
183
|
+
]
|
184
|
+
has_structure = any(
|
185
|
+
any(
|
186
|
+
indicator in doc.get("content", "").lower()
|
187
|
+
for indicator in structure_indicators
|
188
|
+
)
|
189
|
+
for doc in documents
|
190
|
+
)
|
191
|
+
|
192
|
+
# Technical content detection
|
193
|
+
technical_keywords = [
|
194
|
+
"function",
|
195
|
+
"class",
|
196
|
+
"import",
|
197
|
+
"def ",
|
198
|
+
"return",
|
199
|
+
"variable",
|
200
|
+
"algorithm",
|
201
|
+
"api",
|
202
|
+
"code",
|
203
|
+
"programming",
|
204
|
+
"software",
|
205
|
+
"system",
|
206
|
+
"method",
|
207
|
+
"object",
|
208
|
+
"parameter",
|
209
|
+
"configuration",
|
210
|
+
"install",
|
211
|
+
]
|
212
|
+
technical_content_ratio = self._calculate_keyword_ratio(
|
213
|
+
documents, technical_keywords
|
214
|
+
)
|
215
|
+
is_technical = technical_content_ratio > 0.1
|
216
|
+
|
217
|
+
# Content type classification
|
218
|
+
content_types = []
|
219
|
+
if has_structure:
|
220
|
+
content_types.append("structured")
|
221
|
+
if is_technical:
|
222
|
+
content_types.append("technical")
|
223
|
+
if avg_length > 2000:
|
224
|
+
content_types.append("long_form")
|
225
|
+
if len(documents) > 100:
|
226
|
+
content_types.append("large_collection")
|
227
|
+
if technical_content_ratio > 0.3:
|
228
|
+
content_types.append("highly_technical")
|
229
|
+
|
230
|
+
# Complexity score (0.0 to 1.0)
|
231
|
+
complexity_factors = [
|
232
|
+
min(avg_length / 5000, 1.0), # Length complexity
|
233
|
+
min(len(documents) / 200, 1.0), # Collection size complexity
|
234
|
+
technical_content_ratio, # Technical complexity
|
235
|
+
1.0 if has_structure else 0.0, # Structure complexity
|
236
|
+
]
|
237
|
+
complexity_score = sum(complexity_factors) / len(complexity_factors)
|
238
|
+
|
239
|
+
return {
|
240
|
+
"total_docs": len(documents),
|
241
|
+
"avg_length": int(avg_length),
|
242
|
+
"total_length": total_length,
|
243
|
+
"has_structure": has_structure,
|
244
|
+
"is_technical": is_technical,
|
245
|
+
"technical_content_ratio": technical_content_ratio,
|
246
|
+
"content_types": content_types,
|
247
|
+
"complexity_score": complexity_score,
|
248
|
+
"query_analysis": self._analyze_query(query) if query else None,
|
249
|
+
}
|
250
|
+
|
251
|
+
def _calculate_keyword_ratio(
|
252
|
+
self, documents: List[Dict], keywords: List[str]
|
253
|
+
) -> float:
|
254
|
+
"""Calculate ratio of technical keywords in documents"""
|
255
|
+
if not documents:
|
256
|
+
return 0.0
|
257
|
+
|
258
|
+
total_words = 0
|
259
|
+
keyword_matches = 0
|
260
|
+
|
261
|
+
for doc in documents:
|
262
|
+
content = doc.get("content", "").lower()
|
263
|
+
words = content.split()
|
264
|
+
total_words += len(words)
|
265
|
+
|
266
|
+
for keyword in keywords:
|
267
|
+
keyword_matches += content.count(keyword.lower())
|
268
|
+
|
269
|
+
return keyword_matches / max(total_words, 1)
|
270
|
+
|
271
|
+
def _analyze_query(self, query: str) -> Dict[str, Any]:
|
272
|
+
"""Analyze query characteristics"""
|
273
|
+
query_lower = query.lower()
|
274
|
+
|
275
|
+
# Query type detection
|
276
|
+
question_indicators = ["what", "how", "why", "when", "where", "who", "which"]
|
277
|
+
is_question = any(indicator in query_lower for indicator in question_indicators)
|
278
|
+
|
279
|
+
technical_query_keywords = [
|
280
|
+
"function",
|
281
|
+
"code",
|
282
|
+
"api",
|
283
|
+
"error",
|
284
|
+
"install",
|
285
|
+
"configure",
|
286
|
+
]
|
287
|
+
is_technical_query = any(
|
288
|
+
keyword in query_lower for keyword in technical_query_keywords
|
289
|
+
)
|
290
|
+
|
291
|
+
conceptual_keywords = [
|
292
|
+
"explain",
|
293
|
+
"understand",
|
294
|
+
"concept",
|
295
|
+
"idea",
|
296
|
+
"meaning",
|
297
|
+
"definition",
|
298
|
+
]
|
299
|
+
is_conceptual = any(keyword in query_lower for keyword in conceptual_keywords)
|
300
|
+
|
301
|
+
return {
|
302
|
+
"length": len(query),
|
303
|
+
"is_question": is_question,
|
304
|
+
"is_technical": is_technical_query,
|
305
|
+
"is_conceptual": is_conceptual,
|
306
|
+
"complexity": len(query.split())
|
307
|
+
/ 10.0, # Rough complexity based on word count
|
308
|
+
}
|
309
|
+
|
310
|
+
def _format_llm_input(
|
311
|
+
self,
|
312
|
+
analysis: Dict,
|
313
|
+
query: str,
|
314
|
+
user_preferences: Dict,
|
315
|
+
performance_history: Dict,
|
316
|
+
) -> str:
|
317
|
+
"""Format input for LLM strategy selection"""
|
318
|
+
|
319
|
+
input_text = f"""Analyze this RAG use case and recommend the optimal strategy:
|
320
|
+
|
321
|
+
DOCUMENT ANALYSIS:
|
322
|
+
- Total documents: {analysis['total_docs']}
|
323
|
+
- Average length: {analysis['avg_length']} characters
|
324
|
+
- Total content: {analysis['total_length']} characters
|
325
|
+
- Has structure (headings/sections): {analysis['has_structure']}
|
326
|
+
- Technical content: {analysis['is_technical']} (ratio: {analysis.get('technical_content_ratio', 0):.2f})
|
327
|
+
- Content types: {', '.join(analysis['content_types'])}
|
328
|
+
- Complexity score: {analysis['complexity_score']:.2f}/1.0
|
329
|
+
|
330
|
+
QUERY ANALYSIS:"""
|
331
|
+
|
332
|
+
if query:
|
333
|
+
query_analysis = analysis.get("query_analysis", {})
|
334
|
+
input_text += f"""
|
335
|
+
- Query: "{query}"
|
336
|
+
- Is question: {query_analysis.get('is_question', False)}
|
337
|
+
- Technical query: {query_analysis.get('is_technical', False)}
|
338
|
+
- Conceptual query: {query_analysis.get('is_conceptual', False)}
|
339
|
+
- Query complexity: {query_analysis.get('complexity', 0):.2f}"""
|
340
|
+
else:
|
341
|
+
input_text += "\n- No query provided (indexing mode)"
|
342
|
+
|
343
|
+
if user_preferences:
|
344
|
+
input_text += f"\n\nUSER PREFERENCES:\n{user_preferences}"
|
345
|
+
|
346
|
+
if performance_history:
|
347
|
+
input_text += f"\n\nPERFORMANCE HISTORY:\n{performance_history}"
|
348
|
+
|
349
|
+
input_text += "\n\nRecommend the optimal RAG strategy:"
|
350
|
+
|
351
|
+
return input_text
|
352
|
+
|
353
|
+
def _parse_llm_response(self, llm_response: Dict) -> Dict[str, Any]:
|
354
|
+
"""Parse LLM response to extract strategy decision"""
|
355
|
+
try:
|
356
|
+
import json
|
357
|
+
|
358
|
+
# Extract content from LLM response
|
359
|
+
content = llm_response.get("content", "")
|
360
|
+
if isinstance(content, list):
|
361
|
+
content = content[0] if content else ""
|
362
|
+
|
363
|
+
# Try to parse as JSON
|
364
|
+
if "{" in content and "}" in content:
|
365
|
+
json_start = content.find("{")
|
366
|
+
json_end = content.rfind("}") + 1
|
367
|
+
json_str = content[json_start:json_end]
|
368
|
+
|
369
|
+
decision = json.loads(json_str)
|
370
|
+
|
371
|
+
# Validate required fields
|
372
|
+
required_fields = ["recommended_strategy", "reasoning", "confidence"]
|
373
|
+
if all(field in decision for field in required_fields):
|
374
|
+
return decision
|
375
|
+
|
376
|
+
# Fallback parsing
|
377
|
+
return self._parse_fallback_response(content)
|
378
|
+
|
379
|
+
except Exception as e:
|
380
|
+
logger.warning(f"Failed to parse LLM response: {e}")
|
381
|
+
return {
|
382
|
+
"recommended_strategy": "hybrid",
|
383
|
+
"reasoning": "LLM parsing failed, using safe default",
|
384
|
+
"confidence": 0.5,
|
385
|
+
"fallback_strategy": "semantic",
|
386
|
+
}
|
387
|
+
|
388
|
+
def _parse_fallback_response(self, content: str) -> Dict[str, Any]:
|
389
|
+
"""Fallback parsing for non-JSON LLM responses"""
|
390
|
+
content_lower = content.lower()
|
391
|
+
|
392
|
+
# Strategy detection
|
393
|
+
strategies = ["semantic", "statistical", "hybrid", "hierarchical"]
|
394
|
+
detected_strategy = "hybrid" # default
|
395
|
+
|
396
|
+
for strategy in strategies:
|
397
|
+
if strategy in content_lower:
|
398
|
+
detected_strategy = strategy
|
399
|
+
break
|
400
|
+
|
401
|
+
# Extract reasoning (first sentence or up to 100 chars)
|
402
|
+
sentences = content.split(".")
|
403
|
+
reasoning = (
|
404
|
+
sentences[0][:100]
|
405
|
+
if sentences
|
406
|
+
else "Strategy selected based on content analysis"
|
407
|
+
)
|
408
|
+
|
409
|
+
return {
|
410
|
+
"recommended_strategy": detected_strategy,
|
411
|
+
"reasoning": reasoning,
|
412
|
+
"confidence": 0.7,
|
413
|
+
"fallback_strategy": "hybrid",
|
414
|
+
}
|
415
|
+
|
416
|
+
def _fallback_strategy_selection(self, analysis: Dict) -> Dict[str, Any]:
|
417
|
+
"""Rule-based fallback strategy selection when LLM fails"""
|
418
|
+
|
419
|
+
# Rule-based selection
|
420
|
+
if analysis["complexity_score"] > 0.7 and analysis["has_structure"]:
|
421
|
+
strategy = "hierarchical"
|
422
|
+
reasoning = "High complexity with structured content detected"
|
423
|
+
elif (
|
424
|
+
analysis["is_technical"]
|
425
|
+
and analysis.get("technical_content_ratio", 0) > 0.2
|
426
|
+
):
|
427
|
+
strategy = "statistical"
|
428
|
+
reasoning = "Technical content detected, using keyword-based retrieval"
|
429
|
+
elif analysis["total_docs"] > 50 or analysis["avg_length"] > 1000:
|
430
|
+
strategy = "hybrid"
|
431
|
+
reasoning = "Large document collection, using hybrid approach"
|
432
|
+
else:
|
433
|
+
strategy = "semantic"
|
434
|
+
reasoning = "General content, using semantic similarity"
|
435
|
+
|
436
|
+
return {
|
437
|
+
"recommended_strategy": strategy,
|
438
|
+
"reasoning": reasoning,
|
439
|
+
"confidence": 0.8,
|
440
|
+
"fallback_strategy": "hybrid",
|
441
|
+
}
|
442
|
+
|
443
|
+
|
444
|
+
@register_node()
|
445
|
+
class RAGQualityAnalyzerNode(Node):
|
446
|
+
"""
|
447
|
+
RAG Quality Analyzer Node
|
448
|
+
|
449
|
+
Analyzes RAG results quality and provides recommendations for optimization.
|
450
|
+
Tracks performance metrics and suggests improvements.
|
451
|
+
"""
|
452
|
+
|
453
|
+
def __init__(self, name: str = "rag_quality_analyzer"):
|
454
|
+
super().__init__(name)
|
455
|
+
|
456
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
457
|
+
return {
|
458
|
+
"rag_results": NodeParameter(
|
459
|
+
name="rag_results",
|
460
|
+
type=dict,
|
461
|
+
required=True,
|
462
|
+
description="RAG results to analyze",
|
463
|
+
),
|
464
|
+
"query": NodeParameter(
|
465
|
+
name="query",
|
466
|
+
type=str,
|
467
|
+
required=False,
|
468
|
+
description="Original query for relevance assessment",
|
469
|
+
),
|
470
|
+
"expected_results": NodeParameter(
|
471
|
+
name="expected_results",
|
472
|
+
type=list,
|
473
|
+
required=False,
|
474
|
+
description="Expected results for validation (if available)",
|
475
|
+
),
|
476
|
+
}
|
477
|
+
|
478
|
+
def run(self, **kwargs) -> Dict[str, Any]:
|
479
|
+
"""Analyze RAG results quality"""
|
480
|
+
rag_results = kwargs.get("rag_results", {})
|
481
|
+
query = kwargs.get("query", "")
|
482
|
+
expected_results = kwargs.get("expected_results", [])
|
483
|
+
|
484
|
+
# Extract results and scores
|
485
|
+
documents = rag_results.get("results", rag_results.get("documents", []))
|
486
|
+
scores = rag_results.get("scores", [])
|
487
|
+
|
488
|
+
# Quality metrics
|
489
|
+
quality_analysis = {
|
490
|
+
"result_count": len(documents),
|
491
|
+
"has_scores": len(scores) > 0,
|
492
|
+
"avg_score": sum(scores) / len(scores) if scores else 0.0,
|
493
|
+
"min_score": min(scores) if scores else 0.0,
|
494
|
+
"max_score": max(scores) if scores else 0.0,
|
495
|
+
"score_variance": self._calculate_variance(scores) if scores else 0.0,
|
496
|
+
}
|
497
|
+
|
498
|
+
# Content quality analysis
|
499
|
+
content_analysis = self._analyze_content_quality(documents, query)
|
500
|
+
|
501
|
+
# Performance assessment
|
502
|
+
performance_score = self._calculate_performance_score(
|
503
|
+
quality_analysis, content_analysis
|
504
|
+
)
|
505
|
+
|
506
|
+
# Recommendations
|
507
|
+
recommendations = self._generate_recommendations(
|
508
|
+
quality_analysis, content_analysis, rag_results
|
509
|
+
)
|
510
|
+
|
511
|
+
return {
|
512
|
+
"quality_score": performance_score,
|
513
|
+
"quality_analysis": quality_analysis,
|
514
|
+
"content_analysis": content_analysis,
|
515
|
+
"recommendations": recommendations,
|
516
|
+
"passed_quality_check": performance_score > 0.6,
|
517
|
+
"analysis_timestamp": time.time(),
|
518
|
+
}
|
519
|
+
|
520
|
+
def _analyze_content_quality(
|
521
|
+
self, documents: List[Dict], query: str
|
522
|
+
) -> Dict[str, Any]:
|
523
|
+
"""Analyze the quality of retrieved content"""
|
524
|
+
if not documents:
|
525
|
+
return {
|
526
|
+
"diversity_score": 0.0,
|
527
|
+
"avg_content_length": 0,
|
528
|
+
"content_coverage": 0.0,
|
529
|
+
"duplicate_ratio": 0.0,
|
530
|
+
}
|
531
|
+
|
532
|
+
# Content diversity (based on unique content)
|
533
|
+
unique_contents = set()
|
534
|
+
total_length = 0
|
535
|
+
|
536
|
+
for doc in documents:
|
537
|
+
content = doc.get("content", "")
|
538
|
+
unique_contents.add(content[:100]) # First 100 chars for uniqueness
|
539
|
+
total_length += len(content)
|
540
|
+
|
541
|
+
diversity_score = len(unique_contents) / len(documents)
|
542
|
+
avg_content_length = total_length / len(documents)
|
543
|
+
|
544
|
+
# Query coverage (simple keyword matching)
|
545
|
+
coverage_score = 0.0
|
546
|
+
if query:
|
547
|
+
query_words = set(query.lower().split())
|
548
|
+
if query_words:
|
549
|
+
covered_words = 0
|
550
|
+
for doc in documents:
|
551
|
+
doc_words = set(doc.get("content", "").lower().split())
|
552
|
+
covered_words += len(query_words.intersection(doc_words))
|
553
|
+
coverage_score = covered_words / (len(query_words) * len(documents))
|
554
|
+
|
555
|
+
# Duplicate detection
|
556
|
+
duplicate_ratio = 1.0 - diversity_score
|
557
|
+
|
558
|
+
return {
|
559
|
+
"diversity_score": diversity_score,
|
560
|
+
"avg_content_length": avg_content_length,
|
561
|
+
"content_coverage": coverage_score,
|
562
|
+
"duplicate_ratio": duplicate_ratio,
|
563
|
+
}
|
564
|
+
|
565
|
+
def _calculate_variance(self, scores: List[float]) -> float:
|
566
|
+
"""Calculate variance of scores"""
|
567
|
+
if len(scores) < 2:
|
568
|
+
return 0.0
|
569
|
+
|
570
|
+
mean = sum(scores) / len(scores)
|
571
|
+
variance = sum((x - mean) ** 2 for x in scores) / len(scores)
|
572
|
+
return variance
|
573
|
+
|
574
|
+
def _calculate_performance_score(
|
575
|
+
self, quality_analysis: Dict, content_analysis: Dict
|
576
|
+
) -> float:
|
577
|
+
"""Calculate overall performance score"""
|
578
|
+
factors = [
|
579
|
+
min(
|
580
|
+
quality_analysis["result_count"] / 5.0, 1.0
|
581
|
+
), # Result count (max score at 5 results)
|
582
|
+
quality_analysis["avg_score"], # Average relevance score
|
583
|
+
content_analysis["diversity_score"], # Content diversity
|
584
|
+
content_analysis["content_coverage"], # Query coverage
|
585
|
+
1.0 - content_analysis["duplicate_ratio"], # Inverse of duplicate ratio
|
586
|
+
]
|
587
|
+
|
588
|
+
# Weighted average
|
589
|
+
weights = [0.2, 0.3, 0.2, 0.2, 0.1]
|
590
|
+
performance_score = sum(f * w for f, w in zip(factors, weights))
|
591
|
+
|
592
|
+
return min(max(performance_score, 0.0), 1.0)
|
593
|
+
|
594
|
+
def _generate_recommendations(
|
595
|
+
self, quality_analysis: Dict, content_analysis: Dict, rag_results: Dict
|
596
|
+
) -> List[str]:
|
597
|
+
"""Generate recommendations for improving RAG performance"""
|
598
|
+
recommendations = []
|
599
|
+
|
600
|
+
# Result count recommendations
|
601
|
+
if quality_analysis["result_count"] < 3:
|
602
|
+
recommendations.append(
|
603
|
+
"Consider lowering similarity threshold to retrieve more results"
|
604
|
+
)
|
605
|
+
elif quality_analysis["result_count"] > 10:
|
606
|
+
recommendations.append(
|
607
|
+
"Consider raising similarity threshold to get more focused results"
|
608
|
+
)
|
609
|
+
|
610
|
+
# Score quality recommendations
|
611
|
+
if quality_analysis["avg_score"] < 0.5:
|
612
|
+
recommendations.append(
|
613
|
+
"Low relevance scores detected - consider different chunking strategy"
|
614
|
+
)
|
615
|
+
|
616
|
+
if quality_analysis["score_variance"] > 0.3:
|
617
|
+
recommendations.append(
|
618
|
+
"High score variance - results quality is inconsistent"
|
619
|
+
)
|
620
|
+
|
621
|
+
# Content quality recommendations
|
622
|
+
if content_analysis["diversity_score"] < 0.7:
|
623
|
+
recommendations.append(
|
624
|
+
"High duplicate content - consider improving deduplication"
|
625
|
+
)
|
626
|
+
|
627
|
+
if content_analysis["content_coverage"] < 0.3:
|
628
|
+
recommendations.append(
|
629
|
+
"Poor query coverage - consider hybrid retrieval strategy"
|
630
|
+
)
|
631
|
+
|
632
|
+
if content_analysis["avg_content_length"] < 100:
|
633
|
+
recommendations.append(
|
634
|
+
"Very short content chunks - consider larger chunk sizes"
|
635
|
+
)
|
636
|
+
elif content_analysis["avg_content_length"] > 2000:
|
637
|
+
recommendations.append(
|
638
|
+
"Very long content chunks - consider smaller chunk sizes"
|
639
|
+
)
|
640
|
+
|
641
|
+
# Strategy-specific recommendations
|
642
|
+
strategy_used = rag_results.get("strategy_used", "unknown")
|
643
|
+
if strategy_used == "semantic" and quality_analysis["avg_score"] < 0.6:
|
644
|
+
recommendations.append(
|
645
|
+
"Consider switching to hybrid strategy for better coverage"
|
646
|
+
)
|
647
|
+
elif (
|
648
|
+
strategy_used == "statistical"
|
649
|
+
and content_analysis["content_coverage"] < 0.4
|
650
|
+
):
|
651
|
+
recommendations.append(
|
652
|
+
"Consider switching to semantic strategy for better understanding"
|
653
|
+
)
|
654
|
+
|
655
|
+
return recommendations
|
656
|
+
|
657
|
+
|
658
|
+
@register_node()
|
659
|
+
class RAGPerformanceMonitorNode(Node):
|
660
|
+
"""
|
661
|
+
RAG Performance Monitor Node
|
662
|
+
|
663
|
+
Monitors RAG system performance over time and provides insights
|
664
|
+
for optimization and strategy adjustment.
|
665
|
+
"""
|
666
|
+
|
667
|
+
def __init__(self, name: str = "rag_performance_monitor"):
|
668
|
+
self.performance_history = []
|
669
|
+
super().__init__(name)
|
670
|
+
|
671
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
672
|
+
return {
|
673
|
+
"rag_results": NodeParameter(
|
674
|
+
name="rag_results",
|
675
|
+
type=dict,
|
676
|
+
required=True,
|
677
|
+
description="RAG results to monitor",
|
678
|
+
),
|
679
|
+
"execution_time": NodeParameter(
|
680
|
+
name="execution_time",
|
681
|
+
type=float,
|
682
|
+
required=False,
|
683
|
+
description="Execution time in seconds",
|
684
|
+
),
|
685
|
+
"strategy_used": NodeParameter(
|
686
|
+
name="strategy_used",
|
687
|
+
type=str,
|
688
|
+
required=False,
|
689
|
+
description="RAG strategy that was used",
|
690
|
+
),
|
691
|
+
"query_type": NodeParameter(
|
692
|
+
name="query_type",
|
693
|
+
type=str,
|
694
|
+
required=False,
|
695
|
+
description="Type of query (technical, conceptual, etc.)",
|
696
|
+
),
|
697
|
+
}
|
698
|
+
|
699
|
+
def run(self, **kwargs) -> Dict[str, Any]:
|
700
|
+
"""Monitor and record RAG performance"""
|
701
|
+
rag_results = kwargs.get("rag_results", {})
|
702
|
+
execution_time = kwargs.get("execution_time", 0.0)
|
703
|
+
strategy_used = kwargs.get("strategy_used", "unknown")
|
704
|
+
query_type = kwargs.get("query_type", "general")
|
705
|
+
|
706
|
+
# Create performance record
|
707
|
+
performance_record = {
|
708
|
+
"timestamp": time.time(),
|
709
|
+
"strategy_used": strategy_used,
|
710
|
+
"query_type": query_type,
|
711
|
+
"execution_time": execution_time,
|
712
|
+
"result_count": len(rag_results.get("results", [])),
|
713
|
+
"avg_score": self._calculate_avg_score(rag_results),
|
714
|
+
"success": len(rag_results.get("results", [])) > 0,
|
715
|
+
}
|
716
|
+
|
717
|
+
# Add to history
|
718
|
+
self.performance_history.append(performance_record)
|
719
|
+
|
720
|
+
# Keep only last 100 records
|
721
|
+
if len(self.performance_history) > 100:
|
722
|
+
self.performance_history = self.performance_history[-100:]
|
723
|
+
|
724
|
+
# Calculate metrics
|
725
|
+
metrics = self._calculate_metrics()
|
726
|
+
|
727
|
+
# Generate insights
|
728
|
+
insights = self._generate_insights(metrics)
|
729
|
+
|
730
|
+
return {
|
731
|
+
"current_performance": performance_record,
|
732
|
+
"metrics": metrics,
|
733
|
+
"insights": insights,
|
734
|
+
"performance_history_size": len(self.performance_history),
|
735
|
+
}
|
736
|
+
|
737
|
+
def _calculate_avg_score(self, rag_results: Dict) -> float:
|
738
|
+
"""Calculate average score from RAG results"""
|
739
|
+
scores = rag_results.get("scores", [])
|
740
|
+
return sum(scores) / len(scores) if scores else 0.0
|
741
|
+
|
742
|
+
def _calculate_metrics(self) -> Dict[str, Any]:
|
743
|
+
"""Calculate performance metrics from history"""
|
744
|
+
if not self.performance_history:
|
745
|
+
return {}
|
746
|
+
|
747
|
+
recent_records = self.performance_history[-20:] # Last 20 records
|
748
|
+
|
749
|
+
# Overall metrics
|
750
|
+
avg_execution_time = sum(r["execution_time"] for r in recent_records) / len(
|
751
|
+
recent_records
|
752
|
+
)
|
753
|
+
avg_result_count = sum(r["result_count"] for r in recent_records) / len(
|
754
|
+
recent_records
|
755
|
+
)
|
756
|
+
avg_score = sum(r["avg_score"] for r in recent_records) / len(recent_records)
|
757
|
+
success_rate = sum(1 for r in recent_records if r["success"]) / len(
|
758
|
+
recent_records
|
759
|
+
)
|
760
|
+
|
761
|
+
# Strategy performance
|
762
|
+
strategy_performance = {}
|
763
|
+
for record in recent_records:
|
764
|
+
strategy = record["strategy_used"]
|
765
|
+
if strategy not in strategy_performance:
|
766
|
+
strategy_performance[strategy] = []
|
767
|
+
strategy_performance[strategy].append(record)
|
768
|
+
|
769
|
+
# Calculate per-strategy metrics
|
770
|
+
strategy_metrics = {}
|
771
|
+
for strategy, records in strategy_performance.items():
|
772
|
+
strategy_metrics[strategy] = {
|
773
|
+
"count": len(records),
|
774
|
+
"avg_execution_time": sum(r["execution_time"] for r in records)
|
775
|
+
/ len(records),
|
776
|
+
"avg_score": sum(r["avg_score"] for r in records) / len(records),
|
777
|
+
"success_rate": sum(1 for r in records if r["success"]) / len(records),
|
778
|
+
}
|
779
|
+
|
780
|
+
return {
|
781
|
+
"overall": {
|
782
|
+
"avg_execution_time": avg_execution_time,
|
783
|
+
"avg_result_count": avg_result_count,
|
784
|
+
"avg_score": avg_score,
|
785
|
+
"success_rate": success_rate,
|
786
|
+
},
|
787
|
+
"by_strategy": strategy_metrics,
|
788
|
+
"total_queries": len(self.performance_history),
|
789
|
+
}
|
790
|
+
|
791
|
+
def _generate_insights(self, metrics: Dict) -> List[str]:
|
792
|
+
"""Generate performance insights and recommendations"""
|
793
|
+
insights = []
|
794
|
+
|
795
|
+
if not metrics:
|
796
|
+
return ["Insufficient data for insights"]
|
797
|
+
|
798
|
+
overall = metrics.get("overall", {})
|
799
|
+
by_strategy = metrics.get("by_strategy", {})
|
800
|
+
|
801
|
+
# Execution time insights
|
802
|
+
if overall.get("avg_execution_time", 0) > 5.0:
|
803
|
+
insights.append(
|
804
|
+
"High average execution time detected - consider optimizing chunk sizes or vector DB"
|
805
|
+
)
|
806
|
+
elif overall.get("avg_execution_time", 0) < 0.5:
|
807
|
+
insights.append("Excellent response times - system is well optimized")
|
808
|
+
|
809
|
+
# Success rate insights
|
810
|
+
success_rate = overall.get("success_rate", 0)
|
811
|
+
if success_rate < 0.8:
|
812
|
+
insights.append(
|
813
|
+
"Low success rate - consider adjusting similarity thresholds"
|
814
|
+
)
|
815
|
+
elif success_rate > 0.95:
|
816
|
+
insights.append("Excellent success rate - RAG system is performing well")
|
817
|
+
|
818
|
+
# Score insights
|
819
|
+
avg_score = overall.get("avg_score", 0)
|
820
|
+
if avg_score < 0.5:
|
821
|
+
insights.append(
|
822
|
+
"Low relevance scores - consider different embedding model or chunking strategy"
|
823
|
+
)
|
824
|
+
elif avg_score > 0.8:
|
825
|
+
insights.append("High relevance scores - excellent content matching")
|
826
|
+
|
827
|
+
# Strategy comparison insights
|
828
|
+
if len(by_strategy) > 1:
|
829
|
+
best_strategy = max(by_strategy.items(), key=lambda x: x[1]["avg_score"])
|
830
|
+
worst_strategy = min(by_strategy.items(), key=lambda x: x[1]["avg_score"])
|
831
|
+
|
832
|
+
if best_strategy[1]["avg_score"] - worst_strategy[1]["avg_score"] > 0.2:
|
833
|
+
insights.append(
|
834
|
+
f"Strategy '{best_strategy[0]}' significantly outperforms '{worst_strategy[0]}'"
|
835
|
+
)
|
836
|
+
|
837
|
+
return insights
|