oppy_google / backend /main.py
Luli Maruyama
Run real backend app in Space (not Hello World scaffold)
37a8fe3
Raw History Blame Contribute Delete
4.32 kB
import asyncio
import json
import os
from contextlib import asynccontextmanager
from dotenv import load_dotenv
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, Response
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from sse_starlette.sse import EventSourceResponse
from backend.agent import chat_with_operator, create_chat_session, run_operator
from backend.mock_data import MOCK_EMAILS, MOCK_EVENTS, MOCK_SEARCH
from backend.tts import generate_speech
from backend.urgency import load_model
load_dotenv()
# Fail fast if API key is missing
api_key = os.getenv("GOOGLE_API_KEY")
if not api_key or api_key == "your_key_here":
raise RuntimeError("GOOGLE_API_KEY not set. Check backend/.env")
# Persistent chat session (created on first /api/chat call)
_chat_client = None
_chat_session = None
def get_chat_session():
global _chat_client, _chat_session
if _chat_session is None:
config = load_config()
_chat_client, _chat_session = create_chat_session(config["projects"])
return _chat_session
@asynccontextmanager
async def lifespan(app):
load_model()
yield
app = FastAPI(title="Oppy", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def load_config():
from pathlib import Path
config_path = Path(__file__).parent / "config.json"
with open(config_path) as f:
return json.load(f)
@app.get("/api/run")
async def run():
config = load_config()
queue = asyncio.Queue()
async def on_event(event: dict):
await queue.put(event)
async def generator():
task = asyncio.create_task(run_operator(config["projects"], on_event))
while True:
try:
event = await asyncio.wait_for(queue.get(), timeout=1.0)
yield {"event": event["type"], "data": json.dumps(event, ensure_ascii=False)}
if event["type"] == "brief":
break
except asyncio.TimeoutError:
if task.done():
break
continue
await task
return EventSourceResponse(generator())
class TTSRequest(BaseModel):
text: str
class ChatRequest(BaseModel):
message: str
@app.post("/api/tts")
async def tts(req: TTSRequest):
audio_bytes = generate_speech(req.text)
return Response(content=audio_bytes, media_type="audio/wav")
@app.post("/api/chat")
async def chat(req: ChatRequest):
session = get_chat_session()
queue = asyncio.Queue()
async def on_event(event: dict):
await queue.put(event)
async def generator():
task = asyncio.create_task(
chat_with_operator(req.message, session, on_event)
)
while True:
try:
event = await asyncio.wait_for(queue.get(), timeout=1.0)
yield {"event": event["type"], "data": json.dumps(event, ensure_ascii=False)}
if event["type"] == "chat_reply":
break
except asyncio.TimeoutError:
if task.done():
break
continue
await task
return EventSourceResponse(generator())
@app.get("/api/project/{project_id}/sources")
async def project_sources(project_id: str):
emails = MOCK_EMAILS.get(project_id, [])
events = MOCK_EVENTS.get(project_id, [])
search = MOCK_SEARCH.get(project_id, "")
return {
"emails": emails,
"events": events,
"search": search,
}
@app.get("/api/health")
async def health():
return {"status": "ok"}
# --- Serve frontend static files (for Replit / production) ---
_static_dir = Path(__file__).parent / "static"
if _static_dir.exists():
app.mount("/assets", StaticFiles(directory=_static_dir / "assets"), name="assets")
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
"""Serve the React SPA for any non-API route."""
file_path = _static_dir / full_path
if file_path.is_file():
return FileResponse(file_path)
return FileResponse(_static_dir / "index.html")