Spaces:
Runtime error
Runtime error
File size: 5,730 Bytes
f7e2a40 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | """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())
|