Spaces:
Building
Building
""" | |
Flare – Chat Handler (Spark /generate + safe-intent + config-validate + short-msg guard) | |
======================================================================================= | |
""" | |
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 | |
# --------------------------------------------------------------------------- # | |
# CONFIG LOAD | |
# --------------------------------------------------------------------------- # | |
def load_config(path: str = "service_config.jsonc") -> dict: | |
try: | |
with open(path, encoding="utf-8") as f: | |
cfg = commentjson.load(f) | |
log("✅ service_config.jsonc parsed successfully.") | |
return cfg | |
except (JSONLibraryException, FileNotFoundError) as e: | |
log(f"❌ CONFIG ERROR: {e}") | |
sys.exit(1) | |
CFG = load_config() | |
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]] = [] | |
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:], | |
"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() | |
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 | |
# --------------------------------------------------------------------------- # | |
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?") | |
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 detect | |
gen_prompt = s.project["versions"][0]["general_prompt"] | |
intent_raw = await spark_generate(s, gen_prompt, user_msg) | |
# small-talk? | |
if not intent_raw.startswith("#DETECTED_INTENT:"): | |
s.history.append({"role": "assistant", "content": intent_raw}) | |
return ChatResponse(session_id=s.id, answer=intent_raw) | |
intent_name = intent_raw.split(":", 1)[1].strip() | |
# short-message guard: tek/iki kelime selamlaşma + intent bastıysa yok say | |
if len(user_msg.split()) < 3: | |
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) | |
# allowed-set kontrolü | |
if intent_name not in ALLOWED_INTENTS: | |
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 (değişmeyen kısımlar) | |
# --------------------------------------------------------------------------- # | |
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:") and not _process_params(s, intent_cfg, p_raw): | |
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:") or _process_params(s, intent_cfg, p_raw): | |
return "Üzgünüm, anlayamadım." | |
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, raw): | |
try: | |
data = json.loads(raw[len("#PARAMETERS:"):]) | |
except json.JSONDecodeError: | |
return True | |
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 True | |
s.variables[p_cfg["variable_name"]] = pair["value"] | |
return False | |
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, "") | |