Spaces:
Building
Building
Update chat_handler.py
Browse files- chat_handler.py +37 -58
chat_handler.py
CHANGED
@@ -1,14 +1,11 @@
|
|
1 |
"""
|
2 |
-
Flare – Chat Handler (Spark /generate + safe-intent + config-validate)
|
3 |
-
|
4 |
-
• X-Session-ID header
|
5 |
-
• Config JSONC parse hatası -> log + graceful exit
|
6 |
"""
|
7 |
|
8 |
import re, json, uuid, sys, httpx, commentjson
|
9 |
from datetime import datetime
|
10 |
from typing import Dict, List, Optional
|
11 |
-
|
12 |
from fastapi import APIRouter, HTTPException, Header
|
13 |
from pydantic import BaseModel
|
14 |
from commentjson import JSONLibraryException
|
@@ -16,7 +13,7 @@ from commentjson import JSONLibraryException
|
|
16 |
from prompt_builder import build_intent_prompt, build_parameter_prompt, log
|
17 |
|
18 |
# --------------------------------------------------------------------------- #
|
19 |
-
# CONFIG
|
20 |
# --------------------------------------------------------------------------- #
|
21 |
def load_config(path: str = "service_config.jsonc") -> dict:
|
22 |
try:
|
@@ -24,16 +21,11 @@ def load_config(path: str = "service_config.jsonc") -> dict:
|
|
24 |
cfg = commentjson.load(f)
|
25 |
log("✅ service_config.jsonc parsed successfully.")
|
26 |
return cfg
|
27 |
-
except JSONLibraryException as e:
|
28 |
-
log("❌ CONFIG
|
29 |
-
log(str(e))
|
30 |
-
sys.exit(1)
|
31 |
-
except FileNotFoundError:
|
32 |
-
log(f"❌ Config file '{path}' not found.")
|
33 |
sys.exit(1)
|
34 |
|
35 |
CFG = load_config()
|
36 |
-
|
37 |
PROJECTS = {p["name"]: p for p in CFG["projects"]}
|
38 |
APIS = {a["name"]: a for a in CFG["apis"]}
|
39 |
SPARK_URL = CFG["config"]["spark_endpoint"].rstrip("/") + "/generate"
|
@@ -62,16 +54,14 @@ async def spark_generate(session: Session,
|
|
62 |
payload = {
|
63 |
"project_name": session.project["name"],
|
64 |
"user_input": user_input,
|
65 |
-
"context": session.history[-10:],
|
66 |
"system_prompt": system_prompt
|
67 |
}
|
68 |
async with httpx.AsyncClient(timeout=60) as c:
|
69 |
r = await c.post(SPARK_URL, json=payload)
|
70 |
r.raise_for_status()
|
71 |
data = r.json()
|
72 |
-
return (data.get("assistant") or
|
73 |
-
data.get("model_answer") or
|
74 |
-
data.get("text", "")).strip()
|
75 |
|
76 |
# --------------------------------------------------------------------------- #
|
77 |
# FASTAPI ROUTER
|
@@ -84,10 +74,8 @@ def health():
|
|
84 |
|
85 |
class StartSessionRequest(BaseModel):
|
86 |
project_name: str
|
87 |
-
|
88 |
class ChatBody(BaseModel):
|
89 |
user_input: str
|
90 |
-
|
91 |
class ChatResponse(BaseModel):
|
92 |
session_id: str
|
93 |
answer: str
|
@@ -104,8 +92,7 @@ async def start_session(req: StartSessionRequest):
|
|
104 |
return ChatResponse(session_id=s.id, answer="Nasıl yardımcı olabilirim?")
|
105 |
|
106 |
@router.post("/chat", response_model=ChatResponse)
|
107 |
-
async def chat(body: ChatBody,
|
108 |
-
x_session_id: str = Header(...)):
|
109 |
if x_session_id not in SESSIONS:
|
110 |
raise HTTPException(404, "Invalid session")
|
111 |
|
@@ -113,21 +100,30 @@ async def chat(body: ChatBody,
|
|
113 |
user_msg = body.user_input.strip()
|
114 |
s.history.append({"role": "user", "content": user_msg})
|
115 |
|
116 |
-
# ----------------
|
117 |
if s.awaiting:
|
118 |
answer = await _followup(s, user_msg)
|
119 |
s.history.append({"role": "assistant", "content": answer})
|
120 |
return ChatResponse(session_id=s.id, answer=answer)
|
121 |
|
122 |
-
# ----------------
|
123 |
gen_prompt = s.project["versions"][0]["general_prompt"]
|
124 |
intent_raw = await spark_generate(s, gen_prompt, user_msg)
|
125 |
|
|
|
126 |
if not intent_raw.startswith("#DETECTED_INTENT:"):
|
127 |
s.history.append({"role": "assistant", "content": intent_raw})
|
128 |
return ChatResponse(session_id=s.id, answer=intent_raw)
|
129 |
|
130 |
intent_name = intent_raw.split(":", 1)[1].strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
if intent_name not in ALLOWED_INTENTS:
|
132 |
clean = intent_raw.split("#DETECTED_INTENT")[0].split("\nassistant")[0].strip()
|
133 |
s.history.append({"role": "assistant", "content": clean})
|
@@ -144,30 +140,25 @@ async def chat(body: ChatBody,
|
|
144 |
return ChatResponse(session_id=s.id, answer=answer)
|
145 |
|
146 |
# --------------------------------------------------------------------------- #
|
147 |
-
# HELPER FUNCS
|
148 |
# --------------------------------------------------------------------------- #
|
149 |
def _find_intent(project, name_):
|
150 |
-
return next((i for i in project["versions"][0]["intents"]
|
151 |
-
if i["name"] == name_), None)
|
152 |
|
153 |
def _missing(s, intent_cfg):
|
154 |
-
return [p["name"] for p in intent_cfg["parameters"]
|
155 |
-
if p["variable_name"] not in s.variables]
|
156 |
|
157 |
async def _handle_intent(s, intent_cfg, user_msg):
|
158 |
missing = _missing(s, intent_cfg)
|
159 |
if missing:
|
160 |
p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, s.history)
|
161 |
-
p_raw
|
162 |
-
if p_raw.startswith("#PARAMETERS:"):
|
163 |
-
if bad := _process_params(s, intent_cfg, p_raw):
|
164 |
-
return bad
|
165 |
missing = _missing(s, intent_cfg)
|
166 |
|
167 |
if missing:
|
168 |
s.awaiting = {"intent": intent_cfg, "missing": missing}
|
169 |
-
cap = next(p for p in intent_cfg["parameters"]
|
170 |
-
if p["name"] == missing[0])["caption"]
|
171 |
return f"{cap} nedir?"
|
172 |
|
173 |
s.awaiting = None
|
@@ -175,36 +166,30 @@ async def _handle_intent(s, intent_cfg, user_msg):
|
|
175 |
|
176 |
async def _followup(s, user_msg):
|
177 |
intent_cfg = s.awaiting["intent"]
|
178 |
-
missing
|
179 |
p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, s.history)
|
180 |
-
p_raw
|
181 |
-
if not p_raw.startswith("#PARAMETERS:"):
|
182 |
return "Üzgünüm, anlayamadım."
|
183 |
-
if bad := _process_params(s, intent_cfg, p_raw):
|
184 |
-
return bad
|
185 |
-
|
186 |
missing = _missing(s, intent_cfg)
|
187 |
if missing:
|
188 |
s.awaiting["missing"] = missing
|
189 |
-
cap = next(p for p in intent_cfg["parameters"]
|
190 |
-
if p["name"] == missing[0])["caption"]
|
191 |
return f"{cap} nedir?"
|
192 |
-
|
193 |
s.awaiting = None
|
194 |
return await _call_api(s, intent_cfg)
|
195 |
|
196 |
-
def _process_params(s, intent_cfg,
|
197 |
try:
|
198 |
-
data = json.loads(
|
199 |
except json.JSONDecodeError:
|
200 |
-
return
|
201 |
for pair in data.get("extracted", []):
|
202 |
-
p_cfg = next(p for p in intent_cfg["parameters"]
|
203 |
-
if p["name"] == pair["name"])
|
204 |
if not _valid(p_cfg, pair["value"]):
|
205 |
-
return
|
206 |
s.variables[p_cfg["variable_name"]] = pair["value"]
|
207 |
-
return
|
208 |
|
209 |
def _valid(p_cfg, val):
|
210 |
rx = p_cfg.get("validation_regex")
|
@@ -214,22 +199,16 @@ async def _call_api(s, intent_cfg):
|
|
214 |
api = APIS[intent_cfg["action"]]
|
215 |
token = "testtoken"
|
216 |
headers = {k: v.replace("{{token}}", token) for k, v in api["headers"].items()}
|
217 |
-
|
218 |
body = json.loads(json.dumps(api["body_template"]))
|
219 |
for k, v in body.items():
|
220 |
if isinstance(v, str) and v.startswith("{{") and v.endswith("}}"):
|
221 |
body[k] = s.variables.get(v[2:-2], "")
|
222 |
-
|
223 |
try:
|
224 |
async with httpx.AsyncClient(timeout=api["timeout_seconds"]) as c:
|
225 |
-
r = await c.request(api["method"], api["url"],
|
226 |
-
headers=headers, json=body)
|
227 |
r.raise_for_status()
|
228 |
api_json = r.json()
|
229 |
except Exception:
|
230 |
return intent_cfg["fallback_error_prompt"]
|
231 |
-
|
232 |
-
summary_prompt = api["response_prompt"].replace(
|
233 |
-
"{{api_response}}", json.dumps(api_json, ensure_ascii=False)
|
234 |
-
)
|
235 |
return await spark_generate(s, summary_prompt, "")
|
|
|
1 |
"""
|
2 |
+
Flare – Chat Handler (Spark /generate + safe-intent + config-validate + short-msg guard)
|
3 |
+
=======================================================================================
|
|
|
|
|
4 |
"""
|
5 |
|
6 |
import re, json, uuid, sys, httpx, commentjson
|
7 |
from datetime import datetime
|
8 |
from typing import Dict, List, Optional
|
|
|
9 |
from fastapi import APIRouter, HTTPException, Header
|
10 |
from pydantic import BaseModel
|
11 |
from commentjson import JSONLibraryException
|
|
|
13 |
from prompt_builder import build_intent_prompt, build_parameter_prompt, log
|
14 |
|
15 |
# --------------------------------------------------------------------------- #
|
16 |
+
# CONFIG LOAD
|
17 |
# --------------------------------------------------------------------------- #
|
18 |
def load_config(path: str = "service_config.jsonc") -> dict:
|
19 |
try:
|
|
|
21 |
cfg = commentjson.load(f)
|
22 |
log("✅ service_config.jsonc parsed successfully.")
|
23 |
return cfg
|
24 |
+
except (JSONLibraryException, FileNotFoundError) as e:
|
25 |
+
log(f"❌ CONFIG ERROR: {e}")
|
|
|
|
|
|
|
|
|
26 |
sys.exit(1)
|
27 |
|
28 |
CFG = load_config()
|
|
|
29 |
PROJECTS = {p["name"]: p for p in CFG["projects"]}
|
30 |
APIS = {a["name"]: a for a in CFG["apis"]}
|
31 |
SPARK_URL = CFG["config"]["spark_endpoint"].rstrip("/") + "/generate"
|
|
|
54 |
payload = {
|
55 |
"project_name": session.project["name"],
|
56 |
"user_input": user_input,
|
57 |
+
"context": session.history[-10:],
|
58 |
"system_prompt": system_prompt
|
59 |
}
|
60 |
async with httpx.AsyncClient(timeout=60) as c:
|
61 |
r = await c.post(SPARK_URL, json=payload)
|
62 |
r.raise_for_status()
|
63 |
data = r.json()
|
64 |
+
return (data.get("assistant") or data.get("model_answer") or data.get("text", "")).strip()
|
|
|
|
|
65 |
|
66 |
# --------------------------------------------------------------------------- #
|
67 |
# FASTAPI ROUTER
|
|
|
74 |
|
75 |
class StartSessionRequest(BaseModel):
|
76 |
project_name: str
|
|
|
77 |
class ChatBody(BaseModel):
|
78 |
user_input: str
|
|
|
79 |
class ChatResponse(BaseModel):
|
80 |
session_id: str
|
81 |
answer: str
|
|
|
92 |
return ChatResponse(session_id=s.id, answer="Nasıl yardımcı olabilirim?")
|
93 |
|
94 |
@router.post("/chat", response_model=ChatResponse)
|
95 |
+
async def chat(body: ChatBody, x_session_id: str = Header(...)):
|
|
|
96 |
if x_session_id not in SESSIONS:
|
97 |
raise HTTPException(404, "Invalid session")
|
98 |
|
|
|
100 |
user_msg = body.user_input.strip()
|
101 |
s.history.append({"role": "user", "content": user_msg})
|
102 |
|
103 |
+
# ---------------- follow-up?
|
104 |
if s.awaiting:
|
105 |
answer = await _followup(s, user_msg)
|
106 |
s.history.append({"role": "assistant", "content": answer})
|
107 |
return ChatResponse(session_id=s.id, answer=answer)
|
108 |
|
109 |
+
# ---------------- intent detect
|
110 |
gen_prompt = s.project["versions"][0]["general_prompt"]
|
111 |
intent_raw = await spark_generate(s, gen_prompt, user_msg)
|
112 |
|
113 |
+
# small-talk?
|
114 |
if not intent_raw.startswith("#DETECTED_INTENT:"):
|
115 |
s.history.append({"role": "assistant", "content": intent_raw})
|
116 |
return ChatResponse(session_id=s.id, answer=intent_raw)
|
117 |
|
118 |
intent_name = intent_raw.split(":", 1)[1].strip()
|
119 |
+
|
120 |
+
# short-message guard: tek/iki kelime selamlaşma + intent bastıysa yok say
|
121 |
+
if len(user_msg.split()) < 3:
|
122 |
+
clean = intent_raw.split("#DETECTED_INTENT")[0].split("\nassistant")[0].strip()
|
123 |
+
s.history.append({"role": "assistant", "content": clean})
|
124 |
+
return ChatResponse(session_id=s.id, answer=clean)
|
125 |
+
|
126 |
+
# allowed-set kontrolü
|
127 |
if intent_name not in ALLOWED_INTENTS:
|
128 |
clean = intent_raw.split("#DETECTED_INTENT")[0].split("\nassistant")[0].strip()
|
129 |
s.history.append({"role": "assistant", "content": clean})
|
|
|
140 |
return ChatResponse(session_id=s.id, answer=answer)
|
141 |
|
142 |
# --------------------------------------------------------------------------- #
|
143 |
+
# HELPER FUNCS (değişmeyen kısımlar)
|
144 |
# --------------------------------------------------------------------------- #
|
145 |
def _find_intent(project, name_):
|
146 |
+
return next((i for i in project["versions"][0]["intents"] if i["name"] == name_), None)
|
|
|
147 |
|
148 |
def _missing(s, intent_cfg):
|
149 |
+
return [p["name"] for p in intent_cfg["parameters"] if p["variable_name"] not in s.variables]
|
|
|
150 |
|
151 |
async def _handle_intent(s, intent_cfg, user_msg):
|
152 |
missing = _missing(s, intent_cfg)
|
153 |
if missing:
|
154 |
p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, s.history)
|
155 |
+
p_raw = await spark_generate(s, p_prompt, user_msg)
|
156 |
+
if p_raw.startswith("#PARAMETERS:") and not _process_params(s, intent_cfg, p_raw):
|
|
|
|
|
157 |
missing = _missing(s, intent_cfg)
|
158 |
|
159 |
if missing:
|
160 |
s.awaiting = {"intent": intent_cfg, "missing": missing}
|
161 |
+
cap = next(p for p in intent_cfg["parameters"] if p["name"] == missing[0])["caption"]
|
|
|
162 |
return f"{cap} nedir?"
|
163 |
|
164 |
s.awaiting = None
|
|
|
166 |
|
167 |
async def _followup(s, user_msg):
|
168 |
intent_cfg = s.awaiting["intent"]
|
169 |
+
missing = s.awaiting["missing"]
|
170 |
p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, s.history)
|
171 |
+
p_raw = await spark_generate(s, p_prompt, user_msg)
|
172 |
+
if not p_raw.startswith("#PARAMETERS:") or _process_params(s, intent_cfg, p_raw):
|
173 |
return "Üzgünüm, anlayamadım."
|
|
|
|
|
|
|
174 |
missing = _missing(s, intent_cfg)
|
175 |
if missing:
|
176 |
s.awaiting["missing"] = missing
|
177 |
+
cap = next(p for p in intent_cfg["parameters"] if p["name"] == missing[0])["caption"]
|
|
|
178 |
return f"{cap} nedir?"
|
|
|
179 |
s.awaiting = None
|
180 |
return await _call_api(s, intent_cfg)
|
181 |
|
182 |
+
def _process_params(s, intent_cfg, raw):
|
183 |
try:
|
184 |
+
data = json.loads(raw[len("#PARAMETERS:"):])
|
185 |
except json.JSONDecodeError:
|
186 |
+
return True
|
187 |
for pair in data.get("extracted", []):
|
188 |
+
p_cfg = next(p for p in intent_cfg["parameters"] if p["name"] == pair["name"])
|
|
|
189 |
if not _valid(p_cfg, pair["value"]):
|
190 |
+
return True
|
191 |
s.variables[p_cfg["variable_name"]] = pair["value"]
|
192 |
+
return False
|
193 |
|
194 |
def _valid(p_cfg, val):
|
195 |
rx = p_cfg.get("validation_regex")
|
|
|
199 |
api = APIS[intent_cfg["action"]]
|
200 |
token = "testtoken"
|
201 |
headers = {k: v.replace("{{token}}", token) for k, v in api["headers"].items()}
|
|
|
202 |
body = json.loads(json.dumps(api["body_template"]))
|
203 |
for k, v in body.items():
|
204 |
if isinstance(v, str) and v.startswith("{{") and v.endswith("}}"):
|
205 |
body[k] = s.variables.get(v[2:-2], "")
|
|
|
206 |
try:
|
207 |
async with httpx.AsyncClient(timeout=api["timeout_seconds"]) as c:
|
208 |
+
r = await c.request(api["method"], api["url"], headers=headers, json=body)
|
|
|
209 |
r.raise_for_status()
|
210 |
api_json = r.json()
|
211 |
except Exception:
|
212 |
return intent_cfg["fallback_error_prompt"]
|
213 |
+
summary_prompt = api["response_prompt"].replace("{{api_response}}", json.dumps(api_json, ensure_ascii=False))
|
|
|
|
|
|
|
214 |
return await spark_generate(s, summary_prompt, "")
|