Spaces:
Running
Running
File size: 7,616 Bytes
a252004 e72db51 dd6bafd 447c15c a63397b 14352db 01da95c 447c15c a63397b 14352db e72db51 3f2fcc0 7279c20 e72db51 14352db 01da95c cf59f92 14352db e72db51 14352db e72db51 14352db e72db51 a63397b e72db51 c4a5a79 e72db51 14352db e72db51 01da95c e72db51 01da95c 14352db e72db51 14352db 7279c20 e72db51 14352db e72db51 0040505 e72db51 14352db e72db51 14352db e72db51 14352db e72db51 14352db e72db51 14352db e72db51 01da95c e72db51 01da95c e72db51 14352db e72db51 14352db e72db51 14352db e72db51 cf59f92 e72db51 3f2fcc0 0a42fdb e72db51 0a42fdb e72db51 eed09fe e72db51 df458ea e72db51 df458ea e72db51 df458ea e72db51 c4a5a79 14352db e72db51 14352db e72db51 14352db e72db51 14352db e72db51 3f2fcc0 e72db51 14352db e72db51 01da95c 14352db e72db51 c4a5a79 e72db51 c4a5a79 e72db51 c4a5a79 e72db51 c4a5a79 e72db51 df458ea e72db51 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
"""
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()
@router.get("/") # 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
@router.post("/start_session", response_model=Resp)
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)
@router.post("/chat", response_model=Resp)
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
|