kailash 0.3.2__py3-none-any.whl → 0.4.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 (146) hide show
  1. kailash/__init__.py +33 -1
  2. kailash/access_control/__init__.py +129 -0
  3. kailash/access_control/managers.py +461 -0
  4. kailash/access_control/rule_evaluators.py +467 -0
  5. kailash/access_control_abac.py +825 -0
  6. kailash/config/__init__.py +27 -0
  7. kailash/config/database_config.py +359 -0
  8. kailash/database/__init__.py +28 -0
  9. kailash/database/execution_pipeline.py +499 -0
  10. kailash/middleware/__init__.py +306 -0
  11. kailash/middleware/auth/__init__.py +33 -0
  12. kailash/middleware/auth/access_control.py +436 -0
  13. kailash/middleware/auth/auth_manager.py +422 -0
  14. kailash/middleware/auth/jwt_auth.py +477 -0
  15. kailash/middleware/auth/kailash_jwt_auth.py +616 -0
  16. kailash/middleware/communication/__init__.py +37 -0
  17. kailash/middleware/communication/ai_chat.py +989 -0
  18. kailash/middleware/communication/api_gateway.py +802 -0
  19. kailash/middleware/communication/events.py +470 -0
  20. kailash/middleware/communication/realtime.py +710 -0
  21. kailash/middleware/core/__init__.py +21 -0
  22. kailash/middleware/core/agent_ui.py +890 -0
  23. kailash/middleware/core/schema.py +643 -0
  24. kailash/middleware/core/workflows.py +396 -0
  25. kailash/middleware/database/__init__.py +63 -0
  26. kailash/middleware/database/base.py +113 -0
  27. kailash/middleware/database/base_models.py +525 -0
  28. kailash/middleware/database/enums.py +106 -0
  29. kailash/middleware/database/migrations.py +12 -0
  30. kailash/{api/database.py → middleware/database/models.py} +183 -291
  31. kailash/middleware/database/repositories.py +685 -0
  32. kailash/middleware/database/session_manager.py +19 -0
  33. kailash/middleware/mcp/__init__.py +38 -0
  34. kailash/middleware/mcp/client_integration.py +585 -0
  35. kailash/middleware/mcp/enhanced_server.py +576 -0
  36. kailash/nodes/__init__.py +25 -3
  37. kailash/nodes/admin/__init__.py +35 -0
  38. kailash/nodes/admin/audit_log.py +794 -0
  39. kailash/nodes/admin/permission_check.py +864 -0
  40. kailash/nodes/admin/role_management.py +823 -0
  41. kailash/nodes/admin/security_event.py +1519 -0
  42. kailash/nodes/admin/user_management.py +944 -0
  43. kailash/nodes/ai/a2a.py +24 -7
  44. kailash/nodes/ai/ai_providers.py +1 -0
  45. kailash/nodes/ai/embedding_generator.py +11 -11
  46. kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
  47. kailash/nodes/ai/llm_agent.py +407 -2
  48. kailash/nodes/ai/self_organizing.py +85 -10
  49. kailash/nodes/api/auth.py +287 -6
  50. kailash/nodes/api/rest.py +151 -0
  51. kailash/nodes/auth/__init__.py +17 -0
  52. kailash/nodes/auth/directory_integration.py +1228 -0
  53. kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
  54. kailash/nodes/auth/mfa.py +2338 -0
  55. kailash/nodes/auth/risk_assessment.py +872 -0
  56. kailash/nodes/auth/session_management.py +1093 -0
  57. kailash/nodes/auth/sso.py +1040 -0
  58. kailash/nodes/base.py +344 -13
  59. kailash/nodes/base_cycle_aware.py +4 -2
  60. kailash/nodes/base_with_acl.py +1 -1
  61. kailash/nodes/code/python.py +283 -10
  62. kailash/nodes/compliance/__init__.py +9 -0
  63. kailash/nodes/compliance/data_retention.py +1888 -0
  64. kailash/nodes/compliance/gdpr.py +2004 -0
  65. kailash/nodes/data/__init__.py +22 -2
  66. kailash/nodes/data/async_connection.py +469 -0
  67. kailash/nodes/data/async_sql.py +757 -0
  68. kailash/nodes/data/async_vector.py +598 -0
  69. kailash/nodes/data/readers.py +767 -0
  70. kailash/nodes/data/retrieval.py +360 -1
  71. kailash/nodes/data/sharepoint_graph.py +397 -21
  72. kailash/nodes/data/sql.py +94 -5
  73. kailash/nodes/data/streaming.py +68 -8
  74. kailash/nodes/data/vector_db.py +54 -4
  75. kailash/nodes/enterprise/__init__.py +13 -0
  76. kailash/nodes/enterprise/batch_processor.py +741 -0
  77. kailash/nodes/enterprise/data_lineage.py +497 -0
  78. kailash/nodes/logic/convergence.py +31 -9
  79. kailash/nodes/logic/operations.py +14 -3
  80. kailash/nodes/mixins/__init__.py +8 -0
  81. kailash/nodes/mixins/event_emitter.py +201 -0
  82. kailash/nodes/mixins/mcp.py +9 -4
  83. kailash/nodes/mixins/security.py +165 -0
  84. kailash/nodes/monitoring/__init__.py +7 -0
  85. kailash/nodes/monitoring/performance_benchmark.py +2497 -0
  86. kailash/nodes/rag/__init__.py +284 -0
  87. kailash/nodes/rag/advanced.py +1615 -0
  88. kailash/nodes/rag/agentic.py +773 -0
  89. kailash/nodes/rag/conversational.py +999 -0
  90. kailash/nodes/rag/evaluation.py +875 -0
  91. kailash/nodes/rag/federated.py +1188 -0
  92. kailash/nodes/rag/graph.py +721 -0
  93. kailash/nodes/rag/multimodal.py +671 -0
  94. kailash/nodes/rag/optimized.py +933 -0
  95. kailash/nodes/rag/privacy.py +1059 -0
  96. kailash/nodes/rag/query_processing.py +1335 -0
  97. kailash/nodes/rag/realtime.py +764 -0
  98. kailash/nodes/rag/registry.py +547 -0
  99. kailash/nodes/rag/router.py +837 -0
  100. kailash/nodes/rag/similarity.py +1854 -0
  101. kailash/nodes/rag/strategies.py +566 -0
  102. kailash/nodes/rag/workflows.py +575 -0
  103. kailash/nodes/security/__init__.py +19 -0
  104. kailash/nodes/security/abac_evaluator.py +1411 -0
  105. kailash/nodes/security/audit_log.py +91 -0
  106. kailash/nodes/security/behavior_analysis.py +1893 -0
  107. kailash/nodes/security/credential_manager.py +401 -0
  108. kailash/nodes/security/rotating_credentials.py +760 -0
  109. kailash/nodes/security/security_event.py +132 -0
  110. kailash/nodes/security/threat_detection.py +1103 -0
  111. kailash/nodes/testing/__init__.py +9 -0
  112. kailash/nodes/testing/credential_testing.py +499 -0
  113. kailash/nodes/transform/__init__.py +10 -2
  114. kailash/nodes/transform/chunkers.py +592 -1
  115. kailash/nodes/transform/processors.py +484 -14
  116. kailash/nodes/validation.py +321 -0
  117. kailash/runtime/access_controlled.py +1 -1
  118. kailash/runtime/async_local.py +41 -7
  119. kailash/runtime/docker.py +1 -1
  120. kailash/runtime/local.py +474 -55
  121. kailash/runtime/parallel.py +1 -1
  122. kailash/runtime/parallel_cyclic.py +1 -1
  123. kailash/runtime/testing.py +210 -2
  124. kailash/utils/migrations/__init__.py +25 -0
  125. kailash/utils/migrations/generator.py +433 -0
  126. kailash/utils/migrations/models.py +231 -0
  127. kailash/utils/migrations/runner.py +489 -0
  128. kailash/utils/secure_logging.py +342 -0
  129. kailash/workflow/__init__.py +16 -0
  130. kailash/workflow/cyclic_runner.py +3 -4
  131. kailash/workflow/graph.py +70 -2
  132. kailash/workflow/resilience.py +249 -0
  133. kailash/workflow/templates.py +726 -0
  134. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
  135. kailash-0.4.0.dist-info/RECORD +223 -0
  136. kailash/api/__init__.py +0 -17
  137. kailash/api/__main__.py +0 -6
  138. kailash/api/studio_secure.py +0 -893
  139. kailash/mcp/__main__.py +0 -13
  140. kailash/mcp/server_new.py +0 -336
  141. kailash/mcp/servers/__init__.py +0 -12
  142. kailash-0.3.2.dist-info/RECORD +0 -136
  143. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
  144. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
  145. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
  146. {kailash-0.3.2.dist-info → kailash-0.4.0.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