winniekariuki's picture
Phase 3 complete: working chatbot with policy gate, tests, Gradio UI
f7e2a40
Raw
History Blame Contribute Delete
5.73 kB
"""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())