Spaces:
Paused
Paused
| """ | |
| Flare β Chat Handler (v1.7 Β· parameter parsing dΓΌzeltmesi) | |
| ========================================== | |
| """ | |
| import os | |
| 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 validation_engine import validate | |
| from session import session_store, Session | |
| from llm_interface import LLMInterface, SparkLLM, GPT4oLLM | |
| # βββββββββββββββββββββββββ CONFIG βββββββββββββββββββββββββ # | |
| # Lazy loading for config | |
| _cfg = None | |
| def get_config(): | |
| """Get or reload config""" | |
| global _cfg | |
| if _cfg is None: | |
| from config_provider import ConfigProvider | |
| _cfg = ConfigProvider.get() | |
| return _cfg | |
| def reload_config(): | |
| """Force reload config""" | |
| global _cfg | |
| from config_provider import ConfigProvider | |
| ConfigProvider._instance = None | |
| _cfg = ConfigProvider.get() | |
| return _cfg | |
| # Global LLM instance | |
| llm_provider: Optional[LLMInterface] = None | |
| # βββββββββββββββββββββββββ 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 | |
| # βββββββββββββββββββββββββ SPARK βββββββββββββββββββββββββ # | |
| def initialize_llm(force_reload=False): | |
| """Initialize LLM provider based on work_mode""" | |
| global llm_provider | |
| # Get fresh config if forced or first time | |
| if force_reload: | |
| cfg = reload_config() | |
| else: | |
| cfg = get_config() | |
| work_mode = cfg.global_config.work_mode | |
| if cfg.global_config.is_gpt_mode(): | |
| # GPT mode | |
| api_key = cfg.global_config.get_plain_token() | |
| if not api_key: | |
| raise ValueError("OpenAI API key not configured") | |
| model = cfg.global_config.get_gpt_model() | |
| llm_provider = GPT4oLLM(api_key, model) | |
| log(f"β Initialized {model} provider") | |
| else: | |
| # Spark mode | |
| spark_token = _get_spark_token() | |
| if not spark_token: | |
| raise ValueError("Spark token not configured") | |
| spark_endpoint = str(cfg.global_config.spark_endpoint) | |
| llm_provider = SparkLLM(spark_endpoint, spark_token) | |
| log("β Initialized Spark provider") | |
| # βββββββββββββββββββββββββ SPARK βββββββββββββββββββββββββ # | |
| def _get_spark_token() -> Optional[str]: | |
| """Get Spark token based on work_mode""" | |
| cfg = get_config() | |
| work_mode = cfg.global_config.work_mode | |
| if work_mode in ("hfcloud", "cloud"): | |
| # Cloud mode - use HuggingFace Secrets | |
| token = os.getenv("SPARK_TOKEN") | |
| if not token: | |
| log("β SPARK_TOKEN not found in HuggingFace Secrets!") | |
| return token | |
| else: | |
| # On-premise mode - use .env file | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| return os.getenv("SPARK_TOKEN") | |
| async def spark_generate(s: Session, prompt: str, user_msg: str) -> str: | |
| """Call LLM provider with proper error handling""" | |
| try: | |
| # Always reinitialize to get fresh config | |
| initialize_llm(force_reload=True) | |
| if not llm_provider: | |
| raise ValueError("Failed to initialize LLM provider") | |
| # Use the abstract interface | |
| raw = await llm_provider.generate(prompt, user_msg, s.chat_history) | |
| log(f"πͺ LLM raw response: {raw[:120]!r}") | |
| return raw | |
| except Exception as e: | |
| log(f"β LLM error: {e}") | |
| raise | |
| # βββββββββββββββββββββββββ FASTAPI βββββββββββββββββββββββββ # | |
| router = APIRouter() | |
| def health_check(): | |
| """Health check endpoint for monitoring""" | |
| return { | |
| "status": "ok", | |
| "sessions": len(session_store._sessions), | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| class StartRequest(BaseModel): | |
| project_name: str | |
| class ChatRequest(BaseModel): | |
| user_input: str | |
| class ChatResponse(BaseModel): | |
| session_id: str | |
| answer: str | |
| async def start_session(req: StartRequest): | |
| """Create new session""" | |
| try: | |
| cfg = get_config() | |
| # 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)) | |
| 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}") | |
| log(f"π Chat history length before adding: {len(session.chat_history)}") | |
| session.add_turn("user", user_input) | |
| # Debug - chat history'yi kontrol et | |
| log(f"π Chat history after adding: {session.chat_history}") | |
| cfg = get_config() | |
| # 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, | |
| session.project_name | |
| ) | |
| # Get LLM response | |
| raw = await spark_generate(session, prompt, user_input) | |
| # Empty response fallback | |
| if not raw: | |
| log("β οΈ Empty response from LLM") | |
| 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) | |
| # β Session'daki intent'lerle validate et | |
| valid_intents = {intent.name for intent in version.intents} | |
| if intent_name not in valid_intents: | |
| log(f"β οΈ Invalid intent: {intent_name} (valid: {valid_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": | |
| 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 | |
| # Date tipi iΓ§in ΓΆzel kontrol | |
| if param_config.type == "date": | |
| try: | |
| # ISO format kontrolΓΌ | |
| from datetime import datetime | |
| datetime.strptime(str(param_value), "%Y-%m-%d") | |
| except ValueError: | |
| log(f"β Invalid date format for {param_name}: {param_value}") | |
| 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]}") | |
| # Fallback: Try to extract simple values from user input | |
| # This is especially useful for single parameter responses | |
| if session.state == "await_param" and len(session.awaiting_parameters) > 0: | |
| # Get the first missing parameter | |
| first_missing = session.awaiting_parameters[0] | |
| param_config = next( | |
| (p for p in intent_config.parameters if p.name == first_missing), | |
| None | |
| ) | |
| if param_config and session.chat_history: | |
| # Get the last user input | |
| last_user_input = session.chat_history[-1].get("content", "").strip() | |
| # For simple inputs like city names, try direct assignment | |
| if param_config.type in ["str", "string"] and len(last_user_input.split()) <= 3: | |
| if validate(last_user_input, param_config): | |
| session.variables[param_config.variable_name] = last_user_input | |
| log(f"β Fallback extraction: {first_missing}={last_user_input}") | |
| return True | |
| 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 | |
| cfg = get_config() | |
| 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)) | |
| # Trim response to remove any trailing "assistant" artifacts | |
| trimmed_response = _trim_response(human_response) | |
| session.reset_flow() | |
| return trimmed_response if trimmed_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." |