ciyidogan commited on
Commit
14352db
·
verified ·
1 Parent(s): 2167be3

Update chat_handler.py

Browse files
Files changed (1) hide show
  1. chat_handler.py +235 -145
chat_handler.py CHANGED
@@ -1,154 +1,244 @@
1
  """
2
- Flare – Chat Handler (state-machine + header session)
 
 
 
 
3
  """
4
 
5
- from __future__ import annotations
6
- import json, re
7
- from typing import Dict, List
8
- from fastapi import APIRouter, HTTPException, Request
 
 
 
 
 
9
  from pydantic import BaseModel
10
- from config_provider import ConfigProvider, VersionConfig, IntentConfig, ParameterConfig
11
- from prompt_builder import (
12
- build_detection_prompt, build_param_extract_prompt,
13
- build_missing_param_prompt, build_api_humanize_prompt)
14
- from validation_engine import validate
15
- from api_executor import call_api
16
- from session import session_store, Session
17
- from utils import log
18
-
19
- cfg=ConfigProvider.get()
20
- router=APIRouter()
21
-
22
- _cancel_words=re.compile(r"\b(vazgeç|iptal|boşver|cancel)\b",re.I)
23
-
24
- # -------- helper ----------
25
- def detect_intent(version: VersionConfig, text: str) -> IntentConfig | None:
26
- for it in version.intents:
27
- prompt=build_detection_prompt(version.general_prompt,it)
28
- if it.name.lower() in text.lower(): # placeholder: ask_llm(prompt,text,"classification")
29
- return it
30
- return None
31
 
32
- # --------- endpoints -------
33
- class SessionStartRequest(BaseModel):
34
- project_name:str
35
- class SessionStartResponse(BaseModel):
36
- session_id:str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- @router.post("/start_session",response_model=SessionStartResponse)
39
- def start_session(body:SessionStartRequest):
40
- s=session_store.create_session(body.project_name)
41
- return SessionStartResponse(session_id=s.session_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  class ChatRequest(BaseModel):
44
- user_input:str
 
 
45
  class ChatResponse(BaseModel):
46
- reply:str
47
- session:Dict
48
-
49
- @router.post("/chat",response_model=ChatResponse)
50
- def chat(req:Request, body:ChatRequest):
51
- sid=req.headers.get("X-Session-ID")
52
- if not sid: raise HTTPException(400,"X-Session-ID header missing")
53
- s=session_store.get_session(sid)
54
- if not s: raise HTTPException(404,"Session not found")
55
-
56
- version=_get_live_version(s.project_name)
57
- user_text=body.user_input
58
- s.add_turn("user",user_text)
59
-
60
- # quick cancel
61
- if _cancel_words.search(user_text):
62
- s.reset_flow()
63
- reply="Elbette, başka bir konuda nasıl yardımcı olabilirim?"
64
- s.add_turn("assistant",reply)
65
- return ChatResponse(reply=reply,session=s.__dict__)
66
-
67
- # ---- idle state: intent arar
68
- if s.state=="idle":
69
- intent=detect_intent(version,user_text)
70
- if not intent:
71
- reply="(LLM-sohbet cevabı)" # burada ask_llm kullanılacak
72
- s.add_turn("assistant",reply)
73
- return ChatResponse(reply=reply,session=s.__dict__)
74
-
75
- s.last_intent=intent.name
76
- s.state="await_param"
77
- s.awaiting_parameters=[p.name for p in intent.parameters]
78
- s.variables={} # yeni intentte öncekileri sıfırla
79
-
80
- # ---- await_param state
81
- if s.state=="await_param":
82
- intent=next(i for i in version.intents if i.name==s.last_intent)
83
- # intent değişti mi?
84
- new_int=detect_intent(version,user_text)
85
- if new_int and new_int.name!=intent.name:
86
- log("🔄 new intent overrides current flow")
87
- s.reset_flow()
88
- s.last_intent=new_int.name
89
- s.state="await_param" if new_int.parameters else "call_api"
90
- s.awaiting_parameters=[p.name for p in new_int.parameters]
91
- intent=new_int
92
-
93
- # param extraction
94
- for p in intent.parameters:
95
- if p.name in s.variables: continue
96
- if p.name in user_text.lower(): # basit demo extraction
97
- val=user_text
98
- if not validate(val,p):
99
- reply=p.invalid_prompt or f"{p.caption or p.name} değerini doğrulayamadım."
100
- s.add_turn("assistant",reply)
101
- return ChatResponse(reply=reply,session=s.__dict__)
102
- s.variables[p.name]=val
103
- s.awaiting_parameters.remove(p.name)
104
-
105
- if s.awaiting_parameters:
106
- s.missing_ask_count+=1
107
- if s.missing_ask_count>=2:
108
- s.reset_flow()
109
- reply="Başka bir konuda yardımcı olabilir miyim?"
110
- s.add_turn("assistant",reply)
111
- return ChatResponse(reply=reply,session=s.__dict__)
112
-
113
- ask=build_missing_param_prompt(s.awaiting_parameters)
114
- s.add_turn("assistant",ask)
115
- return ChatResponse(reply=ask,session=s.__dict__)
116
-
117
- s.state="call_api"
118
-
119
- # ---- call_api state
120
- if s.state=="call_api":
121
- intent=next(i for i in version.intents if i.name==s.last_intent)
122
- api=cfg.get_api(intent.action)
123
- if not api: raise HTTPException(500,"API not found")
124
-
125
- try:
126
- resp=call_api(api,s.variables)
127
- except Exception as e:
128
- log(f"❌ API error {e}")
129
- s.reset_flow()
130
- reply=intent.fallback_error_prompt or "Hata oluştu."
131
- s.add_turn("assistant",reply)
132
- return ChatResponse(reply=reply,session=s.__dict__)
133
-
134
- s.api_raw=resp.json() # debug
135
- s.state="humanize"
136
-
137
- # ---- humanize
138
- if s.state=="humanize":
139
- intent=next(i for i in version.intents if i.name==s.last_intent)
140
- api=cfg.get_api(intent.action)
141
- human_prompt=build_api_humanize_prompt(
142
- version.general_prompt,
143
- api.response_prompt or "",
144
- json.dumps(s.api_raw,ensure_ascii=False,indent=2)
145
- )
146
- reply="(LLM-humanize)" # burada ask_llm kullanılacak
147
- s.add_turn("assistant",reply)
148
- s.reset_flow()
149
- return ChatResponse(reply=reply,session=s.__dict__)
150
-
151
-
152
- def _get_live_version(project_name:str)->VersionConfig:
153
- proj=next(p for p in cfg.projects if p.name==project_name and p.enabled)
154
- return max((v for v in proj.versions if v.published),key=lambda x:x.id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Flare – Chat Handler (v2)
3
+ ==========================
4
+ • /start_session • /chat
5
+ Intent tespiti, parametre çıkarımı, regex doğrulama, session değişkeni,
6
+ backend API çağrısı ve cevap özetleme.
7
  """
8
 
9
+ import re
10
+ import json
11
+ import uuid
12
+ import httpx
13
+ import commentjson
14
+ from datetime import datetime
15
+ from typing import Dict, List, Optional
16
+
17
+ from fastapi import FastAPI, HTTPException
18
  from pydantic import BaseModel
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ from prompt_builder import build_intent_prompt, build_parameter_prompt, log
21
+
22
+ # ----------------------------------------------------------------------------
23
+ # CONFIG LOAD (service_config.jsonc)
24
+ # ----------------------------------------------------------------------------
25
+ with open("service_config.jsonc", "r", encoding="utf-8") as f:
26
+ CFG = commentjson.load(f)
27
+
28
+ PROJECTS = {p["name"]: p for p in CFG["projects"]}
29
+ APIS = {a["name"]: a for a in CFG["apis"]}
30
+
31
+
32
+ # ----------------------------------------------------------------------------
33
+ # SESSION OBJECT
34
+ # ----------------------------------------------------------------------------
35
+ class Session:
36
+ def __init__(self, project_name: str):
37
+ self.id = str(uuid.uuid4())
38
+ self.project = PROJECTS[project_name]
39
+ self.history: List[Dict[str, str]] = []
40
+ self.variables: Dict[str, str] = {} # variable_name -> value
41
+ self.awaiting: Optional[Dict] = None # {"intent":..., "missing":[...]}
42
+ log(f"🆕 Session {self.id} for {project_name}")
43
+
44
+
45
+ SESSIONS: Dict[str, Session] = {}
46
+
47
 
48
+ # ----------------------------------------------------------------------------
49
+ # SPARK LLM WRAPPER (basit HTTP JSON)
50
+ # ----------------------------------------------------------------------------
51
+ async def spark_generate(prompt: str) -> str:
52
+ url = CFG["config"]["spark_endpoint"]
53
+ async with httpx.AsyncClient(timeout=60) as c:
54
+ r = await c.post(url, json={"prompt": prompt})
55
+ r.raise_for_status()
56
+ return r.json()["text"]
57
+
58
+
59
+ # ----------------------------------------------------------------------------
60
+ # FASTAPI APP
61
+ # ----------------------------------------------------------------------------
62
+ app = FastAPI()
63
+
64
+ @app.get("/")
65
+ def health():
66
+ return {"status": "ok"}
67
+
68
+
69
+ # ----------------------------------------------------------------------------
70
+ # SCHEMAS
71
+ # ----------------------------------------------------------------------------
72
+ class StartSessionRequest(BaseModel):
73
+ project_name: str
74
 
75
  class ChatRequest(BaseModel):
76
+ session_id: str
77
+ user_input: str
78
+
79
  class ChatResponse(BaseModel):
80
+ session_id: str
81
+ answer: str
82
+
83
+
84
+ # ----------------------------------------------------------------------------
85
+ # ENDPOINTS
86
+ # ----------------------------------------------------------------------------
87
+ @app.post("/start_session", response_model=ChatResponse)
88
+ async def start_session(req: StartSessionRequest):
89
+ if req.project_name not in PROJECTS:
90
+ raise HTTPException(404, "Unknown project")
91
+ s = Session(req.project_name)
92
+ SESSIONS[s.id] = s
93
+ return ChatResponse(session_id=s.id, answer="Nasıl yardımcı olabilirim?")
94
+
95
+ @app.post("/chat", response_model=ChatResponse)
96
+ async def chat(req: ChatRequest):
97
+ if req.session_id not in SESSIONS:
98
+ raise HTTPException(404, "Invalid session")
99
+
100
+ s = SESSIONS[req.session_id]
101
+ user_msg = req.user_input.strip()
102
+ s.history.append({"role": "user", "content": user_msg})
103
+
104
+ # ---------------- Parametre follow-up modunda mı? ----------------
105
+ if s.awaiting:
106
+ answer = await _followup_flow(s, user_msg)
107
+ s.history.append({"role": "assistant", "content": answer})
108
+ return ChatResponse(session_id=s.id, answer=answer)
109
+
110
+ # ---------------- Intent detection ----------------
111
+ gen_prompt = s.project["versions"][0]["general_prompt"]
112
+ prompt = build_intent_prompt(gen_prompt, s.history, user_msg)
113
+ llm_out = await spark_generate(prompt)
114
+
115
+ if not llm_out.startswith("#DETECTED_INTENT:"):
116
+ # Small-talk cevabı
117
+ s.history.append({"role": "assistant", "content": llm_out})
118
+ return ChatResponse(session_id=s.id, answer=llm_out)
119
+
120
+ intent_name = llm_out.split(":", 1)[1].strip()
121
+ intent_cfg = _get_intent_cfg(s.project, intent_name)
122
+ if not intent_cfg:
123
+ err = "Üzgünüm, bu konuda yardımcı olamıyorum."
124
+ s.history.append({"role": "assistant", "content": err})
125
+ return ChatResponse(session_id=s.id, answer=err)
126
+
127
+ answer = await _handle_intent(s, intent_cfg, user_msg)
128
+ s.history.append({"role": "assistant", "content": answer})
129
+ return ChatResponse(session_id=s.id, answer=answer)
130
+
131
+
132
+ # ----------------------------------------------------------------------------
133
+ # CORE HELPERS
134
+ # ----------------------------------------------------------------------------
135
+ def _get_intent_cfg(project: Dict, name_: str) -> Optional[Dict]:
136
+ for it in project["versions"][0]["intents"]:
137
+ if it["name"] == name_:
138
+ return it
139
+ return None
140
+
141
+ def _current_missing(session: Session, intent_cfg: Dict) -> List[str]:
142
+ return [
143
+ p["name"]
144
+ for p in intent_cfg["parameters"]
145
+ if p["variable_name"] not in session.variables
146
+ ]
147
+
148
+ async def _handle_intent(session: Session,
149
+ intent_cfg: Dict,
150
+ user_msg: str) -> str:
151
+ missing = _current_missing(session, intent_cfg)
152
+
153
+ # --- Parametre extraction denemesi (ilk mesajda) ---
154
+ if missing:
155
+ p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, session.history)
156
+ llm_out = await spark_generate(p_prompt)
157
+
158
+ if llm_out.startswith("#PARAMETERS:"):
159
+ ok = _process_param_output(session, intent_cfg, llm_out)
160
+ if not ok:
161
+ return ok # invalid prompt dönebilir
162
+ missing = _current_missing(session, intent_cfg)
163
+
164
+ # --- Hâlâ eksik mi? follow-up sor ---
165
+ if missing:
166
+ session.awaiting = {"intent": intent_cfg, "missing": missing}
167
+ first_caption = next(p for p in intent_cfg["parameters"]
168
+ if p["name"] == missing[0])["caption"]
169
+ return f"{first_caption} nedir?"
170
+
171
+ # --- Tüm parametreler hazır → API çağır ---
172
+ return await _call_api(session, intent_cfg)
173
+
174
+ async def _followup_flow(session: Session,
175
+ user_msg: str) -> str:
176
+ intent_cfg = session.awaiting["intent"]
177
+ missing = session.awaiting["missing"]
178
+
179
+ p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, session.history)
180
+ llm_out = await spark_generate(p_prompt)
181
+
182
+ if not llm_out.startswith("#PARAMETERS:"):
183
+ return "Üzgünüm, bilgileri anlayamadım."
184
+
185
+ ok = _process_param_output(session, intent_cfg, llm_out)
186
+ if not ok:
187
+ return ok
188
+
189
+ missing = _current_missing(session, intent_cfg)
190
+ if missing:
191
+ session.awaiting["missing"] = missing
192
+ first_caption = next(p for p in intent_cfg["parameters"]
193
+ if p["name"] == missing[0])["caption"]
194
+ return f"{first_caption} nedir?"
195
+
196
+ session.awaiting = None
197
+ return await _call_api(session, intent_cfg)
198
+
199
+ def _process_param_output(session: Session,
200
+ intent_cfg: Dict,
201
+ llm_out: str) -> Optional[str]:
202
+ try:
203
+ data = json.loads(llm_out[len("#PARAMETERS:"):])
204
+ except json.JSONDecodeError:
205
+ return "Üzgünüm, parametreleri çözemedim."
206
+
207
+ for pair in data.get("extracted", []):
208
+ p_cfg = next(p for p in intent_cfg["parameters"] if p["name"] == pair["name"])
209
+ if not _validate(p_cfg, pair["value"]):
210
+ return p_cfg.get("invalid_prompt", "Geçersiz değer.")
211
+ session.variables[p_cfg["variable_name"]] = pair["value"]
212
+ return None # success
213
+
214
+ def _validate(p_cfg: Dict, value: str) -> bool:
215
+ pattern = p_cfg.get("validation_regex")
216
+ return re.match(pattern, value) is not None if pattern else True
217
+
218
+ async def _call_api(session: Session, intent_cfg: Dict) -> str:
219
+ api = APIS[intent_cfg["action"]]
220
+
221
+ # Simple token
222
+ token = "testtoken"
223
+ headers = {k: v.replace("{{token}}", token) for k, v in api["headers"].items()}
224
+
225
+ body = json.loads(json.dumps(api["body_template"])) # deep copy
226
+ for k, v in body.items():
227
+ if isinstance(v, str) and v.startswith("{{") and v.endswith("}}"):
228
+ body[k] = session.variables.get(v[2:-2], "")
229
+
230
+ log(f"➡️ {api['name']} {body}")
231
+ try:
232
+ async with httpx.AsyncClient(timeout=api["timeout_seconds"]) as c:
233
+ r = await c.request(api["method"], api["url"], headers=headers, json=body)
234
+ r.raise_for_status()
235
+ api_json = r.json()
236
+ except Exception as ex:
237
+ log(f"❌ API error: {ex}")
238
+ return intent_cfg["fallback_error_prompt"]
239
+
240
+ # LLM’ye özetlet
241
+ summary_prompt = api["response_prompt"].replace(
242
+ "{{api_response}}", json.dumps(api_json, ensure_ascii=False)
243
+ )
244
+ return await spark_generate(summary_prompt)