""" Flare – Chat Handler (v1.7 · parameter parsing düzeltmesi) ========================================== """ 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 import requests from prompt_builder import build_intent_prompt, build_parameter_prompt from utils import 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) # Remove 'assistant' suffix if exists if name.endswith("assistant"): name = name[:-9] # Remove last 9 chars ("assistant") log(f"🔧 Removed 'assistant' suffix from intent name") tail = raw[m.end():] log(f"🎯 Parsed intent: {name}") 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]}...") log(f"📋 Prompt preview (first 200 chars): {prompt[:200]}...") 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") log(f"💬 User input: {user_input}") log(f"📊 Session state: {session.state}, last_intent: {session.last_intent}") 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": log(f"🔄 Handling parameter followup for missing: {session.awaiting_parameters}") answer = await _handle_parameter_followup(session, user_input, version) else: log("🆕 Handling new message") 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: log("⚠️ Empty response from Spark") 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 log("💬 No intent detected, returning small talk") return _trim_response(raw) # Parse intent intent_name, tail = _safe_intent_parse(raw) # Validate intent if intent_name not in ALLOWED_INTENTS: log(f"⚠️ Invalid intent: {intent_name}") 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": log(f"⚠️ Message too short ({len(user_input.split())} words) for intent {intent_name}") 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: log(f"❌ Intent config not found for: {intent_name}") return "Üzgünüm, bu işlemi gerçekleştiremiyorum." # Set intent in session session.last_intent = intent_name log(f"✅ Intent set: {intent_name}") # Log intent parameters log(f"📋 Intent parameters: {[p.name for p in intent_config.parameters]}") # 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: log("⚠️ No last intent in session") 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: log(f"❌ Intent config not found for: {session.last_intent}") session.reset_flow() return "Bir hata oluştu. Lütfen tekrar deneyin." # Try to extract missing parameters missing = session.awaiting_parameters log(f"🔍 Trying to extract missing params: {missing}") prompt = build_parameter_prompt(intent_config, missing, user_input, session.chat_history) raw = await spark_generate(session, prompt, user_input) # Try parsing with or without #PARAMETERS: prefix success = _process_parameters(session, intent_config, raw) if not success: # Increment miss count session.missing_ask_count += 1 log(f"⚠️ No parameters extracted, miss count: {session.missing_ask_count}") 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?" # Check if we have all required parameters missing = _get_missing_parameters(session, intent_config) log(f"📊 Still missing params: {missing}") 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 log("✅ All parameters collected, calling 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) log(f"🔍 Missing parameters: {missing}") if not missing: # All parameters already available log("✅ 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) # Try processing with flexible parsing success = _process_parameters(session, intent_config, raw) if success: missing = _get_missing_parameters(session, intent_config) log(f"📊 After extraction, missing: {missing}") else: log("⚠️ Failed to extract parameters from response") 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]) log(f"❓ Asking for parameter: {param.name} ({param.caption})") return f"{param.caption} bilgisini alabilir miyim?" # All parameters collected log("✅ All parameters collected after extraction") return await _execute_api_call(session, intent_config) def _get_missing_parameters(session: Session, intent_config) -> List[str]: """Get list of missing required parameters""" missing = [ p.name for p in intent_config.parameters if p.required and p.variable_name not in session.variables ] log(f"📊 Session variables: {list(session.variables.keys())}") return missing def _process_parameters(session: Session, intent_config, raw: str) -> bool: """Process parameter extraction response with flexible parsing""" try: # Try to parse JSON, handling both with and without #PARAMETERS: prefix json_str = raw if raw.startswith("#PARAMETERS:"): json_str = raw[len("#PARAMETERS:"):] log(f"🔍 Found #PARAMETERS: prefix, removing it") # Clean up any trailing content after JSON # Find the closing brace for the JSON object brace_count = 0 json_end = -1 in_string = False escape_next = False for i, char in enumerate(json_str): if escape_next: escape_next = False continue if char == '\\': escape_next = True continue if char == '"' and not escape_next: in_string = not in_string continue if not in_string: if char == '{': brace_count += 1 elif char == '}': brace_count -= 1 if brace_count == 0: json_end = i + 1 break if json_end > 0: json_str = json_str[:json_end] log(f"🔍 Cleaned JSON string: {json_str[:200]}") data = json.loads(json_str) extracted = data.get("extracted", []) log(f"📦 Extracted data: {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: log(f"⚠️ Invalid param data: {param_data}") continue # Find parameter config param_config = next( (p for p in intent_config.parameters if p.name == param_name), None ) if not param_config: log(f"⚠️ Parameter config not found for: {param_name}") 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} → {param_config.variable_name}") else: log(f"❌ Invalid {param_name}={param_value}") return any_valid except json.JSONDecodeError as e: log(f"❌ JSON parsing error: {e}") log(f"❌ Failed to parse: {raw[:200]}") return False 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: log(f"❌ API config not found: {api_name}") session.reset_flow() return intent_config.fallback_error_prompt or "İşlem başarısız oldu." log(f"📡 Calling API: {api_name}") log(f"📦 API variables: {session.variables}") # Execute API call with session response = execute_api(api_config, session) api_json = response.json() log(f"✅ API response: {api_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 requests.exceptions.Timeout: log(f"⏱️ API timeout: {api_name}") session.reset_flow() return intent_config.fallback_timeout_prompt or "İşlem zaman aşımına uğradı." except Exception as e: log(f"❌ API call error: {e}") session.reset_flow() return intent_config.fallback_error_prompt or "İşlem sırasında bir hata oluştu."