Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, EmailStr, Field | |
| from typing import List, Dict, Any | |
| from fastapi.responses import FileResponse | |
| import httpx | |
| import os | |
| import json | |
| from dotenv import load_dotenv | |
| import logging | |
| from datetime import datetime, timezone | |
| # Setup logging with timestamp | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Load environment variables | |
| load_dotenv() | |
| # System information | |
| CURRENT_UTC = "2025-05-27 17:27:01" | |
| CURRENT_USER = "farhanwew" | |
| # Get API key from environment | |
| OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY") | |
| if not OPENROUTER_API_KEY: | |
| raise ValueError("OPENROUTER_API_KEY not found in environment variables") | |
| app = FastAPI( | |
| title="Cuan.in API", | |
| description="API untuk rekomendasi bisnis UMKM yang personalized", | |
| version="1.0.0", | |
| ) | |
| # Add CORS middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # Adjust in production | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Models | |
| class BusinessProfile(BaseModel): | |
| business_interest: str = Field(..., description="Minat bisnis pengguna") | |
| target_market: str = Field(..., description="Target pasar yang diinginkan") | |
| description: str = Field(..., description="Deskripsi usaha yang diinginkan") | |
| email: EmailStr = Field(..., description="Email pengguna") | |
| class RecommendationResponse(BaseModel): | |
| primary_recommendation: Dict[str, Any] | |
| alternative_recommendations: List[Dict[str, Any]] | |
| success_factors: List[str] | |
| challenges: List[str] | |
| next_steps: List[str] | |
| def extract_json(text: str) -> str: | |
| """ | |
| Try to extract JSON substring from text, | |
| if there's text before/after JSON. | |
| """ | |
| try: | |
| # Find from first curly brace to last curly brace | |
| start = text.index("{") | |
| end = text.rindex("}") + 1 | |
| return text[start:end] | |
| except ValueError: | |
| # If not found, return original text | |
| return text | |
| async def call_llm(prompt: str) -> dict: | |
| for attempt in range(2): # Try maximum 2 times | |
| async with httpx.AsyncClient(timeout=60.0) as client: | |
| try: | |
| response = await client.post( | |
| "https://openrouter.ai/api/v1/chat/completions", | |
| headers={ | |
| "Authorization": f"Bearer {OPENROUTER_API_KEY}", | |
| "Content-Type": "application/json", | |
| "HTTP-Referer": "https://cuan.in", # Replace with your actual domain | |
| "X-Title": "Cuan.in - UMKM Business Recommendations" | |
| }, | |
| json={ | |
| "model": "google/gemini-2.0-flash-001", | |
| "messages": [{"role": "user", "content": prompt}], | |
| "response_format": {"type": "json_object"}, | |
| "max_tokens": 8048, | |
| }, | |
| ) | |
| if response.status_code != 200: | |
| logging.error(f"Error from LLM API: {response.status_code} - {response.text}") | |
| raise HTTPException(status_code=response.status_code, detail=f"Error from LLM API: {response.text}") | |
| result = response.json() | |
| llm_response_raw = result["choices"][0]["message"]["content"] | |
| # Log the raw response for debugging | |
| logging.info(f"Raw LLM response text: {llm_response_raw}") | |
| # Clean and validate the response | |
| cleaned_response = extract_json(llm_response_raw) | |
| # Add additional validation before parsing | |
| if not cleaned_response.strip().startswith("{"): | |
| logging.error(f"Invalid JSON structure: {cleaned_response}") | |
| raise json.JSONDecodeError("Invalid JSON structure", cleaned_response, 0) | |
| parsed_response = json.loads(cleaned_response) | |
| # Validate the response structure | |
| required_keys = { | |
| "primary_recommendation", | |
| "alternative_recommendations", | |
| "success_factors", | |
| "challenges", | |
| "next_steps" | |
| } | |
| if not all(key in parsed_response for key in required_keys): | |
| logging.error(f"Missing required keys in response: {parsed_response}") | |
| raise ValueError("Response missing required keys") | |
| return parsed_response | |
| except json.JSONDecodeError as e: | |
| logging.error(f"JSON decoding error on attempt {attempt+1}: {e}") | |
| logging.error(f"Problematic JSON content: {cleaned_response}") | |
| if attempt == 1: # Last attempt | |
| raise HTTPException( | |
| status_code=502, | |
| detail=f"Invalid JSON from LLM. Original error: {str(e)}" | |
| ) | |
| logging.info("Retrying due to JSON decode error...") | |
| except Exception as e: | |
| logging.error(f"Unexpected error on attempt {attempt+1}: {str(e)}") | |
| if attempt == 1: # Last attempt | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Error processing LLM response: {str(e)}" | |
| ) | |
| logging.info("Retrying due to unexpected error...") | |
| # API endpoints | |
| async def root(): | |
| return { | |
| "message": "Welcome to Cuan.in API - Rekomendasi Bisnis UMKM", | |
| "version": "1.0.0", | |
| "current_utc": CURRENT_UTC, | |
| "status": "operational" | |
| } | |
| async def favicon(): | |
| return FileResponse("static/favicon.ico") | |
| async def status(): | |
| return { | |
| "status": "operational", | |
| "timestamp": CURRENT_UTC, | |
| "user": CURRENT_USER, | |
| "api_version": "1.0.0" | |
| } | |
| async def get_recommendations(profile: BusinessProfile): | |
| prompt = f""" | |
| Berikan respons dalam format JSON yang valid dengan struktur berikut tanpa teks tambahan apapun: | |
| {{ | |
| "primary_recommendation": {{ | |
| "nama_usaha": "string", | |
| "deskripsi": "string", | |
| "modal_yang_dibutuhkan": "string", | |
| "perkiraan_keuntungan": "string", | |
| "skala_usaha": "string", | |
| "target_pasar": "string" | |
| }}, | |
| "alternative_recommendations": [ | |
| {{ | |
| "nama_usaha": "string", | |
| "deskripsi_singkat": "string", | |
| "modal_yang_dibutuhkan": "string" | |
| }} | |
| ], | |
| "success_factors": ["string"], | |
| "challenges": ["string"], | |
| "next_steps": ["string"] | |
| }} | |
| Berikan rekomendasi bisnis UMKM berdasarkan profil berikut: | |
| - Minat Bisnis: {profile.business_interest} | |
| - Target Pasar: {profile.target_market} | |
| - Deskripsi: {profile.description} | |
| PENTING: | |
| 1. Pastikan output adalah JSON yang valid dengan tanda kutip ganda (\") dan tanpa teks tambahan di luar struktur JSON. | |
| 2. Berikan minimal 2 alternative_recommendations. | |
| 3. Berikan minimal 3 item untuk success_factors, challenges, dan next_steps. | |
| 4. Semua nilai harus dalam Bahasa Indonesia. | |
| 5. Modal dan keuntungan harus dalam format angka dengan mata uang Rupiah (contoh: "Rp 5.000.000"). | |
| """ | |
| # Log the request | |
| logging.info(f"Received recommendation request for email: {profile.email}") | |
| try: | |
| recommendations = await call_llm(prompt) | |
| # Log successful response | |
| logging.info(f"Successfully generated recommendations for email: {profile.email}") | |
| return recommendations | |
| except Exception as e: | |
| # Log the error | |
| logging.error(f"Error generating recommendations for email {profile.email}: {str(e)}") | |
| raise | |
| # Startup and shutdown events | |
| async def startup_event(): | |
| logging.info(f"API Started - Current UTC: {CURRENT_UTC}") | |
| logging.info(f"Current User: {CURRENT_USER}") | |
| async def shutdown_event(): | |
| logging.info("API Shutting down") | |
| # Run with: uvicorn main:app --reload | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=8000) |