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.
- kailash/__init__.py +1 -7
- kailash/cli/__init__.py +11 -1
- kailash/cli/validation_audit.py +570 -0
- kailash/core/actors/supervisor.py +1 -1
- kailash/core/resilience/circuit_breaker.py +71 -1
- kailash/core/resilience/health_monitor.py +172 -0
- kailash/edge/compliance.py +33 -0
- kailash/edge/consistency.py +609 -0
- kailash/edge/coordination/__init__.py +30 -0
- kailash/edge/coordination/global_ordering.py +355 -0
- kailash/edge/coordination/leader_election.py +217 -0
- kailash/edge/coordination/partition_detector.py +296 -0
- kailash/edge/coordination/raft.py +485 -0
- kailash/edge/discovery.py +63 -1
- kailash/edge/migration/__init__.py +19 -0
- kailash/edge/migration/edge_migrator.py +832 -0
- kailash/edge/monitoring/__init__.py +21 -0
- kailash/edge/monitoring/edge_monitor.py +736 -0
- kailash/edge/prediction/__init__.py +10 -0
- kailash/edge/prediction/predictive_warmer.py +591 -0
- kailash/edge/resource/__init__.py +102 -0
- kailash/edge/resource/cloud_integration.py +796 -0
- kailash/edge/resource/cost_optimizer.py +949 -0
- kailash/edge/resource/docker_integration.py +919 -0
- kailash/edge/resource/kubernetes_integration.py +893 -0
- kailash/edge/resource/platform_integration.py +913 -0
- kailash/edge/resource/predictive_scaler.py +959 -0
- kailash/edge/resource/resource_analyzer.py +824 -0
- kailash/edge/resource/resource_pools.py +610 -0
- kailash/integrations/dataflow_edge.py +261 -0
- kailash/mcp_server/registry_integration.py +1 -1
- kailash/monitoring/__init__.py +18 -0
- kailash/monitoring/alerts.py +646 -0
- kailash/monitoring/metrics.py +677 -0
- kailash/nodes/__init__.py +2 -0
- kailash/nodes/ai/__init__.py +17 -0
- kailash/nodes/ai/a2a.py +1914 -43
- kailash/nodes/ai/a2a_backup.py +1807 -0
- kailash/nodes/ai/hybrid_search.py +972 -0
- kailash/nodes/ai/semantic_memory.py +558 -0
- kailash/nodes/ai/streaming_analytics.py +947 -0
- kailash/nodes/base.py +545 -0
- kailash/nodes/edge/__init__.py +36 -0
- kailash/nodes/edge/base.py +240 -0
- kailash/nodes/edge/cloud_node.py +710 -0
- kailash/nodes/edge/coordination.py +239 -0
- kailash/nodes/edge/docker_node.py +825 -0
- kailash/nodes/edge/edge_data.py +582 -0
- kailash/nodes/edge/edge_migration_node.py +392 -0
- kailash/nodes/edge/edge_monitoring_node.py +421 -0
- kailash/nodes/edge/edge_state.py +673 -0
- kailash/nodes/edge/edge_warming_node.py +393 -0
- kailash/nodes/edge/kubernetes_node.py +652 -0
- kailash/nodes/edge/platform_node.py +766 -0
- kailash/nodes/edge/resource_analyzer_node.py +378 -0
- kailash/nodes/edge/resource_optimizer_node.py +501 -0
- kailash/nodes/edge/resource_scaler_node.py +397 -0
- kailash/nodes/ports.py +676 -0
- kailash/runtime/local.py +344 -1
- kailash/runtime/validation/__init__.py +20 -0
- kailash/runtime/validation/connection_context.py +119 -0
- kailash/runtime/validation/enhanced_error_formatter.py +202 -0
- kailash/runtime/validation/error_categorizer.py +164 -0
- kailash/runtime/validation/metrics.py +380 -0
- kailash/runtime/validation/performance.py +615 -0
- kailash/runtime/validation/suggestion_engine.py +212 -0
- kailash/testing/fixtures.py +2 -2
- kailash/workflow/builder.py +234 -8
- kailash/workflow/contracts.py +418 -0
- kailash/workflow/edge_infrastructure.py +369 -0
- kailash/workflow/migration.py +3 -3
- kailash/workflow/type_inference.py +669 -0
- {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/METADATA +44 -27
- {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/RECORD +78 -28
- kailash/nexus/__init__.py +0 -21
- kailash/nexus/cli/__init__.py +0 -5
- kailash/nexus/cli/__main__.py +0 -6
- kailash/nexus/cli/main.py +0 -176
- kailash/nexus/factory.py +0 -413
- kailash/nexus/gateway.py +0 -545
- {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/WHEEL +0 -0
- {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/entry_points.txt +0 -0
- {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/licenses/LICENSE +0 -0
- {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 :]
|