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.
Files changed (75) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control.py +740 -0
  3. kailash/api/__main__.py +6 -0
  4. kailash/api/auth.py +668 -0
  5. kailash/api/custom_nodes.py +285 -0
  6. kailash/api/custom_nodes_secure.py +377 -0
  7. kailash/api/database.py +620 -0
  8. kailash/api/studio.py +915 -0
  9. kailash/api/studio_secure.py +893 -0
  10. kailash/mcp/__init__.py +53 -0
  11. kailash/mcp/__main__.py +13 -0
  12. kailash/mcp/ai_registry_server.py +712 -0
  13. kailash/mcp/client.py +447 -0
  14. kailash/mcp/client_new.py +334 -0
  15. kailash/mcp/server.py +293 -0
  16. kailash/mcp/server_new.py +336 -0
  17. kailash/mcp/servers/__init__.py +12 -0
  18. kailash/mcp/servers/ai_registry.py +289 -0
  19. kailash/nodes/__init__.py +4 -2
  20. kailash/nodes/ai/__init__.py +2 -0
  21. kailash/nodes/ai/a2a.py +714 -67
  22. kailash/nodes/ai/intelligent_agent_orchestrator.py +31 -37
  23. kailash/nodes/ai/iterative_llm_agent.py +1280 -0
  24. kailash/nodes/ai/llm_agent.py +324 -1
  25. kailash/nodes/ai/self_organizing.py +5 -6
  26. kailash/nodes/base.py +15 -2
  27. kailash/nodes/base_async.py +45 -0
  28. kailash/nodes/base_cycle_aware.py +374 -0
  29. kailash/nodes/base_with_acl.py +338 -0
  30. kailash/nodes/code/python.py +135 -27
  31. kailash/nodes/data/readers.py +16 -6
  32. kailash/nodes/data/writers.py +16 -6
  33. kailash/nodes/logic/__init__.py +8 -0
  34. kailash/nodes/logic/convergence.py +642 -0
  35. kailash/nodes/logic/loop.py +153 -0
  36. kailash/nodes/logic/operations.py +187 -27
  37. kailash/nodes/mixins/__init__.py +11 -0
  38. kailash/nodes/mixins/mcp.py +228 -0
  39. kailash/nodes/mixins.py +387 -0
  40. kailash/runtime/__init__.py +2 -1
  41. kailash/runtime/access_controlled.py +458 -0
  42. kailash/runtime/local.py +106 -33
  43. kailash/runtime/parallel_cyclic.py +529 -0
  44. kailash/sdk_exceptions.py +90 -5
  45. kailash/security.py +845 -0
  46. kailash/tracking/manager.py +38 -15
  47. kailash/tracking/models.py +1 -1
  48. kailash/tracking/storage/filesystem.py +30 -2
  49. kailash/utils/__init__.py +8 -0
  50. kailash/workflow/__init__.py +18 -0
  51. kailash/workflow/convergence.py +270 -0
  52. kailash/workflow/cycle_analyzer.py +768 -0
  53. kailash/workflow/cycle_builder.py +573 -0
  54. kailash/workflow/cycle_config.py +709 -0
  55. kailash/workflow/cycle_debugger.py +760 -0
  56. kailash/workflow/cycle_exceptions.py +601 -0
  57. kailash/workflow/cycle_profiler.py +671 -0
  58. kailash/workflow/cycle_state.py +338 -0
  59. kailash/workflow/cyclic_runner.py +985 -0
  60. kailash/workflow/graph.py +500 -39
  61. kailash/workflow/migration.py +768 -0
  62. kailash/workflow/safety.py +365 -0
  63. kailash/workflow/templates.py +744 -0
  64. kailash/workflow/validation.py +693 -0
  65. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/METADATA +256 -12
  66. kailash-0.2.0.dist-info/RECORD +125 -0
  67. kailash/nodes/mcp/__init__.py +0 -11
  68. kailash/nodes/mcp/client.py +0 -554
  69. kailash/nodes/mcp/resource.py +0 -682
  70. kailash/nodes/mcp/server.py +0 -577
  71. kailash-0.1.5.dist-info/RECORD +0 -88
  72. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
  73. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
  74. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
  75. {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, Callable, Dict, List, Optional, Set, Tuple
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
- agent_id = kwargs["agent_id"]
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
- # Extract important insights
649
- insights = self._extract_insights(response_content, agent_role)
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]: # Limit to top 5 most relevant
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
- # Simple heuristic-based extraction
698
- lines = response.split("\n")
699
- for line in lines:
700
- line = line.strip()
701
- if not line:
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
- # High importance indicators
705
- high_importance_keywords = [
706
- "critical",
707
- "important",
708
- "key finding",
709
- "conclusion",
710
- "discovered",
711
- ]
712
- importance = 0.5
713
-
714
- if any(keyword in line.lower() for keyword in high_importance_keywords):
715
- importance = 0.8
716
-
717
- # Tag extraction based on role
718
- tags = [agent_role]
719
- if "data" in line.lower():
720
- tags.append("data")
721
- if "pattern" in line.lower():
722
- tags.append("pattern")
723
- if "insight" in line.lower():
724
- tags.append("insight")
725
-
726
- # Only save substantive lines
727
- if len(line) > 20:
728
- insights.append(
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
- "content": line,
731
- "importance": importance,
732
- "tags": tags,
733
- "segment": agent_role,
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
- return insights[:3] # Limit to top 3 insights per response
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(Node):
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
- """Execute coordination action."""
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
- return self._register_agent(kwargs)
1336
+ result = self._register_agent(kwargs, context)
893
1337
  elif action == "delegate":
894
- return self._delegate_task(kwargs)
1338
+ result = self._delegate_task(
1339
+ kwargs, context, coordination_history, agent_performance_history
1340
+ )
895
1341
  elif action == "broadcast":
896
- return self._broadcast_message(kwargs)
1342
+ result = self._broadcast_message(kwargs, context)
897
1343
  elif action == "consensus":
898
- return self._manage_consensus(kwargs)
1344
+ result = self._manage_consensus(kwargs, context, coordination_history)
899
1345
  elif action == "coordinate":
900
- return self._coordinate_workflow(kwargs)
1346
+ result = self._coordinate_workflow(kwargs, context, iteration)
901
1347
  else:
902
- return {"success": False, "error": f"Unknown action: {action}"}
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
- def _register_agent(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
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(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
929
- """Delegate task to most suitable agent."""
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
- # Select agent based on strategy
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._find_best_match(task, available_agents)
1456
+ selected_agent = self._find_best_match_cycle_aware(
1457
+ task, available_agents, agent_performance, iteration
1458
+ )
947
1459
  elif strategy == "round_robin":
948
- selected_agent = available_agents[0] # Simple round-robin
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._run_auction(task, available_agents)
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(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
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(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
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(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
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