""" Flare – Chat Handler (header edition) ==================================== /start_session -> JSON body döner /chat -> X-Session-ID header + {"user_input": "..."} """ import re, json, uuid, httpx, commentjson from datetime import datetime from typing import Dict, List, Optional from fastapi import APIRouter, HTTPException, Header, Request 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"]} # ---------------------------------------------------------------------------- # 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 # ---------------------------------------------------------------------------- async def spark_generate(prompt: str) -> str: async with httpx.AsyncClient(timeout=60) as c: r = await c.post(CFG["config"]["spark_endpoint"], json={"prompt": prompt}) r.raise_for_status() return r.json()["text"] # ---------------------------------------------------------------------------- # FASTAPI ROUTER # ---------------------------------------------------------------------------- router = APIRouter() @router.get("/") def health(): return {"status": "ok"} # Schemas class StartSessionRequest(BaseModel): project_name: str class ChatBody(BaseModel): user_input: str # session_id optional for legacy session_id: Optional[str] = None 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, request: Request, x_session_id: Optional[str] = Header(None)): # 1) Session ID alma: header > body sid = x_session_id or body.session_id if not sid or sid not in SESSIONS: raise HTTPException(404, "Invalid or missing session") s = SESSIONS[sid] user_msg = body.user_input.strip() s.history.append({"role": "user", "content": user_msg}) # ---------------- follow-up modunda mı? 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_prompt = build_intent_prompt(gen_prompt, s.history, user_msg) intent_out = await spark_generate(intent_prompt) if not intent_out.startswith("#DETECTED_INTENT:"): s.history.append({"role": "assistant", "content": intent_out}) return ChatResponse(session_id=s.id, answer=intent_out) intent_name = intent_out.split(":", 1)[1].strip() 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 (aynı) # ---------------------------------------------------------------------------- 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) out = await spark_generate(p_prompt) if out.startswith("#PARAMETERS:"): if bad := _process_params(s, intent_cfg, out): 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) out = await spark_generate(p_prompt) if not out.startswith("#PARAMETERS:"): return "Üzgünüm, anlayamadım." if bad := _process_params(s, intent_cfg, out): 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, out): try: data = json.loads(out[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 = api["response_prompt"].replace( "{{api_response}}", json.dumps(api_json, ensure_ascii=False) ) return await spark_generate(summary)