feat: align ramble defaults with shared ai config

This commit is contained in:
rob 2025-11-01 14:42:04 -03:00
parent 57255a7e09
commit 3eca60148c
3 changed files with 146 additions and 24 deletions

View File

@ -18,6 +18,11 @@ import argparse, datetime, json, os, re, subprocess, sys
from pathlib import Path from pathlib import Path
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
try:
from automation.ai_config import load_ai_settings
except ImportError:
load_ai_settings = None # type: ignore
# --------- helpers --------- # --------- helpers ---------
def say(msg: str) -> None: def say(msg: str) -> None:
print(msg, flush=True) print(msg, flush=True)
@ -204,14 +209,47 @@ def main():
ap.add_argument("--dir", help="Repo root (defaults to git root or CWD)") ap.add_argument("--dir", help="Repo root (defaults to git root or CWD)")
ap.add_argument("--title", help="Feature title (useful without Ramble)") ap.add_argument("--title", help="Feature title (useful without Ramble)")
ap.add_argument("--no-ramble", action="store_true", help="Disable Ramble UI") ap.add_argument("--no-ramble", action="store_true", help="Disable Ramble UI")
ap.add_argument("--provider", choices=["mock", "claude"], default="mock") ap.add_argument("--provider", help="Override Ramble provider (defaults to config)")
ap.add_argument("--claude-cmd", default="claude") ap.add_argument("--claude-cmd", help="Override claude CLI path")
args = ap.parse_args() args = ap.parse_args()
start = Path(args.dir).expanduser().resolve() if args.dir else Path.cwd() start = Path(args.dir).expanduser().resolve() if args.dir else Path.cwd()
repo = git_root_or_cwd(start) repo = git_root_or_cwd(start)
say(f"[=] Using repository: {repo}") say(f"[=] Using repository: {repo}")
provider = args.provider
claude_cmd = args.claude_cmd
provider_map: Dict[str, Dict[str, object]] = {}
default_provider = "mock"
if load_ai_settings is not None:
try:
settings = load_ai_settings(repo)
provider_map = dict(settings.ramble.providers)
candidate_default = settings.ramble.default_provider
if isinstance(candidate_default, str) and candidate_default.strip():
default_provider = candidate_default.strip()
except Exception:
provider_map = {}
if "mock" not in provider_map:
provider_map["mock"] = {"kind": "mock"}
if not provider:
provider = default_provider if default_provider in provider_map else "mock"
elif provider not in provider_map:
say(f"[WARN] Unknown Ramble provider '{provider}', defaulting to {default_provider}")
provider = default_provider if default_provider in provider_map else "mock"
if not claude_cmd:
selected = provider_map.get(provider, {})
maybe_cmd = selected.get("command") if isinstance(selected, dict) else None
if isinstance(maybe_cmd, str) and maybe_cmd.strip():
claude_cmd = maybe_cmd.strip()
if not claude_cmd:
claude_cmd = "claude"
tmpl_path = repo / "process" / "templates" / "feature_request.md" tmpl_path = repo / "process" / "templates" / "feature_request.md"
tmpl = read_text(tmpl_path) tmpl = read_text(tmpl_path)
parsed_fields = find_template_fields(tmpl) or [(f, "") for f in default_fields()] parsed_fields = find_template_fields(tmpl) or [(f, "") for f in default_fields()]
@ -222,7 +260,7 @@ def main():
# Try Ramble unless disabled # Try Ramble unless disabled
fields: Dict[str, str] | None = None fields: Dict[str, str] | None = None
if not args.no_ramble: if not args.no_ramble:
fields = try_ramble(repo, field_labels, provider=args.provider, claude_cmd=args.claude_cmd) fields = try_ramble(repo, field_labels, provider=provider, claude_cmd=claude_cmd)
# Terminal prompts fallback # Terminal prompts fallback
if not fields: if not fields:

View File

@ -35,6 +35,7 @@ Requirements
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple, Protocol, runtime_checkable, Mapping, cast from typing import Dict, List, Optional, Any, Tuple, Protocol, runtime_checkable, Mapping, cast
import os, sys, json, textwrap, base64, re, time, shutil, subprocess, argparse, threading import os, sys, json, textwrap, base64, re, time, shutil, subprocess, argparse, threading
@ -43,6 +44,11 @@ try:
except ImportError: except ImportError:
requests = None requests = None
try:
from automation.ai_config import load_ai_settings
except ImportError:
load_ai_settings = None # type: ignore
# ── Qt (PySide6 preferred; PyQt5 fallback) ──────────────────────────────────── # ── Qt (PySide6 preferred; PyQt5 fallback) ────────────────────────────────────
QT_LIB = None QT_LIB = None
try: try:
@ -690,38 +696,116 @@ def open_ramble_dialog(
# ── CLI demo ────────────────────────────────────────────────────────────────── # ── CLI demo ──────────────────────────────────────────────────────────────────
def parse_args(): def load_ramble_preferences(repo_root: Path) -> Tuple[List[str], str, Dict[str, Any], Dict[str, Dict[str, Any]]]:
"""Return (choices, default_provider, claude_defaults, provider_map)."""
provider_map: Dict[str, Dict[str, Any]] = {}
default_provider = "mock"
if load_ai_settings is not None:
try:
settings = load_ai_settings(repo_root)
provider_map = dict(settings.ramble.providers)
candidate_default = settings.ramble.default_provider
if isinstance(candidate_default, str) and candidate_default.strip():
default_provider = candidate_default.strip()
except Exception:
provider_map = {}
if "mock" not in provider_map:
provider_map["mock"] = {"kind": "mock"}
provider_choices = sorted(provider_map.keys())
if default_provider not in provider_choices:
default_provider = "mock"
claude_defaults = provider_map.get("claude", {})
if not isinstance(claude_defaults, dict):
claude_defaults = {}
return provider_choices, default_provider, claude_defaults, provider_map
def build_provider(name: str, args: "argparse.Namespace", providers: Dict[str, Dict[str, Any]]) -> RambleProvider:
meta = providers.get(name, {})
if not isinstance(meta, dict):
meta = {}
kind = meta.get("kind")
if not isinstance(kind, str) or not kind:
kind = "mock" if name == "mock" else name
if kind == "mock":
return cast(RambleProvider, MockProvider())
if kind in {"claude", "claude_cli"}:
cmd = args.claude_cmd or meta.get("command") or "claude"
extra_args = meta.get("args", []) or []
if isinstance(extra_args, str):
extra_args = [extra_args]
extra_args = [str(x) for x in extra_args]
use_arg_p = bool(meta.get("use_arg_p", True))
log_path = str(meta.get("log_path", "/tmp/ramble_claude.log"))
timeout_s = int(args.timeout)
tail_chars = int(args.tail)
debug_flag = bool(args.debug or meta.get("debug", False))
return cast(
RambleProvider,
ClaudeCLIProvider(
cmd=cmd,
extra_args=extra_args,
timeout_s=timeout_s,
tail_chars=tail_chars,
use_arg_p=use_arg_p,
debug=debug_flag,
log_path=log_path,
),
)
raise ValueError(f"Unknown Ramble provider kind: {kind}")
def parse_args(
provider_choices: List[str],
default_provider: str,
claude_defaults: Dict[str, Any],
):
p = argparse.ArgumentParser(description="Ramble → Generate (PlantUML + optional images)") p = argparse.ArgumentParser(description="Ramble → Generate (PlantUML + optional images)")
p.add_argument("--provider", choices=["mock", "claude"], default="mock") p.add_argument("--provider", choices=provider_choices, default=default_provider)
p.add_argument("--claude-cmd", default="claude", help="Path to claude CLI")
claude_cmd_default = str(claude_defaults.get("command", "claude"))
try:
timeout_default = int(claude_defaults.get("timeout_s", 90))
except (TypeError, ValueError):
timeout_default = 90
try:
tail_default = int(claude_defaults.get("tail_chars", 6000))
except (TypeError, ValueError):
tail_default = 6000
p.add_argument("--claude-cmd", default=claude_cmd_default, help="Path to claude CLI")
p.add_argument("--stability", action="store_true", help="Enable Stability AI images (needs STABILITY_API_KEY)") p.add_argument("--stability", action="store_true", help="Enable Stability AI images (needs STABILITY_API_KEY)")
p.add_argument("--pexels", action="store_true", help="Enable Pexels images (needs PEXELS_API_KEY); ignored if --stability set") p.add_argument("--pexels", action="store_true", help="Enable Pexels images (needs PEXELS_API_KEY); ignored if --stability set")
p.add_argument("--prompt", default="Explain your new feature idea") p.add_argument("--prompt", default="Explain your new feature idea")
p.add_argument("--fields", nargs="+", default=["Summary","Title","Intent","ProblemItSolves","BriefOverview"]) p.add_argument("--fields", nargs="+", default=["Summary","Title","Intent","ProblemItSolves","BriefOverview"])
p.add_argument("--criteria", default="", help="JSON mapping of field -> criteria") p.add_argument("--criteria", default="", help="JSON mapping of field -> criteria")
p.add_argument("--hints", default="", help="JSON list of hint strings") p.add_argument("--hints", default="", help="JSON list of hint strings")
p.add_argument("--timeout", type=int, default=90) p.add_argument("--timeout", type=int, default=timeout_default)
p.add_argument("--tail", type=int, default=6000) p.add_argument("--tail", type=int, default=tail_default)
p.add_argument("--debug", action="store_true") p.add_argument("--debug", action="store_true")
return p.parse_args() return p.parse_args()
if __name__ == "__main__": if __name__ == "__main__":
args = parse_args() repo_root = Path.cwd()
provider_choices, default_provider, claude_defaults, provider_map = load_ramble_preferences(repo_root)
args = parse_args(provider_choices, default_provider, claude_defaults)
# Field criteria parsing # Provider selection with fallback
criteria: Dict[str, str] = {} try:
if args.criteria.strip(): provider = build_provider(args.provider, args, provider_map)
try: except Exception as exc:
criteria = json.loads(args.criteria) print(f"[WARN] Ramble provider '{args.provider}' unavailable ({exc}); falling back to mock", file=sys.stderr)
except Exception as e:
print(f"[WARN] Could not parse --criteria JSON: {e}", file=sys.stderr)
# Provider
if args.provider == "claude":
provider = cast(RambleProvider, ClaudeCLIProvider(
cmd=args.claude_cmd, use_arg_p=True, timeout_s=args.timeout, tail_chars=args.tail, debug=args.debug,
))
else:
provider = cast(RambleProvider, MockProvider()) provider = cast(RambleProvider, MockProvider())
# Ensure PlantUML # Ensure PlantUML

View File

@ -24,7 +24,7 @@ ramble:
providers: providers:
mock: mock:
kind: mock kind: mock
claude_cli: claude:
kind: claude_cli kind: claude_cli
command: "claude" command: "claude"
args: [] args: []