ciyidogan commited on
Commit
e72db51
·
verified ·
1 Parent(s): 3f2fcc0

Update chat_handler.py

Browse files
Files changed (1) hide show
  1. chat_handler.py +111 -195
chat_handler.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- Flare – Chat Handler (regex-intent, small-talk trim, boş-yanıt fallback)
3
- ========================================================================
4
  """
5
 
6
  import re, json, uuid, sys, httpx, commentjson
@@ -9,255 +9,171 @@ from typing import Dict, List, Optional
9
  from fastapi import APIRouter, HTTPException, Header
10
  from pydantic import BaseModel
11
  from commentjson import JSONLibraryException
12
-
13
  from prompt_builder import build_intent_prompt, build_parameter_prompt, log
14
 
15
-
16
- # ─────────────────────────
17
- # HELPERS
18
- # ─────────────────────────
19
- def _trim_smalltalk(raw: str) -> str:
20
- """Keep only the very first greeting sentence; cut everything else."""
21
- # intent etiketi öncesini al
22
- pos_intent = raw.find("#DETECTED_INTENT")
23
- if pos_intent != -1:
24
- raw = raw[:pos_intent]
25
-
26
- # model kendi prompt'umuzu eklediyse kes
27
- pos_sys = raw.find("⚠️")
28
- if pos_sys != -1:
29
- raw = raw[:pos_sys]
30
-
31
- # ilk 'assistant' bloğundan sonrası at
32
- for marker in ["assistant\n\n", "assistant\n", "assistant"]:
33
- idx = raw.lower().find(marker)
34
- if 0 <= idx <= 2:
35
- raw = raw[idx + len(marker):]
36
-
37
- # resmî selam: Hoş geldiniz
38
  raw = re.sub(r"Hoş[\s-]?geldin(iz)?", "Hoş geldiniz", raw, flags=re.IGNORECASE)
39
  return raw.strip()
40
 
41
-
42
- # ---------------------------------------------------------------------- #
43
- # CONFIG LOAD
44
- # ---------------------------------------------------------------------- #
45
- def load_config(path: str = "service_config.jsonc") -> dict:
46
- try:
47
- with open(path, encoding="utf-8") as f:
48
- cfg = commentjson.load(f)
49
- log("✅ service_config.jsonc parsed successfully.")
50
- return cfg
51
- except (JSONLibraryException, FileNotFoundError) as e:
52
- log(f"❌ CONFIG ERROR: {e}")
53
- sys.exit(1)
54
-
55
- CFG = load_config()
56
  PROJECTS = {p["name"]: p for p in CFG["projects"]}
57
  APIS = {a["name"]: a for a in CFG["apis"]}
58
  SPARK_URL = CFG["config"]["spark_endpoint"].rstrip("/") + "/generate"
59
  ALLOWED_INTENTS = {"flight-booking", "flight-info", "booking-cancel"}
60
 
61
-
62
- # ---------------------------------------------------------------------- #
63
- # SESSION
64
- # ---------------------------------------------------------------------- #
65
  class Session:
66
- def __init__(self, project_name: str):
67
  self.id = str(uuid.uuid4())
68
- self.project = PROJECTS[project_name]
69
  self.history: List[Dict[str, str]] = []
70
- self.variables: Dict[str, str] = {}
71
  self.awaiting: Optional[Dict] = None
72
- log(f"🆕 Session {self.id} for {project_name}")
73
 
74
  SESSIONS: Dict[str, Session] = {}
75
 
76
-
77
- # ---------------------------------------------------------------------- #
78
- # SPARK CLIENT
79
- # ---------------------------------------------------------------------- #
80
- async def spark_generate(session: Session,
81
- system_prompt: str,
82
- user_input: str) -> str:
83
  payload = {
84
- "project_name": session.project["name"],
85
- "user_input": user_input,
86
- "context": session.history[-10:],
87
- "system_prompt": system_prompt
88
  }
89
  async with httpx.AsyncClient(timeout=60) as c:
90
- r = await c.post(SPARK_URL, json=payload)
91
  r.raise_for_status()
92
  d = r.json()
93
- raw = (d.get("assistant") or d.get("model_answer") or d.get("text", ""))
94
- log(f"🪄 Spark raw: {raw!r}")
95
- return raw.strip()
96
 
97
-
98
- # ---------------------------------------------------------------------- #
99
- # FASTAPI ROUTER
100
- # ---------------------------------------------------------------------- #
101
  router = APIRouter()
 
 
102
 
103
- @router.get("/")
104
- def health():
105
- return {"status": "ok"}
106
-
107
- class StartSessionRequest(BaseModel):
108
- project_name: str
109
- class ChatBody(BaseModel):
110
- user_input: str
111
- class ChatResponse(BaseModel):
112
- session_id: str
113
- answer: str
114
 
115
-
116
- # ---------------------------------------------------------------------- #
117
- # ENDPOINTS
118
- # ---------------------------------------------------------------------- #
119
- @router.post("/start_session", response_model=ChatResponse)
120
- async def start_session(req: StartSessionRequest):
121
  if req.project_name not in PROJECTS:
122
- raise HTTPException(404, "Unknown project")
123
  s = Session(req.project_name)
124
  SESSIONS[s.id] = s
125
- return ChatResponse(session_id=s.id,
126
- answer="Hoş geldiniz! Size nasıl yardımcı olabilirim?")
127
 
128
- @router.post("/chat", response_model=ChatResponse)
129
- async def chat(body: ChatBody, x_session_id: str = Header(...)):
130
  if x_session_id not in SESSIONS:
131
- raise HTTPException(404, "Invalid session")
132
-
133
  s = SESSIONS[x_session_id]
134
- user_msg = body.user_input.strip()
135
- s.history.append({"role": "user", "content": user_msg})
136
 
137
- # follow-up?
138
  if s.awaiting:
139
- answer = await _followup(s, user_msg)
140
- s.history.append({"role": "assistant", "content": answer})
141
- return ChatResponse(session_id=s.id, answer=answer)
142
 
143
- # intent detect
144
- gen_prompt = s.project["versions"][0]["general_prompt"]
145
- intents_cfg = s.project["versions"][0]["intents"]
146
- intent_raw = await spark_generate(
147
- s,
148
- build_intent_prompt(gen_prompt, s.history, user_msg, intents_cfg),
149
- user_msg
150
- )
151
 
152
- # boş çıktıyı yakala
153
- if intent_raw.strip() == "":
154
  fallback = "Üzgünüm, mesajınızı anlayamadım. Lütfen tekrar dener misiniz?"
155
  s.history.append({"role": "assistant", "content": fallback})
156
- return ChatResponse(session_id=s.id, answer=fallback)
157
 
158
- # küçük selamlaşma yolu
159
- if not intent_raw.startswith("#DETECTED_INTENT:"):
160
- clean = _trim_smalltalk(intent_raw)
161
  s.history.append({"role": "assistant", "content": clean})
162
- return ChatResponse(session_id=s.id, answer=clean)
163
-
164
- # regex ile intent + extras ayır
165
- m = re.search(r"#DETECTED_INTENT:\s*([A-Za-z0-9_-]+)", intent_raw)
166
- intent_name = m.group(1) if m else ""
167
- extras = intent_raw[m.end():].strip() if m else ""
168
 
169
- # kısa mesaj guard
170
- if len(user_msg.split()) < 3 or intent_name not in ALLOWED_INTENTS:
171
- clean = _trim_smalltalk(extras or intent_raw)
172
  s.history.append({"role": "assistant", "content": clean})
173
- return ChatResponse(session_id=s.id, answer=clean)
174
 
175
- intent_cfg = _find_intent(s.project, intent_name)
176
- if not intent_cfg:
177
  err = "Üzgünüm, anlayamadım."
178
  s.history.append({"role": "assistant", "content": err})
179
- return ChatResponse(session_id=s.id, answer=err)
180
 
181
- answer = await _handle_intent(s, intent_cfg, user_msg)
182
  s.history.append({"role": "assistant", "content": answer})
183
- return ChatResponse(session_id=s.id, answer=answer)
184
 
 
 
 
185
 
186
- # ---------------------------------------------------------------------- #
187
- # HELPER FUNCS (değişmedi)
188
- # ---------------------------------------------------------------------- #
189
- def _find_intent(project, name_):
190
- return next((i for i in project["versions"][0]["intents"] if i["name"] == name_), None)
191
-
192
- def _missing(s, intent_cfg):
193
- return [p["name"] for p in intent_cfg["parameters"] if p["variable_name"] not in s.variables]
194
-
195
- async def _handle_intent(s, intent_cfg, user_msg):
196
- missing = _missing(s, intent_cfg)
197
  if missing:
198
- p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, s.history)
199
- p_raw = await spark_generate(s, p_prompt, user_msg)
200
- if p_raw.startswith("#PARAMETERS:") and not _process_params(s, intent_cfg, p_raw):
201
- missing = _missing(s, intent_cfg)
202
 
203
  if missing:
204
- s.awaiting = {"intent": intent_cfg, "missing": missing}
205
- cap = next(p for p in intent_cfg["parameters"] if p["name"] == missing[0])["caption"]
206
  return f"{cap} nedir?"
207
-
208
  s.awaiting = None
209
- return await _call_api(s, intent_cfg)
210
 
211
- async def _followup(s, user_msg):
212
- intent_cfg = s.awaiting["intent"]
213
- missing = s.awaiting["missing"]
214
- p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, s.history)
215
- p_raw = await spark_generate(s, p_prompt, user_msg)
216
- if not p_raw.startswith("#PARAMETERS:") or _process_params(s, intent_cfg, p_raw):
217
  return "Üzgünüm, anlayamadım."
218
- missing = _missing(s, intent_cfg)
219
- if missing:
220
- s.awaiting["missing"] = missing
221
- cap = next(p for p in intent_cfg["parameters"]
222
- if p["name"] == missing[0])["caption"]
223
  return f"{cap} nedir?"
224
  s.awaiting = None
225
- return await _call_api(s, intent_cfg)
226
-
227
- def _process_params(s, intent_cfg, raw):
228
- try:
229
- data = json.loads(raw[len("#PARAMETERS:"):])
230
- except json.JSONDecodeError:
231
- return True
232
- for pair in data.get("extracted", []):
233
- p_cfg = next(p for p in intent_cfg["parameters"] if p["name"] == pair["name"])
234
- if not _valid(p_cfg, pair["value"]):
235
  return True
236
- s.variables[p_cfg["variable_name"]] = pair["value"]
237
- return False
238
-
239
- def _valid(p_cfg, val):
240
- rx = p_cfg.get("validation_regex")
241
- return re.match(rx, val) is not None if rx else True
242
-
243
- async def _call_api(s, intent_cfg):
244
- api = APIS[intent_cfg["action"]]
245
- token = "testtoken"
246
- headers = {k: v.replace("{{token}}", token) for k, v in api["headers"].items()}
247
- body = json.loads(json.dumps(api["body_template"]))
248
- for k, v in body.items():
249
- if isinstance(v, str) and v.startswith("{{") and v.endswith("}}"):
250
- body[k] = s.variables.get(v[2:-2], "")
251
- try:
252
- async with httpx.AsyncClient(timeout=api["timeout_seconds"]) as c:
253
- r = await c.request(api["method"], api["url"], headers=headers, json=body)
254
- r.raise_for_status()
255
- api_json = r.json()
256
- except Exception:
257
- return intent_cfg["fallback_error_prompt"]
258
-
259
- sum_prompt = api["response_prompt"].replace("{{api_response}}", json.dumps(api_json, ensure_ascii=False))
260
- summary = await spark_generate(s, sum_prompt, "")
261
- if summary.strip() == "":
262
- summary = f"İşlem sonucu: {json.dumps(api_json, ensure_ascii=False)}"
263
- return summary
 
1
  """
2
+ Flare – Chat Handler (v1.3 · robust trimming + param validation)
3
+ =================================================================
4
  """
5
 
6
  import re, json, uuid, sys, httpx, commentjson
 
9
  from fastapi import APIRouter, HTTPException, Header
10
  from pydantic import BaseModel
11
  from commentjson import JSONLibraryException
 
12
  from prompt_builder import build_intent_prompt, build_parameter_prompt, log
13
 
14
+ # ───────────────────────── HELPERS ───────────────────────── #
15
+ def _trim_response(raw: str) -> str:
16
+ """
17
+ Remove everything after the first logical assistant block or intent tag.
18
+ Also strips trailing 'assistant' artifacts and prompt injections.
19
+ """
20
+ # Stop at our own rules if model leaked them
21
+ for stop in ["#DETECTED_INTENT", "⚠️", "\nassistant", "assistant\n", "assistant"]:
22
+ idx = raw.find(stop)
23
+ if idx != -1:
24
+ raw = raw[:idx]
25
+ # Normalise selamlama
 
 
 
 
 
 
 
 
 
 
 
26
  raw = re.sub(r"Hoş[\s-]?geldin(iz)?", "Hoş geldiniz", raw, flags=re.IGNORECASE)
27
  return raw.strip()
28
 
29
+ def _safe_intent_parse(raw: str) -> (str, str):
30
+ """Extract intent name and extra tail."""
31
+ m = re.search(r"#DETECTED_INTENT:\s*([A-Za-z0-9_-]+)", raw)
32
+ if not m:
33
+ return "", raw
34
+ name = m.group(1)
35
+ tail = raw[m.end():]
36
+ return name, tail
37
+
38
+ # ───────────────────────── CONFIG ───────────────────────── #
39
+ CFG = commentjson.load(open("service_config.jsonc", encoding="utf-8"))
 
 
 
 
40
  PROJECTS = {p["name"]: p for p in CFG["projects"]}
41
  APIS = {a["name"]: a for a in CFG["apis"]}
42
  SPARK_URL = CFG["config"]["spark_endpoint"].rstrip("/") + "/generate"
43
  ALLOWED_INTENTS = {"flight-booking", "flight-info", "booking-cancel"}
44
 
45
+ # ───────────────────────── SESSION ───────────────────────── #
 
 
 
46
  class Session:
47
+ def __init__(self, project: str):
48
  self.id = str(uuid.uuid4())
49
+ self.project = PROJECTS[project]
50
  self.history: List[Dict[str, str]] = []
51
+ self.vars: Dict[str, str] = {}
52
  self.awaiting: Optional[Dict] = None
53
+ log(f"🆕 Session {self.id}")
54
 
55
  SESSIONS: Dict[str, Session] = {}
56
 
57
+ # ───────────────────────── SPARK ───────────────────────── #
58
+ async def spark_generate(s: Session, prompt: str, user_msg: str) -> str:
 
 
 
 
 
59
  payload = {
60
+ "project_name": s.project["name"],
61
+ "user_input": user_msg,
62
+ "context": s.history[-10:],
63
+ "system_prompt": prompt
64
  }
65
  async with httpx.AsyncClient(timeout=60) as c:
66
+ r = await c.post(SPARK_URL + "/generate", json=payload)
67
  r.raise_for_status()
68
  d = r.json()
69
+ raw = (d.get("assistant") or d.get("model_answer") or d.get("text", "")).strip()
70
+ log(f"🪄 Spark raw: {raw[:120]!r}")
71
+ return raw
72
 
73
+ # ───────────────────────── FASTAPI ───────────────────────── #
 
 
 
74
  router = APIRouter()
75
+ @router.get("/") # health
76
+ def health(): return {"ok": True}
77
 
78
+ class Start(BaseModel): project_name: str
79
+ class Body(BaseModel): user_input: str
80
+ class Resp(BaseModel): session_id: str; answer: str
 
 
 
 
 
 
 
 
81
 
82
+ @router.post("/start_session", response_model=Resp)
83
+ async def start_session(req: Start):
 
 
 
 
84
  if req.project_name not in PROJECTS:
85
+ raise HTTPException(404, "project")
86
  s = Session(req.project_name)
87
  SESSIONS[s.id] = s
88
+ greet = "Hoş geldiniz! Size nasıl yardımcı olabilirim?"
89
+ return Resp(session_id=s.id, answer=greet)
90
 
91
+ @router.post("/chat", response_model=Resp)
92
+ async def chat(body: Body, x_session_id: str = Header(...)):
93
  if x_session_id not in SESSIONS:
94
+ raise HTTPException(404, "session")
 
95
  s = SESSIONS[x_session_id]
96
+ user = body.user_input.strip()
97
+ s.history.append({"role": "user", "content": user})
98
 
99
+ # ---------- follow-up ----------
100
  if s.awaiting:
101
+ ans = await _followup(s, user)
102
+ s.history.append({"role": "assistant", "content": ans})
103
+ return Resp(session_id=s.id, answer=ans)
104
 
105
+ # ---------- intent detect ----------
106
+ p = build_intent_prompt(
107
+ s.project["versions"][0]["general_prompt"],
108
+ s.history, user, s.project["versions"][0]["intents"])
109
+ raw = await spark_generate(s, p, user)
 
 
 
110
 
111
+ if raw == "":
 
112
  fallback = "Üzgünüm, mesajınızı anlayamadım. Lütfen tekrar dener misiniz?"
113
  s.history.append({"role": "assistant", "content": fallback})
114
+ return Resp(session_id=s.id, answer=fallback)
115
 
116
+ # small-talk yolu
117
+ if not raw.startswith("#DETECTED_INTENT"):
118
+ clean = _trim_response(raw)
119
  s.history.append({"role": "assistant", "content": clean})
120
+ return Resp(session_id=s.id, answer=clean)
 
 
 
 
 
121
 
122
+ intent, tail = _safe_intent_parse(raw)
123
+ if intent not in ALLOWED_INTENTS or len(user.split()) < 3:
124
+ clean = _trim_response(tail)
125
  s.history.append({"role": "assistant", "content": clean})
126
+ return Resp(session_id=s.id, answer=clean)
127
 
128
+ cfg = _find_intent(s, intent)
129
+ if not cfg:
130
  err = "Üzgünüm, anlayamadım."
131
  s.history.append({"role": "assistant", "content": err})
132
+ return Resp(session_id=s.id, answer=err)
133
 
134
+ answer = await _handle_intent(s, cfg, user)
135
  s.history.append({"role": "assistant", "content": answer})
136
+ return Resp(session_id=s.id, answer=answer)
137
 
138
+ # ────────────────── INTENT / PARAM / API HELPERS ────────────────── #
139
+ def _find_intent(s, name): return next((i for i in s.project["versions"][0]["intents"] if i["name"] == name), None)
140
+ def _missing(s, cfg): return [p["name"] for p in cfg["parameters"] if p["variable_name"] not in s.vars]
141
 
142
+ async def _handle_intent(s, cfg, user):
143
+ missing = _missing(s, cfg)
 
 
 
 
 
 
 
 
 
144
  if missing:
145
+ prmpt = build_parameter_prompt(cfg, missing, user, s.history)
146
+ raw = await spark_generate(s, prmpt, user)
147
+ if raw.startswith("#PARAMETERS:") and not _proc_params(s, cfg, raw):
148
+ missing = _missing(s, cfg)
149
 
150
  if missing:
151
+ s.awaiting = {"intent": cfg, "missing": missing}
152
+ cap = next(p for p in cfg["parameters"] if p["name"] == missing[0])["caption"]
153
  return f"{cap} nedir?"
 
154
  s.awaiting = None
155
+ return await _call_api(s, cfg)
156
 
157
+ async def _followup(s, user):
158
+ cfg = s.awaiting["intent"]; miss = s.awaiting["missing"]
159
+ prmpt = build_parameter_prompt(cfg, miss, user, s.history)
160
+ raw = await spark_generate(s, prmpt, user)
161
+ if not raw.startswith("#PARAMETERS:") or _proc_params(s, cfg, raw):
 
162
  return "Üzgünüm, anlayamadım."
163
+ miss = _missing(s, cfg)
164
+ if miss:
165
+ s.awaiting["missing"] = miss
166
+ cap = next(p for p in cfg["parameters"] if p["name"] == miss[0])["caption"]
 
167
  return f"{cap} nedir?"
168
  s.awaiting = None
169
+ return await _call_api(s, cfg)
170
+
171
+ def _proc_params(s, cfg, raw):
172
+ try: d = json.loads(raw[len("#PARAMETERS:"):])
173
+ except: return True
174
+ for pr in d.get("extracted", []):
175
+ pcfg = next(p for p in cfg["parameters"] if p["name"] == pr["name"])
176
+ if not re.fullmatch(pcfg.get("validation_regex", ".*"), pr["value"]):
 
 
177
  return True
178
+ s.vars[pcfg["variable_name"]] = pr["value"]
179
+ return Fals