jessejohnson/plg4-dev-server
0
1from backend.utils.request_dto.chat_response import ChatResponse2from backend.utils.request_dto.scrape_request import ScrapeRequest3from backend.utils.types import ChatMessage4from fastapi import FastAPI, HTTPException, BackgroundTasks, Header5from fastapi.middleware.cors import CORSMiddleware6import os7from typing import Type8from fastapi.middleware.cors import CORSMiddleware9from data_minning.dto.stream_opts import StreamOptions10from data_minning.base_scrapper import BaseRecipeScraper, JsonArraySink, MongoSink11from data_minning.all_nigerian_recipe_scraper import AllNigerianRecipesScraper12from data_minning.yummy_medley_scraper import YummyMedleyScraper13from backend.config.settings import settings14from backend.config.logging_config import setup_default_logging, get_logger15from backend.utils.sanitization import sanitize_user_input16from backend.services.vector_store import vector_store_service17# Setup logging first, before importing services18setup_default_logging()19logger = get_logger("app")20 21# Import services after logging is configured22from backend.services.llm_service import llm_service23 24SCRAPERS: dict[str, Type[BaseRecipeScraper]] = {25 "yummy": YummyMedleyScraper,26 "anr": AllNigerianRecipesScraper,27}28 29app = FastAPI(30 title="Recipe Recommendation Bot API",31 description="AI-powered recipe recommendation system with RAG capabilities",32 version="1.0.0"33)34 35logger.info("๐ Starting Recipe Recommendation Bot API")36logger.info(f"Environment: {settings.ENVIRONMENT}")37logger.info(f"Provider: {settings.get_llm_config()['provider']} (LLM + Embeddings)")38 39# Add CORS middleware40app.add_middleware(41 CORSMiddleware,42 allow_origins=settings.CORS_ORIGINS or ["*"],43 allow_credentials=settings.CORS_ALLOW_CREDENTIALS or True,44 allow_methods=settings.CORS_ALLOW_METHODS or ["*"],45 allow_headers=settings.CORS_ALLOW_HEADERS or ["*"],46)47 48# Remove OpenAI direct setup - now handled by LLM service49# if settings.OPENAI_API_KEY:50# openai.api_key = settings.OPENAI_API_KEY51 52@app.get("/")53def index():54 logger.info("๐ก Root endpoint accessed")55 return {56 "message": "Recipe Recommendation Bot API",57 "version": "1.0.0",58 "status": "running"59 }60 61@app.get("/health")62def health_check():63 logger.info("๐ฅ Health check endpoint accessed")64 return {65 "status": "healthy",66 "environment": settings.ENVIRONMENT,67 "llm_service_initialized": llm_service is not None68 }69 70@app.post("/chat", response_model=ChatResponse)71async def chat(chat_message: ChatMessage):72 """Main chatbot endpoint - Recipe recommendation with ConversationalRetrievalChain"""73 try:74 # Message is already sanitized by the Pydantic validator75 # Find the last user message in the messages list76 last_user_message = chat_message.get_latest_message()77 if not last_user_message:78 raise ValueError("No valid user message found")79 user_text = last_user_message.parts[0].text80 81 response_text = llm_service.ask_question(user_text)82 return ChatResponse(response=response_text)83 84 except ValueError as e:85 # Handle validation/sanitization errors86 logger.warning(f"โ ๏ธ Invalid input received: {str(e)}")87 raise HTTPException(status_code=400, detail=f"Invalid input: {str(e)}")88 89 except Exception as e:90 logger.error(f"โ Chat service error: {str(e)}", exc_info=True)91 raise HTTPException(status_code=500, detail=f"Chat service error: {str(e)}")92 93@app.get("/demo")94def demo(prompt: str = "What recipes do you have?"):95 """Demo endpoint - uses simple chat completion without RAG"""96 logger.info(f"๐ฏ Demo request: '{prompt[:50]}...'")97 98 try:99 # Sanitize the demo prompt using the same sanitization method100 sanitized_prompt = sanitize_user_input(prompt) 101 response_text = llm_service.simple_chat_completion(sanitized_prompt)102 return {"prompt": sanitized_prompt, "reply": response_text}103 104 except ValueError as e:105 # Handle validation/sanitization errors106 logger.warning(f"โ ๏ธ Invalid demo prompt: {str(e)}")107 return {"error": f"Invalid prompt: {str(e)}", "prompt": prompt}108 109 except Exception as e:110 logger.error(f"โ Demo endpoint error: {str(e)}", exc_info=True)111 return {"error": f"Failed to get response: {str(e)}"}112 113@app.post("/clear-memory")114def clear_conversation_memory():115 """Clear conversation memory"""116 logger.info("๐งน Memory clear request received")117 118 try:119 success = llm_service.clear_memory()120 121 if success:122 logger.info("โ
Conversation memory cleared successfully")123 return {"status": "success", "message": "Conversation memory cleared"}124 else:125 logger.warning("โ ๏ธ Memory clear operation failed")126 return {"status": "failed", "message": "Failed to clear conversation memory"}127 128 except Exception as e:129 logger.error(f"โ Memory clear error: {str(e)}", exc_info=True)130 return {"status": "error", "message": str(e)}131 132 133 134def run_job(job_id: str, site: str, limit: int, output_type: str):135 '''136 Background job to run the scraper137 Uses global JOBS dict to track status138 Outputs to JSON file or MongoDB based on output_type139 '''140 s = SCRAPERS[site]()141 s.embedder = vector_store_service._create_sentence_transformer_wrapper("sentence-transformers/all-MiniLM-L6-v2")142 s.embedding_fields = [(("title", "ingredients", "instructions"), "recipe_emb")]143 sink = None144 if output_type == "json":145 sink = JsonArraySink("./data/recipes_unified.json")146 elif output_type == "mongo":147 sink = MongoSink() if os.getenv("MONGODB_URI") else None148 149 stream_opts = StreamOptions(150 delay=0.3,151 limit=500,152 batch_size=limit,153 resume_file="recipes.resume",154 progress_callback=make_progress_cb(job_id),155 )156 try:157 JOBS[job_id] = {"status": "running", "count": 0}158 s.stream( sink=sink, options=stream_opts)159 JOBS[job_id]["status"] = "done"160 except Exception as e:161 JOBS[job_id] = {"status": "error", "error": str(e)}162 163def make_progress_cb(job_id: str):164 ''' Create a progress callback to update JOBS dict 165 '''166 def _cb(n: int):167 JOBS[job_id]["count"] = n168 return _cb169 170 171 172 173 174# super-lightweight in-memory job store (reset on restart)175JOBS: dict[str, any] = {}176 177@app.post("/scrape")178def scrape(body: ScrapeRequest, background: BackgroundTasks, x_api_key: str = Header(None)):179 if body.site not in SCRAPERS:180 raise HTTPException(status_code=400, detail="Unknown site")181 182 job_id = f"{body.site}-{os.urandom(4).hex()}"183 # use thread via BackgroundTasks to avoid blocking the request184 background.add_task(run_job, job_id, body.site, body.limit, body.output_type)185 return {"job_id": job_id, "status": "queued"}186 187@app.get("/jobs/{job_id}")188def job_status(job_id: str):189 return JOBS.get(job_id, {"status": "unknown"})190 191@app.get("/jobs")192def list_jobs():193 return JOBS194 