kailash 0.8.3__py3-none-any.whl → 0.8.5__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 (84) hide show
  1. kailash/__init__.py +1 -7
  2. kailash/cli/__init__.py +11 -1
  3. kailash/cli/validation_audit.py +570 -0
  4. kailash/core/actors/supervisor.py +1 -1
  5. kailash/core/resilience/circuit_breaker.py +71 -1
  6. kailash/core/resilience/health_monitor.py +172 -0
  7. kailash/edge/compliance.py +33 -0
  8. kailash/edge/consistency.py +609 -0
  9. kailash/edge/coordination/__init__.py +30 -0
  10. kailash/edge/coordination/global_ordering.py +355 -0
  11. kailash/edge/coordination/leader_election.py +217 -0
  12. kailash/edge/coordination/partition_detector.py +296 -0
  13. kailash/edge/coordination/raft.py +485 -0
  14. kailash/edge/discovery.py +63 -1
  15. kailash/edge/migration/__init__.py +19 -0
  16. kailash/edge/migration/edge_migrator.py +832 -0
  17. kailash/edge/monitoring/__init__.py +21 -0
  18. kailash/edge/monitoring/edge_monitor.py +736 -0
  19. kailash/edge/prediction/__init__.py +10 -0
  20. kailash/edge/prediction/predictive_warmer.py +591 -0
  21. kailash/edge/resource/__init__.py +102 -0
  22. kailash/edge/resource/cloud_integration.py +796 -0
  23. kailash/edge/resource/cost_optimizer.py +949 -0
  24. kailash/edge/resource/docker_integration.py +919 -0
  25. kailash/edge/resource/kubernetes_integration.py +893 -0
  26. kailash/edge/resource/platform_integration.py +913 -0
  27. kailash/edge/resource/predictive_scaler.py +959 -0
  28. kailash/edge/resource/resource_analyzer.py +824 -0
  29. kailash/edge/resource/resource_pools.py +610 -0
  30. kailash/integrations/dataflow_edge.py +261 -0
  31. kailash/mcp_server/registry_integration.py +1 -1
  32. kailash/monitoring/__init__.py +18 -0
  33. kailash/monitoring/alerts.py +646 -0
  34. kailash/monitoring/metrics.py +677 -0
  35. kailash/nodes/__init__.py +2 -0
  36. kailash/nodes/ai/__init__.py +17 -0
  37. kailash/nodes/ai/a2a.py +1914 -43
  38. kailash/nodes/ai/a2a_backup.py +1807 -0
  39. kailash/nodes/ai/hybrid_search.py +972 -0
  40. kailash/nodes/ai/semantic_memory.py +558 -0
  41. kailash/nodes/ai/streaming_analytics.py +947 -0
  42. kailash/nodes/base.py +545 -0
  43. kailash/nodes/edge/__init__.py +36 -0
  44. kailash/nodes/edge/base.py +240 -0
  45. kailash/nodes/edge/cloud_node.py +710 -0
  46. kailash/nodes/edge/coordination.py +239 -0
  47. kailash/nodes/edge/docker_node.py +825 -0
  48. kailash/nodes/edge/edge_data.py +582 -0
  49. kailash/nodes/edge/edge_migration_node.py +392 -0
  50. kailash/nodes/edge/edge_monitoring_node.py +421 -0
  51. kailash/nodes/edge/edge_state.py +673 -0
  52. kailash/nodes/edge/edge_warming_node.py +393 -0
  53. kailash/nodes/edge/kubernetes_node.py +652 -0
  54. kailash/nodes/edge/platform_node.py +766 -0
  55. kailash/nodes/edge/resource_analyzer_node.py +378 -0
  56. kailash/nodes/edge/resource_optimizer_node.py +501 -0
  57. kailash/nodes/edge/resource_scaler_node.py +397 -0
  58. kailash/nodes/ports.py +676 -0
  59. kailash/runtime/local.py +344 -1
  60. kailash/runtime/validation/__init__.py +20 -0
  61. kailash/runtime/validation/connection_context.py +119 -0
  62. kailash/runtime/validation/enhanced_error_formatter.py +202 -0
  63. kailash/runtime/validation/error_categorizer.py +164 -0
  64. kailash/runtime/validation/metrics.py +380 -0
  65. kailash/runtime/validation/performance.py +615 -0
  66. kailash/runtime/validation/suggestion_engine.py +212 -0
  67. kailash/testing/fixtures.py +2 -2
  68. kailash/workflow/builder.py +234 -8
  69. kailash/workflow/contracts.py +418 -0
  70. kailash/workflow/edge_infrastructure.py +369 -0
  71. kailash/workflow/migration.py +3 -3
  72. kailash/workflow/type_inference.py +669 -0
  73. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/METADATA +44 -27
  74. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/RECORD +78 -28
  75. kailash/nexus/__init__.py +0 -21
  76. kailash/nexus/cli/__init__.py +0 -5
  77. kailash/nexus/cli/__main__.py +0 -6
  78. kailash/nexus/cli/main.py +0 -176
  79. kailash/nexus/factory.py +0 -413
  80. kailash/nexus/gateway.py +0 -545
  81. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/WHEEL +0 -0
  82. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/entry_points.txt +0 -0
  83. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/licenses/LICENSE +0 -0
  84. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,972 @@
1
+ """
2
+ Hybrid search enhancement for A2A agent matching.
3
+
4
+ This module provides advanced search capabilities that combine:
5
+ - Semantic similarity using embeddings
6
+ - Keyword matching using TF-IDF and fuzzy matching
7
+ - Context-aware scoring based on task history
8
+ - Performance-weighted ranking
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import math
14
+ import re
15
+ from collections import Counter, defaultdict
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime, timedelta
18
+ from typing import Any, Dict, List, Optional, Set, Tuple, Union
19
+ from uuid import uuid4
20
+
21
+ import numpy as np
22
+
23
+ from ..base import Node, NodeParameter, register_node
24
+ from .a2a import A2AAgentCard, A2ATask, InsightType, TaskState
25
+ from .semantic_memory import SemanticMemoryItem, SimpleEmbeddingProvider
26
+
27
+
28
+ @dataclass
29
+ class SearchContext:
30
+ """Context information for search operations."""
31
+
32
+ task_history: List[Dict[str, Any]] = field(default_factory=list)
33
+ agent_performance: Dict[str, Dict[str, float]] = field(default_factory=dict)
34
+ recent_interactions: Dict[str, datetime] = field(default_factory=dict)
35
+ domain_expertise: Dict[str, List[str]] = field(default_factory=dict)
36
+ collaboration_patterns: Dict[str, List[str]] = field(default_factory=dict)
37
+
38
+
39
+ @dataclass
40
+ class SearchResult:
41
+ """Enhanced search result with detailed scoring."""
42
+
43
+ agent_id: str
44
+ agent_card: Optional[A2AAgentCard]
45
+ semantic_score: float
46
+ keyword_score: float
47
+ context_score: float
48
+ performance_score: float
49
+ combined_score: float
50
+ explanation: Dict[str, Any]
51
+ confidence: float
52
+
53
+ def to_dict(self) -> Dict[str, Any]:
54
+ """Convert to dictionary for serialization."""
55
+ return {
56
+ "agent_id": self.agent_id,
57
+ "agent_card": self.agent_card.to_dict() if self.agent_card else None,
58
+ "semantic_score": self.semantic_score,
59
+ "keyword_score": self.keyword_score,
60
+ "context_score": self.context_score,
61
+ "performance_score": self.performance_score,
62
+ "combined_score": self.combined_score,
63
+ "explanation": self.explanation,
64
+ "confidence": self.confidence,
65
+ }
66
+
67
+
68
+ class TFIDFVectorizer:
69
+ """Simple TF-IDF vectorizer for keyword matching."""
70
+
71
+ def __init__(self, stop_words: Optional[Set[str]] = None):
72
+ self.stop_words = stop_words or {
73
+ "the",
74
+ "a",
75
+ "an",
76
+ "and",
77
+ "or",
78
+ "but",
79
+ "in",
80
+ "on",
81
+ "at",
82
+ "to",
83
+ "for",
84
+ "of",
85
+ "with",
86
+ "by",
87
+ "is",
88
+ "are",
89
+ "was",
90
+ "were",
91
+ "be",
92
+ "been",
93
+ "have",
94
+ "has",
95
+ "had",
96
+ "do",
97
+ "does",
98
+ "did",
99
+ "will",
100
+ "would",
101
+ "could",
102
+ "should",
103
+ }
104
+ self.vocabulary = {}
105
+ self.idf_scores = {}
106
+ self.document_count = 0
107
+
108
+ def _tokenize(self, text: str) -> List[str]:
109
+ """Tokenize text into words."""
110
+ text = text.lower()
111
+ tokens = re.findall(r"\b\w+\b", text)
112
+ return [
113
+ token for token in tokens if token not in self.stop_words and len(token) > 2
114
+ ]
115
+
116
+ def fit(self, documents: List[str]):
117
+ """Fit the vectorizer on documents."""
118
+ self.document_count = len(documents)
119
+ document_frequencies = defaultdict(int)
120
+
121
+ # Count document frequencies
122
+ for doc in documents:
123
+ tokens = set(self._tokenize(doc))
124
+ for token in tokens:
125
+ document_frequencies[token] += 1
126
+
127
+ # Build vocabulary and calculate IDF scores
128
+ for token, df in document_frequencies.items():
129
+ self.vocabulary[token] = len(self.vocabulary)
130
+ self.idf_scores[token] = math.log(self.document_count / df)
131
+
132
+ def transform(self, documents: List[str]) -> np.ndarray:
133
+ """Transform documents to TF-IDF vectors."""
134
+ vectors = []
135
+
136
+ for doc in documents:
137
+ tokens = self._tokenize(doc)
138
+ token_counts = Counter(tokens)
139
+
140
+ vector = np.zeros(len(self.vocabulary))
141
+ for token, count in token_counts.items():
142
+ if token in self.vocabulary:
143
+ tf = count / len(tokens) if tokens else 0
144
+ idf = self.idf_scores[token]
145
+ vector[self.vocabulary[token]] = tf * idf
146
+
147
+ vectors.append(vector)
148
+
149
+ return np.array(vectors)
150
+
151
+ def cosine_similarity(self, vec1: np.ndarray, vec2: np.ndarray) -> float:
152
+ """Calculate cosine similarity between two vectors."""
153
+ if np.linalg.norm(vec1) == 0 or np.linalg.norm(vec2) == 0:
154
+ return 0.0
155
+ return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
156
+
157
+
158
+ class FuzzyMatcher:
159
+ """Fuzzy string matching for capability matching."""
160
+
161
+ def __init__(self):
162
+ self.synonyms = {
163
+ "code": ["coding", "programming", "development", "software"],
164
+ "test": ["testing", "qa", "quality", "validation"],
165
+ "research": ["analysis", "investigation", "study", "exploration"],
166
+ "data": ["information", "dataset", "statistics", "analytics"],
167
+ "debug": ["troubleshoot", "fix", "resolve", "diagnose"],
168
+ "design": ["architecture", "planning", "structure", "blueprint"],
169
+ "review": ["evaluation", "assessment", "inspection", "audit"],
170
+ "optimize": ["improve", "enhance", "performance", "efficiency"],
171
+ }
172
+
173
+ def expand_terms(self, terms: List[str]) -> Set[str]:
174
+ """Expand terms with synonyms."""
175
+ expanded = set(terms)
176
+ for term in terms:
177
+ term_lower = term.lower()
178
+ for base_term, synonyms in self.synonyms.items():
179
+ if term_lower in synonyms or term_lower == base_term:
180
+ expanded.update(synonyms)
181
+ expanded.add(base_term)
182
+ return expanded
183
+
184
+ def calculate_fuzzy_score(self, text1: str, text2: str) -> float:
185
+ """Calculate fuzzy matching score between two texts."""
186
+ tokens1 = set(text1.lower().split())
187
+ tokens2 = set(text2.lower().split())
188
+
189
+ # Expand with synonyms
190
+ expanded1 = self.expand_terms(list(tokens1))
191
+ expanded2 = self.expand_terms(list(tokens2))
192
+
193
+ # Calculate intersection over union
194
+ intersection = len(expanded1.intersection(expanded2))
195
+ union = len(expanded1.union(expanded2))
196
+
197
+ return intersection / union if union > 0 else 0.0
198
+
199
+
200
+ class ContextualScorer:
201
+ """Contextual scoring based on task history and agent performance."""
202
+
203
+ def __init__(self):
204
+ self.task_success_weight = 0.4
205
+ self.recency_weight = 0.3
206
+ self.collaboration_weight = 0.2
207
+ self.domain_expertise_weight = 0.1
208
+
209
+ def calculate_context_score(
210
+ self, agent_id: str, task_requirements: List[str], context: SearchContext
211
+ ) -> Tuple[float, Dict[str, Any]]:
212
+ """Calculate contextual score for an agent."""
213
+ explanation = {}
214
+ total_score = 0.0
215
+
216
+ # Task success history
217
+ task_success_score = self._calculate_task_success_score(agent_id, context)
218
+ total_score += task_success_score * self.task_success_weight
219
+ explanation["task_success"] = {
220
+ "score": task_success_score,
221
+ "weight": self.task_success_weight,
222
+ }
223
+
224
+ # Recency of interactions
225
+ recency_score = self._calculate_recency_score(agent_id, context)
226
+ total_score += recency_score * self.recency_weight
227
+ explanation["recency"] = {"score": recency_score, "weight": self.recency_weight}
228
+
229
+ # Collaboration patterns
230
+ collaboration_score = self._calculate_collaboration_score(agent_id, context)
231
+ total_score += collaboration_score * self.collaboration_weight
232
+ explanation["collaboration"] = {
233
+ "score": collaboration_score,
234
+ "weight": self.collaboration_weight,
235
+ }
236
+
237
+ # Domain expertise
238
+ domain_score = self._calculate_domain_expertise_score(
239
+ agent_id, task_requirements, context
240
+ )
241
+ total_score += domain_score * self.domain_expertise_weight
242
+ explanation["domain_expertise"] = {
243
+ "score": domain_score,
244
+ "weight": self.domain_expertise_weight,
245
+ }
246
+
247
+ return total_score, explanation
248
+
249
+ def _calculate_task_success_score(
250
+ self, agent_id: str, context: SearchContext
251
+ ) -> float:
252
+ """Calculate task success score based on history."""
253
+ performance = context.agent_performance.get(agent_id, {})
254
+ success_rate = performance.get("success_rate", 0.5) # Default to neutral
255
+ quality_score = performance.get("average_quality", 0.5)
256
+
257
+ # Combine success rate and quality
258
+ return success_rate * 0.7 + quality_score * 0.3
259
+
260
+ def _calculate_recency_score(self, agent_id: str, context: SearchContext) -> float:
261
+ """Calculate recency score based on recent interactions."""
262
+ last_interaction = context.recent_interactions.get(agent_id)
263
+ if not last_interaction:
264
+ return 0.5 # Neutral for no interaction history
265
+
266
+ days_since = (datetime.now() - last_interaction).days
267
+ if days_since == 0:
268
+ return 1.0
269
+ elif days_since <= 7:
270
+ return 0.8
271
+ elif days_since <= 30:
272
+ return 0.6
273
+ else:
274
+ return 0.4
275
+
276
+ def _calculate_collaboration_score(
277
+ self, agent_id: str, context: SearchContext
278
+ ) -> float:
279
+ """Calculate collaboration score based on patterns."""
280
+ patterns = context.collaboration_patterns.get(agent_id, [])
281
+ if not patterns:
282
+ return 0.5
283
+
284
+ # Score based on successful collaboration patterns
285
+ positive_patterns = sum(1 for p in patterns if "successful" in p.lower())
286
+ return min(1.0, positive_patterns / len(patterns) + 0.3)
287
+
288
+ def _calculate_domain_expertise_score(
289
+ self, agent_id: str, requirements: List[str], context: SearchContext
290
+ ) -> float:
291
+ """Calculate domain expertise score."""
292
+ expertise = context.domain_expertise.get(agent_id, [])
293
+ if not expertise or not requirements:
294
+ return 0.5
295
+
296
+ # Calculate overlap between requirements and expertise
297
+ req_set = set(req.lower() for req in requirements)
298
+ exp_set = set(exp.lower() for exp in expertise)
299
+
300
+ overlap = len(req_set.intersection(exp_set))
301
+ return min(1.0, overlap / len(req_set) + 0.2)
302
+
303
+
304
+ @register_node()
305
+ class HybridSearchNode(Node):
306
+ """Enhanced hybrid search for A2A agent matching."""
307
+
308
+ def __init__(self, name: str = "hybrid_search", **kwargs):
309
+ """Initialize hybrid search node."""
310
+ self.requirements = None
311
+ self.agents = None
312
+ self.context = None
313
+ self.limit = 10
314
+ self.semantic_weight = 0.3
315
+ self.keyword_weight = 0.3
316
+ self.context_weight = 0.2
317
+ self.performance_weight = 0.2
318
+ self.min_threshold = 0.3
319
+ self.enable_fuzzy_matching = True
320
+ self.enable_tfidf = True
321
+ self.embedding_model = "nomic-embed-text"
322
+ self.embedding_host = "http://localhost:11434"
323
+
324
+ # Set attributes from kwargs
325
+ for key, value in kwargs.items():
326
+ if hasattr(self, key):
327
+ setattr(self, key, value)
328
+
329
+ super().__init__(name=name, **kwargs)
330
+
331
+ # Initialize components
332
+ self.embedding_provider = SimpleEmbeddingProvider(
333
+ model_name=self.embedding_model, host=self.embedding_host
334
+ )
335
+ self.tfidf_vectorizer = TFIDFVectorizer()
336
+ self.fuzzy_matcher = FuzzyMatcher()
337
+ self.contextual_scorer = ContextualScorer()
338
+
339
+ # Fitted state
340
+ self._tfidf_fitted = False
341
+
342
+ def get_parameters(self) -> Dict[str, NodeParameter]:
343
+ """Get node parameters."""
344
+ return {
345
+ "requirements": NodeParameter(
346
+ name="requirements",
347
+ type=list,
348
+ required=True,
349
+ description="Task requirements",
350
+ ),
351
+ "agents": NodeParameter(
352
+ name="agents",
353
+ type=list,
354
+ required=True,
355
+ description="List of agents to search",
356
+ ),
357
+ "context": NodeParameter(
358
+ name="context",
359
+ type=dict,
360
+ required=False,
361
+ description="Search context with history and performance",
362
+ ),
363
+ "limit": NodeParameter(
364
+ name="limit",
365
+ type=int,
366
+ required=False,
367
+ default=10,
368
+ description="Maximum results to return",
369
+ ),
370
+ "semantic_weight": NodeParameter(
371
+ name="semantic_weight",
372
+ type=float,
373
+ required=False,
374
+ default=0.3,
375
+ description="Weight for semantic similarity",
376
+ ),
377
+ "keyword_weight": NodeParameter(
378
+ name="keyword_weight",
379
+ type=float,
380
+ required=False,
381
+ default=0.3,
382
+ description="Weight for keyword matching",
383
+ ),
384
+ "context_weight": NodeParameter(
385
+ name="context_weight",
386
+ type=float,
387
+ required=False,
388
+ default=0.2,
389
+ description="Weight for contextual scoring",
390
+ ),
391
+ "performance_weight": NodeParameter(
392
+ name="performance_weight",
393
+ type=float,
394
+ required=False,
395
+ default=0.2,
396
+ description="Weight for performance scoring",
397
+ ),
398
+ "min_threshold": NodeParameter(
399
+ name="min_threshold",
400
+ type=float,
401
+ required=False,
402
+ default=0.3,
403
+ description="Minimum combined score threshold",
404
+ ),
405
+ "enable_fuzzy_matching": NodeParameter(
406
+ name="enable_fuzzy_matching",
407
+ type=bool,
408
+ required=False,
409
+ default=True,
410
+ description="Enable fuzzy matching",
411
+ ),
412
+ "enable_tfidf": NodeParameter(
413
+ name="enable_tfidf",
414
+ type=bool,
415
+ required=False,
416
+ default=True,
417
+ description="Enable TF-IDF vectorization",
418
+ ),
419
+ "embedding_model": NodeParameter(
420
+ name="embedding_model",
421
+ type=str,
422
+ required=False,
423
+ default="nomic-embed-text",
424
+ description="Embedding model name",
425
+ ),
426
+ "embedding_host": NodeParameter(
427
+ name="embedding_host",
428
+ type=str,
429
+ required=False,
430
+ default="http://localhost:11434",
431
+ description="Embedding service host",
432
+ ),
433
+ }
434
+
435
+ async def run(self, **kwargs) -> Dict[str, Any]:
436
+ """Perform hybrid search with enhanced scoring."""
437
+ # Get parameters
438
+ requirements = kwargs.get("requirements", self.requirements)
439
+ agents = kwargs.get("agents", self.agents)
440
+ context_data = kwargs.get("context", self.context) or {}
441
+ limit = kwargs.get("limit", self.limit)
442
+
443
+ if not requirements or not agents:
444
+ raise ValueError("Requirements and agents are required")
445
+
446
+ # Parse context
447
+ context = self._parse_context(context_data)
448
+
449
+ # Prepare requirements text
450
+ req_text = " ".join(str(req) for req in requirements)
451
+
452
+ # Prepare agent texts
453
+ agent_texts = []
454
+ agent_cards = []
455
+
456
+ for agent in agents:
457
+ if isinstance(agent, dict):
458
+ # Convert dict to agent card if needed
459
+ if "agent_id" in agent and "agent_name" in agent:
460
+ try:
461
+ agent_card = A2AAgentCard.from_dict(agent)
462
+ agent_cards.append(agent_card)
463
+ # Create searchable text from agent card
464
+ agent_text = self._create_agent_text(agent_card)
465
+ except Exception:
466
+ # Fallback if conversion fails
467
+ agent_cards.append(None)
468
+ agent_text = str(agent)
469
+ else:
470
+ agent_cards.append(None)
471
+ agent_text = str(agent)
472
+ else:
473
+ agent_cards.append(None)
474
+ agent_text = str(agent)
475
+
476
+ agent_texts.append(agent_text)
477
+
478
+ # Perform different types of search
479
+ semantic_scores = await self._calculate_semantic_scores(req_text, agent_texts)
480
+ keyword_scores = await self._calculate_keyword_scores(req_text, agent_texts)
481
+ context_scores = await self._calculate_context_scores(
482
+ requirements, agent_cards, context
483
+ )
484
+ performance_scores = await self._calculate_performance_scores(
485
+ agent_cards, context
486
+ )
487
+
488
+ # Combine scores and create results
489
+ results = []
490
+ for i, agent in enumerate(agents):
491
+ agent_id = self._get_agent_id(agent, i)
492
+
493
+ # Calculate combined score
494
+ combined_score = (
495
+ semantic_scores[i] * self.semantic_weight
496
+ + keyword_scores[i] * self.keyword_weight
497
+ + context_scores[i] * self.context_weight
498
+ + performance_scores[i] * self.performance_weight
499
+ )
500
+
501
+ # Apply minimum threshold
502
+ if combined_score >= self.min_threshold:
503
+ confidence = self._calculate_confidence(
504
+ semantic_scores[i],
505
+ keyword_scores[i],
506
+ context_scores[i],
507
+ performance_scores[i],
508
+ )
509
+
510
+ explanation = {
511
+ "semantic": {
512
+ "score": semantic_scores[i],
513
+ "weight": self.semantic_weight,
514
+ },
515
+ "keyword": {
516
+ "score": keyword_scores[i],
517
+ "weight": self.keyword_weight,
518
+ },
519
+ "context": {
520
+ "score": context_scores[i],
521
+ "weight": self.context_weight,
522
+ },
523
+ "performance": {
524
+ "score": performance_scores[i],
525
+ "weight": self.performance_weight,
526
+ },
527
+ "threshold_met": combined_score >= self.min_threshold,
528
+ }
529
+
530
+ result = SearchResult(
531
+ agent_id=agent_id,
532
+ agent_card=agent_cards[i],
533
+ semantic_score=semantic_scores[i],
534
+ keyword_score=keyword_scores[i],
535
+ context_score=context_scores[i],
536
+ performance_score=performance_scores[i],
537
+ combined_score=combined_score,
538
+ explanation=explanation,
539
+ confidence=confidence,
540
+ )
541
+
542
+ results.append(result)
543
+
544
+ # Sort by combined score
545
+ results.sort(key=lambda x: x.combined_score, reverse=True)
546
+
547
+ # Limit results
548
+ results = results[:limit]
549
+
550
+ return {
551
+ "success": True,
552
+ "requirements": requirements,
553
+ "results": [r.to_dict() for r in results],
554
+ "count": len(results),
555
+ "search_type": "hybrid_enhanced",
556
+ "weights": {
557
+ "semantic": self.semantic_weight,
558
+ "keyword": self.keyword_weight,
559
+ "context": self.context_weight,
560
+ "performance": self.performance_weight,
561
+ },
562
+ "threshold": self.min_threshold,
563
+ }
564
+
565
+ def _parse_context(self, context_data: Dict[str, Any]) -> SearchContext:
566
+ """Parse context data into SearchContext object."""
567
+ return SearchContext(
568
+ task_history=context_data.get("task_history", []),
569
+ agent_performance=context_data.get("agent_performance", {}),
570
+ recent_interactions={
571
+ k: datetime.fromisoformat(v) if isinstance(v, str) else v
572
+ for k, v in context_data.get("recent_interactions", {}).items()
573
+ },
574
+ domain_expertise=context_data.get("domain_expertise", {}),
575
+ collaboration_patterns=context_data.get("collaboration_patterns", {}),
576
+ )
577
+
578
+ def _create_agent_text(self, agent_card: A2AAgentCard) -> str:
579
+ """Create searchable text from agent card."""
580
+ text_parts = [
581
+ agent_card.agent_name,
582
+ agent_card.description,
583
+ " ".join(agent_card.tags),
584
+ " ".join(cap.name for cap in agent_card.primary_capabilities),
585
+ " ".join(cap.description for cap in agent_card.primary_capabilities),
586
+ " ".join(cap.domain for cap in agent_card.primary_capabilities),
587
+ ]
588
+ return " ".join(filter(None, text_parts))
589
+
590
+ def _get_agent_id(self, agent: Any, index: int) -> str:
591
+ """Get agent ID from agent data."""
592
+ if isinstance(agent, dict):
593
+ return agent.get("agent_id", f"agent_{index}")
594
+ elif hasattr(agent, "agent_id"):
595
+ return agent.agent_id
596
+ else:
597
+ return f"agent_{index}"
598
+
599
+ async def _calculate_semantic_scores(
600
+ self, req_text: str, agent_texts: List[str]
601
+ ) -> List[float]:
602
+ """Calculate semantic similarity scores."""
603
+ try:
604
+ # Generate embeddings
605
+ all_texts = [req_text] + agent_texts
606
+ result = await self.embedding_provider.embed_text(all_texts)
607
+
608
+ req_embedding = result.embeddings[0]
609
+ agent_embeddings = result.embeddings[1:]
610
+
611
+ # Calculate similarities
612
+ scores = []
613
+ for agent_embedding in agent_embeddings:
614
+ similarity = np.dot(req_embedding, agent_embedding) / (
615
+ np.linalg.norm(req_embedding) * np.linalg.norm(agent_embedding)
616
+ )
617
+ scores.append(max(0.0, similarity))
618
+
619
+ return scores
620
+
621
+ except Exception:
622
+ # Fallback to simple text similarity
623
+ return [0.5] * len(agent_texts)
624
+
625
+ async def _calculate_keyword_scores(
626
+ self, req_text: str, agent_texts: List[str]
627
+ ) -> List[float]:
628
+ """Calculate keyword-based similarity scores."""
629
+ scores = []
630
+
631
+ # TF-IDF scoring
632
+ if self.enable_tfidf and len(agent_texts) > 1:
633
+ try:
634
+ if not self._tfidf_fitted:
635
+ self.tfidf_vectorizer.fit([req_text] + agent_texts)
636
+ self._tfidf_fitted = True
637
+
638
+ vectors = self.tfidf_vectorizer.transform([req_text] + agent_texts)
639
+ req_vector = vectors[0]
640
+ agent_vectors = vectors[1:]
641
+
642
+ for agent_vector in agent_vectors:
643
+ similarity = self.tfidf_vectorizer.cosine_similarity(
644
+ req_vector, agent_vector
645
+ )
646
+ scores.append(max(0.0, similarity))
647
+ except Exception:
648
+ scores = [0.5] * len(agent_texts)
649
+ else:
650
+ # Fallback to simple keyword matching
651
+ req_words = set(req_text.lower().split())
652
+ for agent_text in agent_texts:
653
+ agent_words = set(agent_text.lower().split())
654
+ overlap = len(req_words.intersection(agent_words))
655
+ union = len(req_words.union(agent_words))
656
+ score = overlap / union if union > 0 else 0.0
657
+ scores.append(score)
658
+
659
+ # Add fuzzy matching boost
660
+ if self.enable_fuzzy_matching:
661
+ for i, agent_text in enumerate(agent_texts):
662
+ fuzzy_score = self.fuzzy_matcher.calculate_fuzzy_score(
663
+ req_text, agent_text
664
+ )
665
+ scores[i] = max(scores[i], fuzzy_score * 0.8) # Boost but not dominate
666
+
667
+ return scores
668
+
669
+ async def _calculate_context_scores(
670
+ self,
671
+ requirements: List[str],
672
+ agent_cards: List[Optional[A2AAgentCard]],
673
+ context: SearchContext,
674
+ ) -> List[float]:
675
+ """Calculate contextual scores."""
676
+ scores = []
677
+
678
+ for agent_card in agent_cards:
679
+ if agent_card:
680
+ agent_id = agent_card.agent_id
681
+ score, _ = self.contextual_scorer.calculate_context_score(
682
+ agent_id, requirements, context
683
+ )
684
+ scores.append(score)
685
+ else:
686
+ scores.append(0.5) # Neutral for unknown agents
687
+
688
+ return scores
689
+
690
+ async def _calculate_performance_scores(
691
+ self, agent_cards: List[Optional[A2AAgentCard]], context: SearchContext
692
+ ) -> List[float]:
693
+ """Calculate performance-based scores."""
694
+ scores = []
695
+
696
+ for agent_card in agent_cards:
697
+ if agent_card:
698
+ # Use agent card performance metrics
699
+ performance = agent_card.performance
700
+ success_rate = performance.success_rate
701
+ quality_score = performance.insight_quality_score
702
+
703
+ # Combine different performance metrics
704
+ perf_score = (
705
+ success_rate * 0.4
706
+ + quality_score * 0.4
707
+ + min(1.0, performance.total_tasks / 100) * 0.2 # Experience factor
708
+ )
709
+ scores.append(perf_score)
710
+ else:
711
+ scores.append(0.5) # Neutral for unknown agents
712
+
713
+ return scores
714
+
715
+ def _calculate_confidence(
716
+ self, semantic: float, keyword: float, context: float, performance: float
717
+ ) -> float:
718
+ """Calculate confidence score based on agreement between different scorers."""
719
+ scores = [semantic, keyword, context, performance]
720
+
721
+ # Calculate standard deviation (lower = more agreement = higher confidence)
722
+ mean_score = sum(scores) / len(scores)
723
+ variance = sum((s - mean_score) ** 2 for s in scores) / len(scores)
724
+ std_dev = math.sqrt(variance)
725
+
726
+ # Convert to confidence (0-1 scale)
727
+ # Scale standard deviation to 0-1 range more appropriately
728
+ confidence = max(0.0, 1.0 - (std_dev / 0.5)) # Assume max std_dev of 0.5
729
+
730
+ # Boost confidence for high scores
731
+ if mean_score > 0.7:
732
+ confidence = min(1.0, confidence + 0.1)
733
+
734
+ return confidence
735
+
736
+
737
+ @register_node()
738
+ class AdaptiveSearchNode(Node):
739
+ """Adaptive search that learns from feedback and improves over time."""
740
+
741
+ def __init__(self, name: str = "adaptive_search", **kwargs):
742
+ """Initialize adaptive search node."""
743
+ self.requirements = None
744
+ self.agents = None
745
+ self.feedback_history = None
746
+ self.adaptation_rate = 0.1
747
+ self.memory_window = 100 # Number of recent searches to remember
748
+
749
+ # Adaptive weights (will be learned)
750
+ self.semantic_weight = 0.3
751
+ self.keyword_weight = 0.3
752
+ self.context_weight = 0.2
753
+ self.performance_weight = 0.2
754
+
755
+ # Set attributes from kwargs
756
+ for key, value in kwargs.items():
757
+ if hasattr(self, key):
758
+ setattr(self, key, value)
759
+
760
+ super().__init__(name=name, **kwargs)
761
+
762
+ # Learning history
763
+ self.search_history = []
764
+ self.weight_history = []
765
+
766
+ # Initialize hybrid search
767
+ self.hybrid_search = HybridSearchNode(
768
+ name="internal_hybrid_search",
769
+ semantic_weight=self.semantic_weight,
770
+ keyword_weight=self.keyword_weight,
771
+ context_weight=self.context_weight,
772
+ performance_weight=self.performance_weight,
773
+ )
774
+
775
+ def get_parameters(self) -> Dict[str, NodeParameter]:
776
+ """Get node parameters."""
777
+ return {
778
+ "requirements": NodeParameter(
779
+ name="requirements",
780
+ type=list,
781
+ required=True,
782
+ description="Task requirements",
783
+ ),
784
+ "agents": NodeParameter(
785
+ name="agents",
786
+ type=list,
787
+ required=True,
788
+ description="List of agents to search",
789
+ ),
790
+ "feedback_history": NodeParameter(
791
+ name="feedback_history",
792
+ type=list,
793
+ required=False,
794
+ description="History of search feedback for learning",
795
+ ),
796
+ "adaptation_rate": NodeParameter(
797
+ name="adaptation_rate",
798
+ type=float,
799
+ required=False,
800
+ default=0.1,
801
+ description="Rate of weight adaptation",
802
+ ),
803
+ "memory_window": NodeParameter(
804
+ name="memory_window",
805
+ type=int,
806
+ required=False,
807
+ default=100,
808
+ description="Number of searches to remember",
809
+ ),
810
+ }
811
+
812
+ async def run(self, **kwargs) -> Dict[str, Any]:
813
+ """Perform adaptive search with learning."""
814
+ # Get parameters
815
+ requirements = kwargs.get("requirements", self.requirements)
816
+ agents = kwargs.get("agents", self.agents)
817
+ feedback_history = kwargs.get("feedback_history", self.feedback_history) or []
818
+
819
+ if not requirements or not agents:
820
+ raise ValueError("Requirements and agents are required")
821
+
822
+ # Learn from feedback
823
+ if feedback_history:
824
+ self._learn_from_feedback(feedback_history)
825
+
826
+ # Update hybrid search weights
827
+ self.hybrid_search.semantic_weight = self.semantic_weight
828
+ self.hybrid_search.keyword_weight = self.keyword_weight
829
+ self.hybrid_search.context_weight = self.context_weight
830
+ self.hybrid_search.performance_weight = self.performance_weight
831
+
832
+ # Perform search
833
+ # Filter kwargs to avoid duplicate parameters
834
+ filtered_kwargs = {
835
+ k: v
836
+ for k, v in kwargs.items()
837
+ if k
838
+ not in [
839
+ "requirements",
840
+ "agents",
841
+ "feedback_history",
842
+ "adaptation_rate",
843
+ "memory_window",
844
+ ]
845
+ }
846
+
847
+ result = await self.hybrid_search.run(
848
+ requirements=requirements, agents=agents, **filtered_kwargs
849
+ )
850
+
851
+ # Add adaptive information
852
+ result.update(
853
+ {
854
+ "adaptive_weights": {
855
+ "semantic": self.semantic_weight,
856
+ "keyword": self.keyword_weight,
857
+ "context": self.context_weight,
858
+ "performance": self.performance_weight,
859
+ },
860
+ "learning_enabled": True,
861
+ "search_history_size": len(self.search_history),
862
+ }
863
+ )
864
+
865
+ # Store search in history
866
+ self._store_search_history(requirements, agents, result)
867
+
868
+ return result
869
+
870
+ def _learn_from_feedback(self, feedback_history: List[Dict[str, Any]]):
871
+ """Learn and adapt weights from feedback."""
872
+ if not feedback_history:
873
+ return
874
+
875
+ # Process recent feedback
876
+ recent_feedback = feedback_history[-self.memory_window :]
877
+
878
+ # Calculate performance by component
879
+ semantic_performance = []
880
+ keyword_performance = []
881
+ context_performance = []
882
+ performance_performance = []
883
+
884
+ for feedback in recent_feedback:
885
+ if "component_scores" in feedback:
886
+ scores = feedback["component_scores"]
887
+ success = feedback.get("success", 0.5)
888
+
889
+ semantic_performance.append(scores.get("semantic", 0.5) * success)
890
+ keyword_performance.append(scores.get("keyword", 0.5) * success)
891
+ context_performance.append(scores.get("context", 0.5) * success)
892
+ performance_performance.append(scores.get("performance", 0.5) * success)
893
+
894
+ if not semantic_performance:
895
+ return
896
+
897
+ # Calculate average performance per component
898
+ avg_semantic = sum(semantic_performance) / len(semantic_performance)
899
+ avg_keyword = sum(keyword_performance) / len(keyword_performance)
900
+ avg_context = sum(context_performance) / len(context_performance)
901
+ avg_performance = sum(performance_performance) / len(performance_performance)
902
+
903
+ # Adjust weights based on performance
904
+ total_performance = avg_semantic + avg_keyword + avg_context + avg_performance
905
+
906
+ if total_performance > 0:
907
+ # Normalize to proportional weights
908
+ target_semantic = avg_semantic / total_performance
909
+ target_keyword = avg_keyword / total_performance
910
+ target_context = avg_context / total_performance
911
+ target_performance = avg_performance / total_performance
912
+
913
+ # Gradually adjust weights
914
+ self.semantic_weight += (
915
+ target_semantic - self.semantic_weight
916
+ ) * self.adaptation_rate
917
+ self.keyword_weight += (
918
+ target_keyword - self.keyword_weight
919
+ ) * self.adaptation_rate
920
+ self.context_weight += (
921
+ target_context - self.context_weight
922
+ ) * self.adaptation_rate
923
+ self.performance_weight += (
924
+ target_performance - self.performance_weight
925
+ ) * self.adaptation_rate
926
+
927
+ # Ensure weights sum to 1
928
+ total_weight = (
929
+ self.semantic_weight
930
+ + self.keyword_weight
931
+ + self.context_weight
932
+ + self.performance_weight
933
+ )
934
+ if total_weight > 0:
935
+ self.semantic_weight /= total_weight
936
+ self.keyword_weight /= total_weight
937
+ self.context_weight /= total_weight
938
+ self.performance_weight /= total_weight
939
+
940
+ def _store_search_history(
941
+ self, requirements: List[str], agents: List[Any], result: Dict[str, Any]
942
+ ):
943
+ """Store search in history for learning."""
944
+ search_record = {
945
+ "timestamp": datetime.now().isoformat(),
946
+ "requirements": requirements,
947
+ "agent_count": len(agents),
948
+ "result_count": result.get("count", 0),
949
+ "weights": {
950
+ "semantic": self.semantic_weight,
951
+ "keyword": self.keyword_weight,
952
+ "context": self.context_weight,
953
+ "performance": self.performance_weight,
954
+ },
955
+ }
956
+
957
+ self.search_history.append(search_record)
958
+
959
+ # Keep only recent history
960
+ if len(self.search_history) > self.memory_window:
961
+ self.search_history = self.search_history[-self.memory_window :]
962
+
963
+ # Store weight history
964
+ self.weight_history.append(
965
+ {
966
+ "timestamp": datetime.now().isoformat(),
967
+ "weights": search_record["weights"].copy(),
968
+ }
969
+ )
970
+
971
+ if len(self.weight_history) > self.memory_window:
972
+ self.weight_history = self.weight_history[-self.memory_window :]