rairo commited on
Commit
f6ae6a2
·
verified ·
1 Parent(s): c109445

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +149 -83
main.py CHANGED
@@ -8,6 +8,7 @@ import traceback
8
  import math
9
  import requests
10
  import threading
 
11
  from concurrent.futures import ThreadPoolExecutor
12
  from datetime import datetime, timedelta
13
 
@@ -19,6 +20,15 @@ from PIL import Image
19
  from google import genai
20
  from google.genai import types
21
 
 
 
 
 
 
 
 
 
 
22
  # -----------------------------------------------------------------------------
23
  # 1. CONFIGURATION & INITIALIZATION
24
  # -----------------------------------------------------------------------------
@@ -28,6 +38,7 @@ CORS(app)
28
 
29
  # --- Firebase Initialization ---
30
  try:
 
31
  credentials_json_string = os.environ.get("FIREBASE")
32
  if not credentials_json_string:
33
  raise ValueError("The FIREBASE environment variable is not set.")
@@ -41,9 +52,9 @@ try:
41
  'databaseURL': firebase_db_url,
42
  'storageBucket': firebase_storage_bucket
43
  })
44
- print("Firebase Admin SDK initialized successfully.")
45
  except Exception as e:
46
- print(f"FATAL: Error initializing Firebase: {e}")
47
  exit(1)
48
 
49
  bucket = storage.bucket()
@@ -51,14 +62,15 @@ db_ref = db.reference()
51
 
52
  # --- Google GenAI Client Initialization (Gemini 3.0 Flash) ---
53
  try:
 
54
  api_key = os.environ.get("Gemini")
55
  if not api_key:
56
  raise ValueError("The 'Gemini' API key is not set.")
57
 
58
  client = genai.Client(api_key=api_key)
59
- print("Google GenAI (Gemini 3.0) Client initialized successfully.")
60
  except Exception as e:
61
- print(f"FATAL: Error initializing GenAI Client: {e}")
62
  exit(1)
63
 
64
  # Model Constants
@@ -74,42 +86,65 @@ OPENALEX_MAILTO = os.environ.get("OPENALEX_MAILTO", "rairo@sozofix.tech")
74
  # -----------------------------------------------------------------------------
75
 
76
  def verify_token(auth_header):
77
- if not auth_header or not auth_header.startswith('Bearer '): return None
 
 
78
  token = auth_header.split('Bearer ')[1]
79
  try:
80
  decoded_token = auth.verify_id_token(token)
81
  return decoded_token['uid']
82
- except: return None
 
 
83
 
84
  def verify_admin(auth_header):
85
  uid = verify_token(auth_header)
86
  if not uid: raise PermissionError('Invalid or missing token')
87
  user_data = db_ref.child(f'users/{uid}').get()
88
  if not user_data or not user_data.get('is_admin', False):
 
89
  raise PermissionError('Admin access required')
90
  return uid
91
 
92
  def upload_to_storage(data_bytes, destination_blob_name, content_type):
93
- blob = bucket.blob(destination_blob_name)
94
- blob.upload_from_string(data_bytes, content_type=content_type)
95
- blob.make_public()
96
- return blob.public_url
 
 
 
 
97
 
98
  def query_wolfram_alpha(query):
99
- if not WOLFRAM_APP_ID: return "Wolfram|Alpha grounding unavailable."
 
 
 
100
  try:
101
  url = f"http://api.wolframalpha.com/v1/result?appid={WOLFRAM_APP_ID}&i={query}"
102
  response = requests.get(url, timeout=5)
103
- return response.text if response.status_code == 200 else "Fact-check pending."
104
- except: return "Grounding timeout."
 
 
 
 
 
 
 
105
 
106
  def query_openalex(topic):
 
107
  try:
108
  url = f"https://api.openalex.org/works?search={topic}&mailto={OPENALEX_MAILTO}"
109
  resp = requests.get(url, timeout=5).json()
110
  results = resp.get('results', [])
 
111
  return [{"title": r['title'], "url": r['doi'] or r['id'], "year": r['publication_year']} for r in results[:3]]
112
- except: return []
 
 
113
 
114
  # -----------------------------------------------------------------------------
115
  # 3. ASYNCHRONOUS VOICE ORCHESTRATION (ATHENA VOICE)
@@ -118,8 +153,11 @@ def query_openalex(topic):
118
  def generate_single_narration(text, uid, epiphany_id, layer_name):
119
  """Deepgram Aura-Luna generation for a single layer."""
120
  try:
 
121
  api_key = os.environ.get("DEEPGRAM_API_KEY")
122
- if not api_key: return layer_name, None
 
 
123
 
124
  DEEPGRAM_URL = "https://api.deepgram.com/v1/speak?model=aura-luna-en"
125
  headers = {"Authorization": f"Token {api_key}", "Content-Type": "text/plain"}
@@ -129,13 +167,15 @@ def generate_single_narration(text, uid, epiphany_id, layer_name):
129
 
130
  audio_path = f"users/{uid}/epiphanies/{epiphany_id}/narrations/{layer_name}.mp3"
131
  url = upload_to_storage(response.content, audio_path, 'audio/mpeg')
 
132
  return layer_name, url
133
  except Exception as e:
134
- print(f"TTS Error [{layer_name}]: {e}")
135
  return layer_name, None
136
 
137
  def generate_all_narrations_async(data_dict, uid, epiphany_id):
138
  """Uses ThreadPoolExecutor to generate all 4 layers in parallel."""
 
139
  layers = ['genesis', 'scientific_core', 'engineering_edge', 'cross_pollination']
140
  results = {}
141
  with ThreadPoolExecutor(max_workers=4) as executor:
@@ -151,36 +191,48 @@ def generate_all_narrations_async(data_dict, uid, epiphany_id):
151
 
152
  @app.route('/api/epiphany/generate', methods=['POST'])
153
  def generate_epiphany():
 
154
  uid = verify_token(request.headers.get('Authorization'))
155
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
156
 
 
157
  user_ref = db_ref.child(f'users/{uid}')
158
  user_data = user_ref.get()
 
159
  if not user_data or user_data.get('credits', 0) < 1:
 
160
  return jsonify({'error': 'Insufficient sparks for an Epiphany.'}), 402
161
 
162
  if 'image' not in request.files:
 
163
  return jsonify({'error': 'Visual input is required.'}), 400
164
 
165
  image_file = request.files['image']
166
  image_bytes = image_file.read()
167
- pil_image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
168
-
169
  try:
 
 
 
170
  # Step 1: Rapid Identification
 
171
  id_prompt = "Identify this precisely. If biological, include the Latin name. If mechanical, include the inventor if known. Reply with ONLY the name."
172
- subject = client.models.generate_content(model=ATHENA_MODEL, contents=[id_prompt, pil_image]).text.strip()
 
 
173
 
174
  # Step 2: Grounding (Parallel Dispatch)
 
175
  physics_fact = query_wolfram_alpha(f"constants of {subject}")
176
  papers = query_openalex(subject)
177
 
178
  # Step 3: Synthesis (Feynman Technique)
 
179
  synthesis_prompt = f"""
180
  Act as Athena, the Systems Synthesizer. Reveal the first principles of '{subject}'.
181
  Style: Richard Feynman. Simple analogies, profound scientific truths.
182
 
183
- Grounding: {physics_fact}
184
 
185
  Output JSON:
186
  {{
@@ -198,26 +250,37 @@ def generate_epiphany():
198
  config=types.GenerateContentConfig(response_mime_type='application/json')
199
  )
200
 
201
- data = json.loads(synth_response.text)
 
 
 
 
 
 
 
 
202
  epiphany_id = str(uuid.uuid4())
 
203
 
204
- # Step 4: Parallel Audio Generation (Hackathon Latency Optimization)
 
205
  narration_urls = generate_all_narrations_async(data, uid, epiphany_id)
206
 
207
  # Step 5: Persistence
 
208
  image_url = upload_to_storage(image_bytes, f"users/{uid}/epiphanies/{epiphany_id}/vision.jpg", 'image/jpeg')
209
 
210
  epiphany_record = {
211
  "epiphanyId": epiphany_id,
212
  "uid": uid,
213
- "title": data['title'],
214
  "subject": subject,
215
  "imageURL": image_url,
216
  "layers": {
217
- "genesis": {"text": data['genesis'], "audio": narration_urls.get('genesis')},
218
- "scientific_core": {"text": data['scientific_core'], "audio": narration_urls.get('scientific_core')},
219
- "engineering_edge": {"text": data['engineering_edge'], "audio": narration_urls.get('engineering_edge')},
220
- "cross_pollination": {"text": data['cross_pollination'], "audio": narration_urls.get('cross_pollination')}
221
  },
222
  "grounding": {"physics": physics_fact, "papers": papers},
223
  "createdAt": datetime.utcnow().isoformat()
@@ -225,30 +288,37 @@ def generate_epiphany():
225
 
226
  db_ref.child(f'epiphanies/{epiphany_id}').set(epiphany_record)
227
  user_ref.update({'credits': user_data.get('credits', 0) - 1})
 
228
 
229
  return jsonify(epiphany_record), 201
230
 
 
 
 
231
  except Exception as e:
232
- print(traceback.format_exc())
 
233
  return jsonify({'error': str(e)}), 500
234
 
235
  @app.route('/api/epiphany/deep-dive', methods=['POST'])
236
  def deep_dive():
237
- """Zoom-based recursive reasoning."""
238
  uid = verify_token(request.headers.get('Authorization'))
239
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
240
 
241
  image_file = request.files['image']
242
- pil_image = Image.open(io.BytesIO(image_file.read())).convert('RGB')
243
-
244
- dive_prompt = "Act as an Engineering Lead. In 50 words, explain the microscopic or mechanical significance of this specific detail."
245
-
246
  try:
 
 
 
247
  res = client.models.generate_content(model=ATHENA_MODEL, contents=[dive_prompt, pil_image])
 
 
248
  user_ref = db_ref.child(f'users/{uid}')
249
  user_ref.update({'credits': max(0, user_ref.get().get('credits', 0) - 1)})
250
  return jsonify({"analysis": res.text.strip()}), 200
251
  except Exception as e:
 
252
  return jsonify({'error': str(e)}), 500
253
 
254
  # -----------------------------------------------------------------------------
@@ -257,12 +327,11 @@ def deep_dive():
257
 
258
  @app.route('/api/user/call-briefing', methods=['GET'])
259
  def get_chiron_briefing():
260
- """Enhanced Briefing: Passes grounding papers to ElevenLabs context."""
261
  uid = verify_token(request.headers.get('Authorization'))
262
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
263
 
264
  try:
265
- # Get last Epiphany for context
266
  last_epiphany = db_ref.child('epiphanies').order_by_child('uid').equal_to(uid).limit_to_last(1).get() or {}
267
 
268
  context_data = ""
@@ -273,6 +342,7 @@ def get_chiron_briefing():
273
  paper_titles = [p['title'] for p in papers]
274
  context_data = f"Current focus: {e_data['subject']}. Recent papers unlocked: {', '.join(paper_titles)}."
275
 
 
276
  brief_prompt = f"""
277
  Prep Chiron, the Socratic Mentor.
278
  User Context: {context_data}
@@ -282,10 +352,12 @@ def get_chiron_briefing():
282
  response = client.models.generate_content(model=BRIEFING_MODEL, contents=[brief_prompt])
283
  return jsonify({"memory_summary": response.text.strip(), "grounding_context": context_data}), 200
284
  except Exception as e:
 
285
  return jsonify({'error': str(e)}), 500
286
 
287
  @app.route('/api/log-call-usage', methods=['POST'])
288
  def log_call_usage():
 
289
  uid = verify_token(request.headers.get('Authorization'))
290
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
291
 
@@ -294,10 +366,12 @@ def log_call_usage():
294
  transcript = data.get("transcript", "")
295
 
296
  cost = math.ceil(duration / 60) * 3
 
297
 
298
  try:
299
  user_ref = db_ref.child(f'users/{uid}')
300
- new_bal = max(0, user_ref.get().get('credits', 0) - cost)
 
301
  user_ref.update({'credits': new_bal})
302
 
303
  if transcript:
@@ -305,8 +379,11 @@ def log_call_usage():
305
  "text": transcript,
306
  "createdAt": datetime.utcnow().isoformat()
307
  })
 
 
308
  return jsonify({"success": True, "remainingCredits": new_bal}), 200
309
  except Exception as e:
 
310
  return jsonify({'error': str(e)}), 500
311
 
312
  # -----------------------------------------------------------------------------
@@ -318,45 +395,39 @@ def submit_feedback():
318
  uid = verify_token(request.headers.get('Authorization'))
319
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
320
  data = request.get_json()
321
- fb_ref = db_ref.child('feedback').push()
322
- fb_ref.set({
323
- "userId": uid,
324
- "message": data.get('message'),
325
- "type": data.get('type', 'general'),
326
- "status": "open",
327
- "createdAt": datetime.utcnow().isoformat()
328
- })
329
- return jsonify({"success": True}), 201
 
 
 
 
 
330
 
331
  @app.route('/api/user/request-credits', methods=['POST'])
332
  def request_credits():
333
  uid = verify_token(request.headers.get('Authorization'))
334
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
335
  data = request.get_json()
336
- req_ref = db_ref.child('credit_requests').push()
337
- req_ref.set({
338
- "userId": uid,
339
- "requested_amount": data.get('amount', 50),
340
- "status": "pending",
341
- "createdAt": datetime.utcnow().isoformat()
342
- })
343
- return jsonify({"success": True}), 201
344
-
345
- @app.route('/api/admin/dashboard', methods=['GET'])
346
- def admin_dashboard():
347
  try:
348
- verify_admin(request.headers.get('Authorization'))
349
- users = db_ref.child('users').get() or {}
350
- epiphanies = db_ref.child('epiphanies').get() or {}
351
- requests = db_ref.child('credit_requests').get() or {}
352
-
353
- return jsonify({
354
- "total_users": len(users),
355
- "total_epiphanies": len(epiphanies),
356
- "pending_requests": len([r for r in requests.values() if r.get('status') == 'pending'])
357
- }), 200
358
  except Exception as e:
359
- return jsonify({'error': str(e)}), 403
 
360
 
361
  # -----------------------------------------------------------------------------
362
  # 7. AUTHENTICATION & PROFILE
@@ -364,11 +435,7 @@ def admin_dashboard():
364
 
365
  @app.route('/api/auth/signup', methods=['POST'])
366
  def signup():
367
- """
368
- Refined Signup: Handles syncing after the Frontend creates the user.
369
- Does not try to re-create the user in Firebase Auth.
370
- """
371
- # 1. Verify the token sent by the frontend
372
  uid = verify_token(request.headers.get('Authorization'))
373
  if not uid:
374
  return jsonify({'error': 'Invalid or expired token'}), 401
@@ -377,30 +444,25 @@ def signup():
377
  user_ref = db_ref.child(f'users/{uid}')
378
  user_data = user_ref.get()
379
 
380
- # 2. If user already exists in DB, just return it (avoid error)
381
  if user_data:
 
382
  return jsonify({'uid': uid, **user_data}), 200
383
 
384
- # 3. User is new to the DB, initialize their profile
385
  data = request.get_json()
386
- email = data.get('email')
387
- display_name = data.get('displayName', 'Seeker')
388
-
389
  new_user_record = {
390
- 'email': email,
391
- 'displayName': display_name,
392
- 'credits': 30, # Ensure they get their starting Sparks
393
  'is_admin': False,
394
  'createdAt': datetime.utcnow().isoformat()
395
  }
396
 
397
  user_ref.set(new_user_record)
398
- print(f"Successfully initialized Seeker profile for UID: {uid}")
399
-
400
  return jsonify({'success': True, 'uid': uid, **new_user_record}), 201
401
 
402
  except Exception as e:
403
- print(f"Signup Sync Error: {e}")
404
  return jsonify({'error': str(e)}), 400
405
 
406
  @app.route('/api/auth/social-signin', methods=['POST'])
@@ -412,6 +474,7 @@ def social_signin():
412
  user_data = user_ref.get()
413
 
414
  if not user_data:
 
415
  firebase_user = auth.get_user(uid)
416
  user_data = {
417
  'email': firebase_user.email,
@@ -434,6 +497,7 @@ def get_profile():
434
  def list_epiphanies():
435
  uid = verify_token(request.headers.get('Authorization'))
436
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
 
437
  results = db_ref.child('epiphanies').order_by_child('uid').equal_to(uid).get() or {}
438
  return jsonify(list(results.values()))
439
 
@@ -442,4 +506,6 @@ def list_epiphanies():
442
  # -----------------------------------------------------------------------------
443
 
444
  if __name__ == '__main__':
 
 
445
  app.run(debug=False, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
 
8
  import math
9
  import requests
10
  import threading
11
+ import logging
12
  from concurrent.futures import ThreadPoolExecutor
13
  from datetime import datetime, timedelta
14
 
 
20
  from google import genai
21
  from google.genai import types
22
 
23
+ # -----------------------------------------------------------------------------
24
+ # 0. LOGGING CONFIGURATION
25
+ # -----------------------------------------------------------------------------
26
+ logging.basicConfig(
27
+ level=logging.INFO,
28
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
29
+ )
30
+ logger = logging.getLogger("SOZO_ATHENA")
31
+
32
  # -----------------------------------------------------------------------------
33
  # 1. CONFIGURATION & INITIALIZATION
34
  # -----------------------------------------------------------------------------
 
38
 
39
  # --- Firebase Initialization ---
40
  try:
41
+ logger.info("Initializing Firebase Admin SDK...")
42
  credentials_json_string = os.environ.get("FIREBASE")
43
  if not credentials_json_string:
44
  raise ValueError("The FIREBASE environment variable is not set.")
 
52
  'databaseURL': firebase_db_url,
53
  'storageBucket': firebase_storage_bucket
54
  })
55
+ logger.info("Firebase Admin SDK initialized successfully.")
56
  except Exception as e:
57
+ logger.error(f"FATAL: Error initializing Firebase: {e}")
58
  exit(1)
59
 
60
  bucket = storage.bucket()
 
62
 
63
  # --- Google GenAI Client Initialization (Gemini 3.0 Flash) ---
64
  try:
65
+ logger.info("Initializing Google GenAI Client...")
66
  api_key = os.environ.get("Gemini")
67
  if not api_key:
68
  raise ValueError("The 'Gemini' API key is not set.")
69
 
70
  client = genai.Client(api_key=api_key)
71
+ logger.info("Google GenAI (Gemini 3.0) Client initialized successfully.")
72
  except Exception as e:
73
+ logger.error(f"FATAL: Error initializing GenAI Client: {e}")
74
  exit(1)
75
 
76
  # Model Constants
 
86
  # -----------------------------------------------------------------------------
87
 
88
  def verify_token(auth_header):
89
+ if not auth_header or not auth_header.startswith('Bearer '):
90
+ logger.warning("Missing or invalid Authorization header.")
91
+ return None
92
  token = auth_header.split('Bearer ')[1]
93
  try:
94
  decoded_token = auth.verify_id_token(token)
95
  return decoded_token['uid']
96
+ except Exception as e:
97
+ logger.error(f"Token verification failed: {e}")
98
+ return None
99
 
100
  def verify_admin(auth_header):
101
  uid = verify_token(auth_header)
102
  if not uid: raise PermissionError('Invalid or missing token')
103
  user_data = db_ref.child(f'users/{uid}').get()
104
  if not user_data or not user_data.get('is_admin', False):
105
+ logger.warning(f"Admin access denied for user: {uid}")
106
  raise PermissionError('Admin access required')
107
  return uid
108
 
109
  def upload_to_storage(data_bytes, destination_blob_name, content_type):
110
+ try:
111
+ blob = bucket.blob(destination_blob_name)
112
+ blob.upload_from_string(data_bytes, content_type=content_type)
113
+ blob.make_public()
114
+ return blob.public_url
115
+ except Exception as e:
116
+ logger.error(f"Failed to upload to Firebase Storage: {e}")
117
+ return None
118
 
119
  def query_wolfram_alpha(query):
120
+ logger.info(f"Querying Wolfram Alpha: {query}")
121
+ if not WOLFRAM_APP_ID:
122
+ logger.warning("WOLFRAM_APP_ID missing.")
123
+ return "Wolfram|Alpha grounding unavailable."
124
  try:
125
  url = f"http://api.wolframalpha.com/v1/result?appid={WOLFRAM_APP_ID}&i={query}"
126
  response = requests.get(url, timeout=5)
127
+ if response.status_code == 200:
128
+ logger.info("Wolfram Alpha check successful.")
129
+ return response.text
130
+ else:
131
+ logger.warning(f"Wolfram Alpha returned status {response.status_code}")
132
+ return "Fact-check pending."
133
+ except Exception as e:
134
+ logger.error(f"Wolfram Alpha query failed: {e}")
135
+ return "Grounding timeout."
136
 
137
  def query_openalex(topic):
138
+ logger.info(f"Querying OpenAlex for topic: {topic}")
139
  try:
140
  url = f"https://api.openalex.org/works?search={topic}&mailto={OPENALEX_MAILTO}"
141
  resp = requests.get(url, timeout=5).json()
142
  results = resp.get('results', [])
143
+ logger.info(f"OpenAlex found {len(results)} results.")
144
  return [{"title": r['title'], "url": r['doi'] or r['id'], "year": r['publication_year']} for r in results[:3]]
145
+ except Exception as e:
146
+ logger.error(f"OpenAlex query failed: {e}")
147
+ return []
148
 
149
  # -----------------------------------------------------------------------------
150
  # 3. ASYNCHRONOUS VOICE ORCHESTRATION (ATHENA VOICE)
 
153
  def generate_single_narration(text, uid, epiphany_id, layer_name):
154
  """Deepgram Aura-Luna generation for a single layer."""
155
  try:
156
+ logger.info(f"Generating TTS for layer: {layer_name}")
157
  api_key = os.environ.get("DEEPGRAM_API_KEY")
158
+ if not api_key:
159
+ logger.error("DEEPGRAM_API_KEY is not set.")
160
+ return layer_name, None
161
 
162
  DEEPGRAM_URL = "https://api.deepgram.com/v1/speak?model=aura-luna-en"
163
  headers = {"Authorization": f"Token {api_key}", "Content-Type": "text/plain"}
 
167
 
168
  audio_path = f"users/{uid}/epiphanies/{epiphany_id}/narrations/{layer_name}.mp3"
169
  url = upload_to_storage(response.content, audio_path, 'audio/mpeg')
170
+ logger.info(f"TTS Uploaded: {url}")
171
  return layer_name, url
172
  except Exception as e:
173
+ logger.error(f"TTS Error [{layer_name}]: {e}")
174
  return layer_name, None
175
 
176
  def generate_all_narrations_async(data_dict, uid, epiphany_id):
177
  """Uses ThreadPoolExecutor to generate all 4 layers in parallel."""
178
+ logger.info(f"Starting parallel TTS generation for epiphany: {epiphany_id}")
179
  layers = ['genesis', 'scientific_core', 'engineering_edge', 'cross_pollination']
180
  results = {}
181
  with ThreadPoolExecutor(max_workers=4) as executor:
 
191
 
192
  @app.route('/api/epiphany/generate', methods=['POST'])
193
  def generate_epiphany():
194
+ logger.info(">>> START generate_epiphany request")
195
  uid = verify_token(request.headers.get('Authorization'))
196
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
197
 
198
+ logger.info(f"User validated: {uid}")
199
  user_ref = db_ref.child(f'users/{uid}')
200
  user_data = user_ref.get()
201
+
202
  if not user_data or user_data.get('credits', 0) < 1:
203
+ logger.warning(f"User {uid} has insufficient credits.")
204
  return jsonify({'error': 'Insufficient sparks for an Epiphany.'}), 402
205
 
206
  if 'image' not in request.files:
207
+ logger.error("No image file provided in request.")
208
  return jsonify({'error': 'Visual input is required.'}), 400
209
 
210
  image_file = request.files['image']
211
  image_bytes = image_file.read()
212
+
 
213
  try:
214
+ pil_image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
215
+ logger.info("Image bytes successfully converted to PIL.")
216
+
217
  # Step 1: Rapid Identification
218
+ logger.info("Step 1: Running Rapid Identification...")
219
  id_prompt = "Identify this precisely. If biological, include the Latin name. If mechanical, include the inventor if known. Reply with ONLY the name."
220
+ id_response = client.models.generate_content(model=ATHENA_MODEL, contents=[id_prompt, pil_image])
221
+ subject = id_response.text.strip()
222
+ logger.info(f"Subject Identified: {subject}")
223
 
224
  # Step 2: Grounding (Parallel Dispatch)
225
+ logger.info("Step 2: Grounding Dispatch...")
226
  physics_fact = query_wolfram_alpha(f"constants of {subject}")
227
  papers = query_openalex(subject)
228
 
229
  # Step 3: Synthesis (Feynman Technique)
230
+ logger.info("Step 3: Generating Feynman Synthesis JSON...")
231
  synthesis_prompt = f"""
232
  Act as Athena, the Systems Synthesizer. Reveal the first principles of '{subject}'.
233
  Style: Richard Feynman. Simple analogies, profound scientific truths.
234
 
235
+ Grounding Context: {physics_fact}
236
 
237
  Output JSON:
238
  {{
 
250
  config=types.GenerateContentConfig(response_mime_type='application/json')
251
  )
252
 
253
+ # Log raw response for debugging 500s
254
+ raw_text = synth_response.text
255
+ logger.info(f"Raw Synthesis Response: {raw_text}")
256
+
257
+ # Cleaning up markdown code blocks if present
258
+ if raw_text.startswith("```json"):
259
+ raw_text = raw_text.replace("```json", "").replace("```", "").strip()
260
+
261
+ data = json.loads(raw_text)
262
  epiphany_id = str(uuid.uuid4())
263
+ logger.info(f"Epiphany ID generated: {epiphany_id}")
264
 
265
+ # Step 4: Parallel Audio Generation
266
+ logger.info("Step 4: Running Parallel TTS...")
267
  narration_urls = generate_all_narrations_async(data, uid, epiphany_id)
268
 
269
  # Step 5: Persistence
270
+ logger.info("Step 5: Finalizing record and uploading image...")
271
  image_url = upload_to_storage(image_bytes, f"users/{uid}/epiphanies/{epiphany_id}/vision.jpg", 'image/jpeg')
272
 
273
  epiphany_record = {
274
  "epiphanyId": epiphany_id,
275
  "uid": uid,
276
+ "title": data.get('title', 'Unknown Discovery'),
277
  "subject": subject,
278
  "imageURL": image_url,
279
  "layers": {
280
+ "genesis": {"text": data.get('genesis', ''), "audio": narration_urls.get('genesis')},
281
+ "scientific_core": {"text": data.get('scientific_core', ''), "audio": narration_urls.get('scientific_core')},
282
+ "engineering_edge": {"text": data.get('engineering_edge', ''), "audio": narration_urls.get('engineering_edge')},
283
+ "cross_pollination": {"text": data.get('cross_pollination', ''), "audio": narration_urls.get('cross_pollination')}
284
  },
285
  "grounding": {"physics": physics_fact, "papers": papers},
286
  "createdAt": datetime.utcnow().isoformat()
 
288
 
289
  db_ref.child(f'epiphanies/{epiphany_id}').set(epiphany_record)
290
  user_ref.update({'credits': user_data.get('credits', 0) - 1})
291
+ logger.info(f"EP-GEN SUCCESS: {epiphany_id}")
292
 
293
  return jsonify(epiphany_record), 201
294
 
295
+ except json.JSONDecodeError as je:
296
+ logger.error(f"JSON Parsing Error: {je}. Raw output was: {synth_response.text}")
297
+ return jsonify({'error': 'AI generated invalid JSON. Try again.'}), 500
298
  except Exception as e:
299
+ logger.error(f"Generate Epiphany Global Error: {e}")
300
+ logger.error(traceback.format_exc())
301
  return jsonify({'error': str(e)}), 500
302
 
303
  @app.route('/api/epiphany/deep-dive', methods=['POST'])
304
  def deep_dive():
305
+ logger.info(">>> START deep-dive request")
306
  uid = verify_token(request.headers.get('Authorization'))
307
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
308
 
309
  image_file = request.files['image']
 
 
 
 
310
  try:
311
+ pil_image = Image.open(io.BytesIO(image_file.read())).convert('RGB')
312
+ dive_prompt = "Act as an Engineering Lead. In 50 words, explain the microscopic or mechanical significance of this specific detail."
313
+
314
  res = client.models.generate_content(model=ATHENA_MODEL, contents=[dive_prompt, pil_image])
315
+ logger.info("Deep dive reasoning completed.")
316
+
317
  user_ref = db_ref.child(f'users/{uid}')
318
  user_ref.update({'credits': max(0, user_ref.get().get('credits', 0) - 1)})
319
  return jsonify({"analysis": res.text.strip()}), 200
320
  except Exception as e:
321
+ logger.error(f"Deep Dive Error: {e}")
322
  return jsonify({'error': str(e)}), 500
323
 
324
  # -----------------------------------------------------------------------------
 
327
 
328
  @app.route('/api/user/call-briefing', methods=['GET'])
329
  def get_chiron_briefing():
330
+ logger.info(">>> START get_chiron_briefing request")
331
  uid = verify_token(request.headers.get('Authorization'))
332
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
333
 
334
  try:
 
335
  last_epiphany = db_ref.child('epiphanies').order_by_child('uid').equal_to(uid).limit_to_last(1).get() or {}
336
 
337
  context_data = ""
 
342
  paper_titles = [p['title'] for p in papers]
343
  context_data = f"Current focus: {e_data['subject']}. Recent papers unlocked: {', '.join(paper_titles)}."
344
 
345
+ logger.info(f"Generating briefing with context: {context_data}")
346
  brief_prompt = f"""
347
  Prep Chiron, the Socratic Mentor.
348
  User Context: {context_data}
 
352
  response = client.models.generate_content(model=BRIEFING_MODEL, contents=[brief_prompt])
353
  return jsonify({"memory_summary": response.text.strip(), "grounding_context": context_data}), 200
354
  except Exception as e:
355
+ logger.error(f"Call Briefing Error: {e}")
356
  return jsonify({'error': str(e)}), 500
357
 
358
  @app.route('/api/log-call-usage', methods=['POST'])
359
  def log_call_usage():
360
+ logger.info(">>> START log_call_usage request")
361
  uid = verify_token(request.headers.get('Authorization'))
362
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
363
 
 
366
  transcript = data.get("transcript", "")
367
 
368
  cost = math.ceil(duration / 60) * 3
369
+ logger.info(f"Call ended for {uid}. Duration: {duration}s, Cost: {cost} sparks.")
370
 
371
  try:
372
  user_ref = db_ref.child(f'users/{uid}')
373
+ user_data = user_ref.get()
374
+ new_bal = max(0, user_data.get('credits', 0) - cost)
375
  user_ref.update({'credits': new_bal})
376
 
377
  if transcript:
 
379
  "text": transcript,
380
  "createdAt": datetime.utcnow().isoformat()
381
  })
382
+ logger.info("Transcript saved to database.")
383
+
384
  return jsonify({"success": True, "remainingCredits": new_bal}), 200
385
  except Exception as e:
386
+ logger.error(f"Usage Logging Error: {e}")
387
  return jsonify({'error': str(e)}), 500
388
 
389
  # -----------------------------------------------------------------------------
 
395
  uid = verify_token(request.headers.get('Authorization'))
396
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
397
  data = request.get_json()
398
+ try:
399
+ fb_ref = db_ref.child('feedback').push()
400
+ fb_ref.set({
401
+ "userId": uid,
402
+ "message": data.get('message'),
403
+ "type": data.get('type', 'general'),
404
+ "status": "open",
405
+ "createdAt": datetime.utcnow().isoformat()
406
+ })
407
+ logger.info(f"Feedback received from user {uid}.")
408
+ return jsonify({"success": True}), 201
409
+ except Exception as e:
410
+ logger.error(f"Feedback Submission Error: {e}")
411
+ return jsonify({'error': str(e)}), 500
412
 
413
  @app.route('/api/user/request-credits', methods=['POST'])
414
  def request_credits():
415
  uid = verify_token(request.headers.get('Authorization'))
416
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
417
  data = request.get_json()
 
 
 
 
 
 
 
 
 
 
 
418
  try:
419
+ req_ref = db_ref.child('credit_requests').push()
420
+ req_ref.set({
421
+ "userId": uid,
422
+ "requested_amount": data.get('amount', 50),
423
+ "status": "pending",
424
+ "createdAt": datetime.utcnow().isoformat()
425
+ })
426
+ logger.info(f"Spark request logged for user {uid}.")
427
+ return jsonify({"success": True}), 201
 
428
  except Exception as e:
429
+ logger.error(f"Credit Request Error: {e}")
430
+ return jsonify({'error': str(e)}), 500
431
 
432
  # -----------------------------------------------------------------------------
433
  # 7. AUTHENTICATION & PROFILE
 
435
 
436
  @app.route('/api/auth/signup', methods=['POST'])
437
  def signup():
438
+ logger.info(">>> START signup sync")
 
 
 
 
439
  uid = verify_token(request.headers.get('Authorization'))
440
  if not uid:
441
  return jsonify({'error': 'Invalid or expired token'}), 401
 
444
  user_ref = db_ref.child(f'users/{uid}')
445
  user_data = user_ref.get()
446
 
 
447
  if user_data:
448
+ logger.info(f"User {uid} already exists, skipping initialization.")
449
  return jsonify({'uid': uid, **user_data}), 200
450
 
 
451
  data = request.get_json()
 
 
 
452
  new_user_record = {
453
+ 'email': data.get('email'),
454
+ 'displayName': data.get('displayName', 'Seeker'),
455
+ 'credits': 30,
456
  'is_admin': False,
457
  'createdAt': datetime.utcnow().isoformat()
458
  }
459
 
460
  user_ref.set(new_user_record)
461
+ logger.info(f"New seeker initialized: {uid} with 30 Sparks.")
 
462
  return jsonify({'success': True, 'uid': uid, **new_user_record}), 201
463
 
464
  except Exception as e:
465
+ logger.error(f"Signup Sync Error: {e}")
466
  return jsonify({'error': str(e)}), 400
467
 
468
  @app.route('/api/auth/social-signin', methods=['POST'])
 
474
  user_data = user_ref.get()
475
 
476
  if not user_data:
477
+ logger.info(f"Initializing social user: {uid}")
478
  firebase_user = auth.get_user(uid)
479
  user_data = {
480
  'email': firebase_user.email,
 
497
  def list_epiphanies():
498
  uid = verify_token(request.headers.get('Authorization'))
499
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
500
+ logger.info(f"Listing epiphanies for user {uid}.")
501
  results = db_ref.child('epiphanies').order_by_child('uid').equal_to(uid).get() or {}
502
  return jsonify(list(results.values()))
503
 
 
506
  # -----------------------------------------------------------------------------
507
 
508
  if __name__ == '__main__':
509
+ # Standard HF Port 7860
510
+ logger.info("Starting Sozo Athena Server on port 7860...")
511
  app.run(debug=False, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))