Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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