Files
Cortex-Inara/cortex/routers/google_chat.py
Scott Idem 2f675ee4bf Initial commit — Cortex API + Inara identity
Cortex: FastAPI backend serving Inara via Claude/Gemini CLI backends.
Includes SSE streaming chat, session persistence, Google Chat webhook
handler, and Docker support.

Inara: Identity files (persona, soul, protocols, memory, context tiers)
mounted read-only into the container at runtime.

Features in initial cut:
- /chat endpoint with SSE keepalive + LLM fallback
- Session store with rolling history window
- Markdown rendering, copy-to-clipboard, links open in new tab
- Stacked right-column input controls (height selector, enter toggle,
  note mode with public/private) — semi-hidden until textarea grows
- /note endpoint for injecting public context into session history
- Docker Compose config (local dev runs natively; Docker for server)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 03:41:00 -05:00

75 lines
2.7 KiB
Python

import asyncio
import logging
from fastapi import APIRouter, Request, Response
from context_loader import load_context
from llm_client import complete
from session_logger import log_turn
from session_store import load as load_session, save as save_session
from config import settings
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/channels/google-chat")
@router.post("")
async def receive(request: Request):
body = await request.json()
event_type = body.get("type")
if event_type == "ADDED_TO_SPACE":
space_type = body.get("space", {}).get("type", "")
greeting = "✨ Hello! I'm Inara. Send me a message and I'll do my best to help."
if space_type == "DM":
greeting = "✨ Hello! I'm Inara. What can I help you with?"
return {"text": greeting}
if event_type == "REMOVED_FROM_SPACE":
return Response(status_code=200)
if event_type != "MESSAGE":
return Response(status_code=200)
message = body.get("message", {})
sender = message.get("sender", {})
space = body.get("space", {})
# argumentText strips the @BotName mention in Spaces; fall back to full text in DMs
user_text = (message.get("argumentText") or message.get("text", "")).strip()
if not user_text:
return Response(status_code=200)
sender_display = sender.get("displayName", "User")
space_name = space.get("name", "unknown")
space_type = space.get("type", "")
# Session keyed per space — one conversation per DM or Space
session_id = "gc_" + space_name.replace("/", "_")
logger.info("Google Chat message from %s in %s (%s)", sender_display, space_name, space_type)
system_prompt = load_context(settings.default_tier)
history = load_session(session_id)
history.append({"role": "user", "content": user_text})
try:
response_text, actual_backend = await asyncio.wait_for(
complete(
system_prompt=system_prompt,
messages=history,
model=settings.google_chat_backend,
),
timeout=settings.google_chat_timeout,
)
except asyncio.TimeoutError:
logger.warning("Google Chat request timed out for session %s", session_id)
return {"text": "⏳ Still thinking — this is taking a bit longer than usual. Try again in a moment."}
except Exception as e:
logger.error("Google Chat error for session %s: %s", session_id, e)
return {"text": f"⚠️ Something went wrong on my end. Try again shortly."}
history.append({"role": "assistant", "content": response_text})
save_session(session_id, history)
log_turn(session_id, user_text, response_text)
return {"text": response_text}