flare / chat_handler.py
ciyidogan's picture
Update chat_handler.py
0040505 verified
raw
history blame
7.52 kB
"""
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)