Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/codegen/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from codegen.cli.commands.agents.main import agents_app
from codegen.cli.commands.claude.main import claude
from codegen.cli.commands.config.main import config_command
from codegen.cli.commands.council.main import council_app
from codegen.cli.commands.init.main import init
from codegen.cli.commands.integrations.main import integrations_app
from codegen.cli.commands.login.main import login
Expand Down Expand Up @@ -83,6 +84,7 @@ def version_callback(value: bool):
# Add Typer apps as sub-applications (these will handle their own sub-command logging)
main.add_typer(agents_app, name="agents")
main.add_typer(config_command, name="config")
main.add_typer(council_app, name="council")
main.add_typer(integrations_app, name="integrations")
main.add_typer(profile_app, name="profile")

Expand Down
2 changes: 2 additions & 0 deletions src/codegen/cli/commands/council/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Council command for multi-agent collaboration."""

167 changes: 167 additions & 0 deletions src/codegen/cli/commands/council/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""CLI command for running multi-agent councils."""

import typer
from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

from codegen.agents.agent import Agent
from codegen.cli.auth.token_manager import get_current_token
from codegen.cli.rich.spinners import create_spinner
from codegen.cli.utils.org import resolve_org_id
from codegen.council.models import AgentConfig, CouncilConfig
from codegen.council.orchestrator import CouncilOrchestrator

console = Console()

council_app = typer.Typer(help="Run multi-agent councils for collaborative problem-solving")


@council_app.command("run")
def run_council(
prompt: str = typer.Option(..., "--prompt", "-p", help="The prompt/question for the council"),
models: str = typer.Option(
"gpt-4o,claude-3-5-sonnet-20241022,gemini-2.0-flash-exp",
"--models",
"-m",
help="Comma-separated list of models to use",
),
candidates: int = typer.Option(3, "--candidates", "-c", help="Number of candidates per model"),
disable_ranking: bool = typer.Option(False, "--no-ranking", help="Skip Stage 2 peer ranking"),
synthesis_model: str = typer.Option(
"claude-3-5-sonnet-20241022",
"--synthesis-model",
help="Model to use for final synthesis",
),
org_id: int | None = typer.Option(None, help="Organization ID (defaults to saved org)"),
poll_interval: float = typer.Option(5.0, "--poll", help="Seconds between status checks"),
):
"""Run a multi-agent council to collaboratively solve a problem.

Example:
codegen council run --prompt "How can I optimize my Python code?" --models gpt-4o,claude-3-5-sonnet
"""
# Get token
token = get_current_token()
if not token:
console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.")
raise typer.Exit(1)

# Resolve org ID
resolved_org_id = resolve_org_id(org_id)
if resolved_org_id is None:
console.print(
"[red]Error:[/red] Organization ID not provided. "
"Pass --org-id, set CODEGEN_ORG_ID, or run 'codegen login'."
)
raise typer.Exit(1)

# Parse models
model_list = [m.strip() for m in models.split(",")]

# Build config
agent_configs = [AgentConfig(model=model) for model in model_list]

config = CouncilConfig(
agents=agent_configs,
num_candidates=candidates,
enable_ranking=not disable_ranking,
synthesis_model=synthesis_model,
)

console.print(
Panel(
f"[cyan]Models:[/cyan] {', '.join(model_list)}\n"
f"[cyan]Candidates per model:[/cyan] {candidates}\n"
f"[cyan]Total agent runs:[/cyan] {len(model_list) * candidates}\n"
f"[cyan]Ranking enabled:[/cyan] {'Yes' if not disable_ranking else 'No'}\n"
f"[cyan]Synthesis model:[/cyan] {synthesis_model}",
title="πŸ›οΈ [bold]Council Configuration[/bold]",
border_style="blue",
box=box.ROUNDED,
)
)

# Run council
orchestrator = CouncilOrchestrator(
token=token,
org_id=resolved_org_id,
config=config,
)

spinner = create_spinner("Running council...")
spinner.start()

try:
result = orchestrator.run(prompt, poll_interval=poll_interval)
except Exception as e:
spinner.stop()
console.print(f"[red]Error running council:[/red] {e}")
raise typer.Exit(1)
finally:
spinner.stop()

# Display results
console.print("\n")
console.print(
Panel(
result.stage3_synthesis.content if result.stage3_synthesis else "No synthesis generated",
title="✨ [bold]Final Synthesized Answer[/bold]",
border_style="green",
box=box.ROUNDED,
padding=(1, 2),
)
)

# Show candidate responses
if result.stage1_candidates:
console.print("\n[bold]Stage 1: Candidate Responses[/bold]")
table = Table(box=box.ROUNDED)
table.add_column("Model", style="cyan")
table.add_column("Agent Run", style="magenta")
table.add_column("Preview", style="dim")

for cand in result.stage1_candidates:
preview = cand.content[:100] + "..." if len(cand.content) > 100 else cand.content
table.add_row(
cand.model,
f"#{cand.agent_run_id}",
preview,
)

console.print(table)

# Show aggregate rankings
if result.aggregate_rankings:
console.print("\n[bold]Stage 2: Aggregate Rankings[/bold]")
rank_table = Table(box=box.ROUNDED)
rank_table.add_column("Rank", style="yellow", justify="center")
rank_table.add_column("Model", style="cyan")
rank_table.add_column("Avg Score", style="green", justify="right")
rank_table.add_column("Judgments", style="dim", justify="right")

for idx, ranking in enumerate(result.aggregate_rankings, start=1):
rank_table.add_row(
f"#{idx}",
ranking["model"],
f"{ranking['average_rank']:.2f}",
str(ranking["rankings_count"]),
)

console.print(rank_table)

# Show synthesis info
if result.stage3_synthesis:
console.print("\n[dim]πŸ’‘ Synthesis Details:[/dim]")
console.print(f" Method: {result.stage3_synthesis.method}")
console.print(f" Agent Run: #{result.stage3_synthesis.agent_run_id}")
if result.stage3_synthesis.web_url:
console.print(f" View: {result.stage3_synthesis.web_url}")

console.print("\n[green]βœ“[/green] Council completed successfully!")


# Make council_app the default export for CLI integration
council = council_app

19 changes: 19 additions & 0 deletions src/codegen/council/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Multi-agent council orchestration for Codegen.

This module provides a council-based approach where multiple agents with different
models collaborate to solve complex problems through:
1. Parallel generation of candidate responses
2. Peer ranking and evaluation
3. Synthesis of final answer
"""

from .models import AgentConfig, CouncilConfig, CouncilResult
from .orchestrator import CouncilOrchestrator

__all__ = [
"AgentConfig",
"CouncilConfig",
"CouncilResult",
"CouncilOrchestrator",
]

154 changes: 154 additions & 0 deletions src/codegen/council/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Data models for council orchestration."""

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional


@dataclass
class AgentConfig:
"""Configuration for a single agent in the council.

Attributes:
model: Model identifier to use for this agent
role: Optional role description for the agent
temperature: Sampling temperature (0-1, higher = more creative)
prompt_variation: Optional prompt modification strategy
"""

model: str
role: Optional[str] = None
temperature: float = 0.9
prompt_variation: Optional[str] = None


@dataclass
class CouncilConfig:
"""Configuration for council execution.

Attributes:
agents: List of agent configurations to use
num_candidates: Number of parallel candidates to generate per agent
enable_ranking: Whether to run Stage 2 (peer ranking)
synthesis_model: Model to use for final synthesis
synthesis_temperature: Temperature for synthesis
tournament_threshold: Use tournament synthesis if candidates exceed this
group_size: Size of groups for tournament synthesis
"""

agents: List[AgentConfig]
num_candidates: int = 3
enable_ranking: bool = True
synthesis_model: str = "claude-3-5-sonnet-20241022"
synthesis_temperature: float = 0.2
tournament_threshold: int = 20
group_size: int = 10


@dataclass
class CandidateResponse:
"""A single candidate response from an agent.

Attributes:
agent_run_id: ID of the codegen agent run
model: Model that generated this response
content: The response content
web_url: URL to view the agent run
metadata: Additional metadata from the run
"""

agent_run_id: int
model: str
content: str
web_url: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class RankingResult:
"""Ranking of candidates by a judging agent.

Attributes:
judge_model: Model that performed the ranking
agent_run_id: ID of the ranking agent run
ranking_text: Full text of the ranking explanation
parsed_ranking: Ordered list of response labels (best to worst)
web_url: URL to view the ranking agent run
"""

judge_model: str
agent_run_id: int
ranking_text: str
parsed_ranking: List[str]
web_url: Optional[str] = None


@dataclass
class SynthesisResult:
"""Final synthesized response.

Attributes:
agent_run_id: ID of the synthesis agent run
model: Model that performed synthesis
content: The final synthesized response
web_url: URL to view the synthesis agent run
method: Synthesis method used ('simple' or 'tournament')
"""

agent_run_id: int
model: str
content: str
web_url: Optional[str] = None
method: str = "simple"


@dataclass
class CouncilResult:
"""Complete result from a council execution.

Attributes:
stage1_candidates: All candidate responses generated
stage2_rankings: Rankings from peer evaluation (if enabled)
stage3_synthesis: Final synthesized response
aggregate_rankings: Aggregated ranking scores across all judges
label_to_model: Mapping from anonymous labels to model names
"""

stage1_candidates: List[CandidateResponse]
stage2_rankings: Optional[List[RankingResult]] = None
stage3_synthesis: Optional[SynthesisResult] = None
aggregate_rankings: Optional[List[Dict[str, Any]]] = None
label_to_model: Optional[Dict[str, str]] = None

def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for serialization."""
return {
"stage1_candidates": [
{
"agent_run_id": c.agent_run_id,
"model": c.model,
"content": c.content,
"web_url": c.web_url,
}
for c in self.stage1_candidates
],
"stage2_rankings": [
{
"judge_model": r.judge_model,
"agent_run_id": r.agent_run_id,
"ranking_text": r.ranking_text,
"parsed_ranking": r.parsed_ranking,
"web_url": r.web_url,
}
for r in (self.stage2_rankings or [])
],
"stage3_synthesis": {
"agent_run_id": self.stage3_synthesis.agent_run_id,
"model": self.stage3_synthesis.model,
"content": self.stage3_synthesis.content,
"web_url": self.stage3_synthesis.web_url,
"method": self.stage3_synthesis.method,
} if self.stage3_synthesis else None,
"aggregate_rankings": self.aggregate_rankings,
"label_to_model": self.label_to_model,
}

Loading