Spaces:
Running
Running
""" | |
Flare β Chat Handler (v1.3 Β· robust trimming + param validation) | |
================================================================= | |
""" | |
import re, json, uuid, sys, httpx, commentjson | |
from datetime import datetime | |
from typing import Dict, List, Optional | |
from fastapi import APIRouter, HTTPException, Header | |
from pydantic import BaseModel | |
from commentjson import JSONLibraryException | |
from prompt_builder import build_intent_prompt, build_parameter_prompt, log | |
# βββββββββββββββββββββββββ 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) -> (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 = commentjson.load(open("service_config.jsonc", encoding="utf-8")) | |
PROJECTS = {p["name"]: p for p in CFG["projects"]} | |
APIS = {a["name"]: a for a in CFG["apis"]} | |
SPARK_URL = CFG["config"]["spark_endpoint"].rstrip("/") + "/generate" | |
ALLOWED_INTENTS = {"flight-booking", "flight-info", "booking-cancel"} | |
# βββββββββββββββββββββββββ SESSION βββββββββββββββββββββββββ # | |
class Session: | |
def __init__(self, project: str): | |
self.id = str(uuid.uuid4()) | |
self.project = PROJECTS[project] | |
self.history: List[Dict[str, str]] = [] | |
self.vars: Dict[str, str] = {} | |
self.awaiting: Optional[Dict] = None | |
log(f"π Session {self.id}") | |
SESSIONS: Dict[str, Session] = {} | |
# βββββββββββββββββββββββββ SPARK βββββββββββββββββββββββββ # | |
async def spark_generate(s: Session, prompt: str, user_msg: str) -> str: | |
payload = { | |
"project_name": s.project["name"], | |
"user_input": user_msg, | |
"context": s.history[-10:], | |
"system_prompt": prompt | |
} | |
async with httpx.AsyncClient(timeout=60) as c: | |
r = await c.post(SPARK_URL + "/generate", json=payload) | |
r.raise_for_status() | |
d = r.json() | |
raw = (d.get("assistant") or d.get("model_answer") or d.get("text", "")).strip() | |
log(f"πͺ Spark raw: {raw[:120]!r}") | |
return raw | |
# βββββββββββββββββββββββββ FASTAPI βββββββββββββββββββββββββ # | |
router = APIRouter() | |
# health | |
def health(): return {"ok": True} | |
class Start(BaseModel): project_name: str | |
class Body(BaseModel): user_input: str | |
class Resp(BaseModel): session_id: str; answer: str | |
async def start_session(req: Start): | |
if req.project_name not in PROJECTS: | |
raise HTTPException(404, "project") | |
s = Session(req.project_name) | |
SESSIONS[s.id] = s | |
greet = "HoΕ geldiniz! Size nasΔ±l yardΔ±mcΔ± olabilirim?" | |
return Resp(session_id=s.id, answer=greet) | |
async def chat(body: Body, x_session_id: str = Header(...)): | |
if x_session_id not in SESSIONS: | |
raise HTTPException(404, "session") | |
s = SESSIONS[x_session_id] | |
user = body.user_input.strip() | |
s.history.append({"role": "user", "content": user}) | |
# ---------- follow-up ---------- | |
if s.awaiting: | |
ans = await _followup(s, user) | |
s.history.append({"role": "assistant", "content": ans}) | |
return Resp(session_id=s.id, answer=ans) | |
# ---------- intent detect ---------- | |
p = build_intent_prompt( | |
s.project["versions"][0]["general_prompt"], | |
s.history, user, s.project["versions"][0]["intents"]) | |
raw = await spark_generate(s, p, user) | |
if raw == "": | |
fallback = "ΓzgΓΌnΓΌm, mesajΔ±nΔ±zΔ± anlayamadΔ±m. LΓΌtfen tekrar dener misiniz?" | |
s.history.append({"role": "assistant", "content": fallback}) | |
return Resp(session_id=s.id, answer=fallback) | |
# small-talk yolu | |
if not raw.startswith("#DETECTED_INTENT"): | |
clean = _trim_response(raw) | |
s.history.append({"role": "assistant", "content": clean}) | |
return Resp(session_id=s.id, answer=clean) | |
intent, tail = _safe_intent_parse(raw) | |
if intent not in ALLOWED_INTENTS or len(user.split()) < 3: | |
clean = _trim_response(tail) | |
s.history.append({"role": "assistant", "content": clean}) | |
return Resp(session_id=s.id, answer=clean) | |
cfg = _find_intent(s, intent) | |
if not cfg: | |
err = "ΓzgΓΌnΓΌm, anlayamadΔ±m." | |
s.history.append({"role": "assistant", "content": err}) | |
return Resp(session_id=s.id, answer=err) | |
answer = await _handle_intent(s, cfg, user) | |
s.history.append({"role": "assistant", "content": answer}) | |
return Resp(session_id=s.id, answer=answer) | |
# ββββββββββββββββββ INTENT / PARAM / API HELPERS ββββββββββββββββββ # | |
def _find_intent(s, name): return next((i for i in s.project["versions"][0]["intents"] if i["name"] == name), None) | |
def _missing(s, cfg): return [p["name"] for p in cfg["parameters"] if p["variable_name"] not in s.vars] | |
async def _handle_intent(s, cfg, user): | |
missing = _missing(s, cfg) | |
if missing: | |
prmpt = build_parameter_prompt(cfg, missing, user, s.history) | |
raw = await spark_generate(s, prmpt, user) | |
if raw.startswith("#PARAMETERS:") and not _proc_params(s, cfg, raw): | |
missing = _missing(s, cfg) | |
if missing: | |
s.awaiting = {"intent": cfg, "missing": missing} | |
cap = next(p for p in cfg["parameters"] if p["name"] == missing[0])["caption"] | |
return f"{cap} nedir?" | |
s.awaiting = None | |
return await _call_api(s, cfg) | |
async def _followup(s, user): | |
cfg = s.awaiting["intent"]; miss = s.awaiting["missing"] | |
prmpt = build_parameter_prompt(cfg, miss, user, s.history) | |
raw = await spark_generate(s, prmpt, user) | |
if not raw.startswith("#PARAMETERS:") or _proc_params(s, cfg, raw): | |
return "ΓzgΓΌnΓΌm, anlayamadΔ±m." | |
miss = _missing(s, cfg) | |
if miss: | |
s.awaiting["missing"] = miss | |
cap = next(p for p in cfg["parameters"] if p["name"] == miss[0])["caption"] | |
return f"{cap} nedir?" | |
s.awaiting = None | |
return await _call_api(s, cfg) | |
def _proc_params(s, cfg, raw): | |
try: d = json.loads(raw[len("#PARAMETERS:"):]) | |
except: return True | |
for pr in d.get("extracted", []): | |
pcfg = next(p for p in cfg["parameters"] if p["name"] == pr["name"]) | |
if not re.fullmatch(pcfg.get("validation_regex", ".*"), pr["value"]): | |
return True | |
s.vars[pcfg["variable_name"]] = pr["value"] | |
return Fals | |