"""Interactive CLI — validates the stack before Gradio wires things together.""" from __future__ import annotations import asyncio import json import os import re import shlex import sys import time import uuid from pathlib import Path from dotenv import load_dotenv from openai import AsyncOpenAI from src.chat_engine import ChatEngine from src.logger import get_recent_events, log_event, summarize_tool_args from src.mcp_client import MCPAuthError, MeridianMCPClient from src.prompts import build_facts_summary from src.session import Session _UUID_RE = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.I) def _parse_customer_auth_response(text: str) -> tuple[str | None, str | None]: uuid_m = _UUID_RE.search(text) customer_id = uuid_m.group(0) if uuid_m else None name: str | None = None for pattern in ( r"(?:customer\s*name|name)\s*[:#]\s*([^\n]+)", r"(?:^|\n)\s*Name\s*[:#]\s*([^\n]+)", ): nm = re.search(pattern, text, re.I) if nm: name = nm.group(1).strip() break return customer_id, name def _require_env(name: str) -> str: value = os.environ.get(name, "").strip() if not value: sys.stderr.write( f"Missing required environment variable {name}. " "Export it or add it to a .env file in the project root.\n" ) raise SystemExit(2) return value async def _cmd_login(mcp: MeridianMCPClient, session: Session, session_id: str, argline: str) -> None: parts = shlex.split(argline) if len(parts) != 2: print("Usage: /login email pin") return email, pin = parts t0 = time.perf_counter() try: payload = await mcp.call_tool( "verify_customer_pin", {"email": email.strip(), "pin": pin.strip()}, ) except MCPAuthError: print("Sign-in failed (check email/PIN).") log_event( "error", session_id=session_id, tool="verify_customer_pin", decision=None, deny_reason="mcp_auth_error", latency_ms=int((time.perf_counter() - t0) * 1000), status="error", args_summary=summarize_tool_args( "verify_customer_pin", {"email": email, "pin": ""}, ), ) return elapsed_ms = int((time.perf_counter() - t0) * 1000) cid, name = _parse_customer_auth_response(payload) if not cid: print("Signed in, but could not parse customer_id from MCP response.") print(payload[:800]) return session.customer_id = cid session.customer_name = name session.customer_email = email.strip() log_event( "tool_call", session_id=session_id, tool="verify_customer_pin", decision=None, deny_reason=None, latency_ms=elapsed_ms, status="success", args_summary=summarize_tool_args( "verify_customer_pin", {"email": email, "pin": ""}, ), ) display = name or cid print(f"Signed in as {display}") def _print_recent_logs(n: int = 10) -> None: events = get_recent_events(50)[-n:] if not events: print("(no logged events yet)") return for row in events: print(json.dumps(row, ensure_ascii=False)) async def main() -> None: load_dotenv(Path(__file__).resolve().parents[1] / ".env") _require_env("OPENAI_API_KEY") mcp_url = _require_env("MCP_SERVER_URL") session_id = str(uuid.uuid4()) session = Session() mcp = MeridianMCPClient(server_url=mcp_url) engine = ChatEngine( mcp, session, AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"]), session_id=session_id, model=os.environ.get("OPENAI_MODEL", "").strip() or None, ) print("Meridian CLI — type /help for commands. Ctrl+D or /quit to exit.") print(f"session_id={session_id}") while True: try: line = input("meridian> ").strip() except (EOFError, KeyboardInterrupt): print("\nBye.") return if not line: continue if line in {"/quit", "/exit"}: print("Bye.") return if line == "/help": print( "Commands:\n" " /login email pin — sign in (PIN never goes to the LLM)\n" " /logout — clear session identity\n" " /facts — print compact facts summary\n" " /log [n] — print last n JSONL events (default 10)\n" " /quit — exit\n" ) continue if line.startswith("/log"): bits = line.split() n = 10 if len(bits) > 1 and bits[1].isdigit(): n = int(bits[1]) _print_recent_logs(n) continue if line == "/facts": print(build_facts_summary(session)) continue if line == "/logout": session.customer_id = None session.customer_name = None session.customer_email = None session.pending_confirmation = None print("Logged out.") continue if line.startswith("/login"): rest = line[len("/login") :].strip() await _cmd_login(mcp, session, session_id, rest) continue result = await engine.handle_turn(line) if result.error: print(f"[engine:{result.error}]") print(result.assistant_text) if __name__ == "__main__": asyncio.run(main())