avciTheProgrammer commited on
Commit
b337403
Β·
verified Β·
1 Parent(s): 2c0c226

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +355 -0
main.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ from fastapi import FastAPI, HTTPException
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from pydantic import BaseModel
6
+ from typing import Optional, List
7
+ import httpx
8
+ from supabase import create_client, Client
9
+ from datetime import datetime
10
+ import uuid
11
+
12
+ app = FastAPI(title="AI Team Chat API")
13
+
14
+ app.add_middleware(
15
+ CORSMiddleware,
16
+ allow_origins=["*"], # Replace with your frontend URL in production
17
+ allow_credentials=True,
18
+ allow_methods=["*"],
19
+ allow_headers=["*"],
20
+ )
21
+
22
+ # ─────────────────────────────────────────────
23
+ # ENV VARS (set in HuggingFace Space secrets)
24
+ # ─────────────────────────────────────────────
25
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
26
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
27
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "")
28
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
29
+
30
+ # Supabase client (optional - graceful fallback if not configured)
31
+ supabase: Optional[Client] = None
32
+ if SUPABASE_URL and SUPABASE_KEY:
33
+ supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
34
+
35
+
36
+ # ─────────────────────────────────────────────
37
+ # MODELS
38
+ # ─────────────────────────────────────────────
39
+ class ChatRequest(BaseModel):
40
+ message: str
41
+ provider: str = "groq" # "groq" or "openai"
42
+ religion: Optional[str] = None # for Spiritual Coach
43
+ session_id: Optional[str] = None # for Supabase history
44
+ conversation_history: Optional[List[dict]] = []
45
+
46
+
47
+ class AgentResponse(BaseModel):
48
+ agent: str
49
+ role: str
50
+ avatar: str
51
+ color: str
52
+ message: str
53
+
54
+
55
+ class ChatResponse(BaseModel):
56
+ session_id: str
57
+ agent_responses: List[AgentResponse]
58
+ summary: str
59
+ question: str
60
+
61
+
62
+ # ─────────────────────────────────────────────
63
+ # LLM WRAPPER
64
+ # ─────────────────────────────────────────────
65
+ async def call_llm(provider: str, system_prompt: str, user_message: str, temperature: float = 0.7) -> str:
66
+ """
67
+ Unified LLM wrapper. Supports Groq and OpenAI.
68
+ """
69
+ if provider == "groq":
70
+ return await call_groq(system_prompt, user_message, temperature)
71
+ elif provider == "openai":
72
+ return await call_openai(system_prompt, user_message, temperature)
73
+ else:
74
+ raise HTTPException(status_code=400, detail=f"Unknown provider: {provider}")
75
+
76
+
77
+ async def call_groq(system_prompt: str, user_message: str, temperature: float) -> str:
78
+ if not GROQ_API_KEY:
79
+ raise HTTPException(status_code=500, detail="GROQ_API_KEY not set")
80
+
81
+ async with httpx.AsyncClient(timeout=30) as client:
82
+ response = await client.post(
83
+ "https://api.groq.com/openai/v1/chat/completions",
84
+ headers={
85
+ "Authorization": f"Bearer {GROQ_API_KEY}",
86
+ "Content-Type": "application/json",
87
+ },
88
+ json={
89
+ "model": "llama-3.3-70b-versatile",
90
+ "messages": [
91
+ {"role": "system", "content": system_prompt},
92
+ {"role": "user", "content": user_message},
93
+ ],
94
+ "temperature": temperature,
95
+ "max_tokens": 300,
96
+ },
97
+ )
98
+ response.raise_for_status()
99
+ data = response.json()
100
+ return data["choices"][0]["message"]["content"].strip()
101
+
102
+
103
+ async def call_openai(system_prompt: str, user_message: str, temperature: float) -> str:
104
+ if not OPENAI_API_KEY:
105
+ raise HTTPException(status_code=500, detail="OPENAI_API_KEY not set")
106
+
107
+ async with httpx.AsyncClient(timeout=30) as client:
108
+ response = await client.post(
109
+ "https://api.openai.com/v1/chat/completions",
110
+ headers={
111
+ "Authorization": f"Bearer {OPENAI_API_KEY}",
112
+ "Content-Type": "application/json",
113
+ },
114
+ json={
115
+ "model": "gpt-4o-mini",
116
+ "messages": [
117
+ {"role": "system", "content": system_prompt},
118
+ {"role": "user", "content": user_message},
119
+ ],
120
+ "temperature": temperature,
121
+ "max_tokens": 300,
122
+ },
123
+ )
124
+ response.raise_for_status()
125
+ data = response.json()
126
+ return data["choices"][0]["message"]["content"].strip()
127
+
128
+
129
+ # ─────────────────────────────────────────────
130
+ # AGENT DEFINITIONS
131
+ # ───────────────────────��─────────────────────
132
+ def get_agents(religion: Optional[str]) -> List[dict]:
133
+ spiritual_note = (
134
+ f"Base your guidance on {religion} principles and teachings. Be respectful and calm."
135
+ if religion and religion.lower() not in ["none", "prefer not to say", ""]
136
+ else "Provide neutral mindfulness and universal spiritual guidance. Avoid referencing any specific religion."
137
+ )
138
+
139
+ return [
140
+ {
141
+ "name": "Dr. Sarah",
142
+ "role": "Doctor",
143
+ "avatar": "🩺",
144
+ "color": "#4FC3F7",
145
+ "system_prompt": (
146
+ "You are Dr. Sarah, a careful and responsible medical advisor. "
147
+ "You DO NOT give diagnoses. You suggest possibilities carefully and always recommend "
148
+ "consulting a licensed physician for personal medical decisions. "
149
+ "Be concise, warm, and professional. Respond in 2-4 lines maximum. "
150
+ "Do not repeat what other experts would say."
151
+ ),
152
+ },
153
+ {
154
+ "name": "Coach Marcus",
155
+ "role": "Fitness Coach",
156
+ "avatar": "πŸ’ͺ",
157
+ "color": "#81C784",
158
+ "system_prompt": (
159
+ "You are Coach Marcus, an energetic and experienced fitness coach. "
160
+ "You focus on physical activity, movement, exercise routines, and safe training. "
161
+ "Be motivating, practical, and concise. Respond in 2-4 lines maximum. "
162
+ "Do not repeat what other experts would say."
163
+ ),
164
+ },
165
+ {
166
+ "name": "Nina",
167
+ "role": "Nutritionist",
168
+ "avatar": "πŸ₯—",
169
+ "color": "#FFB74D",
170
+ "system_prompt": (
171
+ "You are Nina, a certified nutritionist specializing in diet, energy, and food science. "
172
+ "You focus on practical, evidence-based dietary guidance. "
173
+ "Be specific, helpful, and concise. Respond in 2-4 lines maximum. "
174
+ "Do not repeat what other experts would say."
175
+ ),
176
+ },
177
+ {
178
+ "name": "Dr. Mia",
179
+ "role": "Mental Health Coach",
180
+ "avatar": "🧠",
181
+ "color": "#CE93D8",
182
+ "system_prompt": (
183
+ "You are Dr. Mia, an empathetic and supportive mental health coach. "
184
+ "You help with emotional wellbeing, stress, mindset, and psychological patterns. "
185
+ "Be compassionate, grounding, and concise. Respond in 2-4 lines maximum. "
186
+ "Do not repeat what other experts would say."
187
+ ),
188
+ },
189
+ {
190
+ "name": "Sage Aris",
191
+ "role": "Spiritual Coach",
192
+ "avatar": "✨",
193
+ "color": "#F48FB1",
194
+ "system_prompt": (
195
+ f"You are Sage Aris, a gentle and insightful spiritual coach. "
196
+ f"{spiritual_note} "
197
+ f"Be calm, respectful, and uplifting. Respond in 2-4 lines maximum. "
198
+ f"Do not repeat what other experts would say."
199
+ ),
200
+ },
201
+ ]
202
+
203
+
204
+ COORDINATOR_SYSTEM_PROMPT = """You are the Coordinator of an expert AI wellness team consisting of a Doctor, Fitness Coach, Nutritionist, Mental Health Coach, and Spiritual Coach.
205
+
206
+ Your job:
207
+ 1. Read all agent responses carefully
208
+ 2. Write a SHORT summary of the key collective insights (2-3 sentences max)
209
+ 3. Ask ONE clear, thoughtful, combined question to the user to gather more context
210
+
211
+ Rules:
212
+ - Do NOT repeat the agents' responses verbatim
213
+ - Keep it collaborative and warm
214
+ - The question should help the team give better advice next time
215
+
216
+ Output format (strictly follow this):
217
+ Summary: <your short summary here>
218
+ Question: <your single question here>"""
219
+
220
+
221
+ # ─────────────────────────────────────────────
222
+ # SUPABASE HELPERS
223
+ # ─────────────────────────────────────────────
224
+ async def save_to_supabase(session_id: str, user_message: str, agent_responses: list, summary: str, question: str):
225
+ if not supabase:
226
+ return
227
+
228
+ try:
229
+ record = {
230
+ "session_id": session_id,
231
+ "user_message": user_message,
232
+ "agent_responses": agent_responses,
233
+ "summary": summary,
234
+ "question": question,
235
+ "created_at": datetime.utcnow().isoformat(),
236
+ }
237
+ supabase.table("chat_history").insert(record).execute()
238
+ except Exception as e:
239
+ print(f"Supabase save error: {e}")
240
+
241
+
242
+ async def get_session_history(session_id: str) -> list:
243
+ if not supabase or not session_id:
244
+ return []
245
+
246
+ try:
247
+ result = (
248
+ supabase.table("chat_history")
249
+ .select("*")
250
+ .eq("session_id", session_id)
251
+ .order("created_at", desc=False)
252
+ .limit(20)
253
+ .execute()
254
+ )
255
+ return result.data or []
256
+ except Exception as e:
257
+ print(f"Supabase fetch error: {e}")
258
+ return []
259
+
260
+
261
+ # ─────────────────────────────────────────────
262
+ # MAIN ENDPOINT
263
+ # ─────────────────────────────────────────────
264
+ @app.post("/chat", response_model=ChatResponse)
265
+ async def chat(request: ChatRequest):
266
+ session_id = request.session_id or str(uuid.uuid4())
267
+ agents = get_agents(request.religion)
268
+
269
+ # Build context from conversation history
270
+ history_context = ""
271
+ if request.conversation_history:
272
+ history_lines = []
273
+ for turn in request.conversation_history[-6:]: # last 3 exchanges
274
+ history_lines.append(f"User: {turn.get('user', '')}")
275
+ if turn.get("question"):
276
+ history_lines.append(f"Team Question: {turn.get('question', '')}")
277
+ history_context = "\n\nPrevious conversation context:\n" + "\n".join(history_lines)
278
+
279
+ user_prompt = f"{history_context}\n\nUser's current message: {request.message}"
280
+
281
+ # ── Step 1: Run all 5 agents in parallel ──
282
+ async def run_agent(agent: dict) -> AgentResponse:
283
+ message = await call_llm(
284
+ provider=request.provider,
285
+ system_prompt=agent["system_prompt"],
286
+ user_message=user_prompt,
287
+ )
288
+ return AgentResponse(
289
+ agent=agent["name"],
290
+ role=agent["role"],
291
+ avatar=agent["avatar"],
292
+ color=agent["color"],
293
+ message=message,
294
+ )
295
+
296
+ agent_results: List[AgentResponse] = await asyncio.gather(*[run_agent(a) for a in agents])
297
+
298
+ # ── Step 2: Run Coordinator ──
299
+ all_responses_text = "\n\n".join(
300
+ [f"[{r.role} β€” {r.agent}]:\n{r.message}" for r in agent_results]
301
+ )
302
+ coordinator_user_prompt = (
303
+ f"User asked: \"{request.message}\"\n\n"
304
+ f"Agent responses:\n{all_responses_text}"
305
+ )
306
+
307
+ coordinator_raw = await call_llm(
308
+ provider=request.provider,
309
+ system_prompt=COORDINATOR_SYSTEM_PROMPT,
310
+ user_message=coordinator_user_prompt,
311
+ temperature=0.5,
312
+ )
313
+
314
+ # Parse coordinator output
315
+ summary = ""
316
+ question = ""
317
+ for line in coordinator_raw.splitlines():
318
+ if line.lower().startswith("summary:"):
319
+ summary = line[len("summary:"):].strip()
320
+ elif line.lower().startswith("question:"):
321
+ question = line[len("question:"):].strip()
322
+
323
+ if not summary:
324
+ summary = coordinator_raw
325
+ if not question:
326
+ question = "Can you share more details so the team can help you better?"
327
+
328
+ # ── Step 3: Save to Supabase ──
329
+ agent_data = [r.dict() for r in agent_results]
330
+ asyncio.create_task(
331
+ save_to_supabase(session_id, request.message, agent_data, summary, question)
332
+ )
333
+
334
+ return ChatResponse(
335
+ session_id=session_id,
336
+ agent_responses=agent_results,
337
+ summary=summary,
338
+ question=question,
339
+ )
340
+
341
+
342
+ @app.get("/history/{session_id}")
343
+ async def get_history(session_id: str):
344
+ history = await get_session_history(session_id)
345
+ return {"session_id": session_id, "history": history}
346
+
347
+
348
+ @app.get("/health")
349
+ async def health():
350
+ return {
351
+ "status": "ok",
352
+ "groq_configured": bool(GROQ_API_KEY),
353
+ "openai_configured": bool(OPENAI_API_KEY),
354
+ "supabase_configured": bool(supabase),
355
+ }