""" 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, "")