Rahul-Samedavar commited on
Commit
7c553fc
·
1 Parent(s): f40b4fc
Files changed (3) hide show
  1. Dockerfile +14 -0
  2. app.py +303 -0
  3. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.9
3
+
4
+ RUN useradd -m -u 1000 user
5
+ USER user
6
+ ENV PATH="/home/user/.local/bin:$PATH"
7
+
8
+ WORKDIR /app
9
+
10
+ COPY --chown=user ./requirements.txt requirements.txt
11
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
12
+
13
+ COPY --chown=user . /app
14
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Query, Body
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.responses import FileResponse
5
+ from pydantic import BaseModel
6
+ import os
7
+ import time
8
+ import requests
9
+ import json
10
+ from dotenv import load_dotenv
11
+ import logging # 1. Import the logging module'
12
+
13
+ import openai
14
+
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format="%(asctime)s - %(levelname)s - %(message)s",
18
+ handlers=[logging.StreamHandler()]
19
+ )
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # --- Environment and Constants ---
23
+ load_dotenv()
24
+ API_KEY = os.getenv("RECALLAI_API_KEY", "").strip()
25
+ BASE_URL = "https://us-west-2.recall.ai/api/v1"
26
+
27
+
28
+ REQUESTY_API_KEY = os.getenv("OPENAI_API_KEY", "").strip() # load securely
29
+ client = openai.OpenAI(
30
+ api_key=REQUESTY_API_KEY,
31
+ base_url="https://router.requesty.ai/v1",
32
+ default_headers={
33
+ "HTTP-Referer": "https://your-site.com",
34
+ "X-Title": "Your Site Name",
35
+ },
36
+ )
37
+
38
+ MODEL_NAME = "openai/gpt-4o-mini"
39
+
40
+ # --- Configure OpenAI ---
41
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip()
42
+ openai.api_key = OPENAI_API_KEY
43
+ openai.api_base = "https://router.requesty.ai/v1"
44
+
45
+ # --- FastAPI App Initialization ---
46
+ app = FastAPI(title="Recall.ai Meeting API")
47
+
48
+ app.add_middleware(
49
+ CORSMiddleware,
50
+ allow_origins=["*"],
51
+ allow_credentials=True,
52
+ allow_methods=["*"],
53
+ allow_headers=["*"],
54
+ )
55
+
56
+ # --- In-memory Storage (for simplicity) ---
57
+ BOT_STORE = {}
58
+
59
+ # --- Pydantic Models ---
60
+ class AddBotRequest(BaseModel):
61
+ meeting_url: str
62
+
63
+ # --- Recall.ai Helper Functions ---
64
+ # (These functions remain unchanged)
65
+ def create_bot(meeting_url):
66
+ url = f"{BASE_URL}/bot"
67
+ headers = {"Authorization": f"Token {API_KEY}", "Content-Type": "application/json"}
68
+ body = {
69
+ "meeting_url": meeting_url,
70
+ "recording_config": {"transcript": {"provider": {"meeting_captions": {}}}},
71
+ "auto_start": True,
72
+ "auto_end": True
73
+ }
74
+ resp = requests.post(url, headers=headers, json=body)
75
+ resp.raise_for_status()
76
+ return resp.json()["id"]
77
+
78
+ def get_bot(bot_id):
79
+ url = f"{BASE_URL}/bot/{bot_id}"
80
+ headers = {"Authorization": f"Token {API_KEY}", "Accept": "application/json"}
81
+ resp = requests.get(url, headers=headers)
82
+ resp.raise_for_status()
83
+ return resp.json()
84
+
85
+ def parse_transcript(data):
86
+ dialogue = []
87
+ for participant_entry in data:
88
+ participant_name = participant_entry.get("participant", {}).get("name", "Unknown")
89
+ words = participant_entry.get("words", [])
90
+ for word_entry in words:
91
+ dialogue.append({
92
+ "name": participant_name,
93
+ "text": word_entry["text"],
94
+ "start_time": word_entry["start_timestamp"]["absolute"]
95
+ })
96
+ dialogue.sort(key=lambda x: x["start_time"])
97
+ return dialogue
98
+
99
+ def download_transcript(url):
100
+ resp = requests.get(url)
101
+ resp.raise_for_status()
102
+ return parse_transcript(resp.json())
103
+
104
+ # --- API Endpoints with Logging ---
105
+ @app.post("/addBot")
106
+ def add_bot(req: AddBotRequest):
107
+ logger.info(f"Received request to add bot for meeting: {req.meeting_url}")
108
+ try:
109
+ bot_id = create_bot(req.meeting_url)
110
+ BOT_STORE[bot_id] = {"meeting_url": req.meeting_url, "video_url": None, "transcript": None}
111
+ logger.info(f"Successfully created bot with ID: {bot_id} for URL: {req.meeting_url}")
112
+ return {"bot_id": bot_id, "meeting_url": req.meeting_url}
113
+ except Exception as e:
114
+ # exc_info=True includes the full stack trace in the log, which is invaluable for debugging.
115
+ logger.error(f"Failed to create bot for URL {req.meeting_url}: {e}", exc_info=True)
116
+ raise HTTPException(status_code=500, detail=str(e))
117
+
118
+ @app.get("/video")
119
+ def get_video(bot_id: str = Query(..., description="Bot ID")):
120
+ logger.info(f"Received request for video URL for bot_id: {bot_id}")
121
+ if not BOT_STORE.get(bot_id):
122
+ logger.warning(f"Bot ID {bot_id} not found in BOT_STORE for /video request.")
123
+ raise HTTPException(status_code=404, detail="Bot not found")
124
+ try:
125
+ bot_info = get_bot(bot_id)
126
+ recordings = bot_info.get("recordings", [])
127
+ if recordings:
128
+ video_data = recordings[0].get("media_shortcuts", {}).get("video_mixed", {}).get("data", {})
129
+ video_url = video_data.get("download_url")
130
+ if video_url:
131
+ BOT_STORE[bot_id]["video_url"] = video_url
132
+ logger.info(f"Found video URL for bot_id {bot_id}")
133
+ return {"video_url": video_url}
134
+ logger.info(f"Video not yet available for bot_id: {bot_id}")
135
+ return {"message": "Video not available yet."}
136
+ except Exception as e:
137
+ logger.error(f"Error fetching video for bot_id {bot_id}: {e}", exc_info=True)
138
+ raise HTTPException(status_code=500, detail=str(e))
139
+
140
+ @app.get("/transcript")
141
+ def get_transcript(bot_id: str = Query(..., description="Bot ID")):
142
+ logger.info(f"Received request for transcript for bot_id: {bot_id}")
143
+ if not BOT_STORE.get(bot_id):
144
+ logger.warning(f"Bot ID {bot_id} not found in BOT_STORE for /transcript request.")
145
+ raise HTTPException(status_code=404, detail="Bot not found")
146
+ try:
147
+ bot_info = get_bot(bot_id)
148
+ recordings = bot_info.get("recordings", [])
149
+ if recordings:
150
+ transcript_data = recordings[0].get("media_shortcuts", {}).get("transcript", {}).get("data", {})
151
+ transcript_url = transcript_data.get("download_url")
152
+ if transcript_url:
153
+ parsed = download_transcript(transcript_url)
154
+ BOT_STORE[bot_id]["transcript"] = parsed
155
+ logger.info(f"Transcript ready and parsed for bot_id: {bot_id}")
156
+ return {"transcript": parsed}
157
+ logger.info(f"Transcript not yet available for bot_id: {bot_id}")
158
+ return {"message": "Meeting has not ended yet or transcript is not ready."}
159
+ except Exception as e:
160
+ logger.error(f"Error fetching transcript for bot_id {bot_id}: {e}", exc_info=True)
161
+ raise HTTPException(status_code=500, detail=str(e))
162
+
163
+ @app.get("/wait_transcript")
164
+ def wait_transcript(bot_id: str, check_interval: int = 5, timeout: int = 1800):
165
+ logger.info(f"Starting to poll for transcript for bot_id: {bot_id} (timeout={timeout}s)")
166
+ start_time = time.time()
167
+ while time.time() - start_time < timeout:
168
+ try:
169
+ bot_info = get_bot(bot_id)
170
+ recordings = bot_info.get("recordings", [])
171
+ if recordings:
172
+ transcript_data = recordings[0].get("media_shortcuts", {}).get("transcript", {}).get("data", {})
173
+ transcript_url = transcript_data.get("download_url")
174
+ if transcript_url:
175
+ parsed = download_transcript(transcript_url)
176
+ BOT_STORE[bot_id]["transcript"] = parsed
177
+ logger.info(f"Transcript became available for bot_id: {bot_id} after polling.")
178
+ return {"transcript": parsed}
179
+ time.sleep(check_interval)
180
+ except Exception as e:
181
+ logger.error(f"Error during transcript polling for bot_id {bot_id}: {e}", exc_info=True)
182
+ raise HTTPException(status_code=500, detail=str(e))
183
+
184
+ logger.warning(f"Timeout reached while waiting for transcript for bot_id: {bot_id}")
185
+ raise HTTPException(status_code=408, detail="Timeout reached. Transcript not ready.")
186
+
187
+
188
+ def ai_summarize(transcript):
189
+ """
190
+ Summarizes the meeting transcript using requesty.ai LLM.
191
+ """
192
+ combined_text = ""
193
+ last_name = ""
194
+ for entry in transcript:
195
+ if entry["name"] == last_name:
196
+ combined_text += " " + entry["text"]
197
+ else:
198
+ if combined_text:
199
+ combined_text += "\n"
200
+ combined_text += f"{entry['name']}: {entry['text']}"
201
+ last_name = entry["name"]
202
+
203
+ prompt = f"Summarize the following meeting transcript concisely:\n\n{combined_text}\n\nSummary:"
204
+
205
+ try:
206
+ response = client.chat.completions.create(
207
+ model=MODEL_NAME,
208
+ messages=[{"role": "user", "content": prompt}],
209
+ temperature=0.3,
210
+ max_tokens=500
211
+ )
212
+
213
+ if not response.choices:
214
+ raise Exception("No response choices returned from LLM.")
215
+ return response.choices[0].message.content.strip()
216
+
217
+ except openai.OpenAIError as e:
218
+ logger.error(f"OpenAI API error in ai_summarize: {e}", exc_info=True)
219
+ raise HTTPException(status_code=500, detail=str(e))
220
+
221
+ def ai_assign_tasks(transcript, employees=None, extra_input=""):
222
+ """
223
+ Extracts tasks from transcript and optionally assigns them to employees.
224
+ """
225
+ combined_text = ""
226
+ last_name = ""
227
+ for entry in transcript:
228
+ if entry["name"] == last_name:
229
+ combined_text += " " + entry["text"]
230
+ else:
231
+ if combined_text:
232
+ combined_text += "\n"
233
+ combined_text += f"{entry['name']}: {entry['text']}"
234
+ last_name = entry["name"]
235
+
236
+ employee_text = ""
237
+ if employees:
238
+ employee_text = "Employees available:\n" + "\n".join(
239
+ f"- {emp['name']} ({emp['email']})" for emp in employees
240
+ )
241
+
242
+ prompt = f"""
243
+ You are an assistant that reads meeting transcripts and extracts tasks assigned.
244
+
245
+ Transcript:
246
+ {combined_text}
247
+
248
+ {employee_text}
249
+
250
+ Additional context:
251
+ {extra_input}
252
+
253
+ Please return a JSON array of tasks with the following fields:
254
+ - title: brief title of task
255
+ - description: task description
256
+ - deadline: if mentioned, else null
257
+ - assigned_to: list of employees (name + email) assigned to the task
258
+ """
259
+
260
+ try:
261
+ response = client.chat.completions.create(
262
+ model=MODEL_NAME,
263
+ messages=[{"role": "user", "content": prompt}],
264
+ temperature=0.3,
265
+ max_tokens=800,
266
+ )
267
+
268
+ if not response.choices:
269
+ raise Exception("No response choices returned from LLM.")
270
+ tasks_json = response.choices[0].message.content.strip()
271
+ return json.loads(tasks_json)
272
+ except json.JSONDecodeError:
273
+ logger.error(f"Failed to parse AI output: {tasks_json}")
274
+ return {"error": "Failed to parse AI output", "raw": tasks_json}
275
+ except openai.OpenAIError as e:
276
+ logger.error(f"OpenAI API error in ai_assign_tasks: {e}", exc_info=True)
277
+ raise HTTPException(status_code=500, detail=str(e))
278
+
279
+
280
+
281
+ @app.post("/summary")
282
+ def summary_endpoint(transcript: list = Body(...)):
283
+ logger.info("Generating meeting summary using AI")
284
+ try:
285
+ summary_text = ai_summarize(transcript)
286
+ return {"summary": summary_text}
287
+ except Exception as e:
288
+ logger.error(f"Error generating summary: {e}", exc_info=True)
289
+ raise HTTPException(status_code=500, detail=str(e))
290
+
291
+ @app.post("/assign_tasks")
292
+ def assign_tasks_endpoint(
293
+ transcript: list = Body(...),
294
+ extra_input: str = Body("", embed=True),
295
+ employees: list = Body([], embed=True)
296
+ ):
297
+ logger.info("Assigning tasks from transcript using AI")
298
+ try:
299
+ tasks = ai_assign_tasks(transcript, employees=employees, extra_input=extra_input)
300
+ return {"tasks": tasks}
301
+ except Exception as e:
302
+ logger.error(f"Error assigning tasks: {e}", exc_info=True)
303
+ raise HTTPException(status_code=500, detail=str(e))
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ requests
4
+ openai
5
+ pydantic
6
+ dotenv