kailash 0.1.5__py3-none-any.whl → 0.2.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.
- kailash/__init__.py +1 -1
- kailash/access_control.py +740 -0
- kailash/api/__main__.py +6 -0
- kailash/api/auth.py +668 -0
- kailash/api/custom_nodes.py +285 -0
- kailash/api/custom_nodes_secure.py +377 -0
- kailash/api/database.py +620 -0
- kailash/api/studio.py +915 -0
- kailash/api/studio_secure.py +893 -0
- kailash/mcp/__init__.py +53 -0
- kailash/mcp/__main__.py +13 -0
- kailash/mcp/ai_registry_server.py +712 -0
- kailash/mcp/client.py +447 -0
- kailash/mcp/client_new.py +334 -0
- kailash/mcp/server.py +293 -0
- kailash/mcp/server_new.py +336 -0
- kailash/mcp/servers/__init__.py +12 -0
- kailash/mcp/servers/ai_registry.py +289 -0
- kailash/nodes/__init__.py +4 -2
- kailash/nodes/ai/__init__.py +2 -0
- kailash/nodes/ai/a2a.py +714 -67
- kailash/nodes/ai/intelligent_agent_orchestrator.py +31 -37
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +5 -6
- kailash/nodes/base.py +15 -2
- kailash/nodes/base_async.py +45 -0
- kailash/nodes/base_cycle_aware.py +374 -0
- kailash/nodes/base_with_acl.py +338 -0
- kailash/nodes/code/python.py +135 -27
- kailash/nodes/data/readers.py +16 -6
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +187 -27
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- kailash/runtime/__init__.py +2 -1
- kailash/runtime/access_controlled.py +458 -0
- kailash/runtime/local.py +106 -33
- kailash/runtime/parallel_cyclic.py +529 -0
- kailash/sdk_exceptions.py +90 -5
- kailash/security.py +845 -0
- kailash/tracking/manager.py +38 -15
- kailash/tracking/models.py +1 -1
- kailash/tracking/storage/filesystem.py +30 -2
- kailash/utils/__init__.py +8 -0
- kailash/workflow/__init__.py +18 -0
- kailash/workflow/convergence.py +270 -0
- kailash/workflow/cycle_analyzer.py +768 -0
- kailash/workflow/cycle_builder.py +573 -0
- kailash/workflow/cycle_config.py +709 -0
- kailash/workflow/cycle_debugger.py +760 -0
- kailash/workflow/cycle_exceptions.py +601 -0
- kailash/workflow/cycle_profiler.py +671 -0
- kailash/workflow/cycle_state.py +338 -0
- kailash/workflow/cyclic_runner.py +985 -0
- kailash/workflow/graph.py +500 -39
- kailash/workflow/migration.py +768 -0
- kailash/workflow/safety.py +365 -0
- kailash/workflow/templates.py +744 -0
- kailash/workflow/validation.py +693 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/METADATA +256 -12
- kailash-0.2.0.dist-info/RECORD +125 -0
- kailash/nodes/mcp/__init__.py +0 -11
- kailash/nodes/mcp/client.py +0 -554
- kailash/nodes/mcp/resource.py +0 -682
- kailash/nodes/mcp/server.py +0 -577
- kailash-0.1.5.dist-info/RECORD +0 -88
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
kailash/nodes/ai/a2a.py
CHANGED
@@ -2,6 +2,11 @@
|
|
2
2
|
|
3
3
|
This module implements multi-agent communication with selective attention mechanisms,
|
4
4
|
enabling efficient collaboration between AI agents while preventing information overload.
|
5
|
+
|
6
|
+
Design Philosophy:
|
7
|
+
The A2A system enables decentralized multi-agent collaboration through shared
|
8
|
+
memory pools and attention mechanisms. Agents can share insights, coordinate
|
9
|
+
tasks, and build collective intelligence without centralized control.
|
5
10
|
"""
|
6
11
|
|
7
12
|
import json
|
@@ -9,10 +14,11 @@ import time
|
|
9
14
|
import uuid
|
10
15
|
from collections import defaultdict, deque
|
11
16
|
from datetime import datetime
|
12
|
-
from typing import Any,
|
17
|
+
from typing import Any, Dict, List, Optional, Set
|
13
18
|
|
14
19
|
from kailash.nodes.ai.llm_agent import LLMAgentNode
|
15
20
|
from kailash.nodes.base import Node, NodeParameter, register_node
|
21
|
+
from kailash.nodes.base_cycle_aware import CycleAwareNode
|
16
22
|
|
17
23
|
|
18
24
|
@register_node()
|
@@ -211,6 +217,8 @@ class SharedMemoryPoolNode(Node):
|
|
211
217
|
return self._subscribe_agent(kwargs)
|
212
218
|
elif action == "query":
|
213
219
|
return self._semantic_query(kwargs)
|
220
|
+
elif action == "metrics":
|
221
|
+
return self._get_metrics()
|
214
222
|
else:
|
215
223
|
return {"success": False, "error": f"Unknown action: {action}"}
|
216
224
|
|
@@ -321,7 +329,7 @@ class SharedMemoryPoolNode(Node):
|
|
321
329
|
def _semantic_query(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
322
330
|
"""Perform semantic search across memories."""
|
323
331
|
query = kwargs.get("query", "")
|
324
|
-
|
332
|
+
kwargs["agent_id"]
|
325
333
|
|
326
334
|
# Simple keyword matching for now (can be enhanced with embeddings)
|
327
335
|
matching_memories = []
|
@@ -438,6 +446,24 @@ class SharedMemoryPoolNode(Node):
|
|
438
446
|
|
439
447
|
return relevant_agents
|
440
448
|
|
449
|
+
def _get_metrics(self) -> Dict[str, Any]:
|
450
|
+
"""Get memory pool metrics."""
|
451
|
+
total_memories = sum(
|
452
|
+
len(memories) for memories in self.memory_segments.values()
|
453
|
+
)
|
454
|
+
|
455
|
+
return {
|
456
|
+
"success": True,
|
457
|
+
"total_memories": total_memories,
|
458
|
+
"segments": list(self.memory_segments.keys()),
|
459
|
+
"segment_sizes": {
|
460
|
+
segment: len(memories)
|
461
|
+
for segment, memories in self.memory_segments.items()
|
462
|
+
},
|
463
|
+
"total_agents": len(self.agent_subscriptions),
|
464
|
+
"memory_id_counter": self.memory_id_counter,
|
465
|
+
}
|
466
|
+
|
441
467
|
|
442
468
|
@register_node()
|
443
469
|
class A2AAgentNode(LLMAgentNode):
|
@@ -626,6 +652,10 @@ class A2AAgentNode(LLMAgentNode):
|
|
626
652
|
if memory_result.get("success"):
|
627
653
|
shared_context = memory_result.get("memories", [])
|
628
654
|
|
655
|
+
# Store provider and model for use in summarization
|
656
|
+
self._current_provider = kwargs.get("provider", "mock")
|
657
|
+
self._current_model = kwargs.get("model", "mock-model")
|
658
|
+
|
629
659
|
# Enhance messages with shared context
|
630
660
|
messages = kwargs.get("messages", [])
|
631
661
|
if shared_context:
|
@@ -645,10 +675,41 @@ Relevant shared context from other agents:
|
|
645
675
|
if result.get("success") and memory_pool:
|
646
676
|
response_content = result.get("response", {}).get("content", "")
|
647
677
|
|
648
|
-
#
|
649
|
-
|
678
|
+
# Use LLM to extract insights if provider supports it
|
679
|
+
use_llm_extraction = kwargs.get("use_llm_insight_extraction", True)
|
680
|
+
provider = kwargs.get("provider", "mock")
|
681
|
+
|
682
|
+
if use_llm_extraction and provider not in ["mock"]:
|
683
|
+
# Use LLM to extract and analyze insights
|
684
|
+
insights = self._extract_insights_with_llm(
|
685
|
+
response_content, agent_role, agent_id, kwargs
|
686
|
+
)
|
687
|
+
else:
|
688
|
+
# Fallback to rule-based extraction
|
689
|
+
insights = self._extract_insights(response_content, agent_role)
|
690
|
+
|
691
|
+
# Track insight statistics
|
692
|
+
insight_stats = {
|
693
|
+
"total": len(insights),
|
694
|
+
"high_importance": sum(1 for i in insights if i["importance"] >= 0.8),
|
695
|
+
"by_type": {},
|
696
|
+
"extraction_method": (
|
697
|
+
"llm"
|
698
|
+
if use_llm_extraction and provider not in ["mock"]
|
699
|
+
else "rule-based"
|
700
|
+
),
|
701
|
+
}
|
650
702
|
|
651
703
|
for insight in insights:
|
704
|
+
# Update type statistics
|
705
|
+
insight_type = insight.get("metadata", {}).get(
|
706
|
+
"insight_type", "general"
|
707
|
+
)
|
708
|
+
insight_stats["by_type"][insight_type] = (
|
709
|
+
insight_stats["by_type"].get(insight_type, 0) + 1
|
710
|
+
)
|
711
|
+
|
712
|
+
# Write to memory pool with enhanced context
|
652
713
|
memory_pool.run(
|
653
714
|
action="write",
|
654
715
|
agent_id=agent_id,
|
@@ -659,15 +720,31 @@ Relevant shared context from other agents:
|
|
659
720
|
context={
|
660
721
|
"source_message": messages[-1] if messages else None,
|
661
722
|
"agent_role": agent_role,
|
723
|
+
"insight_metadata": insight.get("metadata", {}),
|
724
|
+
"timestamp": kwargs.get("timestamp", time.time()),
|
662
725
|
},
|
663
726
|
)
|
664
727
|
|
728
|
+
# Store insights in local memory for agent's own reference
|
729
|
+
for insight in insights:
|
730
|
+
self.local_memory.append(
|
731
|
+
{
|
732
|
+
"type": "insight",
|
733
|
+
"content": insight["content"],
|
734
|
+
"importance": insight["importance"],
|
735
|
+
"timestamp": time.time(),
|
736
|
+
}
|
737
|
+
)
|
738
|
+
|
665
739
|
# Add A2A metadata to result
|
666
740
|
result["a2a_metadata"] = {
|
667
741
|
"agent_id": agent_id,
|
668
742
|
"agent_role": agent_role,
|
669
743
|
"shared_context_used": len(shared_context),
|
670
744
|
"insights_generated": len(insights) if "insights" in locals() else 0,
|
745
|
+
"insight_statistics": insight_stats if "insight_stats" in locals() else {},
|
746
|
+
"memory_pool_active": memory_pool is not None,
|
747
|
+
"local_memory_size": len(self.local_memory),
|
671
748
|
}
|
672
749
|
|
673
750
|
return result
|
@@ -677,68 +754,382 @@ Relevant shared context from other agents:
|
|
677
754
|
if not shared_context:
|
678
755
|
return "No relevant shared context available."
|
679
756
|
|
757
|
+
# For small context, use simple formatting
|
758
|
+
if len(shared_context) <= 3:
|
759
|
+
summary_parts = []
|
760
|
+
for memory in shared_context:
|
761
|
+
agent_id = memory.get("agent_id", "unknown")
|
762
|
+
content = memory.get("content", "")
|
763
|
+
importance = memory.get("importance", 0)
|
764
|
+
tags = ", ".join(memory.get("tags", []))
|
765
|
+
|
766
|
+
summary_parts.append(
|
767
|
+
f"- Agent {agent_id} ({importance:.1f} importance, tags: {tags}): {content}"
|
768
|
+
)
|
769
|
+
return "\n".join(summary_parts)
|
770
|
+
|
771
|
+
# For larger context, use LLM to create intelligent summary
|
772
|
+
return self._summarize_with_llm(shared_context)
|
773
|
+
|
774
|
+
def _summarize_with_llm(self, shared_context: List[Dict[str, Any]]) -> str:
|
775
|
+
"""Use LLM to create an intelligent summary of shared context."""
|
776
|
+
|
777
|
+
# Prepare context for summarization
|
778
|
+
context_items = []
|
779
|
+
for memory in shared_context[:10]: # Process up to 10 most relevant
|
780
|
+
context_items.append(
|
781
|
+
{
|
782
|
+
"agent": memory.get("agent_id", "unknown"),
|
783
|
+
"content": memory.get("content", ""),
|
784
|
+
"importance": memory.get("importance", 0),
|
785
|
+
"tags": memory.get("tags", []),
|
786
|
+
"type": memory.get("context", {})
|
787
|
+
.get("insight_metadata", {})
|
788
|
+
.get("insight_type", "general"),
|
789
|
+
}
|
790
|
+
)
|
791
|
+
|
792
|
+
# Create summarization prompt
|
793
|
+
summarization_prompt = f"""Summarize the following shared insights from other agents into a concise, actionable briefing.
|
794
|
+
|
795
|
+
Shared Context Items:
|
796
|
+
{json.dumps(context_items, indent=2)}
|
797
|
+
|
798
|
+
Create a summary that:
|
799
|
+
1. Groups related insights by theme
|
800
|
+
2. Highlights the most important findings (importance >= 0.8)
|
801
|
+
3. Identifies consensus points where multiple agents agree
|
802
|
+
4. Notes any contradictions or disagreements
|
803
|
+
5. Extracts key metrics and data points
|
804
|
+
6. Suggests areas needing further investigation
|
805
|
+
|
806
|
+
Format the summary as a brief paragraph (max 200 words) that another agent can quickly understand and act upon.
|
807
|
+
Focus on actionable intelligence rather than just listing what each agent said."""
|
808
|
+
|
809
|
+
try:
|
810
|
+
# Use the current agent's LLM configuration for summarization
|
811
|
+
provider = getattr(self, "_current_provider", "mock")
|
812
|
+
model = getattr(self, "_current_model", "mock-model")
|
813
|
+
|
814
|
+
if provider not in ["mock"]:
|
815
|
+
summary_kwargs = {
|
816
|
+
"provider": provider,
|
817
|
+
"model": model,
|
818
|
+
"temperature": 0.3,
|
819
|
+
"messages": [
|
820
|
+
{
|
821
|
+
"role": "system",
|
822
|
+
"content": "You are an expert at synthesizing information from multiple sources into clear, actionable summaries.",
|
823
|
+
},
|
824
|
+
{"role": "user", "content": summarization_prompt},
|
825
|
+
],
|
826
|
+
"max_tokens": 300,
|
827
|
+
}
|
828
|
+
|
829
|
+
result = super().run(**summary_kwargs)
|
830
|
+
|
831
|
+
if result.get("success"):
|
832
|
+
summary = result.get("response", {}).get("content", "")
|
833
|
+
if summary:
|
834
|
+
return f"Shared Context Summary:\n{summary}"
|
835
|
+
except:
|
836
|
+
pass
|
837
|
+
|
838
|
+
# Fallback to simple summary
|
680
839
|
summary_parts = []
|
681
|
-
for memory in shared_context[:5]:
|
840
|
+
for memory in shared_context[:5]:
|
682
841
|
agent_id = memory.get("agent_id", "unknown")
|
683
|
-
content = memory.get("content", "")
|
842
|
+
content = memory.get("content", "")[:100] + "..."
|
684
843
|
importance = memory.get("importance", 0)
|
685
|
-
tags = ", ".join(memory.get("tags", []))
|
686
844
|
|
687
|
-
summary_parts.append(
|
688
|
-
f"- Agent {agent_id} ({importance:.1f} importance, tags: {tags}): {content}"
|
689
|
-
)
|
845
|
+
summary_parts.append(f"- {agent_id} [{importance:.1f}]: {content}")
|
690
846
|
|
691
|
-
return "\n".join(summary_parts)
|
847
|
+
return "Recent insights:\n" + "\n".join(summary_parts)
|
692
848
|
|
693
849
|
def _extract_insights(self, response: str, agent_role: str) -> List[Dict[str, Any]]:
|
694
|
-
"""Extract important insights from agent response."""
|
850
|
+
"""Extract important insights from agent response using advanced NLP techniques."""
|
695
851
|
insights = []
|
696
852
|
|
697
|
-
#
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
853
|
+
# Enhanced keyword patterns for different types of insights
|
854
|
+
insight_patterns = {
|
855
|
+
"findings": {
|
856
|
+
"keywords": [
|
857
|
+
"found",
|
858
|
+
"discovered",
|
859
|
+
"identified",
|
860
|
+
"revealed",
|
861
|
+
"uncovered",
|
862
|
+
"detected",
|
863
|
+
"observed",
|
864
|
+
"noted",
|
865
|
+
"recognized",
|
866
|
+
],
|
867
|
+
"importance": 0.8,
|
868
|
+
"tags": ["finding", "discovery"],
|
869
|
+
},
|
870
|
+
"conclusions": {
|
871
|
+
"keywords": [
|
872
|
+
"conclude",
|
873
|
+
"therefore",
|
874
|
+
"thus",
|
875
|
+
"hence",
|
876
|
+
"consequently",
|
877
|
+
"as a result",
|
878
|
+
"in summary",
|
879
|
+
"overall",
|
880
|
+
"in conclusion",
|
881
|
+
],
|
882
|
+
"importance": 0.9,
|
883
|
+
"tags": ["conclusion", "summary"],
|
884
|
+
},
|
885
|
+
"comparisons": {
|
886
|
+
"keywords": [
|
887
|
+
"compared to",
|
888
|
+
"versus",
|
889
|
+
"vs",
|
890
|
+
"better than",
|
891
|
+
"worse than",
|
892
|
+
"improvement",
|
893
|
+
"decline",
|
894
|
+
"increase",
|
895
|
+
"decrease",
|
896
|
+
"change",
|
897
|
+
],
|
898
|
+
"importance": 0.7,
|
899
|
+
"tags": ["comparison", "analysis"],
|
900
|
+
},
|
901
|
+
"recommendations": {
|
902
|
+
"keywords": [
|
903
|
+
"recommend",
|
904
|
+
"suggest",
|
905
|
+
"should",
|
906
|
+
"advise",
|
907
|
+
"propose",
|
908
|
+
"best practice",
|
909
|
+
"optimal",
|
910
|
+
"ideal",
|
911
|
+
],
|
912
|
+
"importance": 0.85,
|
913
|
+
"tags": ["recommendation", "advice"],
|
914
|
+
},
|
915
|
+
"problems": {
|
916
|
+
"keywords": [
|
917
|
+
"issue",
|
918
|
+
"problem",
|
919
|
+
"challenge",
|
920
|
+
"limitation",
|
921
|
+
"constraint",
|
922
|
+
"difficulty",
|
923
|
+
"obstacle",
|
924
|
+
"concern",
|
925
|
+
"risk",
|
926
|
+
],
|
927
|
+
"importance": 0.75,
|
928
|
+
"tags": ["problem", "challenge"],
|
929
|
+
},
|
930
|
+
"metrics": {
|
931
|
+
"keywords": [
|
932
|
+
"percent",
|
933
|
+
"%",
|
934
|
+
"score",
|
935
|
+
"rating",
|
936
|
+
"benchmark",
|
937
|
+
"metric",
|
938
|
+
"measurement",
|
939
|
+
"performance",
|
940
|
+
"efficiency",
|
941
|
+
],
|
942
|
+
"importance": 0.65,
|
943
|
+
"tags": ["metric", "measurement"],
|
944
|
+
},
|
945
|
+
}
|
946
|
+
|
947
|
+
# Process response by sentences for better context
|
948
|
+
import re
|
949
|
+
|
950
|
+
sentences = re.split(r"[.!?]+", response)
|
951
|
+
|
952
|
+
for sentence in sentences:
|
953
|
+
sentence = sentence.strip()
|
954
|
+
if not sentence or len(sentence) < 20:
|
702
955
|
continue
|
703
956
|
|
704
|
-
#
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
if
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
957
|
+
# Calculate importance based on multiple factors
|
958
|
+
importance = 0.5 # Base importance
|
959
|
+
matched_tags = set([agent_role])
|
960
|
+
insight_type = None
|
961
|
+
|
962
|
+
# Check for insight patterns
|
963
|
+
sentence_lower = sentence.lower()
|
964
|
+
for pattern_type, pattern_info in insight_patterns.items():
|
965
|
+
if any(
|
966
|
+
keyword in sentence_lower for keyword in pattern_info["keywords"]
|
967
|
+
):
|
968
|
+
importance = max(importance, pattern_info["importance"])
|
969
|
+
matched_tags.update(pattern_info["tags"])
|
970
|
+
insight_type = pattern_type
|
971
|
+
break
|
972
|
+
|
973
|
+
# Extract entities and add as tags
|
974
|
+
# Simple entity extraction - numbers, capitalized words, technical terms
|
975
|
+
numbers = re.findall(r"\b\d+(?:\.\d+)?%?\b", sentence)
|
976
|
+
if numbers:
|
977
|
+
matched_tags.add("quantitative")
|
978
|
+
importance += 0.1
|
979
|
+
|
980
|
+
# Extract technical terms (words with specific patterns)
|
981
|
+
tech_terms = re.findall(r"\b[A-Z][a-z]+(?:[A-Z][a-z]+)*\b", sentence)
|
982
|
+
if tech_terms:
|
983
|
+
matched_tags.update(
|
984
|
+
[term.lower() for term in tech_terms[:2]]
|
985
|
+
) # Limit tags
|
986
|
+
|
987
|
+
# Boost importance for sentences with multiple capital letters (proper nouns)
|
988
|
+
capital_words = re.findall(r"\b[A-Z][A-Za-z]+\b", sentence)
|
989
|
+
if len(capital_words) > 2:
|
990
|
+
importance += 0.05
|
991
|
+
|
992
|
+
# Check for structured data (JSON, lists, etc.)
|
993
|
+
if any(char in sentence for char in ["{", "[", ":", "-"]):
|
994
|
+
matched_tags.add("structured")
|
995
|
+
importance += 0.05
|
996
|
+
|
997
|
+
# Determine segment based on insight type and role
|
998
|
+
segment = f"{agent_role}_{insight_type}" if insight_type else agent_role
|
999
|
+
|
1000
|
+
# Create insight with rich metadata
|
1001
|
+
insight = {
|
1002
|
+
"content": sentence,
|
1003
|
+
"importance": min(importance, 1.0), # Cap at 1.0
|
1004
|
+
"tags": list(matched_tags),
|
1005
|
+
"segment": segment,
|
1006
|
+
"metadata": {
|
1007
|
+
"length": len(sentence),
|
1008
|
+
"has_numbers": bool(numbers),
|
1009
|
+
"insight_type": insight_type or "general",
|
1010
|
+
"extracted_entities": tech_terms[:3] if tech_terms else [],
|
1011
|
+
},
|
1012
|
+
}
|
1013
|
+
|
1014
|
+
insights.append(insight)
|
1015
|
+
|
1016
|
+
# Sort by importance and return top insights
|
1017
|
+
insights.sort(key=lambda x: x["importance"], reverse=True)
|
1018
|
+
|
1019
|
+
# Dynamic limit based on response quality
|
1020
|
+
# If we have many high-quality insights, return more
|
1021
|
+
high_quality_count = sum(1 for i in insights if i["importance"] >= 0.7)
|
1022
|
+
limit = min(5 if high_quality_count > 3 else 3, len(insights))
|
1023
|
+
|
1024
|
+
return insights[:limit]
|
1025
|
+
|
1026
|
+
def _extract_insights_with_llm(
|
1027
|
+
self,
|
1028
|
+
response: str,
|
1029
|
+
agent_role: str,
|
1030
|
+
agent_id: str,
|
1031
|
+
original_kwargs: Dict[str, Any],
|
1032
|
+
) -> List[Dict[str, Any]]:
|
1033
|
+
"""Use LLM to extract and analyze insights from the response."""
|
1034
|
+
|
1035
|
+
# Prepare a focused prompt for insight extraction
|
1036
|
+
insight_extraction_prompt = f"""You are an AI insight extraction specialist. Analyze the following response and extract the most important insights.
|
1037
|
+
|
1038
|
+
Agent Role: {agent_role}
|
1039
|
+
Original Response:
|
1040
|
+
{response}
|
1041
|
+
|
1042
|
+
Extract 3-5 key insights from this response. For each insight:
|
1043
|
+
1. Summarize the core finding or conclusion (max 100 words)
|
1044
|
+
2. Assign an importance score (0.0-1.0) based on:
|
1045
|
+
- Novelty and uniqueness (0.3 weight)
|
1046
|
+
- Impact on decision-making (0.4 weight)
|
1047
|
+
- Supporting evidence quality (0.3 weight)
|
1048
|
+
3. Categorize the insight type: finding, conclusion, comparison, recommendation, problem, metric, or pattern
|
1049
|
+
4. Extract key entities mentioned (products, technologies, metrics, etc.)
|
1050
|
+
5. Suggest relevant tags for categorization
|
1051
|
+
|
1052
|
+
Output your analysis as a JSON array with this structure:
|
1053
|
+
[
|
1054
|
+
{{
|
1055
|
+
"content": "The core insight summarized concisely",
|
1056
|
+
"importance": 0.85,
|
1057
|
+
"type": "finding",
|
1058
|
+
"entities": ["MacBook Air M3", "M2", "battery life"],
|
1059
|
+
"tags": ["performance", "comparison", "hardware"],
|
1060
|
+
"evidence": "Brief supporting evidence from the text"
|
1061
|
+
}}
|
1062
|
+
]
|
1063
|
+
|
1064
|
+
Focus on insights that would be valuable for other agents to know. Ensure the JSON is valid."""
|
1065
|
+
|
1066
|
+
try:
|
1067
|
+
# Create a sub-call to the LLM for insight extraction
|
1068
|
+
extraction_kwargs = {
|
1069
|
+
"provider": original_kwargs.get("provider", "ollama"),
|
1070
|
+
"model": original_kwargs.get("model", "mistral"),
|
1071
|
+
"temperature": 0.3, # Lower temperature for more focused extraction
|
1072
|
+
"messages": [
|
729
1073
|
{
|
730
|
-
"
|
731
|
-
"
|
732
|
-
|
733
|
-
|
734
|
-
|
1074
|
+
"role": "system",
|
1075
|
+
"content": "You are an expert at analyzing text and extracting structured insights. Always respond with valid JSON.",
|
1076
|
+
},
|
1077
|
+
{"role": "user", "content": insight_extraction_prompt},
|
1078
|
+
],
|
1079
|
+
"max_tokens": original_kwargs.get("max_tokens", 1000),
|
1080
|
+
}
|
1081
|
+
|
1082
|
+
# Execute LLM call for insight extraction
|
1083
|
+
extraction_result = super().run(**extraction_kwargs)
|
1084
|
+
|
1085
|
+
if extraction_result.get("success"):
|
1086
|
+
extracted_content = extraction_result.get("response", {}).get(
|
1087
|
+
"content", ""
|
735
1088
|
)
|
736
1089
|
|
737
|
-
|
1090
|
+
# Parse the JSON response
|
1091
|
+
import json
|
1092
|
+
import re
|
1093
|
+
|
1094
|
+
# Try to extract JSON from the response
|
1095
|
+
json_match = re.search(r"\[.*?\]", extracted_content, re.DOTALL)
|
1096
|
+
if json_match:
|
1097
|
+
try:
|
1098
|
+
extracted_insights = json.loads(json_match.group())
|
1099
|
+
|
1100
|
+
# Convert to our insight format
|
1101
|
+
insights = []
|
1102
|
+
for item in extracted_insights[:5]: # Limit to 5 insights
|
1103
|
+
insight = {
|
1104
|
+
"content": item.get("content", ""),
|
1105
|
+
"importance": min(
|
1106
|
+
max(item.get("importance", 0.5), 0.0), 1.0
|
1107
|
+
),
|
1108
|
+
"tags": item.get("tags", []) + [agent_role],
|
1109
|
+
"segment": f"{agent_role}_{item.get('type', 'general')}",
|
1110
|
+
"metadata": {
|
1111
|
+
"insight_type": item.get("type", "general"),
|
1112
|
+
"extracted_entities": item.get("entities", []),
|
1113
|
+
"evidence": item.get("evidence", ""),
|
1114
|
+
"llm_extracted": True,
|
1115
|
+
},
|
1116
|
+
}
|
1117
|
+
insights.append(insight)
|
1118
|
+
|
1119
|
+
return insights
|
1120
|
+
except json.JSONDecodeError:
|
1121
|
+
pass
|
1122
|
+
|
1123
|
+
except Exception:
|
1124
|
+
# Log the error but don't fail - fall back to rule-based extraction
|
1125
|
+
pass
|
1126
|
+
|
1127
|
+
# If LLM extraction fails, fall back to rule-based
|
1128
|
+
return self._extract_insights(response, agent_role)
|
738
1129
|
|
739
1130
|
|
740
1131
|
@register_node()
|
741
|
-
class A2ACoordinatorNode(
|
1132
|
+
class A2ACoordinatorNode(CycleAwareNode):
|
742
1133
|
"""
|
743
1134
|
Coordinates communication and task delegation between A2A agents.
|
744
1135
|
|
@@ -884,24 +1275,134 @@ class A2ACoordinatorNode(Node):
|
|
884
1275
|
),
|
885
1276
|
}
|
886
1277
|
|
887
|
-
def run(self, **kwargs) -> Dict[str, Any]:
|
888
|
-
"""
|
1278
|
+
def run(self, context: Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
1279
|
+
"""
|
1280
|
+
Execute coordination action with cycle awareness.
|
1281
|
+
|
1282
|
+
Routes coordination requests to appropriate handlers based on action
|
1283
|
+
parameter. Tracks coordination history and agent performance across
|
1284
|
+
iterations for cycle-aware optimization.
|
1285
|
+
|
1286
|
+
Args:
|
1287
|
+
context: Execution context with cycle information
|
1288
|
+
**kwargs: Action-specific parameters including:
|
1289
|
+
action (str): Type of coordination action
|
1290
|
+
agent_info (dict): Agent registration details
|
1291
|
+
task (dict): Task to delegate
|
1292
|
+
available_agents (list): Agents available for tasks
|
1293
|
+
coordination_strategy (str): Delegation strategy
|
1294
|
+
|
1295
|
+
Returns:
|
1296
|
+
Dict[str, Any]: Action results with cycle metadata including:
|
1297
|
+
success (bool): Whether action succeeded
|
1298
|
+
cycle_info (dict): Iteration and history information
|
1299
|
+
Additional action-specific fields
|
1300
|
+
|
1301
|
+
Raises:
|
1302
|
+
None - errors returned in result dictionary
|
1303
|
+
|
1304
|
+
Side Effects:
|
1305
|
+
Updates internal agent registry
|
1306
|
+
Modifies coordination history
|
1307
|
+
Updates agent performance metrics
|
1308
|
+
|
1309
|
+
Examples:
|
1310
|
+
>>> coordinator = A2ACoordinatorNode()
|
1311
|
+
>>> result = coordinator.run(context,
|
1312
|
+
... action=\"delegate\",
|
1313
|
+
... task={\"type\": \"analysis\", \"required_skills\": [\"data\"]},
|
1314
|
+
... coordination_strategy=\"best_match\"
|
1315
|
+
... )
|
1316
|
+
>>> assert result[\"success\"] == True
|
1317
|
+
"""
|
889
1318
|
action = kwargs.get("action")
|
890
1319
|
|
1320
|
+
# Get cycle information using CycleAwareNode helpers
|
1321
|
+
iteration = self.get_iteration(context)
|
1322
|
+
is_first = self.is_first_iteration(context)
|
1323
|
+
prev_state = self.get_previous_state(context)
|
1324
|
+
|
1325
|
+
# Initialize cycle-aware coordination state
|
1326
|
+
if is_first:
|
1327
|
+
self.log_cycle_info(context, f"Starting coordination with action: {action}")
|
1328
|
+
coordination_history = []
|
1329
|
+
agent_performance_history = {}
|
1330
|
+
else:
|
1331
|
+
coordination_history = prev_state.get("coordination_history", [])
|
1332
|
+
agent_performance_history = prev_state.get("agent_performance", {})
|
1333
|
+
|
1334
|
+
# Execute the coordination action
|
891
1335
|
if action == "register":
|
892
|
-
|
1336
|
+
result = self._register_agent(kwargs, context)
|
893
1337
|
elif action == "delegate":
|
894
|
-
|
1338
|
+
result = self._delegate_task(
|
1339
|
+
kwargs, context, coordination_history, agent_performance_history
|
1340
|
+
)
|
895
1341
|
elif action == "broadcast":
|
896
|
-
|
1342
|
+
result = self._broadcast_message(kwargs, context)
|
897
1343
|
elif action == "consensus":
|
898
|
-
|
1344
|
+
result = self._manage_consensus(kwargs, context, coordination_history)
|
899
1345
|
elif action == "coordinate":
|
900
|
-
|
1346
|
+
result = self._coordinate_workflow(kwargs, context, iteration)
|
901
1347
|
else:
|
902
|
-
|
1348
|
+
result = {"success": False, "error": f"Unknown action: {action}"}
|
1349
|
+
|
1350
|
+
# Track coordination history for cycle learning
|
1351
|
+
coordination_event = {
|
1352
|
+
"iteration": iteration,
|
1353
|
+
"action": action,
|
1354
|
+
"success": result.get("success", False),
|
1355
|
+
"timestamp": time.time(),
|
1356
|
+
"details": {k: v for k, v in result.items() if k not in ["success"]},
|
1357
|
+
}
|
1358
|
+
coordination_history.append(coordination_event)
|
1359
|
+
|
1360
|
+
# Update agent performance tracking
|
1361
|
+
if action == "delegate" and result.get("success"):
|
1362
|
+
agent_id = result.get("delegated_to")
|
1363
|
+
if agent_id:
|
1364
|
+
if agent_id not in agent_performance_history:
|
1365
|
+
agent_performance_history[agent_id] = {
|
1366
|
+
"assignments": 0,
|
1367
|
+
"success_rate": 1.0,
|
1368
|
+
}
|
1369
|
+
agent_performance_history[agent_id]["assignments"] += 1
|
903
1370
|
|
904
|
-
|
1371
|
+
# Add cycle-aware metadata to result
|
1372
|
+
result.update(
|
1373
|
+
{
|
1374
|
+
"cycle_info": {
|
1375
|
+
"iteration": iteration,
|
1376
|
+
"coordination_history_length": len(coordination_history),
|
1377
|
+
"active_agents": len(self.registered_agents),
|
1378
|
+
"performance_tracked_agents": len(agent_performance_history),
|
1379
|
+
}
|
1380
|
+
}
|
1381
|
+
)
|
1382
|
+
|
1383
|
+
# Log progress
|
1384
|
+
if iteration % 5 == 0: # Log every 5 iterations
|
1385
|
+
self.log_cycle_info(
|
1386
|
+
context,
|
1387
|
+
f"Coordination stats: {len(coordination_history)} events, {len(self.registered_agents)} agents",
|
1388
|
+
)
|
1389
|
+
|
1390
|
+
# Persist state for next iteration
|
1391
|
+
return {
|
1392
|
+
**result,
|
1393
|
+
**self.set_cycle_state(
|
1394
|
+
{
|
1395
|
+
"coordination_history": coordination_history[
|
1396
|
+
-50:
|
1397
|
+
], # Keep last 50 events
|
1398
|
+
"agent_performance": agent_performance_history,
|
1399
|
+
}
|
1400
|
+
),
|
1401
|
+
}
|
1402
|
+
|
1403
|
+
def _register_agent(
|
1404
|
+
self, kwargs: Dict[str, Any], context: Dict[str, Any]
|
1405
|
+
) -> Dict[str, Any]:
|
905
1406
|
"""Register an agent with the coordinator."""
|
906
1407
|
agent_info = kwargs.get("agent_info", {})
|
907
1408
|
agent_id = agent_info.get("id")
|
@@ -925,8 +1426,14 @@ class A2ACoordinatorNode(Node):
|
|
925
1426
|
"registered_agents": list(self.registered_agents.keys()),
|
926
1427
|
}
|
927
1428
|
|
928
|
-
def _delegate_task(
|
929
|
-
|
1429
|
+
def _delegate_task(
|
1430
|
+
self,
|
1431
|
+
kwargs: Dict[str, Any],
|
1432
|
+
context: Dict[str, Any],
|
1433
|
+
coordination_history: List[Dict],
|
1434
|
+
agent_performance: Dict,
|
1435
|
+
) -> Dict[str, Any]:
|
1436
|
+
"""Delegate task to most suitable agent with cycle-aware optimization."""
|
930
1437
|
task = kwargs.get("task", {})
|
931
1438
|
available_agents = kwargs.get("available_agents", [])
|
932
1439
|
strategy = kwargs.get("coordination_strategy", "best_match")
|
@@ -941,13 +1448,22 @@ class A2ACoordinatorNode(Node):
|
|
941
1448
|
if not available_agents:
|
942
1449
|
return {"success": False, "error": "No available agents"}
|
943
1450
|
|
944
|
-
#
|
1451
|
+
# Use cycle-aware agent selection based on performance history
|
1452
|
+
iteration = self.get_iteration(context)
|
1453
|
+
|
1454
|
+
# Select agent based on strategy with cycle learning
|
945
1455
|
if strategy == "best_match":
|
946
|
-
selected_agent = self.
|
1456
|
+
selected_agent = self._find_best_match_cycle_aware(
|
1457
|
+
task, available_agents, agent_performance, iteration
|
1458
|
+
)
|
947
1459
|
elif strategy == "round_robin":
|
948
|
-
|
1460
|
+
# Cycle-aware round-robin based on iteration
|
1461
|
+
agent_index = iteration % len(available_agents)
|
1462
|
+
selected_agent = available_agents[agent_index]
|
949
1463
|
elif strategy == "auction":
|
950
|
-
selected_agent = self.
|
1464
|
+
selected_agent = self._run_auction_cycle_aware(
|
1465
|
+
task, available_agents, agent_performance
|
1466
|
+
)
|
951
1467
|
else:
|
952
1468
|
selected_agent = available_agents[0]
|
953
1469
|
|
@@ -965,9 +1481,15 @@ class A2ACoordinatorNode(Node):
|
|
965
1481
|
"delegated_to": agent_id,
|
966
1482
|
"task": task,
|
967
1483
|
"strategy": strategy,
|
1484
|
+
"agent_performance_score": agent_performance.get(agent_id, {}).get(
|
1485
|
+
"success_rate", 1.0
|
1486
|
+
),
|
1487
|
+
"iteration": iteration,
|
968
1488
|
}
|
969
1489
|
|
970
|
-
def _broadcast_message(
|
1490
|
+
def _broadcast_message(
|
1491
|
+
self, kwargs: Dict[str, Any], context: Dict[str, Any]
|
1492
|
+
) -> Dict[str, Any]:
|
971
1493
|
"""Broadcast message to relevant agents."""
|
972
1494
|
message = kwargs.get("message", {})
|
973
1495
|
target_roles = message.get("target_roles", [])
|
@@ -993,7 +1515,12 @@ class A2ACoordinatorNode(Node):
|
|
993
1515
|
"broadcast_time": time.time(),
|
994
1516
|
}
|
995
1517
|
|
996
|
-
def _manage_consensus(
|
1518
|
+
def _manage_consensus(
|
1519
|
+
self,
|
1520
|
+
kwargs: Dict[str, Any],
|
1521
|
+
context: Dict[str, Any],
|
1522
|
+
coordination_history: List[Dict],
|
1523
|
+
) -> Dict[str, Any]:
|
997
1524
|
"""Manage consensus building among agents."""
|
998
1525
|
proposal = kwargs.get("consensus_proposal", {})
|
999
1526
|
session_id = proposal.get("session_id", str(uuid.uuid4()))
|
@@ -1041,7 +1568,9 @@ class A2ACoordinatorNode(Node):
|
|
1041
1568
|
"votes_needed": int(total_agents * 0.5),
|
1042
1569
|
}
|
1043
1570
|
|
1044
|
-
def _coordinate_workflow(
|
1571
|
+
def _coordinate_workflow(
|
1572
|
+
self, kwargs: Dict[str, Any], context: Dict[str, Any], iteration: int
|
1573
|
+
) -> Dict[str, Any]:
|
1045
1574
|
"""Coordinate a multi-agent workflow."""
|
1046
1575
|
workflow_spec = kwargs.get("task", {})
|
1047
1576
|
steps = workflow_spec.get("steps", [])
|
@@ -1141,3 +1670,121 @@ class A2ACoordinatorNode(Node):
|
|
1141
1670
|
return bids[0]["agent"]
|
1142
1671
|
|
1143
1672
|
return None
|
1673
|
+
|
1674
|
+
def _find_best_match_cycle_aware(
|
1675
|
+
self,
|
1676
|
+
task: Dict[str, Any],
|
1677
|
+
agents: List[Dict[str, Any]],
|
1678
|
+
agent_performance: Dict[str, Dict],
|
1679
|
+
iteration: int,
|
1680
|
+
) -> Optional[Dict[str, Any]]:
|
1681
|
+
"""Find best matching agent using cycle-aware performance data."""
|
1682
|
+
required_skills = task.get("required_skills", [])
|
1683
|
+
if not required_skills:
|
1684
|
+
# When no specific skills required, prefer agents with better historical performance
|
1685
|
+
if agent_performance:
|
1686
|
+
best_agent = None
|
1687
|
+
best_score = 0
|
1688
|
+
for agent in agents:
|
1689
|
+
agent_id = agent.get("id")
|
1690
|
+
perf = agent_performance.get(
|
1691
|
+
agent_id, {"success_rate": 1.0, "assignments": 0}
|
1692
|
+
)
|
1693
|
+
# Balance experience and success rate
|
1694
|
+
experience_factor = min(
|
1695
|
+
perf["assignments"] / 10.0, 1.0
|
1696
|
+
) # Max at 10 assignments
|
1697
|
+
score = perf["success_rate"] * (0.7 + 0.3 * experience_factor)
|
1698
|
+
if score > best_score:
|
1699
|
+
best_score = score
|
1700
|
+
best_agent = agent
|
1701
|
+
return best_agent or (agents[0] if agents else None)
|
1702
|
+
return agents[0] if agents else None
|
1703
|
+
|
1704
|
+
best_agent = None
|
1705
|
+
best_score = 0
|
1706
|
+
|
1707
|
+
for agent in agents:
|
1708
|
+
agent_id = agent.get("id")
|
1709
|
+
agent_skills = set(agent.get("skills", []))
|
1710
|
+
required_set = set(required_skills)
|
1711
|
+
|
1712
|
+
# Calculate skill match score
|
1713
|
+
matches = agent_skills & required_set
|
1714
|
+
skill_score = len(matches) / len(required_set) if required_set else 0
|
1715
|
+
|
1716
|
+
# Get performance history
|
1717
|
+
perf = agent_performance.get(
|
1718
|
+
agent_id, {"success_rate": 1.0, "assignments": 0}
|
1719
|
+
)
|
1720
|
+
performance_score = perf["success_rate"]
|
1721
|
+
|
1722
|
+
# Experience bonus (agents with more assignments get slight preference)
|
1723
|
+
experience_bonus = min(perf["assignments"] * 0.05, 0.2) # Max 20% bonus
|
1724
|
+
|
1725
|
+
# Cycle adaptation: prefer different agents in different iterations to explore
|
1726
|
+
diversity_factor = 1.0
|
1727
|
+
if iteration > 0 and agent_performance:
|
1728
|
+
recent_assignments = sum(
|
1729
|
+
1 for p in agent_performance.values() if p["assignments"] > 0
|
1730
|
+
)
|
1731
|
+
if recent_assignments > 0:
|
1732
|
+
agent_usage_ratio = perf["assignments"] / recent_assignments
|
1733
|
+
if agent_usage_ratio > 0.5: # Over-used agent
|
1734
|
+
diversity_factor = 0.8 # Slight penalty
|
1735
|
+
|
1736
|
+
# Combined score
|
1737
|
+
final_score = (
|
1738
|
+
skill_score * performance_score * diversity_factor
|
1739
|
+
) + experience_bonus
|
1740
|
+
|
1741
|
+
if final_score > best_score:
|
1742
|
+
best_score = final_score
|
1743
|
+
best_agent = agent
|
1744
|
+
|
1745
|
+
return best_agent
|
1746
|
+
|
1747
|
+
def _run_auction_cycle_aware(
|
1748
|
+
self,
|
1749
|
+
task: Dict[str, Any],
|
1750
|
+
agents: List[Dict[str, Any]],
|
1751
|
+
agent_performance: Dict[str, Dict],
|
1752
|
+
) -> Optional[Dict[str, Any]]:
|
1753
|
+
"""Run auction-based task assignment with cycle-aware bidding."""
|
1754
|
+
bids = []
|
1755
|
+
|
1756
|
+
for agent in agents:
|
1757
|
+
agent_id = agent.get("id")
|
1758
|
+
|
1759
|
+
# Calculate bid based on skill match and availability (original logic)
|
1760
|
+
required_skills = set(task.get("required_skills", []))
|
1761
|
+
agent_skills = set(agent.get("skills", []))
|
1762
|
+
|
1763
|
+
skill_match = (
|
1764
|
+
len(required_skills & agent_skills) / len(required_skills)
|
1765
|
+
if required_skills
|
1766
|
+
else 1.0
|
1767
|
+
)
|
1768
|
+
workload = 1.0 - (agent.get("task_count", 0) / 10.0) # Lower bid if busy
|
1769
|
+
|
1770
|
+
# Enhance with performance history
|
1771
|
+
perf = agent_performance.get(
|
1772
|
+
agent_id, {"success_rate": 1.0, "assignments": 0}
|
1773
|
+
)
|
1774
|
+
performance_factor = perf["success_rate"]
|
1775
|
+
|
1776
|
+
# Experience factor (slight preference for experienced agents)
|
1777
|
+
experience_factor = min(
|
1778
|
+
1.0 + (perf["assignments"] * 0.02), 1.2
|
1779
|
+
) # Max 20% boost
|
1780
|
+
|
1781
|
+
bid_value = skill_match * workload * performance_factor * experience_factor
|
1782
|
+
|
1783
|
+
bids.append({"agent": agent, "bid": bid_value})
|
1784
|
+
|
1785
|
+
# Select highest bidder
|
1786
|
+
if bids:
|
1787
|
+
bids.sort(key=lambda x: x["bid"], reverse=True)
|
1788
|
+
return bids[0]["agent"]
|
1789
|
+
|
1790
|
+
return None
|