Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, HTTPException, Query, Body | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse | |
| from pydantic import BaseModel | |
| import os | |
| import time | |
| import requests | |
| import json | |
| from dotenv import load_dotenv | |
| import logging # 1. Import the logging module' | |
| import openai | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s - %(levelname)s - %(message)s", | |
| handlers=[logging.StreamHandler()] | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # --- Environment and Constants --- | |
| load_dotenv() | |
| API_KEY = os.getenv("RECALLAI_API_KEY", "").strip() | |
| BASE_URL = "https://us-west-2.recall.ai/api/v1" | |
| REQUESTY_API_KEY = os.getenv("OPENAI_API_KEY", "").strip() # load securely | |
| client = openai.OpenAI( | |
| api_key=REQUESTY_API_KEY, | |
| base_url="https://router.requesty.ai/v1", | |
| default_headers={ | |
| "HTTP-Referer": "https://your-site.com", | |
| "X-Title": "Your Site Name", | |
| }, | |
| ) | |
| MODEL_NAME = "openai/gpt-4o-mini" | |
| # --- Configure OpenAI --- | |
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip() | |
| openai.api_key = OPENAI_API_KEY | |
| openai.api_base = "https://router.requesty.ai/v1" | |
| # --- FastAPI App Initialization --- | |
| app = FastAPI(title="Recall.ai Meeting API") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # --- In-memory Storage (for simplicity) --- | |
| BOT_STORE = {} | |
| # --- Pydantic Models --- | |
| class AddBotRequest(BaseModel): | |
| meeting_url: str | |
| # --- Recall.ai Helper Functions --- | |
| # (These functions remain unchanged) | |
| def create_bot(meeting_url): | |
| url = f"{BASE_URL}/bot" | |
| headers = {"Authorization": f"Token {API_KEY}", "Content-Type": "application/json"} | |
| body = { | |
| "meeting_url": meeting_url, | |
| "recording_config": {"transcript": {"provider": {"meeting_captions": {}}}}, | |
| "auto_start": True, | |
| "auto_end": True | |
| } | |
| resp = requests.post(url, headers=headers, json=body) | |
| resp.raise_for_status() | |
| return resp.json()["id"] | |
| def get_bot(bot_id): | |
| url = f"{BASE_URL}/bot/{bot_id}" | |
| headers = {"Authorization": f"Token {API_KEY}", "Accept": "application/json"} | |
| resp = requests.get(url, headers=headers) | |
| resp.raise_for_status() | |
| return resp.json() | |
| def parse_transcript(data): | |
| dialogue = [] | |
| for participant_entry in data: | |
| participant_name = participant_entry.get("participant", {}).get("name", "Unknown") | |
| words = participant_entry.get("words", []) | |
| for word_entry in words: | |
| dialogue.append({ | |
| "name": participant_name, | |
| "text": word_entry["text"], | |
| "start_time": word_entry["start_timestamp"]["absolute"] | |
| }) | |
| dialogue.sort(key=lambda x: x["start_time"]) | |
| return dialogue | |
| def download_transcript(url): | |
| resp = requests.get(url) | |
| resp.raise_for_status() | |
| return parse_transcript(resp.json()) | |
| # --- API Endpoints with Logging --- | |
| def add_bot(req: AddBotRequest): | |
| logger.info(f"Received request to add bot for meeting: {req.meeting_url}") | |
| try: | |
| bot_id = create_bot(req.meeting_url) | |
| BOT_STORE[bot_id] = {"meeting_url": req.meeting_url, "video_url": None, "transcript": None} | |
| logger.info(f"Successfully created bot with ID: {bot_id} for URL: {req.meeting_url}") | |
| return {"bot_id": bot_id, "meeting_url": req.meeting_url} | |
| except Exception as e: | |
| # exc_info=True includes the full stack trace in the log, which is invaluable for debugging. | |
| logger.error(f"Failed to create bot for URL {req.meeting_url}: {e}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def get_video(bot_id: str = Query(..., description="Bot ID")): | |
| logger.info(f"Received request for video URL for bot_id: {bot_id}") | |
| if not BOT_STORE.get(bot_id): | |
| logger.warning(f"Bot ID {bot_id} not found in BOT_STORE for /video request.") | |
| raise HTTPException(status_code=404, detail="Bot not found") | |
| try: | |
| bot_info = get_bot(bot_id) | |
| recordings = bot_info.get("recordings", []) | |
| if recordings: | |
| video_data = recordings[0].get("media_shortcuts", {}).get("video_mixed", {}).get("data", {}) | |
| video_url = video_data.get("download_url") | |
| if video_url: | |
| BOT_STORE[bot_id]["video_url"] = video_url | |
| logger.info(f"Found video URL for bot_id {bot_id}") | |
| return {"video_url": video_url} | |
| logger.info(f"Video not yet available for bot_id: {bot_id}") | |
| return {"message": "Video not available yet."} | |
| except Exception as e: | |
| logger.error(f"Error fetching video for bot_id {bot_id}: {e}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def get_transcript(bot_id: str = Query(..., description="Bot ID")): | |
| logger.info(f"Received request for transcript for bot_id: {bot_id}") | |
| if not BOT_STORE.get(bot_id): | |
| logger.warning(f"Bot ID {bot_id} not found in BOT_STORE for /transcript request.") | |
| raise HTTPException(status_code=404, detail="Bot not found") | |
| try: | |
| bot_info = get_bot(bot_id) | |
| recordings = bot_info.get("recordings", []) | |
| if recordings: | |
| transcript_data = recordings[0].get("media_shortcuts", {}).get("transcript", {}).get("data", {}) | |
| transcript_url = transcript_data.get("download_url") | |
| if transcript_url: | |
| parsed = download_transcript(transcript_url) | |
| BOT_STORE[bot_id]["transcript"] = parsed | |
| logger.info(f"Transcript ready and parsed for bot_id: {bot_id}") | |
| return {"transcript": parsed} | |
| logger.info(f"Transcript not yet available for bot_id: {bot_id}") | |
| return {"message": "Meeting has not ended yet or transcript is not ready."} | |
| except Exception as e: | |
| logger.error(f"Error fetching transcript for bot_id {bot_id}: {e}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def wait_transcript(bot_id: str, check_interval: int = 5, timeout: int = 1800): | |
| logger.info(f"Starting to poll for transcript for bot_id: {bot_id} (timeout={timeout}s)") | |
| start_time = time.time() | |
| while time.time() - start_time < timeout: | |
| try: | |
| bot_info = get_bot(bot_id) | |
| recordings = bot_info.get("recordings", []) | |
| if recordings: | |
| transcript_data = recordings[0].get("media_shortcuts", {}).get("transcript", {}).get("data", {}) | |
| transcript_url = transcript_data.get("download_url") | |
| if transcript_url: | |
| parsed = download_transcript(transcript_url) | |
| BOT_STORE[bot_id]["transcript"] = parsed | |
| logger.info(f"Transcript became available for bot_id: {bot_id} after polling.") | |
| return {"transcript": parsed} | |
| time.sleep(check_interval) | |
| except Exception as e: | |
| logger.error(f"Error during transcript polling for bot_id {bot_id}: {e}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| logger.warning(f"Timeout reached while waiting for transcript for bot_id: {bot_id}") | |
| raise HTTPException(status_code=408, detail="Timeout reached. Transcript not ready.") | |
| def ai_summarize(transcript): | |
| """ | |
| Summarizes the meeting transcript using requesty.ai LLM. | |
| """ | |
| combined_text = "" | |
| last_name = "" | |
| for entry in transcript: | |
| if entry["name"] == last_name: | |
| combined_text += " " + entry["text"] | |
| else: | |
| if combined_text: | |
| combined_text += "\n" | |
| combined_text += f"{entry['name']}: {entry['text']}" | |
| last_name = entry["name"] | |
| prompt = f"Summarize the following meeting transcript concisely:\n\n{combined_text}\n\nSummary:" | |
| try: | |
| response = client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=0.3, | |
| max_tokens=500 | |
| ) | |
| if not response.choices: | |
| raise Exception("No response choices returned from LLM.") | |
| return response.choices[0].message.content.strip() | |
| except openai.OpenAIError as e: | |
| logger.error(f"OpenAI API error in ai_summarize: {e}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def ai_assign_tasks(transcript, employees=None, extra_input=""): | |
| """ | |
| Extracts tasks from transcript and optionally assigns them to employees. | |
| """ | |
| combined_text = "" | |
| last_name = "" | |
| for entry in transcript: | |
| if entry["name"] == last_name: | |
| combined_text += " " + entry["text"] | |
| else: | |
| if combined_text: | |
| combined_text += "\n" | |
| combined_text += f"{entry['name']}: {entry['text']}" | |
| last_name = entry["name"] | |
| employee_text = "" | |
| if employees: | |
| employee_text = "Employees available:\n" + "\n".join( | |
| f"- {emp['name']} ({emp['email']})" for emp in employees | |
| ) | |
| prompt = f""" | |
| You are an assistant that reads meeting transcripts and extracts tasks assigned. | |
| Transcript: | |
| {combined_text} | |
| {employee_text} | |
| Additional context: | |
| {extra_input} | |
| Please return a JSON array of tasks with the following fields: | |
| - title: brief title of task | |
| - description: task description | |
| - deadline: if mentioned, else null | |
| - assigned_to: list of employees (name + email) assigned to the task | |
| """ | |
| try: | |
| response = client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=0.3, | |
| max_tokens=800, | |
| ) | |
| if not response.choices: | |
| raise Exception("No response choices returned from LLM.") | |
| tasks_json = response.choices[0].message.content.strip() | |
| return json.loads(tasks_json) | |
| except json.JSONDecodeError: | |
| logger.error(f"Failed to parse AI output: {tasks_json}") | |
| return {"error": "Failed to parse AI output", "raw": tasks_json} | |
| except openai.OpenAIError as e: | |
| logger.error(f"OpenAI API error in ai_assign_tasks: {e}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def summary_endpoint(transcript: list = Body(...)): | |
| logger.info("Generating meeting summary using AI") | |
| try: | |
| summary_text = ai_summarize(transcript) | |
| return {"summary": summary_text} | |
| except Exception as e: | |
| logger.error(f"Error generating summary: {e}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def assign_tasks_endpoint( | |
| transcript: list = Body(...), | |
| extra_input: str = Body("", embed=True), | |
| employees: list = Body([], embed=True) | |
| ): | |
| logger.info("Assigning tasks from transcript using AI") | |
| try: | |
| tasks = ai_assign_tasks(transcript, employees=employees, extra_input=extra_input) | |
| return {"tasks": tasks} | |
| except Exception as e: | |
| logger.error(f"Error assigning tasks: {e}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=str(e)) |