From 9f3a68bd66f3f4bc38cf7fe71bd7aae40d7c358f Mon Sep 17 00:00:00 2001 From: Magnus Hedemark Date: Fri, 10 Jul 2026 00:17:56 -0400 Subject: [PATCH] feat: replace fake personas with real profiles from hermes-profiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of the compose phase fabricating personas with fake backgrounds, the council now draws from 39 real professional profiles via a git submodule (https://github.com/magnus919/hermes-profiles). Key changes: - New select phase reads SOUL.md + profile.yaml from profiles submodule - Auto-updates submodule before selection via git submodule update --remote - --profiles flag for explicit selection (comma-separated names) - Auto-selection by keyword overlap with profile descriptions when omitted - Each agent's identity is their real SOUL.md — actual methodology, values, and operating principles, not invented backgrounds - Falls back to composed personas if profile library is unavailable - Real profiles produce genuine methodological disagreement (debugger said 'unanswerable without a verified process' to naming question) Signed-off-by: Magnus Hedemark --- .gitmodules | 3 + agent-council/agent_council/cli.py | 14 ++ agent-council/agent_council/graph.py | 109 ++++++++++---- .../agent_council/phases/cross_examine.py | 91 +++++++----- .../agent_council/phases/position.py | 65 +++++--- .../agent_council/phases/premortem.py | 39 +++-- agent-council/agent_council/phases/select.py | 139 ++++++++++++++++++ .../agent_council/phases/synthesis.py | 2 +- agent-council/agent_council/state.py | 19 +++ agent-council/profiles | 1 + agent-council/pyproject.toml | 1 + 11 files changed, 386 insertions(+), 97 deletions(-) create mode 100644 .gitmodules create mode 100644 agent-council/agent_council/phases/select.py create mode 160000 agent-council/profiles diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..93f0fd8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "agent-council/profiles"] + path = agent-council/profiles + url = https://github.com/magnus919/hermes-profiles.git diff --git a/agent-council/agent_council/cli.py b/agent-council/agent_council/cli.py index d0c1dfb..fb602bd 100644 --- a/agent-council/agent_council/cli.py +++ b/agent-council/agent_council/cli.py @@ -70,6 +70,14 @@ def main(): default=0.10, help="Convergence threshold for confidence dispersion (default: 0.10)", ) + parser.add_argument( + "--profiles", + type=str, + default=None, + help="Comma-separated profile names from the hermes-profiles library " + "(e.g. 'debugger,researcher,product-manager'). " + "Omit for auto-selection based on the question.", + ) args = parser.parse_args() @@ -95,6 +103,11 @@ def main(): ) sys.exit(1) + # Parse explicit profile list + profile_names = None + if args.profiles: + profile_names = [n.strip() for n in args.profiles.split(",")] + try: state = asyncio.run( run_debate( @@ -105,6 +118,7 @@ def main(): convergence_threshold=args.convergence, verbose=args.verbose, persona_file=args.persona_file, + profile_names=profile_names, ) ) except ValueError as e: diff --git a/agent-council/agent_council/graph.py b/agent-council/agent_council/graph.py index 2e0d8c8..6198b5e 100644 --- a/agent-council/agent_council/graph.py +++ b/agent-council/agent_council/graph.py @@ -1,8 +1,6 @@ """Graph orchestration — runs the debate protocol as a state machine.""" -import asyncio import json -import os import sys import time from datetime import datetime, timezone @@ -10,6 +8,7 @@ from pathlib import Path from agent_council.state import CouncilState from agent_council.phases.compose import compose_personas +from agent_council.phases.select import select_by_names, select_by_question from agent_council.phases.premortem import run_premortems from agent_council.phases.position import run_positions from agent_council.phases.cross_examine import run_cross_examination @@ -30,6 +29,39 @@ def _run_dir() -> Path: return path +def _identity_for( + state: CouncilState, + name: str, + fallback_persona=None, +) -> str: + """Build an identity block for a debate agent. + + If real profiles are loaded, uses the SOUL.md content. + Otherwise falls back to fabricated persona fields. + """ + # Prefer real profiles + for p in state.profiles: + if p.name == name: + return ( + f"You are {name}.\n\n" + f"Your identity and operating principles:\n" + f"{p.soul_content}\n\n" + f"Description: {p.description}" + ) + + # Fallback to fabricated persona + if fallback_persona: + return ( + f"You are {fallback_persona.name}.\n" + f"Background: {fallback_persona.background}\n" + f"Expertise: {fallback_persona.expertise}\n" + f"Approach: {fallback_persona.approach}\n" + f"Bias: {fallback_persona.bias}" + ) + + return f"You are {name}." + + async def run_debate( question: str, num_agents: int = 5, @@ -38,18 +70,16 @@ async def run_debate( convergence_threshold: float = 0.10, verbose: bool = False, persona_file: str | None = None, + profile_names: list[str] | None = None, ) -> CouncilState: """Run the full debate protocol with live progress output. Phases: - 1. Compose — generate/load agent personas + 1. Select/Compose — pick real profiles or generate personas 2. Premortem — each agent envisions failure 3. Position — each agent forms initial position 4. Cross-examine — iterative, convergence-checked rounds 5. Synthesis — produce decision landscape - - Progress is streamed to stdout as each major step completes. - Intermediate outputs are also written to /tmp/agent-council//. """ rundir = _run_dir() state = CouncilState( @@ -59,43 +89,67 @@ async def run_debate( convergence_threshold=convergence_threshold, ) - # Phase 1: Compose personas + # Phase 1: Select or Compose agents _stream("🏛 Council assembling...") - if persona_file: - with open(persona_file) as f: - data = json.load(f) - from agent_council.state import AgentPersona - state.personas = [AgentPersona(**p) for p in data] - _stream(f" Loaded {len(state.personas)} personas from file") + if profile_names: + # Explicit profile selection + state.profiles = select_by_names(profile_names) + _stream(f" 📂 Loaded {len(state.profiles)} profiles (explicit)") else: - state.personas = await compose_personas(question, num_agents) + # Try auto-selecting profiles from the library + state.profiles = select_by_question(question, num_agents) + if state.profiles: + _stream(f" 📂 Auto-selected {len(state.profiles)} profiles from library") + else: + # Fallback: compose fabricated personas + _stream(" ⚡ No profile library found, composing personas...") + if persona_file: + from agent_council.state import AgentPersona + with open(persona_file) as f: + data = json.load(f) + state.personas = [AgentPersona(**p) for p in data] + _stream(f" Loaded {len(state.personas)} personas from file") + else: + state.personas = await compose_personas(question, num_agents) if verbose: - for p in state.personas: - _stream(f" 👤 {p.name}: {p.expertise}") + for p in state.profiles or state.personas: + name = p.name if hasattr(p, 'name') else p + _stream(f" 👤 {name}") - # Write personas to run dir - with open(rundir / "personas.json", "w") as f: - f.write(json.dumps([p.model_dump() for p in state.personas], indent=2)) - _stream(f" ✅ {len(state.personas)} personas composed") + # Write agent identities to run dir + with open(rundir / "agents.json", "w") as f: + agents = { + "profiles": [ + {"name": p.name, "description": p.description} + for p in state.profiles + ], + "personas": [ + {"name": p.name, "expertise": p.expertise} + for p in state.personas + ], + } + f.write(json.dumps(agents, indent=2, default=str)) + _stream(f" ✅ {len(state.profiles or state.personas)} agents ready") # Phase 2: Premortem _stream(" 🔮 Pre-mortem phase...") t0 = time.time() - state.premortems = await run_premortems(question, state.personas) + state.premortems = await run_premortems(question, state, verbose) _stream(f" ✅ Pre-mortem complete ({len(state.premortems)} agents, {time.time()-t0:.0f}s)") with open(rundir / "premortems.json", "w") as f: f.write(json.dumps( {k: v.model_dump() for k, v in state.premortems.items()}, indent=2, + default=str, )) # Phase 3: Position _stream(" 📋 Position phase...") t0 = time.time() - state.positions = await run_positions(question, state.personas, state.premortems) + state.positions = await run_positions(question, state, verbose) confidences = [p.confidence for p in state.positions.values()] avg_conf = sum(confidences) / len(confidences) if confidences else 0 _stream(f" ✅ Positions formed ({len(state.positions)} agents, avg confidence {avg_conf:.2f}, {time.time()-t0:.0f}s)") @@ -108,6 +162,7 @@ async def run_debate( f.write(json.dumps( {k: v.model_dump() for k, v in state.positions.items()}, indent=2, + default=str, )) # Phase 4: Iterative cross-examination @@ -120,12 +175,7 @@ async def run_debate( _stream(f" Round {round_num}... ", end="") t0 = time.time() - cross_results = await run_cross_examination( - question, - state.personas, - state.positions, - state.cross_examination_rounds if state.cross_examination_rounds else None, - ) + cross_results = await run_cross_examination(question, state, verbose) state.cross_examination_rounds.append(cross_results) metrics = compute_round_metrics(state) @@ -139,11 +189,11 @@ async def run_debate( f"({elapsed:.0f}s) → {stop_reason}" ) - # Write round output with open(rundir / f"round_{round_num}.json", "w") as f: f.write(json.dumps( {k: v.model_dump() for k, v in cross_results.items()}, indent=2, + default=str, )) if stop_reason != "continue": @@ -159,7 +209,6 @@ async def run_debate( stop_reason = getattr(state, "_stopped_reason", "max_rounds") state.synthesis.stopped_reason = stop_reason # type: ignore - # Write synthesis with open(rundir / "synthesis.json", "w") as f: f.write(state.synthesis.model_dump_json(indent=2)) diff --git a/agent-council/agent_council/phases/cross_examine.py b/agent-council/agent_council/phases/cross_examine.py index bafe733..2dd31d8 100644 --- a/agent-council/agent_council/phases/cross_examine.py +++ b/agent-council/agent_council/phases/cross_examine.py @@ -3,7 +3,7 @@ import asyncio from pydantic_ai import Agent from agent_council.state import ( - AgentPersona, + CouncilState, Position, CrossExamination, ) @@ -31,42 +31,23 @@ def _format_other_positions( async def run_cross_examination( question: str, - personas: list[AgentPersona], - positions: dict[str, Position], - prior_rounds: list[dict[str, CrossExamination]] | None = None, + state: CouncilState, + verbose: bool = False, ) -> dict[str, CrossExamination]: """Each agent reads all other positions and responds. - This is the core convergence mechanism — agents confront alternative - perspectives and either shift, dig in, or identify new evidence needs. + Uses real profiles if available, falls back to fabricated personas. """ cfg = load_config() - async def _cross(persona: AgentPersona) -> tuple[str, CrossExamination]: - other_positions = _format_other_positions(persona.name, positions) - round_context = "" - if prior_rounds: - round_context = "\n\nPrevious round context:\n" - for i, rnd in enumerate(prior_rounds): - if persona.name in rnd: - prev = rnd[persona.name] - round_context += f"Round {i + 1} — your reflection: {prev.reflection}\n" - if prev.concessions: - round_context += ( - f" You conceded: {'; '.join(prev.concessions)}\n" - ) - if prev.remaining_disagreements: - round_context += ( - f" Still in dispute: " - f"{'; '.join(prev.remaining_disagreements)}\n" - ) - + async def _cross( + agent_id: str, + identity_block: str, + other_positions: str, + round_context: str, + ) -> tuple[str, CrossExamination]: system = ( - f"You are {persona.name}.\n" - f"Background: {persona.background}\n" - f"Expertise: {persona.expertise}\n" - f"Approach: {persona.approach}\n" - f"Bias: {persona.bias}\n\n" + f"{identity_block}\n\n" "You are in a structured debate. You have read every other agent's " "position on the question.\n\n" "Other agents' positions:\n" @@ -82,9 +63,53 @@ async def run_cross_examination( agent = Agent(cfg["model"], output_type=CrossExamination, system_prompt=system) result = await agent.run(question) output = result.output - output.agent_name = persona.name - return persona.name, output + output.agent_name = agent_id + return agent_id, output + + # Build list of agent identities + agents = [] + if state.profiles: + for p in state.profiles: + identity = ( + f"You are {p.name}.\n\n" + f"Your identity and operating principles:\n" + f"{p.soul_content}\n\n" + f"Description: {p.description}" + ) + agents.append((p.name, identity)) + else: + for p in state.personas: + identity = ( + f"You are {p.name}.\n" + f"Background: {p.background}\n" + f"Expertise: {p.expertise}\n" + f"Approach: {p.approach}\n" + f"Bias: {p.bias}" + ) + agents.append((p.name, identity)) + + prior_rounds = state.cross_examination_rounds + + tasks = [] + for agent_id, identity_block in agents: + other_positions = _format_other_positions(agent_id, state.positions) + + round_context = "" + if prior_rounds: + round_context = "\n\nPrevious round context:\n" + for i, rnd in enumerate(prior_rounds): + if agent_id in rnd: + prev = rnd[agent_id] + round_context += f"Round {i + 1} — your reflection: {prev.reflection}\n" + if prev.concessions: + round_context += f" You conceded: {'; '.join(prev.concessions)}\n" + if prev.remaining_disagreements: + round_context += ( + f" Still in dispute: " + f"{'; '.join(prev.remaining_disagreements)}\n" + ) + + tasks.append(_cross(agent_id, identity_block, other_positions, round_context)) - tasks = [_cross(p) for p in personas] results = await asyncio.gather(*tasks) return dict(results) diff --git a/agent-council/agent_council/phases/position.py b/agent-council/agent_council/phases/position.py index 267cab8..b47ccd8 100644 --- a/agent-council/agent_council/phases/position.py +++ b/agent-council/agent_council/phases/position.py @@ -2,46 +2,43 @@ import asyncio from pydantic_ai import Agent -from agent_council.state import AgentPersona, Position, Premortem +from agent_council.state import CouncilState, Position, Premortem from agent_council.config import load_config from agent_council.guardrails import FACTUAL_CLAIM_GUARDRAIL async def run_positions( question: str, - personas: list[AgentPersona], - premortems: dict[str, Premortem], + state: CouncilState, + verbose: bool = False, ) -> dict[str, Position]: """Each agent forms an independent initial position. Agents see their own premortem (to maintain continuity) but NOT other - agents' positions or premortems. This ensures independent thought. + agents' positions or premortems. Ensures independent thought. + Uses real profiles if available, falls back to fabricated personas. """ cfg = load_config() - async def _position(persona: AgentPersona) -> tuple[str, Position]: - my_premortem = premortems.get(persona.name) - + async def _position( + agent_id: str, + identity_block: str, + premortem: Premortem | None, + ) -> tuple[str, Position]: system = ( - f"You are {persona.name}.\n" - f"Background: {persona.background}\n" - f"Expertise: {persona.expertise}\n" - f"Approach: {persona.approach}\n" - f"Bias: {persona.bias}\n\n" + f"{identity_block}\n\n" "You are in a structured debate. Your task: form your initial " "position on the question. Be specific about your stance, your " - "reasoning, and what assumptions you're making.\n" - f"Your pre-mortem identified these failure modes:" + "reasoning, and what assumptions you're making." ) - if my_premortem: + if premortem: system += ( - f"\n - Failure scenario: {my_premortem.failure_scenario}\n" - f" - Root causes: {'; '.join(my_premortem.root_causes)}\n" - f" - Warning signals: {'; '.join(my_premortem.early_warning_signals)}\n\n" + f"\n\nYour pre-mortem identified these failure modes:\n" + f" - Failure scenario: {premortem.failure_scenario}\n" + f" - Root causes: {'; '.join(premortem.root_causes)}\n" + f" - Warning signals: {'; '.join(premortem.early_warning_signals)}\n\n" "Your position should account for these risks." ) - else: - system += "\n (none recorded)\n" system += ( "\n\nReturn your position with a confidence score (0-1) and " @@ -52,9 +49,31 @@ async def run_positions( agent = Agent(cfg["model"], output_type=Position, system_prompt=system) result = await agent.run(question) output = result.output - output.agent_name = persona.name - return persona.name, output + output.agent_name = agent_id + return agent_id, output + + tasks = [] + if state.profiles: + for p in state.profiles: + identity = ( + f"You are {p.name}.\n\n" + f"Your identity and operating principles:\n" + f"{p.soul_content}\n\n" + f"Description: {p.description}" + ) + pm = state.premortems.get(p.name) + tasks.append(_position(p.name, identity, pm)) + else: + for p in state.personas: + identity = ( + f"You are {p.name}.\n" + f"Background: {p.background}\n" + f"Expertise: {p.expertise}\n" + f"Approach: {p.approach}\n" + f"Bias: {p.bias}" + ) + pm = state.premortems.get(p.name) + tasks.append(_position(p.name, identity, pm)) - tasks = [_position(p) for p in personas] results = await asyncio.gather(*tasks) return dict(results) diff --git a/agent-council/agent_council/phases/premortem.py b/agent-council/agent_council/phases/premortem.py index 309a96e..015a9b5 100644 --- a/agent-council/agent_council/phases/premortem.py +++ b/agent-council/agent_council/phases/premortem.py @@ -2,32 +2,30 @@ import asyncio from pydantic_ai import Agent -from agent_council.state import AgentPersona, Premortem +from agent_council.state import CouncilState, Premortem from agent_council.config import load_config from agent_council.guardrails import FACTUAL_CLAIM_GUARDRAIL async def run_premortems( question: str, - personas: list[AgentPersona], + state: CouncilState, + verbose: bool = False, ) -> dict[str, Premortem]: """Each agent independently writes a failure scenario. Agents do NOT see each other's premortems — this runs before any positions are formed, bypassing positional commitment bias. + Uses real profiles if available, falls back to fabricated personas. """ cfg = load_config() - async def _premortem(persona: AgentPersona) -> tuple[str, Premortem]: + async def _premortem(agent_id: str, identity_block: str) -> tuple[str, Premortem]: agent = Agent( cfg["model"], output_type=Premortem, system_prompt=( - f"You are {persona.name}.\n" - f"Background: {persona.background}\n" - f"Expertise: {persona.expertise}\n" - f"Approach: {persona.approach}\n" - f"Bias: {persona.bias}\n\n" + f"{identity_block}\n\n" "You are in a structured debate. Your first task: write a " "pre-mortem — imagine it is 6 months in the future and the " "decision about to be discussed has ALREADY FAILED. Write " @@ -38,8 +36,29 @@ async def run_premortems( ), ) result = await agent.run(question) - return persona.name, result.output + return agent_id, result.output + + # Build agent identities + tasks = [] + if state.profiles: + for p in state.profiles: + identity = ( + f"You are {p.name}.\n\n" + f"Your identity and operating principles:\n" + f"{p.soul_content}\n\n" + f"Description: {p.description}" + ) + tasks.append(_premortem(p.name, identity)) + else: + for p in state.personas: + identity = ( + f"You are {p.name}.\n" + f"Background: {p.background}\n" + f"Expertise: {p.expertise}\n" + f"Approach: {p.approach}\n" + f"Bias: {p.bias}" + ) + tasks.append(_premortem(p.name, identity)) - tasks = [_premortem(p) for p in personas] results = await asyncio.gather(*tasks) return dict(results) diff --git a/agent-council/agent_council/phases/select.py b/agent-council/agent_council/phases/select.py new file mode 100644 index 0000000..3a44597 --- /dev/null +++ b/agent-council/agent_council/phases/select.py @@ -0,0 +1,139 @@ +"""Select phase — picks real profiles from the hermes-profiles library.""" + +import json +import os +import subprocess +import sys +from pathlib import Path + +import yaml + +from agent_council.state import ProfileInfo + + +# Path to the profiles submodule within the skill directory +PROFILES_DIR = Path(__file__).resolve().parent.parent.parent / "profiles" / "profiles" + + +def _update_submodule() -> None: + """Pull the latest profiles from the hermes-profiles submodule.""" + skill_root = PROFILES_DIR.parent # agent-council/profiles/ + try: + subprocess.run( + ["git", "submodule", "update", "--remote", "--init"], + cwd=skill_root.parent, # agent-council/ + capture_output=True, + timeout=30, + ) + except Exception: + pass # Non-fatal — use whatever version we have + + +def _list_available() -> list[str]: + """List all available profile names.""" + if not PROFILES_DIR.exists(): + return [] + return sorted( + d.name for d in PROFILES_DIR.iterdir() + if d.is_dir() and not d.name.startswith(".") + ) + + +def _load_profile(name: str) -> ProfileInfo | None: + """Load a single profile's SOUL.md and profile.yaml.""" + profile_dir = PROFILES_DIR / name + soul_path = profile_dir / "SOUL.md" + yaml_path = profile_dir / "profile.yaml" + + if not soul_path.exists(): + return None + + soul_content = soul_path.read_text(encoding="utf-8") + + description = "" + if yaml_path.exists(): + try: + with open(yaml_path) as f: + data = yaml.safe_load(f) + description = data.get("description", "") or "" + except Exception: + pass + + return ProfileInfo(name=name, description=description, soul_content=soul_content) + + +def load_all() -> list[ProfileInfo]: + """Load all available profiles. Updates submodule first.""" + _update_submodule() + profiles = [] + for name in _list_available(): + p = _load_profile(name) + if p: + profiles.append(p) + return profiles + + +def select_by_names(names: list[str]) -> list[ProfileInfo]: + """Load specific profiles by name.""" + _update_submodule() + profiles = [] + for name in names: + name = name.strip().lower() + p = _load_profile(name) + if p: + profiles.append(p) + else: + print( + f"Warning: profile '{name}' not found. " + f"Available: {', '.join(_list_available())}", + file=sys.stderr, + ) + return profiles + + +def select_by_question(question: str, count: int = 5) -> list[ProfileInfo]: + """Auto-select the most relevant profiles for a question. + + Scores each profile by keyword overlap between the question + and the profile's description. Returns the top N profiles. + """ + all_profiles = load_all() + if not all_profiles: + return [] + + question_lower = question.lower() + question_words = set(question_lower.split()) + + scored = [] + for p in all_profiles: + desc_words = set(p.description.lower().split()) + # Also score on profile name + name_words = set(p.name.lower().replace("-", " ").split()) + + # Count overlapping words + overlap = len(question_words & desc_words) + len(question_words & name_words) + + # Bonus for exact phrase matches + if p.name.lower().replace("-", " ") in question_lower: + overlap += 3 + + scored.append((overlap, p)) + + scored.sort(key=lambda x: -x[0]) + + # Pick top N, ensure diversity (skip if too similar description) + selected = [] + seen_descriptions = set() + for _, p in scored: + desc_key = p.description[:80] + if desc_key not in seen_descriptions or len(selected) < 3: + selected.append(p) + seen_descriptions.add(desc_key) + if len(selected) >= count: + break + + # Fallback: if somehow empty, grab first N + if not selected and all_profiles: + selected = all_profiles[:count] + + return selected diff --git a/agent-council/agent_council/phases/synthesis.py b/agent-council/agent_council/phases/synthesis.py index a232a31..e86e604 100644 --- a/agent-council/agent_council/phases/synthesis.py +++ b/agent-council/agent_council/phases/synthesis.py @@ -119,7 +119,7 @@ async def synthesize(state: CouncilState) -> Synthesis: return Synthesis( question=state.question, mode=state.mode, - num_agents=len(state.personas), + num_agents=len(state.profiles) or len(state.personas), rounds_completed=len(state.cross_examination_rounds), stopped_reason=stopped_reason, # type: ignore confidence_history=history, diff --git a/agent-council/agent_council/state.py b/agent-council/agent_council/state.py index b5f5474..2542ae8 100644 --- a/agent-council/agent_council/state.py +++ b/agent-council/agent_council/state.py @@ -6,6 +6,22 @@ from typing import Literal from pydantic import BaseModel, Field +# ── Profile info (from submoduled hermes-profiles) ── + + +@dataclass +class ProfileInfo: + """A real profile drawn from the hermes-profiles library. + + Used in place of fabricated AgentPersona when profiles are available. + The name is the profile directory name (e.g. 'debugger'). + The soul_content is the full identity document. + """ + name: str + description: str + soul_content: str + + # ── Phase output schemas (validated Pydantic models) ── @@ -158,6 +174,9 @@ class CouncilState: convergence_threshold: float = 0.10 personas: list[AgentPersona] = field(default_factory=list) + profiles: list[ProfileInfo] = field(default_factory=list) + """Real profiles from the hermes-profiles library, if available. + Takes priority over fabricated personas when present.""" premortems: dict[str, Premortem] = field(default_factory=dict) positions: dict[str, Position] = field(default_factory=dict) cross_examination_rounds: list[dict[str, CrossExamination]] = field( diff --git a/agent-council/profiles b/agent-council/profiles new file mode 160000 index 0000000..867a555 --- /dev/null +++ b/agent-council/profiles @@ -0,0 +1 @@ +Subproject commit 867a555e13bae53a3945aa5328b1e09d347268cc diff --git a/agent-council/pyproject.toml b/agent-council/pyproject.toml index 0c3bb7a..974db83 100644 --- a/agent-council/pyproject.toml +++ b/agent-council/pyproject.toml @@ -7,6 +7,7 @@ license = {text = "MIT"} requires-python = ">=3.10" dependencies = [ "pydantic-ai>=1.0.0", + "pyyaml>=6.0", ] [project.scripts]