Initial commit: Meeting Room v0.1.0
Multi-agent discussion framework: - Custom/AutoGen/CrewAI backends - Multi-provider API (role→model binding) - Tools system (files, web, commands) - Markdown scenarios with YAML frontmatter - Workspace init (meeting-room init) - Session save (--save)
This commit is contained in:
343
meeting_room/cli.py
Normal file
343
meeting_room/cli.py
Normal file
@@ -0,0 +1,343 @@
|
||||
"""Meeting Room CLI — main entry point."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
import yaml
|
||||
from datetime import datetime
|
||||
|
||||
from meeting_room.scenario_loader import load_scenario, list_scenarios as _list_scenarios
|
||||
|
||||
PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def load_config(config_path: str | None = None) -> dict:
|
||||
"""Load config from file, resolve env vars, apply overrides."""
|
||||
if config_path is None:
|
||||
config_path = os.path.join(PACKAGE_DIR, "config", "config.yaml")
|
||||
|
||||
with open(config_path) as f:
|
||||
raw = f.read()
|
||||
|
||||
# Resolve ${ENV_VAR} placeholders
|
||||
import re
|
||||
def _env_replace(match):
|
||||
var = match.group(1)
|
||||
return os.environ.get(var, match.group(0))
|
||||
raw = re.sub(r'\$\{(\w+)\}', _env_replace, raw)
|
||||
|
||||
config = yaml.safe_load(raw)
|
||||
|
||||
# Override API keys from environment if set
|
||||
for prov_name, prov_cfg in config.get("providers", {}).items():
|
||||
env_key = f"MEETING_ROOM_{prov_name.upper()}_API_KEY"
|
||||
if os.environ.get(env_key):
|
||||
prov_cfg["api_key"] = os.environ[env_key]
|
||||
env_url = f"MEETING_ROOM_{prov_name.upper()}_BASE_URL"
|
||||
if os.environ.get(env_url):
|
||||
prov_cfg["base_url"] = os.environ[env_url]
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def list_scenarios(scenarios_dir: str | None = None):
|
||||
if scenarios_dir is None:
|
||||
scenarios_dir = os.path.join(PACKAGE_DIR, "scenarios")
|
||||
items = _list_scenarios(scenarios_dir)
|
||||
if not items:
|
||||
print("No scenarios found. Create .md or .yaml in scenarios/")
|
||||
return
|
||||
print("\nAvailable scenarios:")
|
||||
print("-" * 40)
|
||||
for s in items:
|
||||
participants = ", ".join(s["participants"]) if s["participants"] else "-"
|
||||
print(f" {s['file']}")
|
||||
print(f" {s['name']}")
|
||||
print(f" Participants: {participants}")
|
||||
print()
|
||||
|
||||
|
||||
def list_roles(config: dict):
|
||||
roles = config.get("roles", {})
|
||||
providers = config.get("providers", {})
|
||||
print("\nRoles and model bindings:")
|
||||
print("-" * 60)
|
||||
for role_id, role in roles.items():
|
||||
prov = role.get("provider", "?")
|
||||
model = role.get("model", "?")
|
||||
temp = role.get("temperature", 0.7)
|
||||
tools = role.get("tools", "none")
|
||||
prov_url = providers.get(prov, {}).get("base_url", "?")
|
||||
print(f" {role['name']} ({role_id})")
|
||||
print(f" API: {prov} — {prov_url}")
|
||||
print(f" Model: {model}")
|
||||
print(f" Temperature: {temp}")
|
||||
print(f" Tools: {tools}")
|
||||
print()
|
||||
|
||||
|
||||
def init_workspace(path: str = "."):
|
||||
"""Initialize a .meeting-room workspace in given directory."""
|
||||
target = os.path.abspath(path)
|
||||
ws_dir = os.path.join(target, ".meeting-room")
|
||||
|
||||
if os.path.exists(ws_dir):
|
||||
print(f"Workspace already exists: {ws_dir}")
|
||||
return
|
||||
|
||||
os.makedirs(os.path.join(ws_dir, "scenarios"), exist_ok=True)
|
||||
os.makedirs(os.path.join(ws_dir, "sessions"), exist_ok=True)
|
||||
|
||||
# Write default config (overrides package defaults)
|
||||
default_config = {
|
||||
"providers": {
|
||||
"routerai": {
|
||||
"base_url": "${MEETING_ROOM_ROUTERAI_BASE_URL}",
|
||||
"api_key": "${MEETING_ROOM_ROUTERAI_API_KEY}",
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"moderator": {
|
||||
"name": "Moderator",
|
||||
"provider": "routerai",
|
||||
"model": "gpt-4o-mini",
|
||||
"temperature": 0.7,
|
||||
"tools": "none",
|
||||
"system_prompt": "You are the moderator of a discussion.",
|
||||
},
|
||||
"skeptic": {
|
||||
"name": "Skeptic",
|
||||
"provider": "routerai",
|
||||
"model": "gpt-4o-mini",
|
||||
"temperature": 0.9,
|
||||
"tools": "readonly",
|
||||
"system_prompt": "You are a skeptic and critic.",
|
||||
},
|
||||
"idea_generator": {
|
||||
"name": "Idea Generator",
|
||||
"provider": "routerai",
|
||||
"model": "gpt-4o-mini",
|
||||
"temperature": 1.0,
|
||||
"tools": "web",
|
||||
"system_prompt": "You are a creative idea generator.",
|
||||
},
|
||||
"analyst": {
|
||||
"name": "Analyst",
|
||||
"provider": "routerai",
|
||||
"model": "gpt-4o-mini",
|
||||
"temperature": 0.5,
|
||||
"tools": "all",
|
||||
"system_prompt": "You are an analyst.",
|
||||
},
|
||||
},
|
||||
"defaults": {
|
||||
"max_rounds": 8,
|
||||
"framework": "custom",
|
||||
"workdir": "..", # parent of .meeting-room
|
||||
},
|
||||
}
|
||||
|
||||
config_path = os.path.join(ws_dir, "config.yaml")
|
||||
with open(config_path, "w") as f:
|
||||
yaml.dump(default_config, f, default_flow_style=False, allow_unicode=True)
|
||||
|
||||
# Write example scenario
|
||||
example = """---
|
||||
name: "Example Discussion"
|
||||
participants:
|
||||
- moderator
|
||||
- skeptic
|
||||
- idea_generator
|
||||
- analyst
|
||||
max_rounds: 6
|
||||
---
|
||||
|
||||
# Problem Title
|
||||
|
||||
Describe the problem here in Markdown...
|
||||
|
||||
## Context
|
||||
- Factor 1
|
||||
- Factor 2
|
||||
|
||||
## Questions
|
||||
1. Question 1?
|
||||
2. Question 2?
|
||||
"""
|
||||
with open(os.path.join(ws_dir, "scenarios", "example.md"), "w") as f:
|
||||
f.write(example)
|
||||
|
||||
# Write .gitignore
|
||||
with open(os.path.join(ws_dir, ".gitignore"), "w") as f:
|
||||
f.write("sessions/\n")
|
||||
|
||||
print(f"Workspace initialized: {ws_dir}")
|
||||
print(f"""
|
||||
Structure:
|
||||
{ws_dir}/
|
||||
├── config.yaml — override default config
|
||||
├── scenarios/ — your .md scenario files
|
||||
│ └── example.md
|
||||
├── sessions/ — saved discussion results (gitignored)
|
||||
└── .gitignore
|
||||
|
||||
Usage:
|
||||
cd {ws_dir}
|
||||
meeting-room -s scenarios/example.md
|
||||
meeting-room -s scenarios/example.md --save
|
||||
""")
|
||||
|
||||
|
||||
def save_session(conversation: list[dict], scenario_name: str, sessions_dir: str = "sessions"):
|
||||
"""Save discussion result to a JSON file."""
|
||||
os.makedirs(sessions_dir, exist_ok=True)
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
||||
safe_name = scenario_name.replace(" ", "-").lower()
|
||||
filename = f"{timestamp}_{safe_name}.json"
|
||||
filepath = os.path.join(sessions_dir, filename)
|
||||
|
||||
session_data = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"scenario": scenario_name,
|
||||
"messages": conversation,
|
||||
}
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(session_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"Session saved: {filepath}")
|
||||
return filepath
|
||||
|
||||
|
||||
def find_workspace_config() -> str | None:
|
||||
"""Search for .meeting-room/config.yaml starting from CWD upward."""
|
||||
current = os.getcwd()
|
||||
while current != "/":
|
||||
candidate = os.path.join(current, ".meeting-room", "config.yaml")
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
parent = os.path.dirname(current)
|
||||
if parent == current:
|
||||
break
|
||||
current = parent
|
||||
return None
|
||||
|
||||
|
||||
def run_discussion(backend: str, scenario: str, config: dict, save: bool = False):
|
||||
scenario_path = scenario if os.path.isabs(scenario) else os.path.join(os.getcwd(), scenario)
|
||||
if not os.path.exists(scenario_path):
|
||||
# Try bundled scenarios
|
||||
scenario_path = os.path.join(PACKAGE_DIR, "scenarios", scenario)
|
||||
if not os.path.exists(scenario_path):
|
||||
print(f"Scenario not found: {scenario}")
|
||||
sys.exit(1)
|
||||
|
||||
if backend == "custom":
|
||||
from meeting_room.backends.custom.run import run
|
||||
conversation = run(scenario_path, config)
|
||||
elif backend == "autogen":
|
||||
from meeting_room.backends.autogen.run import run
|
||||
conversation = run(scenario_path, config)
|
||||
elif backend == "crewai":
|
||||
from meeting_room.backends.crewai.run import run
|
||||
conversation = run(scenario_path, config)
|
||||
else:
|
||||
print(f"Unknown backend: {backend}")
|
||||
sys.exit(1)
|
||||
|
||||
if save and conversation:
|
||||
# Determine sessions dir
|
||||
ws_config = find_workspace_config()
|
||||
if ws_config:
|
||||
sessions_dir = os.path.join(os.path.dirname(ws_config), "sessions")
|
||||
else:
|
||||
sessions_dir = os.path.join(os.getcwd(), "sessions")
|
||||
|
||||
scenario_data = load_scenario(scenario_path)
|
||||
save_session(conversation, scenario_data.get("name", "discussion"), sessions_dir)
|
||||
|
||||
return conversation
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Meeting Room — Multi-Agent Discussion Framework",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-b", "--backend",
|
||||
choices=["custom", "autogen", "crewai"],
|
||||
default="custom",
|
||||
help="Discussion framework (default: custom)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s", "--scenario",
|
||||
default="example-saas.md",
|
||||
help="Scenario file .md or .yaml",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--config",
|
||||
default=None,
|
||||
help="Path to custom config.yaml",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-w", "--workdir",
|
||||
default=None,
|
||||
help="Working directory for file tools",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--save", action="store_true",
|
||||
help="Save discussion result to sessions/",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-scenarios", action="store_true",
|
||||
help="List available scenarios",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-roles", action="store_true",
|
||||
help="List roles and model bindings",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
init_parser = subparsers.add_parser("init", help="Initialize .meeting-room workspace")
|
||||
init_parser.add_argument("path", nargs="?", default=".", help="Where to create .meeting-room")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Handle init command
|
||||
if args.command == "init":
|
||||
init_workspace(args.path)
|
||||
return
|
||||
|
||||
# Find config: CLI arg > .meeting-room/config.yaml > package default
|
||||
if args.config:
|
||||
config = load_config(args.config)
|
||||
else:
|
||||
ws_config = find_workspace_config()
|
||||
if ws_config:
|
||||
config = load_config(ws_config)
|
||||
else:
|
||||
config = load_config()
|
||||
|
||||
if args.list_scenarios:
|
||||
list_scenarios()
|
||||
cwd_scenarios = os.path.join(os.getcwd(), "scenarios")
|
||||
if os.path.isdir(cwd_scenarios):
|
||||
list_scenarios(cwd_scenarios)
|
||||
# Also check .meeting-room
|
||||
ws_scenarios = os.path.join(os.getcwd(), ".meeting-room", "scenarios")
|
||||
if os.path.isdir(ws_scenarios):
|
||||
list_scenarios(ws_scenarios)
|
||||
return
|
||||
|
||||
if args.list_roles:
|
||||
list_roles(config)
|
||||
return
|
||||
|
||||
if args.workdir:
|
||||
config["defaults"]["workdir"] = args.workdir
|
||||
|
||||
run_discussion(args.backend, args.scenario, config, save=args.save)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user