Spaces:
Building
Building
""" | |
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() | |
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 | |
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)) | |
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]}") | |
# 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 | |
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." |