mirror of https://github.com/aliasrobotics/cai.git
Merge branch 'merge_strategy' into '0.5.0'
Merge strategy See merge request aliasrobotics/alias_research/cai!221
This commit is contained in:
commit
5b1a612df5
|
|
@ -43,10 +43,12 @@ MEMORY_DIR = Path.home() / ".cai" / "memory"
|
|||
MEMORY_INDEX_FILE = MEMORY_DIR / "index.json"
|
||||
|
||||
# Global storage for compacted summaries (deprecated - use file storage)
|
||||
COMPACTED_SUMMARIES: Dict[str, str] = {}
|
||||
# Now supports multiple memories per agent
|
||||
COMPACTED_SUMMARIES: Dict[str, List[str]] = {}
|
||||
|
||||
# Global storage for memory ID mappings per agent
|
||||
APPLIED_MEMORY_IDS: Dict[str, str] = {}
|
||||
# Now supports multiple memory IDs per agent
|
||||
APPLIED_MEMORY_IDS: Dict[str, List[str]] = {}
|
||||
|
||||
|
||||
class MemoryCommand(Command):
|
||||
|
|
@ -69,6 +71,9 @@ class MemoryCommand(Command):
|
|||
self.add_subcommand("merge", "Merge multiple memories into one", self.handle_merge)
|
||||
self.add_subcommand("status", "Show memory status", self.handle_status)
|
||||
self.add_subcommand("compact", "Compact and save agent history", self.handle_compact)
|
||||
self.add_subcommand("remove", "Remove a specific memory from an agent", self.handle_remove)
|
||||
self.add_subcommand("clear", "Clear all memories from an agent", self.handle_clear)
|
||||
self.add_subcommand("list-applied", "Show which memories are applied to an agent", self.handle_list_applied)
|
||||
|
||||
# Remove local compact_model since we'll use the one from compact command
|
||||
|
||||
|
|
@ -93,7 +98,18 @@ class MemoryCommand(Command):
|
|||
return self.handle_show(args)
|
||||
|
||||
# Otherwise show help
|
||||
console.print("[yellow]Unknown subcommand. Use /memory list, save, apply, show, delete, merge, status, or compact[/yellow]")
|
||||
console.print("[yellow]Unknown subcommand. Available commands:[/yellow]")
|
||||
console.print("[dim] • /memory list - List all stored memories[/dim]")
|
||||
console.print("[dim] • /memory save - Save current agent history as memory[/dim]")
|
||||
console.print("[dim] • /memory apply - Apply a memory to an agent[/dim]")
|
||||
console.print("[dim] • /memory show - Show memory content[/dim]")
|
||||
console.print("[dim] • /memory delete - Delete a stored memory[/dim]")
|
||||
console.print("[dim] • /memory merge - Merge multiple memories into one[/dim]")
|
||||
console.print("[dim] • /memory status - Show memory status[/dim]")
|
||||
console.print("[dim] • /memory compact - Compact and save agent history[/dim]")
|
||||
console.print("[dim] • /memory remove - Remove a specific memory from an agent[/dim]")
|
||||
console.print("[dim] • /memory clear - Clear all memories from an agent[/dim]")
|
||||
console.print("[dim] • /memory list-applied - Show which memories are applied to an agent[/dim]")
|
||||
return True
|
||||
|
||||
def _ensure_memory_dir(self):
|
||||
|
|
@ -294,8 +310,13 @@ class MemoryCommand(Command):
|
|||
# Show applied memories
|
||||
if APPLIED_MEMORY_IDS:
|
||||
console.print("\n[bold cyan]:brain: Applied Memories[/bold cyan]")
|
||||
for agent_name, memory_id in APPLIED_MEMORY_IDS.items():
|
||||
console.print(f" • {agent_name}: {memory_id}")
|
||||
for agent_name, memory_ids in APPLIED_MEMORY_IDS.items():
|
||||
if isinstance(memory_ids, list):
|
||||
ids_str = ", ".join(memory_ids) if memory_ids else "None"
|
||||
console.print(f" • {agent_name}: [{ids_str}]")
|
||||
else:
|
||||
# Backward compatibility for single memory ID
|
||||
console.print(f" • {agent_name}: {memory_ids}")
|
||||
|
||||
# Show usage hints
|
||||
console.print("\n[dim]Commands:[/dim]")
|
||||
|
|
@ -307,7 +328,11 @@ class MemoryCommand(Command):
|
|||
console.print("[dim] • /memory delete <ID/name> - Delete a memory[/dim]")
|
||||
console.print("[dim] • /memory merge <ID1> <ID2> - Merge multiple memories[/dim]")
|
||||
console.print("[dim] • /memory compact <agent> - Compact agent history to memory[/dim]")
|
||||
console.print("[dim] • /memory remove <ID> <agent> - Remove a specific memory from agent[/dim]")
|
||||
console.print("[dim] • /memory clear <agent> - Clear all memories from agent[/dim]")
|
||||
console.print("[dim] • /memory list-applied - Show applied memories by agent[/dim]")
|
||||
console.print("[dim]\nNote: You can use memory IDs (e.g., M001) instead of full names[/dim]")
|
||||
console.print("[dim] Agents now support multiple memories![/dim]")
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -435,8 +460,13 @@ Model: {get_compact_model() or os.environ.get("CAI_MODEL", "gpt-4")}
|
|||
console.print(f"[green]✓ Saved memory: {memory_name} (ID: {memory_id})[/green]")
|
||||
|
||||
# Automatically apply the memory to the agent's system prompt
|
||||
COMPACTED_SUMMARIES[agent_name] = summary
|
||||
APPLIED_MEMORY_IDS[agent_name] = memory_id
|
||||
if agent_name not in COMPACTED_SUMMARIES:
|
||||
COMPACTED_SUMMARIES[agent_name] = []
|
||||
APPLIED_MEMORY_IDS[agent_name] = []
|
||||
|
||||
# Clear existing memories and add new one (maintain single memory behavior for save)
|
||||
COMPACTED_SUMMARIES[agent_name] = [summary]
|
||||
APPLIED_MEMORY_IDS[agent_name] = [memory_id]
|
||||
console.print(f"[green]✓ Memory {memory_id} automatically applied to {agent_name}'s system prompt[/green]")
|
||||
os.environ['CAI_MEMORY'] = 'true'
|
||||
|
||||
|
|
@ -549,12 +579,22 @@ Model: {get_compact_model() or os.environ.get("CAI_MODEL", "gpt-4")}
|
|||
success_count = 0
|
||||
for agent_name in target_agents:
|
||||
try:
|
||||
# Inject into system prompt (store for later use)
|
||||
COMPACTED_SUMMARIES[agent_name] = summary
|
||||
# Initialize lists if not present
|
||||
if agent_name not in COMPACTED_SUMMARIES:
|
||||
COMPACTED_SUMMARIES[agent_name] = []
|
||||
APPLIED_MEMORY_IDS[agent_name] = []
|
||||
|
||||
# Check if memory already applied
|
||||
if memory_id and memory_id in APPLIED_MEMORY_IDS[agent_name]:
|
||||
console.print(f"[yellow]Memory {memory_id} already applied to {agent_name}[/yellow]")
|
||||
continue
|
||||
|
||||
# Append memory (supports multiple memories)
|
||||
COMPACTED_SUMMARIES[agent_name].append(summary)
|
||||
|
||||
# Store the memory ID for this agent
|
||||
if memory_id:
|
||||
APPLIED_MEMORY_IDS[agent_name] = memory_id
|
||||
APPLIED_MEMORY_IDS[agent_name].append(memory_id)
|
||||
console.print(f"[green]✓ Applied memory {memory_id} to {agent_name}[/green]")
|
||||
else:
|
||||
console.print(f"[green]✓ Applied memory '{memory_identifier}' to {agent_name}[/green]")
|
||||
|
|
@ -798,8 +838,14 @@ Model: Merged from {len(memory_identifiers)} memories
|
|||
if apply.lower() == 'y':
|
||||
agent_name = self._get_current_agent_name()
|
||||
if agent_name:
|
||||
COMPACTED_SUMMARIES[agent_name] = combined_summary
|
||||
APPLIED_MEMORY_IDS[agent_name] = memory_id
|
||||
# Initialize lists if not present
|
||||
if agent_name not in COMPACTED_SUMMARIES:
|
||||
COMPACTED_SUMMARIES[agent_name] = []
|
||||
APPLIED_MEMORY_IDS[agent_name] = []
|
||||
|
||||
# Append the merged memory
|
||||
COMPACTED_SUMMARIES[agent_name].append(combined_summary)
|
||||
APPLIED_MEMORY_IDS[agent_name].append(memory_id)
|
||||
console.print(f"[green]✓ Applied merged memory {memory_id} to {agent_name}[/green]")
|
||||
# Reload the agent with the new memory
|
||||
self._reload_agent_with_memory(agent_name)
|
||||
|
|
@ -822,10 +868,17 @@ Model: Merged from {len(memory_identifiers)} memories
|
|||
# Show applied memories (from COMPACTED_SUMMARIES)
|
||||
if COMPACTED_SUMMARIES:
|
||||
console.print("\n[yellow]Applied Memories:[/yellow]")
|
||||
for agent_name, summary in COMPACTED_SUMMARIES.items():
|
||||
memory_id = APPLIED_MEMORY_IDS.get(agent_name, "Unknown")
|
||||
for agent_name, summaries in COMPACTED_SUMMARIES.items():
|
||||
memory_ids = APPLIED_MEMORY_IDS.get(agent_name, [])
|
||||
display_name = "Global" if agent_name == "__global__" else agent_name
|
||||
console.print(f" - {display_name}: {len(summary)} chars (ID: {memory_id})")
|
||||
if isinstance(summaries, list):
|
||||
total_chars = sum(len(s) for s in summaries)
|
||||
ids_str = ", ".join(memory_ids) if memory_ids else "Unknown"
|
||||
console.print(f" - {display_name}: {len(summaries)} memories, {total_chars} chars (IDs: {ids_str})")
|
||||
else:
|
||||
# Backward compatibility
|
||||
memory_id = memory_ids if isinstance(memory_ids, str) else "Unknown"
|
||||
console.print(f" - {display_name}: {len(summaries)} chars (ID: {memory_id})")
|
||||
else:
|
||||
console.print("\n[yellow]No memories currently applied[/yellow]")
|
||||
|
||||
|
|
@ -913,8 +966,13 @@ Model: {get_compact_model() or os.environ.get("CAI_MODEL", "gpt-4")}
|
|||
console.print(f"[green]✓ Saved memory: {memory_name} (ID: {memory_id})[/green]")
|
||||
|
||||
# Automatically apply the memory to the agent's system prompt
|
||||
COMPACTED_SUMMARIES[agent_name] = summary
|
||||
APPLIED_MEMORY_IDS[agent_name] = memory_id
|
||||
if agent_name not in COMPACTED_SUMMARIES:
|
||||
COMPACTED_SUMMARIES[agent_name] = []
|
||||
APPLIED_MEMORY_IDS[agent_name] = []
|
||||
|
||||
# Clear existing memories and add new one (maintain single memory behavior for compact all)
|
||||
COMPACTED_SUMMARIES[agent_name] = [summary]
|
||||
APPLIED_MEMORY_IDS[agent_name] = [memory_id]
|
||||
console.print(f"[green]✓ Memory {memory_id} automatically applied to {agent_name}'s system prompt[/green]")
|
||||
|
||||
# Clear the agent's history after saving
|
||||
|
|
@ -1011,8 +1069,13 @@ Model: {get_compact_model() or os.environ.get("CAI_MODEL", "gpt-4")}
|
|||
console.print(f"[green]✓ Saved memory: {memory_name} (ID: {memory_id})[/green]")
|
||||
os.environ['CAI_MEMORY'] = 'true'
|
||||
# Automatically apply the memory to the agent's system prompt
|
||||
COMPACTED_SUMMARIES[agent_name] = summary
|
||||
APPLIED_MEMORY_IDS[agent_name] = memory_id
|
||||
if agent_name not in COMPACTED_SUMMARIES:
|
||||
COMPACTED_SUMMARIES[agent_name] = []
|
||||
APPLIED_MEMORY_IDS[agent_name] = []
|
||||
|
||||
# Clear existing memories and add new one (maintain single memory behavior for compact single)
|
||||
COMPACTED_SUMMARIES[agent_name] = [summary]
|
||||
APPLIED_MEMORY_IDS[agent_name] = [memory_id]
|
||||
console.print(f"[green]✓ Memory {memory_id} automatically applied to {agent_name}'s system prompt[/green]")
|
||||
|
||||
# Ask if user wants to clear history
|
||||
|
|
@ -1380,6 +1443,169 @@ This session is being continued from a previous conversation that ran out of con
|
|||
except Exception as e:
|
||||
console.print(f"[yellow]Warning: Could not reload agent automatically: {e}[/yellow]")
|
||||
console.print("[dim]The memory will be applied on the next agent interaction[/dim]")
|
||||
|
||||
def handle_remove(self, args: Optional[List[str]] = None) -> bool:
|
||||
"""Remove a specific memory from an agent."""
|
||||
if not args or len(args) < 2:
|
||||
console.print("[red]Error: Memory ID and agent name required[/red]")
|
||||
console.print("Usage: /memory remove <memory_id> <agent_name>")
|
||||
return False
|
||||
|
||||
memory_id = args[0].upper()
|
||||
agent_identifier = " ".join(args[1:])
|
||||
agent_name = self._resolve_agent_name(agent_identifier)
|
||||
|
||||
if not agent_name:
|
||||
console.print(f"[red]Error: Could not resolve agent '{agent_identifier}'[/red]")
|
||||
return False
|
||||
|
||||
# Check if agent has memories applied
|
||||
if agent_name not in APPLIED_MEMORY_IDS:
|
||||
console.print(f"[yellow]Agent '{agent_name}' has no memories applied[/yellow]")
|
||||
return True
|
||||
|
||||
memory_ids = APPLIED_MEMORY_IDS[agent_name]
|
||||
summaries = COMPACTED_SUMMARIES.get(agent_name, [])
|
||||
|
||||
# Handle backward compatibility
|
||||
if isinstance(memory_ids, str):
|
||||
if memory_ids == memory_id:
|
||||
del APPLIED_MEMORY_IDS[agent_name]
|
||||
if agent_name in COMPACTED_SUMMARIES:
|
||||
del COMPACTED_SUMMARIES[agent_name]
|
||||
console.print(f"[green]✓ Removed memory {memory_id} from {agent_name}[/green]")
|
||||
self._reload_agent_with_memory(agent_name)
|
||||
return True
|
||||
else:
|
||||
console.print(f"[yellow]Memory {memory_id} not found for agent '{agent_name}'[/yellow]")
|
||||
return True
|
||||
|
||||
# Handle list of memories
|
||||
if memory_id not in memory_ids:
|
||||
console.print(f"[yellow]Memory {memory_id} not found for agent '{agent_name}'[/yellow]")
|
||||
return True
|
||||
|
||||
# Find index and remove
|
||||
idx = memory_ids.index(memory_id)
|
||||
memory_ids.pop(idx)
|
||||
if isinstance(summaries, list) and idx < len(summaries):
|
||||
summaries.pop(idx)
|
||||
|
||||
# Clean up if no memories left
|
||||
if not memory_ids:
|
||||
del APPLIED_MEMORY_IDS[agent_name]
|
||||
if agent_name in COMPACTED_SUMMARIES:
|
||||
del COMPACTED_SUMMARIES[agent_name]
|
||||
|
||||
console.print(f"[green]✓ Removed memory {memory_id} from {agent_name}[/green]")
|
||||
self._reload_agent_with_memory(agent_name)
|
||||
|
||||
return True
|
||||
|
||||
def handle_clear(self, args: Optional[List[str]] = None) -> bool:
|
||||
"""Clear all memories from an agent."""
|
||||
if not args:
|
||||
console.print("[red]Error: Agent name required[/red]")
|
||||
console.print("Usage: /memory clear <agent_name>")
|
||||
return False
|
||||
|
||||
agent_identifier = " ".join(args)
|
||||
agent_name = self._resolve_agent_name(agent_identifier)
|
||||
|
||||
if not agent_name:
|
||||
console.print(f"[red]Error: Could not resolve agent '{agent_identifier}'[/red]")
|
||||
return False
|
||||
|
||||
# Check if agent has memories applied
|
||||
if agent_name not in APPLIED_MEMORY_IDS:
|
||||
console.print(f"[yellow]Agent '{agent_name}' has no memories applied[/yellow]")
|
||||
return True
|
||||
|
||||
# Ask for confirmation
|
||||
memory_ids = APPLIED_MEMORY_IDS.get(agent_name)
|
||||
count = len(memory_ids) if isinstance(memory_ids, list) else 1
|
||||
confirm = console.input(f"Clear {count} memory(ies) from '{agent_name}'? (y/N): ")
|
||||
|
||||
if confirm.lower() == 'y':
|
||||
del APPLIED_MEMORY_IDS[agent_name]
|
||||
if agent_name in COMPACTED_SUMMARIES:
|
||||
del COMPACTED_SUMMARIES[agent_name]
|
||||
console.print(f"[green]✓ Cleared all memories from {agent_name}[/green]")
|
||||
self._reload_agent_with_memory(agent_name)
|
||||
else:
|
||||
console.print("[dim]Cancelled[/dim]")
|
||||
|
||||
return True
|
||||
|
||||
def handle_list_applied(self, args: Optional[List[str]] = None) -> bool:
|
||||
"""Show which memories are applied to an agent."""
|
||||
if not args:
|
||||
# Show all agents with applied memories
|
||||
if not APPLIED_MEMORY_IDS:
|
||||
console.print("[yellow]No memories applied to any agents[/yellow]")
|
||||
return True
|
||||
|
||||
console.print("[bold cyan]Applied Memories by Agent[/bold cyan]\n")
|
||||
|
||||
for agent_name, memory_ids in APPLIED_MEMORY_IDS.items():
|
||||
console.print(f"[green]{agent_name}:[/green]")
|
||||
|
||||
if isinstance(memory_ids, list):
|
||||
for i, memory_id in enumerate(memory_ids):
|
||||
# Try to get memory details
|
||||
index = self._load_index()
|
||||
memory_file = index.get('mappings', {}).get(memory_id, "Unknown")
|
||||
console.print(f" {i+1}. {memory_id} - {memory_file}")
|
||||
else:
|
||||
# Backward compatibility
|
||||
index = self._load_index()
|
||||
memory_file = index.get('mappings', {}).get(memory_ids, "Unknown")
|
||||
console.print(f" 1. {memory_ids} - {memory_file}")
|
||||
|
||||
console.print()
|
||||
else:
|
||||
# Show memories for specific agent
|
||||
agent_identifier = " ".join(args)
|
||||
agent_name = self._resolve_agent_name(agent_identifier)
|
||||
|
||||
if not agent_name:
|
||||
console.print(f"[red]Error: Could not resolve agent '{agent_identifier}'[/red]")
|
||||
return False
|
||||
|
||||
if agent_name not in APPLIED_MEMORY_IDS:
|
||||
console.print(f"[yellow]No memories applied to '{agent_name}'[/yellow]")
|
||||
return True
|
||||
|
||||
memory_ids = APPLIED_MEMORY_IDS[agent_name]
|
||||
summaries = COMPACTED_SUMMARIES.get(agent_name, [])
|
||||
|
||||
console.print(f"[bold cyan]Memories applied to {agent_name}[/bold cyan]\n")
|
||||
|
||||
if isinstance(memory_ids, list):
|
||||
for i, memory_id in enumerate(memory_ids):
|
||||
# Get memory details
|
||||
index = self._load_index()
|
||||
memory_file = index.get('mappings', {}).get(memory_id, "Unknown")
|
||||
|
||||
# Show summary preview
|
||||
summary_preview = ""
|
||||
if isinstance(summaries, list) and i < len(summaries):
|
||||
summary_preview = summaries[i][:100] + "..." if len(summaries[i]) > 100 else summaries[i]
|
||||
|
||||
console.print(f"[green]{i+1}. {memory_id}[/green] - {memory_file}")
|
||||
if summary_preview:
|
||||
console.print(f" [dim]{summary_preview}[/dim]")
|
||||
console.print()
|
||||
else:
|
||||
# Backward compatibility
|
||||
index = self._load_index()
|
||||
memory_file = index.get('mappings', {}).get(memory_ids, "Unknown")
|
||||
console.print(f"[green]1. {memory_ids}[/green] - {memory_file}")
|
||||
if isinstance(summaries, str) and summaries:
|
||||
summary_preview = summaries[:100] + "..." if len(summaries) > 100 else summaries
|
||||
console.print(f" [dim]{summary_preview}[/dim]")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Global instance for access from other modules
|
||||
|
|
@ -1393,6 +1619,7 @@ def get_compacted_summary(agent_name: Optional[str] = None) -> Optional[str]:
|
|||
"""Get compacted summary for injection into system prompt.
|
||||
|
||||
This retrieves any applied memory summaries for the agent.
|
||||
Now supports multiple memories per agent.
|
||||
|
||||
Args:
|
||||
agent_name: Specific agent name or None for global summary
|
||||
|
|
@ -1402,9 +1629,24 @@ def get_compacted_summary(agent_name: Optional[str] = None) -> Optional[str]:
|
|||
"""
|
||||
# First check for applied memories in COMPACTED_SUMMARIES
|
||||
if agent_name and agent_name in COMPACTED_SUMMARIES:
|
||||
return COMPACTED_SUMMARIES[agent_name]
|
||||
summaries = COMPACTED_SUMMARIES[agent_name]
|
||||
if isinstance(summaries, list) and summaries:
|
||||
# Concatenate multiple memories with clear separators
|
||||
memory_ids = APPLIED_MEMORY_IDS.get(agent_name, [])
|
||||
parts = []
|
||||
for i, summary in enumerate(summaries):
|
||||
memory_id = memory_ids[i] if i < len(memory_ids) else "Unknown"
|
||||
parts.append(f"Memory {i+1}/{len(summaries)} (ID: {memory_id}):\n{summary}")
|
||||
return "\n\n---\n\n".join(parts)
|
||||
elif isinstance(summaries, str):
|
||||
# Backward compatibility for single memory
|
||||
return summaries
|
||||
elif "__global__" in COMPACTED_SUMMARIES:
|
||||
return COMPACTED_SUMMARIES["__global__"]
|
||||
global_summaries = COMPACTED_SUMMARIES["__global__"]
|
||||
if isinstance(global_summaries, list) and global_summaries:
|
||||
return "\n\n---\n\n".join(global_summaries)
|
||||
elif isinstance(global_summaries, str):
|
||||
return global_summaries
|
||||
|
||||
# Optionally, could auto-load the most recent memory for this agent
|
||||
# but for now we require explicit application
|
||||
|
|
@ -1414,10 +1656,34 @@ def get_compacted_summary(agent_name: Optional[str] = None) -> Optional[str]:
|
|||
def get_applied_memory_id(agent_name: str) -> Optional[str]:
|
||||
"""Get the ID of the memory currently applied to an agent.
|
||||
|
||||
For backward compatibility, returns first memory ID if multiple exist.
|
||||
|
||||
Args:
|
||||
agent_name: The agent name to check
|
||||
|
||||
Returns:
|
||||
Memory ID if applied, None otherwise
|
||||
"""
|
||||
return APPLIED_MEMORY_IDS.get(agent_name)
|
||||
memory_ids = APPLIED_MEMORY_IDS.get(agent_name)
|
||||
if isinstance(memory_ids, list) and memory_ids:
|
||||
return memory_ids[0] # Return first for backward compatibility
|
||||
elif isinstance(memory_ids, str):
|
||||
return memory_ids
|
||||
return None
|
||||
|
||||
|
||||
def get_applied_memory_ids(agent_name: str) -> List[str]:
|
||||
"""Get all memory IDs currently applied to an agent.
|
||||
|
||||
Args:
|
||||
agent_name: The agent name to check
|
||||
|
||||
Returns:
|
||||
List of memory IDs if applied, empty list otherwise
|
||||
"""
|
||||
memory_ids = APPLIED_MEMORY_IDS.get(agent_name, [])
|
||||
if isinstance(memory_ids, list):
|
||||
return memory_ids
|
||||
elif isinstance(memory_ids, str):
|
||||
return [memory_ids] # Convert single ID to list
|
||||
return []
|
||||
|
|
@ -951,36 +951,91 @@ class ParallelCommand(Command):
|
|||
def _merge_chronological(
|
||||
self, all_histories: dict[str, list], agents_to_merge: list[str]
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Merge histories chronologically by timestamp."""
|
||||
# Collect all messages with agent source
|
||||
all_messages = []
|
||||
|
||||
# Create a global message counter for better chronological ordering
|
||||
global_idx = 0
|
||||
|
||||
for agent_idx, agent_name in enumerate(agents_to_merge):
|
||||
"""Merge histories chronologically by interleaving messages based on conversation flow."""
|
||||
# Collect all messages with agent source and their indices
|
||||
agent_messages = {}
|
||||
for agent_name in agents_to_merge:
|
||||
history = all_histories.get(agent_name, [])
|
||||
for local_idx, msg in enumerate(history):
|
||||
# Create a copy of the message to avoid modifying original
|
||||
agent_messages[agent_name] = []
|
||||
for idx, msg in enumerate(history):
|
||||
msg_copy = msg.copy()
|
||||
# Add metadata about source agent and order
|
||||
msg_copy["_source_agent"] = agent_name
|
||||
msg_copy["_original_index"] = local_idx
|
||||
msg_copy["_agent_index"] = agent_idx
|
||||
# Create a unique timestamp that considers both agent and message order
|
||||
# This ensures messages from different agents are properly interleaved
|
||||
if "_timestamp" not in msg_copy:
|
||||
# Use global index to maintain proper chronological order
|
||||
msg_copy["_timestamp"] = global_idx
|
||||
global_idx += 1
|
||||
all_messages.append(msg_copy)
|
||||
msg_copy["_original_index"] = idx
|
||||
agent_messages[agent_name].append(msg_copy)
|
||||
|
||||
# Sort by timestamp to maintain chronological order
|
||||
all_messages.sort(key=lambda x: x.get("_timestamp", 0))
|
||||
# Create indices to track position in each agent's history
|
||||
indices = {agent: 0 for agent in agents_to_merge}
|
||||
|
||||
# Process messages in an intelligent interleaved fashion
|
||||
all_messages = []
|
||||
|
||||
while any(indices[agent] < len(agent_messages[agent]) for agent in agents_to_merge):
|
||||
# Look for the next user message across all agents
|
||||
next_user_msgs = []
|
||||
for agent in agents_to_merge:
|
||||
if indices[agent] < len(agent_messages[agent]):
|
||||
msg = agent_messages[agent][indices[agent]]
|
||||
if msg.get("role") == "user":
|
||||
next_user_msgs.append((agent, msg))
|
||||
|
||||
if next_user_msgs:
|
||||
# Process the first user message found (they should be similar across agents)
|
||||
chosen_agent, user_msg = next_user_msgs[0]
|
||||
all_messages.append(user_msg)
|
||||
indices[chosen_agent] += 1
|
||||
|
||||
# Skip duplicate user messages from other agents
|
||||
for agent, msg in next_user_msgs[1:]:
|
||||
if msg.get("content") == user_msg.get("content"):
|
||||
indices[agent] += 1
|
||||
|
||||
# Now collect all responses to this user message from all agents
|
||||
responses_collected = True
|
||||
while responses_collected:
|
||||
responses_collected = False
|
||||
|
||||
for agent in agents_to_merge:
|
||||
if indices[agent] < len(agent_messages[agent]):
|
||||
msg = agent_messages[agent][indices[agent]]
|
||||
|
||||
# Collect assistant responses and tool interactions until next user message
|
||||
if msg.get("role") in ["assistant", "tool", "system"]:
|
||||
all_messages.append(msg)
|
||||
indices[agent] += 1
|
||||
responses_collected = True
|
||||
|
||||
# If this is a tool call, look for the corresponding tool response
|
||||
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
||||
tool_call_ids = [tc.get("id") for tc in msg.get("tool_calls", [])]
|
||||
|
||||
# Look ahead for tool responses
|
||||
temp_idx = indices[agent]
|
||||
while temp_idx < len(agent_messages[agent]):
|
||||
next_msg = agent_messages[agent][temp_idx]
|
||||
if next_msg.get("role") == "tool" and next_msg.get("tool_call_id") in tool_call_ids:
|
||||
all_messages.append(next_msg)
|
||||
indices[agent] = temp_idx + 1
|
||||
break
|
||||
elif next_msg.get("role") == "user":
|
||||
# Stop if we hit another user message
|
||||
break
|
||||
temp_idx += 1
|
||||
elif msg.get("role") == "user":
|
||||
# Don't process user messages here - they'll be handled in the next iteration
|
||||
break
|
||||
else:
|
||||
# No more user messages, collect any remaining messages
|
||||
for agent in agents_to_merge:
|
||||
if indices[agent] < len(agent_messages[agent]):
|
||||
msg = agent_messages[agent][indices[agent]]
|
||||
all_messages.append(msg)
|
||||
indices[agent] += 1
|
||||
break # Process one at a time to maintain some order
|
||||
|
||||
# Process messages to create the merged history
|
||||
merged = []
|
||||
seen_tool_calls = {} # Track tool calls by ID to avoid duplicates
|
||||
seen_messages = set() # Track message signatures to avoid duplicates
|
||||
|
||||
# Debug: show total messages collected
|
||||
console.print(f"[dim]Total messages collected from all agents: {len(all_messages)}[/dim]")
|
||||
|
|
@ -995,18 +1050,22 @@ class ParallelCommand(Command):
|
|||
|
||||
for msg in all_messages:
|
||||
should_add = True
|
||||
msg_sig = self._get_message_signature(msg)
|
||||
|
||||
# Check for duplicate messages
|
||||
if msg.get("role") == "user":
|
||||
# Check if we've already seen this exact message
|
||||
if msg_sig and msg_sig in seen_messages:
|
||||
should_add = False
|
||||
|
||||
# Additional checks for specific message types
|
||||
if should_add and msg.get("role") == "user":
|
||||
# For user messages, check if the same content was just added
|
||||
# This helps when both agents received the same user input
|
||||
if (
|
||||
merged
|
||||
and merged[-1].get("role") == "user"
|
||||
and merged[-1].get("content") == msg.get("content")
|
||||
):
|
||||
should_add = False
|
||||
elif msg.get("role") == "assistant" and msg.get("tool_calls"):
|
||||
elif should_add and msg.get("role") == "assistant" and msg.get("tool_calls"):
|
||||
# For tool calls, track by tool call ID
|
||||
for tool_call in msg.get("tool_calls", []):
|
||||
tool_id = tool_call.get("id")
|
||||
|
|
@ -1015,7 +1074,7 @@ class ParallelCommand(Command):
|
|||
should_add = False
|
||||
break
|
||||
seen_tool_calls[tool_id] = msg.get("_source_agent")
|
||||
elif msg.get("role") == "tool":
|
||||
elif should_add and msg.get("role") == "tool":
|
||||
# Tool responses should match their tool calls
|
||||
tool_call_id = msg.get("tool_call_id")
|
||||
if tool_call_id and tool_call_id in seen_tool_calls:
|
||||
|
|
@ -1027,6 +1086,8 @@ class ParallelCommand(Command):
|
|||
# Clean up internal metadata before adding
|
||||
clean_msg = {k: v for k, v in msg.items() if not k.startswith("_")}
|
||||
merged.append(clean_msg)
|
||||
if msg_sig:
|
||||
seen_messages.add(msg_sig)
|
||||
|
||||
return merged
|
||||
|
||||
|
|
|
|||
16
uv.lock
16
uv.lock
|
|
@ -281,7 +281,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "cai-framework"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "dnspython" },
|
||||
|
|
@ -307,6 +307,7 @@ dependencies = [
|
|||
{ name = "paramiko" },
|
||||
{ name = "prompt-toolkit" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pypdf2" },
|
||||
{ name = "requests" },
|
||||
{ name = "rich" },
|
||||
{ name = "types-requests" },
|
||||
|
|
@ -370,6 +371,7 @@ requires-dist = [
|
|||
{ name = "paramiko", specifier = ">=3.5.1" },
|
||||
{ name = "prompt-toolkit", specifier = ">=3.0.39" },
|
||||
{ name = "pydantic", specifier = ">=2.10,<3" },
|
||||
{ name = "pypdf2" },
|
||||
{ name = "requests", specifier = ">=2.0,<3" },
|
||||
{ name = "rich", specifier = ">=13.9.4" },
|
||||
{ name = "types-requests", specifier = ">=2.0,<3" },
|
||||
|
|
@ -3180,6 +3182,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pypdf2"
|
||||
version = "3.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/bb/18dc3062d37db6c491392007dfd1a7f524bb95886eb956569ac38a23a784/PyPDF2-3.0.1.tar.gz", hash = "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440", size = 227419 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/5e/c86a5643653825d3c913719e788e41386bee415c2b87b4f955432f2de6b2/pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928", size = 232572 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.5"
|
||||
|
|
|
|||
Loading…
Reference in New Issue