flare / chat_handler.py
ciyidogan's picture
Update chat_handler.py
7279c20 verified
raw
history blame
9.55 kB
"""
Flare โ€“ Chat Handler (small-talk trim + resmรฎ selamlama)
=========================================================
"""
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_smalltalk(raw: str) -> str:
"""
Kฤฑsa selamlaลŸma รงฤฑktฤฑsฤฑnda yalnฤฑzca ilk blok (etiketsiz) kฤฑsmฤฑnฤฑ dรถndรผrรผr.
"""
# intent etiketi รถncesini al
pos_intent = raw.find("#DETECTED_INTENT")
if pos_intent != -1:
raw = raw[:pos_intent]
# arka arkaya 'assistant' bloklarฤฑ varsa ilkini al
pos_asst = raw.lower().find("assistant")
if pos_asst != -1:
raw = raw[:pos_asst]
return raw.strip()
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 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()
d = r.json()
return (d.get("assistant") or d.get("model_answer") or d.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="HoลŸ geldiniz! Size 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 detect
gen_prompt = s.project["versions"][0]["general_prompt"]
intents_cfg = s.project["versions"][0]["intents"]
intent_raw = await spark_generate(
s,
build_intent_prompt(gen_prompt, s.history, user_msg, intents_cfg),
user_msg
)
# small-talk yolu
if not intent_raw.startswith("#DETECTED_INTENT:"):
clean = _trim_smalltalk(intent_raw)
# Selamlama resmรฎ deฤŸilse dรผzelt
if "HoลŸgeldin" in clean or "HoลŸ geldin" in clean:
clean = clean.replace("HoลŸgeldin", "HoลŸ geldiniz").replace("HoลŸ geldin", "HoลŸ geldiniz")
s.history.append({"role": "assistant", "content": clean})
return ChatResponse(session_id=s.id, answer=clean)
intent_name = intent_raw.split(":", 1)[1].strip()
# kฤฑsa mesaj guard
if len(user_msg.split()) < 3:
clean = _trim_smalltalk(intent_raw)
s.history.append({"role": "assistant", "content": clean})
return ChatResponse(session_id=s.id, answer=clean)
# allowed-set
if intent_name not in ALLOWED_INTENTS:
clean = _trim_smalltalk(intent_raw)
s.history.append({"role": "assistant", "content": clean})
return ChatResponse(session_id=s.id, answer=clean)
# intent handling ...
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, "")