import os import asyncio from typing import List from fastapi import FastAPI, File, HTTPException, UploadFile, Form import sys from pathlib import Path # Fix ModuleNotFoundError if user runs from the backend directory root_dir = Path(__file__).resolve().parent.parent if str(root_dir) not in sys.path: sys.path.insert(0, str(root_dir)) from backend.core.logger import get_logger logger = get_logger(__name__) from backend.agents.gaurdrail import validate_domain from backend.agents.analyst import analyze_conversation from backend.agents.psychology import analyze_psychology from backend.agents.strategy import generate_strategy from backend.agents.perspective import detect_perspective from backend.utils import ( safe_input, safe_json_parse, safe_merge ) from backend.services.ocr_service import ( extract_text_from_images ) from backend.normalize import normalize_analysis app = FastAPI() MAX_FILES = 5 @app.get("/") async def root(): return {"status": "running"} @app.post("/detect_perspective") async def detect_perspective_route( chat: str = Form(""), feelings: str = Form(""), files: List[UploadFile] = File(default=[]), ): logger.info(f"Detect perspective called. Files: {len(files)}, Chat length: {len(chat)}, Feelings length: {len(feelings)}") try: # Save temp files and extract OCR image_paths = [] for file in files: file_bytes = await file.read() if not file_bytes: continue path = f"temp_persp_{file.filename}" with open(path, "wb") as f: f.write(file_bytes) image_paths.append(path) extracted_text = extract_text_from_images(image_paths) # Cleanup for path in image_paths: if os.path.exists(path): os.remove(path) final_chat = f"{chat}\n\n[Extracted Text from Images]:\n{extracted_text}" if extracted_text.strip() else chat result_json = detect_perspective(final_chat, feelings) result_data = safe_json_parse(result_json) return result_data except Exception as e: logger.error(f"Error in detect_perspective: {e}") return {"needs_clarification": True, "confidence": 0.0} @app.post("/analyze") async def analyze( chat: str = Form(""), feelings: str = Form(""), user_perspective: str = Form(""), emotional_goal: str = Form(""), conversation_consistency: str = Form(""), files: List[UploadFile] = File(default=[]), ): image_paths = [] logger.info("Analyze endpoint called") print("CHAT RECEIVED:", repr(chat)) print("FEELINGS RECEIVED:", repr(feelings)) if len(files) > MAX_FILES: logger.warning("Too many files uploaded") raise HTTPException( status_code=400, detail=f"Upload up to {MAX_FILES} images only." ) try: # ----------------------------------- # SAVE FILES # ----------------------------------- for file in files: path = f"temp_{file.filename}" with open(path, "wb") as f: f.write(await file.read()) image_paths.append(path) logger.info(f"Saved {len(image_paths)} image(s)") # ----------------------------------- # OCR # ----------------------------------- ocr_text = extract_text_from_images( image_paths ) logger.info("OCR extraction completed") # ----------------------------------- # FULL CHAT # ----------------------------------- full_chat = f"{chat}\n{ocr_text}" full_chat = safe_input( full_chat, "No conversation provided." ) feelings = safe_input( feelings, "No user feelings provided." ) logger.info("Inputs normalized") # ----------------------------------- # GUARDRAIL # ----------------------------------- # ----------------------------------- # ASSEMBLE CONTEXT WITH CLARIFICATION # ----------------------------------- clarification_context = "" if user_perspective or emotional_goal: clarification_context = f"\n\n[USER CLARIFICATION]\nUser Perspective: {user_perspective}\nEmotional Goal: {emotional_goal}\nConversation Consistency: {conversation_consistency}" guardrail_input = f"Chat:\n{full_chat}\n\nFeelings:\n{feelings}{clarification_context}" logger.info("Running guardrail") guardrail_result = validate_domain( guardrail_input ) guardrail_result = ( guardrail_result .strip() .upper() ) logger.info( f"Guardrail result: {guardrail_result}" ) if guardrail_result == "OUT_OF_SCOPE": logger.warning( "Guardrail blocked request" ) raise HTTPException( status_code=400, detail=( "Velra is focused on emotional communication, " "relationships, dating dynamics, and psychological connection analysis." ) ) # ----------------------------------- # PARALLEL AGENTS # ----------------------------------- logger.info( "Running analyst + psychology agents" ) analyst_task = asyncio.to_thread( analyze_conversation, full_chat + clarification_context, feelings + clarification_context ) psychology_task = asyncio.to_thread( analyze_psychology, full_chat + clarification_context, feelings + clarification_context ) analyst_raw, psychology_raw = ( await asyncio.gather( analyst_task, psychology_task ) ) logger.info("Agents completed") # ----------------------------------- # JSON PARSE # ----------------------------------- analyst = safe_json_parse( analyst_raw ) psychology = safe_json_parse( psychology_raw ) logger.info("JSON parsing completed") # ----------------------------------- # STRATEGY CONTEXT # ----------------------------------- context = f""" === RAW CHAT === {chat} === USER FEELINGS & INTENT === {feelings} === BEHAVIORAL ANALYSIS === {safe_merge(analyst)} === PSYCHOLOGICAL ANALYSIS === {safe_merge(psychology)} """ logger.info("Running strategy agent") strategy_raw = generate_strategy( context ) strategy = safe_json_parse( strategy_raw ) logger.info("Strategy completed") # ----------------------------------- # NORMALIZATION # ----------------------------------- normalized = normalize_analysis({ "analyst": analyst, "psychology": psychology, "strategy": strategy, }) logger.info( "Normalization completed" ) # ----------------------------------- # RESPONSE # ----------------------------------- return { "analysis": { "analyst": analyst, "psychology": psychology, "strategy": strategy, "normalized": normalized }, "result": normalized, } except HTTPException as http_exc: logger.error( f"HTTPException: {http_exc.detail}" ) raise http_exc except Exception as exc: logger.exception( f"Unhandled backend error: {str(exc)}" ) raise HTTPException( status_code=500, detail=str(exc) ) from exc finally: # ----------------------------------- # CLEANUP # ----------------------------------- for path in image_paths: if os.path.exists(path): os.remove(path) logger.info("Temporary files cleaned")