Spaces:
Runtime error
Runtime error
| """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": "<redacted>"}, | |
| ), | |
| ) | |
| 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": "<redacted>"}, | |
| ), | |
| ) | |
| 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()) | |