feat(ai): model profiles, capability discovery, and agentless /ai list|models
Make connecting any model a config step, not a code change: - models.toml named profiles (api_key_env names an env var, never the key) - providers gain available_models(); add preflight + --list-models/--check - /ai list and /ai models in-room; client probes local Ollama for /ai models when no agent is running, and /ai list hints to summon one - docs/providers.md provider guide + examples/echo_provider.py - README: command table, AI section, layout updated Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,40 +14,100 @@ Examples
|
||||
python -m cmd_chat.agent 127.0.0.1 3000 --provider openai \
|
||||
--base-url https://api.groq.com/openai/v1 --model llama-3.1-70b --password hunter2
|
||||
|
||||
# a named profile from models.toml (provider + model + endpoint + key env)
|
||||
python -m cmd_chat.agent 127.0.0.1 3000 --profile groq-llama --password hunter2
|
||||
|
||||
# a custom provider you wrote
|
||||
python -m cmd_chat.agent 127.0.0.1 3000 --provider mypkg.mod:MyProvider
|
||||
|
||||
# discovery / preflight (no room join)
|
||||
python -m cmd_chat.agent --profile groq-llama --list-models
|
||||
python -m cmd_chat.agent --profile groq-llama --check
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from .bridge import AgentBridge
|
||||
from .providers import make_provider
|
||||
from .profiles import load_profiles, provider_from_profile
|
||||
from .providers import make_provider, preflight
|
||||
|
||||
|
||||
def _build_provider(args, ap):
|
||||
"""Resolve a Provider from either --profile or the explicit flags."""
|
||||
if args.profile:
|
||||
profiles = load_profiles(args.models_file)
|
||||
if args.profile not in profiles:
|
||||
known = ", ".join(profiles) or "(none — create models.toml)"
|
||||
ap.error(f"unknown profile '{args.profile}'. known: {known}")
|
||||
prof = profiles[args.profile]
|
||||
provider = provider_from_profile(
|
||||
prof, name=args.profile, model=args.model, base_url=args.base_url
|
||||
)
|
||||
# Profile may also supply non-provider defaults.
|
||||
if args.system is None and prof.get("system"):
|
||||
args.system = prof["system"]
|
||||
if args.context_window == 12 and prof.get("context_window"):
|
||||
args.context_window = int(prof["context_window"])
|
||||
return provider
|
||||
|
||||
opts: dict = {}
|
||||
if args.base_url and (args.provider == "openai" or ":" in args.provider):
|
||||
opts["base_url"] = args.base_url
|
||||
return make_provider(args.provider, model=args.model, **opts)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(
|
||||
prog="cmd_chat.agent", description="hack-house AI agent bridge (PoC)"
|
||||
)
|
||||
ap.add_argument("server")
|
||||
ap.add_argument("port", type=int)
|
||||
ap.add_argument("server", nargs="?", help="room host (omit with --list-models/--check)")
|
||||
ap.add_argument("port", type=int, nargs="?", help="room port")
|
||||
ap.add_argument("--name", default="oracle", help="agent's room display name")
|
||||
ap.add_argument("--password", default=None, help="room password")
|
||||
ap.add_argument("--provider", default="ollama",
|
||||
help="ollama | anthropic | openai | module:Class")
|
||||
ap.add_argument("--profile", default=None,
|
||||
help="named profile from models.toml (overrides --provider/--model)")
|
||||
ap.add_argument("--models-file", default=None,
|
||||
help="path to models.toml (default: $HH_MODELS_FILE, ./models.toml, ~/.config/hh/models.toml)")
|
||||
ap.add_argument("--model", default=None, help="model name (provider default if omitted)")
|
||||
ap.add_argument("--base-url", default=None, help="endpoint for openai-compatible providers")
|
||||
ap.add_argument("--system", default=None, help="override the system prompt")
|
||||
ap.add_argument("--context-window", type=int, default=12)
|
||||
ap.add_argument("--list-models", action="store_true",
|
||||
help="list models the backend can serve, then exit")
|
||||
ap.add_argument("--check", action="store_true",
|
||||
help="run a reachability/model preflight, then exit (0 ok, 1 fail)")
|
||||
ap.add_argument("--insecure", action="store_true", help="skip TLS cert verification")
|
||||
ap.add_argument("--no-tls", action="store_true", help="plain ws/http (local/Tailscale)")
|
||||
args = ap.parse_args()
|
||||
|
||||
opts: dict = {}
|
||||
if args.base_url and (args.provider == "openai" or ":" in args.provider):
|
||||
opts["base_url"] = args.base_url
|
||||
provider = make_provider(args.provider, model=args.model, **opts)
|
||||
provider = _build_provider(args, ap)
|
||||
|
||||
# Discovery / preflight modes never join a room.
|
||||
if args.list_models:
|
||||
discover = getattr(provider, "available_models", None)
|
||||
if discover is None:
|
||||
ap.error(f"provider '{provider.name}' has no model discovery")
|
||||
for m in discover():
|
||||
print(m)
|
||||
return
|
||||
if args.check:
|
||||
ok, msg = preflight(provider)
|
||||
print(("ok: " if ok else "FAIL: ") + msg, file=sys.stderr if not ok else sys.stdout)
|
||||
sys.exit(0 if ok else 1)
|
||||
|
||||
if args.server is None or args.port is None:
|
||||
ap.error("server and port are required to join a room")
|
||||
|
||||
# Non-fatal preflight: warn early, but still try (discovery may be blocked
|
||||
# while completion works).
|
||||
ok, msg = preflight(provider)
|
||||
if not ok:
|
||||
print(f"⚠ preflight: {msg}", file=sys.stderr)
|
||||
|
||||
bridge = AgentBridge(
|
||||
args.server, args.port, name=args.name, provider=provider,
|
||||
|
||||
Reference in New Issue
Block a user