flare / chat_handler.py
ciyidogan's picture
Update chat_handler.py
cf59f92 verified
raw
history blame
8.76 kB
"""
Flare – Chat Handler (Spark /generate + safe-intent)
====================================================
• X-Session-ID header
• Spark payload: {project_name, user_input, context, system_prompt}
• Eğer model #DETECTED_INTENT:<label> döndürür ve <label> izinli liste
dışında ise, mesaj small-talk olarak kabul edilir.
"""
import re, json, uuid, httpx, commentjson
from datetime import datetime
from typing import Dict, List, Optional
from fastapi import APIRouter, HTTPException, Header
from pydantic import BaseModel
from prompt_builder import build_intent_prompt, build_parameter_prompt, log
# --------------------------------------------------------------------------- #
# 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_name: str):
self.id = str(uuid.uuid4())
self.project = PROJECTS[project_name]
self.history: List[Dict[str, str]] = [] # {"role","content"}
self.variables: Dict[str, str] = {}
self.awaiting: Optional[Dict] = None
log(f"🆕 Session {self.id} for {project_name}")
SESSIONS: Dict[str, Session] = {}
# --------------------------------------------------------------------------- #
# SPARK CLIENT
# --------------------------------------------------------------------------- #
async def spark_generate(session: Session,
system_prompt: str,
user_input: str) -> str:
payload = {
"project_name": session.project["name"],
"user_input": user_input,
"context": session.history[-10:], # last 10 turns
"system_prompt": system_prompt
}
async with httpx.AsyncClient(timeout=60) as c:
r = await c.post(SPARK_URL, json=payload)
r.raise_for_status()
data = r.json()
return (data.get("assistant") or
data.get("model_answer") or
data.get("text", "")).strip()
# --------------------------------------------------------------------------- #
# FASTAPI ROUTER
# --------------------------------------------------------------------------- #
router = APIRouter()
@router.get("/")
def health():
return {"status": "ok"}
class StartSessionRequest(BaseModel):
project_name: str
class ChatBody(BaseModel):
user_input: str
class ChatResponse(BaseModel):
session_id: str
answer: str
# --------------------------------------------------------------------------- #
# ENDPOINTS
# --------------------------------------------------------------------------- #
@router.post("/start_session", response_model=ChatResponse)
async def start_session(req: StartSessionRequest):
if req.project_name not in PROJECTS:
raise HTTPException(404, "Unknown project")
s = Session(req.project_name)
SESSIONS[s.id] = s
return ChatResponse(session_id=s.id, answer="Nasıl yardımcı olabilirim?")
@router.post("/chat", response_model=ChatResponse)
async def chat(body: ChatBody,
x_session_id: str = Header(...)):
if x_session_id not in SESSIONS:
raise HTTPException(404, "Invalid session")
s = SESSIONS[x_session_id]
user_msg = body.user_input.strip()
s.history.append({"role": "user", "content": user_msg})
# ---------------- Follow-up? ----------------
if s.awaiting:
answer = await _followup(s, user_msg)
s.history.append({"role": "assistant", "content": answer})
return ChatResponse(session_id=s.id, answer=answer)
# ---------------- Intent detection ----------------
gen_prompt = s.project["versions"][0]["general_prompt"]
intent_raw = await spark_generate(s, gen_prompt, user_msg)
# --- Etiket yoksa → small talk cevabı olduğu gibi döndür
if not intent_raw.startswith("#DETECTED_INTENT:"):
s.history.append({"role": "assistant", "content": intent_raw})
return ChatResponse(session_id=s.id, answer=intent_raw)
# --- Etiket var → ayrıştır
intent_name = intent_raw.split(":", 1)[1].strip()
if intent_name not in ALLOWED_INTENTS:
# Etiket geçersiz → small talk olarak ilk kısmı döndür
clean = intent_raw.split("#DETECTED_INTENT")[0].split("\nassistant")[0].strip()
s.history.append({"role": "assistant", "content": clean})
return ChatResponse(session_id=s.id, answer=clean)
intent_cfg = _find_intent(s.project, intent_name)
if not intent_cfg:
err = "Üzgünüm, anlayamadım."
s.history.append({"role": "assistant", "content": err})
return ChatResponse(session_id=s.id, answer=err)
answer = await _handle_intent(s, intent_cfg, user_msg)
s.history.append({"role": "assistant", "content": answer})
return ChatResponse(session_id=s.id, answer=answer)
# --------------------------------------------------------------------------- #
# HELPER FUNCS
# --------------------------------------------------------------------------- #
def _find_intent(project, name_):
return next((i for i in project["versions"][0]["intents"]
if i["name"] == name_), None)
def _missing(s, intent_cfg):
return [p["name"] for p in intent_cfg["parameters"]
if p["variable_name"] not in s.variables]
async def _handle_intent(s, intent_cfg, user_msg):
missing = _missing(s, intent_cfg)
if missing:
p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, s.history)
p_raw = await spark_generate(s, p_prompt, user_msg)
if p_raw.startswith("#PARAMETERS:"):
if bad := _process_params(s, intent_cfg, p_raw):
return bad
missing = _missing(s, intent_cfg)
if missing:
s.awaiting = {"intent": intent_cfg, "missing": missing}
cap = next(p for p in intent_cfg["parameters"]
if p["name"] == missing[0])["caption"]
return f"{cap} nedir?"
s.awaiting = None
return await _call_api(s, intent_cfg)
async def _followup(s, user_msg):
intent_cfg = s.awaiting["intent"]
missing = s.awaiting["missing"]
p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, s.history)
p_raw = await spark_generate(s, p_prompt, user_msg)
if not p_raw.startswith("#PARAMETERS:"):
return "Üzgünüm, anlayamadım."
if bad := _process_params(s, intent_cfg, p_raw):
return bad
missing = _missing(s, intent_cfg)
if missing:
s.awaiting["missing"] = missing
cap = next(p for p in intent_cfg["parameters"]
if p["name"] == missing[0])["caption"]
return f"{cap} nedir?"
s.awaiting = None
return await _call_api(s, intent_cfg)
def _process_params(s, intent_cfg, p_raw):
try:
data = json.loads(p_raw[len("#PARAMETERS:"):])
except json.JSONDecodeError:
return "Parametreleri çözemedim."
for pair in data.get("extracted", []):
p_cfg = next(p for p in intent_cfg["parameters"]
if p["name"] == pair["name"])
if not _valid(p_cfg, pair["value"]):
return p_cfg.get("invalid_prompt", "Geçersiz değer.")
s.variables[p_cfg["variable_name"]] = pair["value"]
return None
def _valid(p_cfg, val):
rx = p_cfg.get("validation_regex")
return re.match(rx, val) is not None if rx else True
async def _call_api(s, intent_cfg):
api = APIS[intent_cfg["action"]]
token = "testtoken"
headers = {k: v.replace("{{token}}", token) for k, v in api["headers"].items()}
body = json.loads(json.dumps(api["body_template"]))
for k, v in body.items():
if isinstance(v, str) and v.startswith("{{") and v.endswith("}}"):
body[k] = s.variables.get(v[2:-2], "")
try:
async with httpx.AsyncClient(timeout=api["timeout_seconds"]) as c:
r = await c.request(api["method"], api["url"],
headers=headers, json=body)
r.raise_for_status()
api_json = r.json()
except Exception:
return intent_cfg["fallback_error_prompt"]
summary_prompt = api["response_prompt"].replace(
"{{api_response}}", json.dumps(api_json, ensure_ascii=False)
)
return await spark_generate(s, summary_prompt, "")