flare / chat_handler.py
ciyidogan's picture
Update chat_handler.py
14352db verified
raw
history blame
8.97 kB
"""
Flare – Chat Handler (v2)
==========================
• /start_session • /chat
Intent tespiti, parametre çıkarımı, regex doğrulama, session değişkeni,
backend API çağrısı ve cevap özetleme.
"""
import re
import json
import uuid
import httpx
import commentjson
from datetime import datetime
from typing import Dict, List, Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from prompt_builder import build_intent_prompt, build_parameter_prompt, log
# ----------------------------------------------------------------------------
# CONFIG LOAD (service_config.jsonc)
# ----------------------------------------------------------------------------
with open("service_config.jsonc", "r", encoding="utf-8") as f:
CFG = commentjson.load(f)
PROJECTS = {p["name"]: p for p in CFG["projects"]}
APIS = {a["name"]: a for a in CFG["apis"]}
# ----------------------------------------------------------------------------
# SESSION OBJECT
# ----------------------------------------------------------------------------
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] = {} # variable_name -> value
self.awaiting: Optional[Dict] = None # {"intent":..., "missing":[...]}
log(f"🆕 Session {self.id} for {project_name}")
SESSIONS: Dict[str, Session] = {}
# ----------------------------------------------------------------------------
# SPARK LLM WRAPPER (basit HTTP JSON)
# ----------------------------------------------------------------------------
async def spark_generate(prompt: str) -> str:
url = CFG["config"]["spark_endpoint"]
async with httpx.AsyncClient(timeout=60) as c:
r = await c.post(url, json={"prompt": prompt})
r.raise_for_status()
return r.json()["text"]
# ----------------------------------------------------------------------------
# FASTAPI APP
# ----------------------------------------------------------------------------
app = FastAPI()
@app.get("/")
def health():
return {"status": "ok"}
# ----------------------------------------------------------------------------
# SCHEMAS
# ----------------------------------------------------------------------------
class StartSessionRequest(BaseModel):
project_name: str
class ChatRequest(BaseModel):
session_id: str
user_input: str
class ChatResponse(BaseModel):
session_id: str
answer: str
# ----------------------------------------------------------------------------
# ENDPOINTS
# ----------------------------------------------------------------------------
@app.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?")
@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
if req.session_id not in SESSIONS:
raise HTTPException(404, "Invalid session")
s = SESSIONS[req.session_id]
user_msg = req.user_input.strip()
s.history.append({"role": "user", "content": user_msg})
# ---------------- Parametre follow-up modunda mı? ----------------
if s.awaiting:
answer = await _followup_flow(s, user_msg)
s.history.append({"role": "assistant", "content": answer})
return ChatResponse(session_id=s.id, answer=answer)
# ---------------- Intent detection ----------------
gen_prompt = s.project["versions"][0]["general_prompt"]
prompt = build_intent_prompt(gen_prompt, s.history, user_msg)
llm_out = await spark_generate(prompt)
if not llm_out.startswith("#DETECTED_INTENT:"):
# Small-talk cevabı
s.history.append({"role": "assistant", "content": llm_out})
return ChatResponse(session_id=s.id, answer=llm_out)
intent_name = llm_out.split(":", 1)[1].strip()
intent_cfg = _get_intent_cfg(s.project, intent_name)
if not intent_cfg:
err = "Üzgünüm, bu konuda yardımcı olamıyorum."
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)
# ----------------------------------------------------------------------------
# CORE HELPERS
# ----------------------------------------------------------------------------
def _get_intent_cfg(project: Dict, name_: str) -> Optional[Dict]:
for it in project["versions"][0]["intents"]:
if it["name"] == name_:
return it
return None
def _current_missing(session: Session, intent_cfg: Dict) -> List[str]:
return [
p["name"]
for p in intent_cfg["parameters"]
if p["variable_name"] not in session.variables
]
async def _handle_intent(session: Session,
intent_cfg: Dict,
user_msg: str) -> str:
missing = _current_missing(session, intent_cfg)
# --- Parametre extraction denemesi (ilk mesajda) ---
if missing:
p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, session.history)
llm_out = await spark_generate(p_prompt)
if llm_out.startswith("#PARAMETERS:"):
ok = _process_param_output(session, intent_cfg, llm_out)
if not ok:
return ok # invalid prompt dönebilir
missing = _current_missing(session, intent_cfg)
# --- Hâlâ eksik mi? follow-up sor ---
if missing:
session.awaiting = {"intent": intent_cfg, "missing": missing}
first_caption = next(p for p in intent_cfg["parameters"]
if p["name"] == missing[0])["caption"]
return f"{first_caption} nedir?"
# --- Tüm parametreler hazır → API çağır ---
return await _call_api(session, intent_cfg)
async def _followup_flow(session: Session,
user_msg: str) -> str:
intent_cfg = session.awaiting["intent"]
missing = session.awaiting["missing"]
p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, session.history)
llm_out = await spark_generate(p_prompt)
if not llm_out.startswith("#PARAMETERS:"):
return "Üzgünüm, bilgileri anlayamadım."
ok = _process_param_output(session, intent_cfg, llm_out)
if not ok:
return ok
missing = _current_missing(session, intent_cfg)
if missing:
session.awaiting["missing"] = missing
first_caption = next(p for p in intent_cfg["parameters"]
if p["name"] == missing[0])["caption"]
return f"{first_caption} nedir?"
session.awaiting = None
return await _call_api(session, intent_cfg)
def _process_param_output(session: Session,
intent_cfg: Dict,
llm_out: str) -> Optional[str]:
try:
data = json.loads(llm_out[len("#PARAMETERS:"):])
except json.JSONDecodeError:
return "Üzgünüm, 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 _validate(p_cfg, pair["value"]):
return p_cfg.get("invalid_prompt", "Geçersiz değer.")
session.variables[p_cfg["variable_name"]] = pair["value"]
return None # success
def _validate(p_cfg: Dict, value: str) -> bool:
pattern = p_cfg.get("validation_regex")
return re.match(pattern, value) is not None if pattern else True
async def _call_api(session: Session, intent_cfg: Dict) -> str:
api = APIS[intent_cfg["action"]]
# Simple token
token = "testtoken"
headers = {k: v.replace("{{token}}", token) for k, v in api["headers"].items()}
body = json.loads(json.dumps(api["body_template"])) # deep copy
for k, v in body.items():
if isinstance(v, str) and v.startswith("{{") and v.endswith("}}"):
body[k] = session.variables.get(v[2:-2], "")
log(f"➡️ {api['name']} {body}")
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 as ex:
log(f"❌ API error: {ex}")
return intent_cfg["fallback_error_prompt"]
# LLM’ye özetlet
summary_prompt = api["response_prompt"].replace(
"{{api_response}}", json.dumps(api_json, ensure_ascii=False)
)
return await spark_generate(summary_prompt)