"""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()