""" Flare – Chat Handler (v1.5 · modüler yapı) ========================================== """ import re, json, sys, httpx from datetime import datetime from typing import Dict, List, Optional from fastapi import APIRouter, HTTPException, Header from pydantic import BaseModel from prompt_builder import build_intent_prompt, build_parameter_prompt, log from api_executor import call_api as execute_api from config_provider import ConfigProvider from validation_engine import validate from session import session_store, Session # ───────────────────────── HELPERS ───────────────────────── # def _trim_response(raw: str) -> str: """ Remove everything after the first logical assistant block or intent tag. Also strips trailing 'assistant' artifacts and prompt injections. """ # Stop at our own rules if model leaked them for stop in ["#DETECTED_INTENT", "⚠️", "\nassistant", "assistant\n", "assistant"]: idx = raw.find(stop) if idx != -1: raw = raw[:idx] # Normalise selamlama raw = re.sub(r"Hoş[\s-]?geldin(iz)?", "Hoş geldiniz", raw, flags=re.IGNORECASE) return raw.strip() def _safe_intent_parse(raw: str) -> tuple[str, str]: """Extract intent name and extra tail.""" m = re.search(r"#DETECTED_INTENT:\s*([A-Za-z0-9_-]+)", raw) if not m: return "", raw name = m.group(1) tail = raw[m.end():] return name, tail # ───────────────────────── CONFIG ───────────────────────── # cfg = ConfigProvider.get() SPARK_URL = str(cfg.global_config.spark_endpoint).rstrip("/") ALLOWED_INTENTS = {"flight-booking", "flight-info", "booking-cancel"} # ───────────────────────── SPARK ───────────────────────── # async def spark_generate(s: Session, prompt: str, user_msg: str) -> str: """Call Spark with proper error handling""" try: project = next((p for p in cfg.projects if p.name == s.project_name), None) if not project: raise ValueError(f"Project not found: {s.project_name}") version = next((v for v in project.versions if v.published), None) if not version: raise ValueError("No published version found") payload = { "project_name": s.project_name, "user_input": user_msg, "context": s.chat_history[-10:], "system_prompt": prompt } log(f"🚀 Calling Spark for session {s.session_id[:8]}...") async with httpx.AsyncClient(timeout=60) as client: response = await client.post(SPARK_URL + "/generate", json=payload) response.raise_for_status() data = response.json() raw = (data.get("assistant") or data.get("model_answer") or data.get("text", "")).strip() log(f"🪄 Spark raw: {raw[:120]!r}") return raw except httpx.TimeoutException: log(f"⏱️ Spark timeout for session {s.session_id[:8]}") raise except Exception as e: log(f"❌ Spark error: {e}") raise # ───────────────────────── FASTAPI ───────────────────────── # router = APIRouter() @router.get("/") def health(): return {"status": "ok", "sessions": len(session_store._sessions)} class StartRequest(BaseModel): project_name: str class ChatRequest(BaseModel): user_input: str class ChatResponse(BaseModel): session_id: str answer: str @router.post("/start_session", response_model=ChatResponse) async def start_session(req: StartRequest): """Create new session""" try: # Validate project exists project = next((p for p in cfg.projects if p.name == req.project_name and p.enabled), None) if not project: raise HTTPException(404, f"Project '{req.project_name}' not found or disabled") # Create session session = session_store.create_session(req.project_name) greeting = "Hoş geldiniz! Size nasıl yardımcı olabilirim?" session.add_turn("assistant", greeting) return ChatResponse(session_id=session.session_id, answer=greeting) except Exception as e: log(f"❌ Error creating session: {e}") raise HTTPException(500, str(e)) @router.post("/chat", response_model=ChatResponse) async def chat(body: ChatRequest, x_session_id: str = Header(...)): """Process chat message""" try: # Get session session = session_store.get_session(x_session_id) if not session: raise HTTPException(404, "Session not found") user_input = body.user_input.strip() if not user_input: raise HTTPException(400, "Empty message") session.add_turn("user", user_input) # Get project config project = next((p for p in cfg.projects if p.name == session.project_name), None) if not project: raise HTTPException(500, "Project configuration lost") version = next((v for v in project.versions if v.published), None) if not version: raise HTTPException(500, "No published version") # Handle based on state if session.state == "await_param": answer = await _handle_parameter_followup(session, user_input, version) else: answer = await _handle_new_message(session, user_input, version) session.add_turn("assistant", answer) return ChatResponse(session_id=session.session_id, answer=answer) except HTTPException: raise except Exception as e: log(f"❌ Chat error: {e}") session.reset_flow() error_msg = "Bir hata oluştu. Lütfen tekrar deneyin." session.add_turn("assistant", error_msg) return ChatResponse(session_id=x_session_id, answer=error_msg) # ───────────────────────── MESSAGE HANDLERS ───────────────────────── # async def _handle_new_message(session: Session, user_input: str, version) -> str: """Handle new message (not parameter followup)""" # Build intent detection prompt prompt = build_intent_prompt( version.general_prompt, session.chat_history, user_input, version.intents ) # Get Spark response raw = await spark_generate(session, prompt, user_input) # Empty response fallback if not raw: return "Üzgünüm, mesajınızı anlayamadım. Lütfen tekrar dener misiniz?" # Check for intent if not raw.startswith("#DETECTED_INTENT"): # Small talk response return _trim_response(raw) # Parse intent intent_name, tail = _safe_intent_parse(raw) # Validate intent if intent_name not in ALLOWED_INTENTS: return _trim_response(tail) if tail else "Size nasıl yardımcı olabilirim?" # Short message guard (less than 3 words usually means incomplete request) if len(user_input.split()) < 3 and intent_name != "flight-info": return _trim_response(tail) if tail else "Lütfen talebinizi biraz daha detaylandırır mısınız?" # Find intent config intent_config = next((i for i in version.intents if i.name == intent_name), None) if not intent_config: return "Üzgünüm, bu işlemi gerçekleştiremiyorum." # Set intent in session session.last_intent = intent_name # Extract parameters return await _extract_parameters(session, intent_config, user_input) async def _handle_parameter_followup(session: Session, user_input: str, version) -> str: """Handle parameter collection followup""" if not session.last_intent: session.reset_flow() return "Üzgünüm, hangi işlem için bilgi istediğimi unuttum. Baştan başlayalım." # Get intent config intent_config = next((i for i in version.intents if i.name == session.last_intent), None) if not intent_config: session.reset_flow() return "Bir hata oluştu. Lütfen tekrar deneyin." # Try to extract missing parameters missing = session.awaiting_parameters prompt = build_parameter_prompt(intent_config, missing, user_input, session.chat_history) raw = await spark_generate(session, prompt, user_input) if not raw.startswith("#PARAMETERS:"): # Increment miss count session.missing_ask_count += 1 if session.missing_ask_count >= 3: session.reset_flow() return "Üzgünüm, istediğiniz bilgileri anlayamadım. Başka bir konuda yardımcı olabilir miyim?" return "Üzgünüm, anlayamadım. Lütfen tekrar söyler misiniz?" # Process parameters success = _process_parameters(session, intent_config, raw) if not success: return "Girdiğiniz bilgilerde bir hata var. Lütfen kontrol edip tekrar deneyin." # Check if we have all required parameters missing = _get_missing_parameters(session, intent_config) if missing: session.awaiting_parameters = missing param = next(p for p in intent_config.parameters if p.name == missing[0]) return f"{param.caption} bilgisini alabilir miyim?" # All parameters collected, call API session.state = "call_api" return await _execute_api_call(session, intent_config) # ───────────────────────── PARAMETER HANDLING ───────────────────────── # async def _extract_parameters(session: Session, intent_config, user_input: str) -> str: """Extract parameters from user input""" missing = _get_missing_parameters(session, intent_config) if not missing: # All parameters already available return await _execute_api_call(session, intent_config) # Build parameter extraction prompt prompt = build_parameter_prompt(intent_config, missing, user_input, session.chat_history) raw = await spark_generate(session, prompt, user_input) if raw.startswith("#PARAMETERS:"): success = _process_parameters(session, intent_config, raw) if success: missing = _get_missing_parameters(session, intent_config) if missing: # Still missing parameters session.state = "await_param" session.awaiting_parameters = missing session.missing_ask_count = 0 param = next(p for p in intent_config.parameters if p.name == missing[0]) return f"{param.caption} bilgisini alabilir miyim?" # All parameters collected return await _execute_api_call(session, intent_config) def _get_missing_parameters(session: Session, intent_config) -> List[str]: """Get list of missing required parameters""" return [ p.name for p in intent_config.parameters if p.required and p.variable_name not in session.variables ] def _process_parameters(session: Session, intent_config, raw: str) -> bool: """Process parameter extraction response""" try: json_str = raw[len("#PARAMETERS:"):] data = json.loads(json_str) extracted = data.get("extracted", []) any_valid = False for param_data in extracted: param_name = param_data.get("name") param_value = param_data.get("value") if not param_name or not param_value: continue # Find parameter config param_config = next( (p for p in intent_config.parameters if p.name == param_name), None ) if not param_config: continue # Validate parameter if validate(str(param_value), param_config): session.variables[param_config.variable_name] = str(param_value) any_valid = True log(f"✅ Extracted {param_name}={param_value}") else: log(f"❌ Invalid {param_name}={param_value}") return any_valid except Exception as e: log(f"❌ Parameter processing error: {e}") return False # ───────────────────────── API EXECUTION ───────────────────────── # async def _execute_api_call(session: Session, intent_config) -> str: """Execute API call and return humanized response""" try: session.state = "call_api" api_name = intent_config.action api_config = cfg.get_api(api_name) if not api_config: session.reset_flow() return intent_config.get("fallback_error_prompt", "İşlem başarısız oldu.") log(f"📡 Calling API: {api_name}") # Execute API call response = execute_api(api_config, session.variables) api_json = response.json() # Humanize response session.state = "humanize" if api_config.response_prompt: prompt = api_config.response_prompt.replace( "{{api_response}}", json.dumps(api_json, ensure_ascii=False) ) human_response = await spark_generate(session, prompt, json.dumps(api_json)) session.reset_flow() return human_response if human_response else f"İşlem sonucu: {api_json}" else: session.reset_flow() return f"İşlem tamamlandı: {api_json}" except httpx.TimeoutException: session.reset_flow() return intent_config.get("fallback_timeout_prompt", "İşlem zaman aşımına uğradı.") except Exception as e: log(f"❌ API call error: {e}") session.reset_flow() return intent_config.get("fallback_error_prompt", "İşlem sırasında bir hata oluştu.")