luli331/oppy_google
0
1import asyncio2import json3import os4from contextlib import asynccontextmanager5 6from dotenv import load_dotenv7from pathlib import Path8 9from fastapi import FastAPI10from fastapi.middleware.cors import CORSMiddleware11from fastapi.responses import FileResponse, Response12from fastapi.staticfiles import StaticFiles13from pydantic import BaseModel14from sse_starlette.sse import EventSourceResponse15 16from backend.agent import chat_with_operator, create_chat_session, run_operator17from backend.mock_data import MOCK_EMAILS, MOCK_EVENTS, MOCK_SEARCH18from backend.tts import generate_speech19from backend.urgency import load_model20 21load_dotenv()22 23# Fail fast if API key is missing24api_key = os.getenv("GOOGLE_API_KEY")25if not api_key or api_key == "your_key_here":26 raise RuntimeError("GOOGLE_API_KEY not set. Check backend/.env")27 28 29# Persistent chat session (created on first /api/chat call)30_chat_client = None31_chat_session = None32 33 34def get_chat_session():35 global _chat_client, _chat_session36 if _chat_session is None:37 config = load_config()38 _chat_client, _chat_session = create_chat_session(config["projects"])39 return _chat_session40 41 42@asynccontextmanager43async def lifespan(app):44 load_model()45 yield46 47 48app = FastAPI(title="Oppy", lifespan=lifespan)49 50app.add_middleware(51 CORSMiddleware,52 allow_origins=["*"],53 allow_credentials=True,54 allow_methods=["*"],55 allow_headers=["*"],56)57 58 59def load_config():60 from pathlib import Path61 config_path = Path(__file__).parent / "config.json"62 with open(config_path) as f:63 return json.load(f)64 65 66@app.get("/api/run")67async def run():68 config = load_config()69 queue = asyncio.Queue()70 71 async def on_event(event: dict):72 await queue.put(event)73 74 async def generator():75 task = asyncio.create_task(run_operator(config["projects"], on_event))76 77 while True:78 try:79 event = await asyncio.wait_for(queue.get(), timeout=1.0)80 yield {"event": event["type"], "data": json.dumps(event, ensure_ascii=False)}81 if event["type"] == "brief":82 break83 except asyncio.TimeoutError:84 if task.done():85 break86 continue87 88 await task89 90 return EventSourceResponse(generator())91 92 93class TTSRequest(BaseModel):94 text: str95 96 97class ChatRequest(BaseModel):98 message: str99 100 101@app.post("/api/tts")102async def tts(req: TTSRequest):103 audio_bytes = generate_speech(req.text)104 return Response(content=audio_bytes, media_type="audio/wav")105 106 107@app.post("/api/chat")108async def chat(req: ChatRequest):109 session = get_chat_session()110 queue = asyncio.Queue()111 112 async def on_event(event: dict):113 await queue.put(event)114 115 async def generator():116 task = asyncio.create_task(117 chat_with_operator(req.message, session, on_event)118 )119 120 while True:121 try:122 event = await asyncio.wait_for(queue.get(), timeout=1.0)123 yield {"event": event["type"], "data": json.dumps(event, ensure_ascii=False)}124 if event["type"] == "chat_reply":125 break126 except asyncio.TimeoutError:127 if task.done():128 break129 continue130 131 await task132 133 return EventSourceResponse(generator())134 135 136@app.get("/api/project/{project_id}/sources")137async def project_sources(project_id: str):138 emails = MOCK_EMAILS.get(project_id, [])139 events = MOCK_EVENTS.get(project_id, [])140 search = MOCK_SEARCH.get(project_id, "")141 return {142 "emails": emails,143 "events": events,144 "search": search,145 }146 147 148@app.get("/api/health")149async def health():150 return {"status": "ok"}151 152 153# --- Serve frontend static files (for Replit / production) ---154_static_dir = Path(__file__).parent / "static"155if _static_dir.exists():156 app.mount("/assets", StaticFiles(directory=_static_dir / "assets"), name="assets")157 158 @app.get("/{full_path:path}")159 async def serve_spa(full_path: str):160 """Serve the React SPA for any non-API route."""161 file_path = _static_dir / full_path162 if file_path.is_file():163 return FileResponse(file_path)164 return FileResponse(_static_dir / "index.html")165 