| import os |
| import asyncio |
| from typing import List |
|
|
| from fastapi import FastAPI, File, HTTPException, UploadFile, Form |
|
|
| import sys |
| from pathlib import Path |
|
|
| |
| 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: |
|
|
| |
| 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) |
| |
| |
| 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: |
|
|
| |
| |
| |
|
|
| 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_text = extract_text_from_images( |
| image_paths |
| ) |
|
|
| logger.info("OCR extraction completed") |
|
|
| |
| |
| |
|
|
| 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") |
|
|
| |
| |
| |
|
|
| |
| |
| |
| 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." |
| ) |
| ) |
|
|
| |
| |
| |
|
|
| 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") |
|
|
| |
| |
| |
|
|
| analyst = safe_json_parse( |
| analyst_raw |
| ) |
|
|
| psychology = safe_json_parse( |
| psychology_raw |
| ) |
|
|
| logger.info("JSON parsing completed") |
|
|
| |
| |
| |
|
|
| 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") |
|
|
| |
| |
| |
|
|
| normalized = normalize_analysis({ |
| "analyst": analyst, |
| "psychology": psychology, |
| "strategy": strategy, |
| }) |
|
|
| logger.info( |
| "Normalization completed" |
| ) |
|
|
| |
| |
| |
|
|
| 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: |
|
|
| |
| |
| |
|
|
| for path in image_paths: |
|
|
| if os.path.exists(path): |
| os.remove(path) |
|
|
| logger.info("Temporary files cleaned") |