rairo commited on
Commit
2868e10
·
verified ·
1 Parent(s): d1a4128

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +182 -24
main.py CHANGED
@@ -1184,48 +1184,206 @@ def _get_price_trend_analysis_for_chat(crop_type=None, location=None):
1184
  try:
1185
  all_deals = db.reference('deals', app=db_app).order_by_child('status').equal_to('completed').get() or {}
1186
  price_data_points = []
1187
- # ... (rest of data aggregation as before) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1188
  if not price_data_points: return f"Not enough historical data for trends for {crop_type or 'crops'} in {location or 'all locations'}.", []
1189
- # ... (Gemini prompt and call as before) ...
1190
- response = gemini_client.generate_content(model='gemini-2.0-flash', contents=[{'parts': [{'text': "YOUR_CHAT_TREND_PROMPT_HERE"}]}]) # Replace with actual prompt
1191
- return response.text.strip(), price_data_points
1192
- except Exception as e: logger.error(f"Chat: Gemini Trend Error: {e}"); return "Could not generate price trend.", []
1193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1194
 
1195
  def _fetch_platform_data_for_chat(user_message):
1196
  if not FIREBASE_INITIALIZED: return "Firebase not ready for platform data.", []
1197
  try:
1198
- # ... (existing logic for keyword extraction and fetching listings, ensure app=db_app) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1199
  listings_ref = db.reference('listings', app=db_app).order_by_child('status').equal_to('active')
1200
- # ... (rest of the logic) ...
1201
- return "Platform data summary here", [] # Placeholder
1202
- except Exception as e: logger.error(f"Chat: Platform Data Fetch Error: {e}"); return "Could not fetch platform data.", []
 
 
1203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1204
 
 
1205
  @app.route("/api/ai/chat", methods=["POST"])
1206
  def ai_chat():
1207
  auth_header = request.headers.get("Authorization", "");
1208
  uid = None
 
1209
  try:
1210
- if not FIREBASE_INITIALIZED or not gemini_client: return jsonify({'error': 'Server or AI service not ready.'}), 503
 
 
1211
  uid = verify_token(auth_header)
1212
- if not uid: return jsonify({"error": "Authentication required.", "login_required": True}), 401
1213
- data = request.get_json(); user_message = data.get("message", "").strip()
1214
- if not user_message: return jsonify({"error": "Message cannot be empty."}), 400
1215
- # ... (Intent classification logic as before) ...
1216
- intent = "general_agri_info" # Placeholder
1217
- # ... (Context fetching logic as before, using _get_price_trend_analysis_for_chat and _fetch_platform_data_for_chat) ...
1218
- context_for_gemini = "" # Placeholder
1219
- # ... (Main prompt and Gemini call as before) ...
1220
- ai_response_text = "AI response placeholder" # Placeholder
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1221
  db.reference(f'ai_chat_history/{uid}/{str(uuid.uuid4())}', app=db_app).set({
1222
- 'user_message': user_message, 'ai_response': ai_response_text,
1223
- 'intent_classified': intent, 'timestamp': datetime.now(timezone.utc).isoformat()
 
 
1224
  })
1225
  return jsonify({"response": ai_response_text, "intent": intent})
1226
- except Exception as e_auth: return handle_route_auth_errors(e_auth)
1227
- except firebase_exceptions.FirebaseError as fe: logger.error(f"Firebase error in ai_chat: {fe}"); return jsonify({'error': f'Firebase error: {str(fe)}'}), 500
1228
- except Exception as e: logger.error(f"AI Chat: Final response error: {e}"); return jsonify({'error': "Issue processing request."}), 500
 
 
 
 
 
 
 
 
1229
 
1230
  @app.route('/api/user/ai-chat-history', methods=['GET'])
1231
  def get_ai_chat_history():
 
1184
  try:
1185
  all_deals = db.reference('deals', app=db_app).order_by_child('status').equal_to('completed').get() or {}
1186
  price_data_points = []
1187
+ if all_deals:
1188
+ for deal_id, deal in all_deals.items():
1189
+ if not deal: continue # Skip if deal data is malformed or None
1190
+ listing_id = deal.get('listing_id')
1191
+ listing_details = db.reference(f'listings/{listing_id}', app=db_app).get() if listing_id else None
1192
+ if listing_details:
1193
+ deal_crop_type = listing_details.get('crop_type')
1194
+ deal_location = listing_details.get('location')
1195
+
1196
+ match_crop = not crop_type or (deal_crop_type and crop_type.lower() in deal_crop_type.lower())
1197
+ match_location = not location or (deal_location and location.lower() in deal_location.lower())
1198
+
1199
+ if match_crop and match_location:
1200
+ price_data_points.append({
1201
+ 'price': deal.get('agreed_price') or deal.get('proposed_price'),
1202
+ 'date': (deal.get('admin_approved_at') or deal.get('created_at', ''))[:10], # Date only, ensure created_at exists
1203
+ 'crop': deal_crop_type
1204
+ })
1205
  if not price_data_points: return f"Not enough historical data for trends for {crop_type or 'crops'} in {location or 'all locations'}.", []
1206
+
1207
+ data_summary_for_gemini = f"Historical transaction data for {crop_type or 'various crops'} in {location or 'various locations'}:\n"
1208
+ for point in price_data_points[:15]: # Limit data points for chat context
1209
+ data_summary_for_gemini += f"- Crop: {point.get('crop')}, Price: {point.get('price')}, Date: {point.get('date')}\n"
1210
 
1211
+ prompt = f"""
1212
+ Analyze transaction data from Tunasonga Agri. Provide a brief price trend analysis for {crop_type if crop_type else 'the general market'}{f' in {location}' if location else ''}.
1213
+ Is price increasing, decreasing, or stable? Note patterns. Concise. State if data is sparse.
1214
+ Data: {data_summary_for_gemini}
1215
+ Analysis:
1216
+ """
1217
+ response = gemini_client.generate_content(model='gemini-2.0-flash', contents=[{'parts': [{'text': prompt}]}])
1218
+ return response.text.strip(), price_data_points
1219
+ except firebase_exceptions.FirebaseError as fe:
1220
+ logger.error(f"Chat (Trend Helper): Firebase Error: {fe}")
1221
+ return "Database error occurred while fetching trend data.", []
1222
+ except Exception as e:
1223
+ logger.error(f"Chat (Trend Helper): Gemini Trend Error or other: {e}")
1224
+ return "Could not generate price trend analysis at this time.", []
1225
 
1226
  def _fetch_platform_data_for_chat(user_message):
1227
  if not FIREBASE_INITIALIZED: return "Firebase not ready for platform data.", []
1228
  try:
1229
+ keywords = user_message.lower().split()
1230
+ extracted_entities = {'crop_type': None, 'location': None, 'listing_type': None}
1231
+ # Consider making these lists configurable or fetched from DB if they grow large
1232
+ common_crops = ["maize", "beans", "tomatoes", "potatoes", "cabbage", "onions", "sorghum", "millet", "groundnuts", "wheat", "soybeans", "sunflower"]
1233
+ common_locations = ["harare", "bulawayo", "mutare", "gweru", "masvingo", "chinhoyi", "bindura", "marondera", # Zimbabwe
1234
+ "lusaka", "ndola", "kitwe", "livingstone", "chipata", # Zambia
1235
+ "maputo", "beira", "nampula", "tete"] # Mozambique
1236
+
1237
+ for word in keywords:
1238
+ if word in common_crops and not extracted_entities['crop_type']: extracted_entities['crop_type'] = word
1239
+ if word in common_locations and not extracted_entities['location']: extracted_entities['location'] = word
1240
+
1241
+ if any(k in keywords for k in ["listings", "produce", "selling", "for sale", "offerings"]): extracted_entities['listing_type'] = 'produce'
1242
+ elif any(k in keywords for k in ["demands", "requests", "buying", "looking for", "needs"]): extracted_entities['listing_type'] = 'demand'
1243
+
1244
+ if not extracted_entities['listing_type'] and not extracted_entities['crop_type']:
1245
+ return "To search platform data, please specify if you're looking for produce for sale or buyer demands, and optionally a crop type or location.", []
1246
+
1247
  listings_ref = db.reference('listings', app=db_app).order_by_child('status').equal_to('active')
1248
+ all_active_listings = listings_ref.get() or {}
1249
+ found_items = []
1250
+
1251
+ for lid, ldata in all_active_listings.items():
1252
+ if not isinstance(ldata, dict): continue # Skip malformed entries
1253
 
1254
+ match = True
1255
+ if extracted_entities['listing_type'] and ldata.get('listing_type') != extracted_entities['listing_type']:
1256
+ match = False
1257
+ # Use 'in' for partial matches on crop type and location if desired, or exact match
1258
+ if extracted_entities['crop_type'] and not (extracted_entities['crop_type'] in ldata.get('crop_type', '').lower()):
1259
+ match = False
1260
+ if extracted_entities['location'] and not (extracted_entities['location'] in ldata.get('location', '').lower()):
1261
+ match = False
1262
+
1263
+ if match:
1264
+ item_summary = f"- {ldata.get('listing_type', 'Item').capitalize()}: {ldata.get('crop_type', 'N/A')} in {ldata.get('location', 'N/A')}"
1265
+ if ldata.get('listing_type') == 'produce':
1266
+ item_summary += f", Price: {ldata.get('asking_price', 'N/A')}"
1267
+ elif ldata.get('listing_type') == 'demand':
1268
+ item_summary += f", Price Range: {ldata.get('price_range', 'N/A')}"
1269
+ item_summary += f" (Qty: {ldata.get('quantity', 'N/A')})"
1270
+ found_items.append(item_summary)
1271
+
1272
+ if not found_items:
1273
+ return f"No active {extracted_entities['listing_type'] or 'items'} currently found on the platform matching your criteria ({extracted_entities['crop_type'] or 'any crop'}, {extracted_entities['location'] or 'any location'}). You can browse all items in the marketplace section.", []
1274
+
1275
+ summary = f"Here's what I found on the Tunasonga Agri platform based on your query:\n" + "\n".join(found_items[:5]) # Show top 5
1276
+ if len(found_items) > 5:
1277
+ summary += f"\n...and {len(found_items) - 5} more. For a full list, please visit the marketplace section or refine your search terms."
1278
+ return summary, found_items
1279
+ except firebase_exceptions.FirebaseError as fe:
1280
+ logger.error(f"Chat (Platform Data Helper): Firebase Error: {fe}")
1281
+ return "Database error occurred while fetching platform data.", []
1282
+ except Exception as e:
1283
+ logger.error(f"Chat (Platform Data Helper): Error: {e}")
1284
+ return "Could not fetch platform data at this time.", []
1285
 
1286
+ # --- AI Chat Endpoint ---
1287
  @app.route("/api/ai/chat", methods=["POST"])
1288
  def ai_chat():
1289
  auth_header = request.headers.get("Authorization", "");
1290
  uid = None
1291
+ response_obj_gemini = None # For logging Gemini response object in case of AttributeError
1292
  try:
1293
+ if not FIREBASE_INITIALIZED or not gemini_client:
1294
+ return jsonify({'error': 'Server or AI service not ready.'}), 503
1295
+
1296
  uid = verify_token(auth_header)
1297
+ if not uid: # verify_token now raises, so this is a fallback if it somehow returns None without raising
1298
+ return jsonify({"error": "Authentication required.", "login_required": True}), 401
1299
+
1300
+ data = request.get_json()
1301
+ user_message = data.get("message", "").strip()
1302
+ if not user_message:
1303
+ return jsonify({"error": "Message cannot be empty."}), 400
1304
+
1305
+ # 1. Classify Intent
1306
+ classify_prompt = f"""
1307
+ Analyze the user's query for the Tunasonga Agri platform. Classify the primary intent into ONE of these categories:
1308
+ - "platform_data_query": User is asking about current listings, demands, or specific items available *right now* on the platform. (e.g., "any maize for sale?", "who needs beans in Harare?")
1309
+ - "price_trend_query": User is asking about historical price trends, market analysis, or future price predictions. (e.g., "how are maize prices trending?", "what was the price of sorghum last month?")
1310
+ - "general_agri_info": User is asking for general agricultural advice, farming techniques, business tips, or information not tied to current platform data or specific historical trends. (e.g., "how to grow tomatoes?", "best fertilizer for sandy soils", "exporting crops in SADC")
1311
+ - "other": If the query doesn't fit well or is a simple greeting/chit-chat.
1312
+
1313
+ User Query: "{user_message}"
1314
+
1315
+ Return ONLY the category string (e.g., "platform_data_query").
1316
+ """
1317
+ response_obj_gemini = gemini_client.generate_content(model='gemini-2.0-flash', contents=[{'parts': [{'text': classify_prompt}]}])
1318
+ intent = response_obj_gemini.text.strip().replace('"', '')
1319
+ if intent not in ["platform_data_query", "price_trend_query", "general_agri_info", "other"]:
1320
+ logger.warning(f"AI Chat: Unexpected intent: '{intent}' for query '{user_message}'. Defaulting to general_agri_info.")
1321
+ intent = "general_agri_info"
1322
+
1323
+ # 2. Fetch Context Data based on Intent
1324
+ context_for_gemini = ""
1325
+ platform_data_summary = ""
1326
+ trend_analysis_summary = ""
1327
+
1328
+ if intent == "platform_data_query":
1329
+ platform_data_summary, _ = _fetch_platform_data_for_chat(user_message)
1330
+ if platform_data_summary:
1331
+ context_for_gemini += f"Current Platform Data Context (summarized for you):\n{platform_data_summary}\n\n"
1332
+
1333
+ elif intent == "price_trend_query":
1334
+ # Basic entity extraction for trend query (crop, location)
1335
+ crop_match = re.search(r"(?:trend for|prices of|price of|trend of)\s+([\w\s]+?)(?:\s+in\s+([\w\s]+))?$", user_message.lower())
1336
+ trend_crop, trend_location = (None, None)
1337
+ if crop_match:
1338
+ trend_crop = crop_match.group(1).strip()
1339
+ if crop_match.group(2):
1340
+ trend_location = crop_match.group(2).strip()
1341
+
1342
+ trend_analysis_summary, _ = _get_price_trend_analysis_for_chat(crop_type=trend_crop, location=trend_location)
1343
+ if trend_analysis_summary:
1344
+ context_for_gemini += f"Price Trend Analysis Context (generated for you):\n{trend_analysis_summary}\n\n"
1345
+
1346
+ # 3. Generate Final Response with Gemini
1347
+ main_prompt = f"""
1348
+ You are Tunasonga Agri Assistant, an AI for an agricultural marketplace in Zimbabwe and SADC.
1349
+ Your goal is to provide helpful, concise, and accurate information. Your persona is professional, friendly, and supportive of farmers and agri-businesses.
1350
+
1351
+ The user's original query intent was classified as: {intent}
1352
+ {context_for_gemini}
1353
+ Based on the user's query and any provided context above, please formulate your answer to the user.
1354
+ User Query: "{user_message}"
1355
+
1356
+ Specific Instructions based on intent:
1357
+ - If the intent was 'platform_data_query': Use the "Current Platform Data Context" to answer. If the context says no items were found, relay that and suggest they browse the marketplace or refine their search. Do not invent listings.
1358
+ - If the intent was 'price_trend_query': Use the "Price Trend Analysis Context". If the context says trends couldn't be generated, relay that. Do not invent trends.
1359
+ - For 'general_agri_info': Use your broad agricultural knowledge. Focus on practices relevant to the SADC region, smallholder farmers, climate-smart agriculture, market access, and agri-business development. Provide actionable advice if possible.
1360
+ - If the query is unclear, classified as "other", or if the context is insufficient for a specific query: Provide a polite general response, ask for clarification, or gently guide the user on how you can help (e.g., "I can help with finding produce, getting price trends, or general farming advice. What would you like to know?").
1361
+
1362
+ Keep your answers clear and easy to understand. Avoid overly technical jargon unless necessary and explain it.
1363
+ Answer:
1364
+ """
1365
+ response_obj_gemini = gemini_client.generate_content(model='gemini-2.0-flash', contents=[{'parts': [{'text': main_prompt}]}])
1366
+ ai_response_text = response_obj_gemini.text.strip()
1367
+
1368
+ # 4. Store Chat History
1369
  db.reference(f'ai_chat_history/{uid}/{str(uuid.uuid4())}', app=db_app).set({
1370
+ 'user_message': user_message,
1371
+ 'ai_response': ai_response_text,
1372
+ 'intent_classified': intent,
1373
+ 'timestamp': datetime.now(timezone.utc).isoformat()
1374
  })
1375
  return jsonify({"response": ai_response_text, "intent": intent})
1376
+
1377
+ except AttributeError as ae: # Gemini response structure issue
1378
+ logger.error(f"Gemini Response Attribute Error in ai_chat (UID: {uid}): {ae}. Response object: {response_obj_gemini}")
1379
+ ai_response_text = "I'm having a little trouble understanding that. Could you try rephrasing?"
1380
+ try: # Attempt to get text if it's a common alternative structure
1381
+ if response_obj_gemini and response_obj_gemini.candidates:
1382
+ ai_response_text = response_obj_gemini.candidates[0].content.parts[0].text
1383
+ except Exception: pass # Stick to default error message
1384
+ return jsonify({"response": ai_response_text, "intent": intent or "unknown", "error_detail": "AI_RESPONSE_FORMAT_ISSUE"}), 200 # Return 200 with a user-friendly error
1385
+ except Exception as e: # Catches errors from verify_token, Firebase, or other unexpected issues
1386
+ return handle_route_errors(e, uid_context=uid)
1387
 
1388
  @app.route('/api/user/ai-chat-history', methods=['GET'])
1389
  def get_ai_chat_history():