Spaces:
Building
Building
Update chat_handler.py
Browse files- chat_handler.py +80 -138
chat_handler.py
CHANGED
@@ -1,90 +1,69 @@
|
|
1 |
"""
|
2 |
-
Flare – Chat Handler
|
3 |
-
|
4 |
-
|
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
|
18 |
from pydantic import BaseModel
|
19 |
|
20 |
from prompt_builder import build_intent_prompt, build_parameter_prompt, log
|
21 |
|
22 |
# ----------------------------------------------------------------------------
|
23 |
-
# CONFIG
|
24 |
# ----------------------------------------------------------------------------
|
25 |
-
|
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
|
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] = {}
|
41 |
-
self.awaiting: Optional[Dict] = None
|
42 |
log(f"🆕 Session {self.id} for {project_name}")
|
43 |
|
44 |
-
|
45 |
SESSIONS: Dict[str, Session] = {}
|
46 |
|
47 |
-
|
48 |
# ----------------------------------------------------------------------------
|
49 |
-
# SPARK
|
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(
|
55 |
r.raise_for_status()
|
56 |
return r.json()["text"]
|
57 |
|
58 |
-
|
59 |
# ----------------------------------------------------------------------------
|
60 |
-
# FASTAPI
|
61 |
# ----------------------------------------------------------------------------
|
62 |
-
|
63 |
|
64 |
-
@
|
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")
|
@@ -92,35 +71,33 @@ async def start_session(req: StartSessionRequest):
|
|
92 |
SESSIONS[s.id] = s
|
93 |
return ChatResponse(session_id=s.id, answer="Nasıl yardımcı olabilirim?")
|
94 |
|
95 |
-
@
|
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 |
-
#
|
105 |
if s.awaiting:
|
106 |
-
answer = await
|
107 |
s.history.append({"role": "assistant", "content": answer})
|
108 |
return ChatResponse(session_id=s.id, answer=answer)
|
109 |
|
110 |
-
#
|
111 |
gen_prompt = s.project["versions"][0]["general_prompt"]
|
112 |
-
|
113 |
-
|
114 |
|
115 |
-
if not
|
116 |
-
|
117 |
-
s.
|
118 |
-
return ChatResponse(session_id=s.id, answer=llm_out)
|
119 |
|
120 |
-
intent_name =
|
121 |
-
intent_cfg =
|
122 |
if not intent_cfg:
|
123 |
-
err = "Üzgünüm,
|
124 |
s.history.append({"role": "assistant", "content": err})
|
125 |
return ChatResponse(session_id=s.id, answer=err)
|
126 |
|
@@ -128,117 +105,82 @@ async def chat(req: ChatRequest):
|
|
128 |
s.history.append({"role": "assistant", "content": answer})
|
129 |
return ChatResponse(session_id=s.id, answer=answer)
|
130 |
|
131 |
-
|
132 |
# ----------------------------------------------------------------------------
|
133 |
-
#
|
134 |
# ----------------------------------------------------------------------------
|
135 |
-
def
|
136 |
-
for
|
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 |
-
|
149 |
-
|
150 |
-
user_msg: str) -> str:
|
151 |
-
missing = _current_missing(session, intent_cfg)
|
152 |
|
153 |
-
|
|
|
154 |
if missing:
|
155 |
-
p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg,
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
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 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
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 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
|
|
195 |
|
196 |
-
|
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(
|
204 |
except json.JSONDecodeError:
|
205 |
-
return "Üzgünüm, parametreleri
|
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
|
210 |
return p_cfg.get("invalid_prompt", "Geçersiz değer.")
|
211 |
-
|
212 |
-
return None
|
213 |
|
214 |
-
def
|
215 |
-
|
216 |
-
return re.match(
|
217 |
|
218 |
-
async def _call_api(
|
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"]))
|
226 |
for k, v in body.items():
|
227 |
if isinstance(v, str) and v.startswith("{{") and v.endswith("}}"):
|
228 |
-
body[k] =
|
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
|
237 |
-
log(f"❌ API error: {ex}")
|
238 |
return intent_cfg["fallback_error_prompt"]
|
239 |
|
240 |
-
|
241 |
-
summary_prompt = api["response_prompt"].replace(
|
242 |
"{{api_response}}", json.dumps(api_json, ensure_ascii=False)
|
243 |
)
|
244 |
-
return await spark_generate(
|
|
|
1 |
"""
|
2 |
+
Flare – Chat Handler (router edition)
|
3 |
+
=====================================
|
4 |
+
app.py → from chat_handler import router
|
|
|
|
|
5 |
"""
|
6 |
|
7 |
+
import re, json, uuid, httpx, commentjson
|
|
|
|
|
|
|
|
|
8 |
from datetime import datetime
|
9 |
from typing import Dict, List, Optional
|
10 |
|
11 |
+
from fastapi import APIRouter, HTTPException
|
12 |
from pydantic import BaseModel
|
13 |
|
14 |
from prompt_builder import build_intent_prompt, build_parameter_prompt, log
|
15 |
|
16 |
# ----------------------------------------------------------------------------
|
17 |
+
# CONFIG
|
18 |
# ----------------------------------------------------------------------------
|
19 |
+
CFG = commentjson.load(open("service_config.jsonc", encoding="utf-8"))
|
|
|
|
|
20 |
PROJECTS = {p["name"]: p for p in CFG["projects"]}
|
21 |
APIS = {a["name"]: a for a in CFG["apis"]}
|
22 |
|
|
|
23 |
# ----------------------------------------------------------------------------
|
24 |
+
# SESSION
|
25 |
# ----------------------------------------------------------------------------
|
26 |
class Session:
|
27 |
def __init__(self, project_name: str):
|
28 |
self.id = str(uuid.uuid4())
|
29 |
self.project = PROJECTS[project_name]
|
30 |
self.history: List[Dict[str, str]] = []
|
31 |
+
self.variables: Dict[str, str] = {}
|
32 |
+
self.awaiting: Optional[Dict] = None
|
33 |
log(f"🆕 Session {self.id} for {project_name}")
|
34 |
|
|
|
35 |
SESSIONS: Dict[str, Session] = {}
|
36 |
|
|
|
37 |
# ----------------------------------------------------------------------------
|
38 |
+
# SPARK
|
39 |
# ----------------------------------------------------------------------------
|
40 |
async def spark_generate(prompt: str) -> str:
|
|
|
41 |
async with httpx.AsyncClient(timeout=60) as c:
|
42 |
+
r = await c.post(CFG["config"]["spark_endpoint"], json={"prompt": prompt})
|
43 |
r.raise_for_status()
|
44 |
return r.json()["text"]
|
45 |
|
|
|
46 |
# ----------------------------------------------------------------------------
|
47 |
+
# FASTAPI ROUTER
|
48 |
# ----------------------------------------------------------------------------
|
49 |
+
router = APIRouter() # <<–– exported to app.py
|
50 |
|
51 |
+
@router.get("/")
|
52 |
def health():
|
53 |
return {"status": "ok"}
|
54 |
|
55 |
+
# Schemas
|
|
|
|
|
|
|
56 |
class StartSessionRequest(BaseModel):
|
57 |
project_name: str
|
|
|
58 |
class ChatRequest(BaseModel):
|
59 |
session_id: str
|
60 |
user_input: str
|
|
|
61 |
class ChatResponse(BaseModel):
|
62 |
session_id: str
|
63 |
answer: str
|
64 |
|
65 |
+
# Endpoints
|
66 |
+
@router.post("/start_session", response_model=ChatResponse)
|
|
|
|
|
|
|
67 |
async def start_session(req: StartSessionRequest):
|
68 |
if req.project_name not in PROJECTS:
|
69 |
raise HTTPException(404, "Unknown project")
|
|
|
71 |
SESSIONS[s.id] = s
|
72 |
return ChatResponse(session_id=s.id, answer="Nasıl yardımcı olabilirim?")
|
73 |
|
74 |
+
@router.post("/chat", response_model=ChatResponse)
|
75 |
async def chat(req: ChatRequest):
|
76 |
if req.session_id not in SESSIONS:
|
77 |
raise HTTPException(404, "Invalid session")
|
|
|
78 |
s = SESSIONS[req.session_id]
|
79 |
user_msg = req.user_input.strip()
|
80 |
s.history.append({"role": "user", "content": user_msg})
|
81 |
|
82 |
+
# --- follow-up? ---
|
83 |
if s.awaiting:
|
84 |
+
answer = await _followup(s, user_msg)
|
85 |
s.history.append({"role": "assistant", "content": answer})
|
86 |
return ChatResponse(session_id=s.id, answer=answer)
|
87 |
|
88 |
+
# --- intent detect ---
|
89 |
gen_prompt = s.project["versions"][0]["general_prompt"]
|
90 |
+
intent_prompt = build_intent_prompt(gen_prompt, s.history, user_msg)
|
91 |
+
intent_out = await spark_generate(intent_prompt)
|
92 |
|
93 |
+
if not intent_out.startswith("#DETECTED_INTENT:"):
|
94 |
+
s.history.append({"role": "assistant", "content": intent_out})
|
95 |
+
return ChatResponse(session_id=s.id, answer=intent_out)
|
|
|
96 |
|
97 |
+
intent_name = intent_out.split(":", 1)[1].strip()
|
98 |
+
intent_cfg = _find_intent(s.project, intent_name)
|
99 |
if not intent_cfg:
|
100 |
+
err = "Üzgünüm, anlayamadım."
|
101 |
s.history.append({"role": "assistant", "content": err})
|
102 |
return ChatResponse(session_id=s.id, answer=err)
|
103 |
|
|
|
105 |
s.history.append({"role": "assistant", "content": answer})
|
106 |
return ChatResponse(session_id=s.id, answer=answer)
|
107 |
|
|
|
108 |
# ----------------------------------------------------------------------------
|
109 |
+
# Helper funcs (değişmedi, sadece router’a ihtiyaç yok)
|
110 |
# ----------------------------------------------------------------------------
|
111 |
+
def _find_intent(project, name_):
|
112 |
+
return next((i for i in project["versions"][0]["intents"] if i["name"] == name_), None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
|
114 |
+
def _missing(session, intent_cfg):
|
115 |
+
return [p["name"] for p in intent_cfg["parameters"] if p["variable_name"] not in session.variables]
|
|
|
|
|
116 |
|
117 |
+
async def _handle_intent(s, intent_cfg, user_msg):
|
118 |
+
missing = _missing(s, intent_cfg)
|
119 |
if missing:
|
120 |
+
p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, s.history)
|
121 |
+
p_out = await spark_generate(p_prompt)
|
122 |
+
if p_out.startswith("#PARAMETERS:"):
|
123 |
+
if bad := _process_params(s, intent_cfg, p_out):
|
124 |
+
return bad
|
125 |
+
missing = _missing(s, intent_cfg)
|
|
|
|
|
|
|
|
|
126 |
if missing:
|
127 |
+
s.awaiting = {"intent": intent_cfg, "missing": missing}
|
128 |
+
cap = next(p for p in intent_cfg["parameters"] if p["name"] == missing[0])["caption"]
|
129 |
+
return f"{cap} nedir?"
|
130 |
+
s.awaiting = None
|
131 |
+
return await _call_api(s, intent_cfg)
|
132 |
+
|
133 |
+
async def _followup(s, user_msg):
|
134 |
+
intent_cfg = s.awaiting["intent"]
|
135 |
+
missing = s.awaiting["missing"]
|
136 |
+
p_prompt = build_parameter_prompt(intent_cfg, missing, user_msg, s.history)
|
137 |
+
p_out = await spark_generate(p_prompt)
|
138 |
+
if not p_out.startswith("#PARAMETERS:"):
|
139 |
+
return "Üzgünüm, anlayamadım."
|
140 |
+
if bad := _process_params(s, intent_cfg, p_out):
|
141 |
+
return bad
|
142 |
+
missing = _missing(s, intent_cfg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
if missing:
|
144 |
+
s.awaiting["missing"] = missing
|
145 |
+
cap = next(p for p in intent_cfg["parameters"] if p["name"] == missing[0])["caption"]
|
146 |
+
return f"{cap} nedir?"
|
147 |
+
s.awaiting = None
|
148 |
+
return await _call_api(s, intent_cfg)
|
149 |
|
150 |
+
def _process_params(s, intent_cfg, p_out):
|
|
|
|
|
|
|
|
|
|
|
151 |
try:
|
152 |
+
data = json.loads(p_out[len("#PARAMETERS:"):])
|
153 |
except json.JSONDecodeError:
|
154 |
+
return "Üzgünüm, parametreleri çözemiyorum."
|
|
|
155 |
for pair in data.get("extracted", []):
|
156 |
p_cfg = next(p for p in intent_cfg["parameters"] if p["name"] == pair["name"])
|
157 |
+
if not _valid(p_cfg, pair["value"]):
|
158 |
return p_cfg.get("invalid_prompt", "Geçersiz değer.")
|
159 |
+
s.variables[p_cfg["variable_name"]] = pair["value"]
|
160 |
+
return None
|
161 |
|
162 |
+
def _valid(p_cfg, val):
|
163 |
+
rx = p_cfg.get("validation_regex")
|
164 |
+
return re.match(rx, val) is not None if rx else True
|
165 |
|
166 |
+
async def _call_api(s, intent_cfg):
|
167 |
api = APIS[intent_cfg["action"]]
|
168 |
+
token = "testtoken"
|
|
|
|
|
169 |
headers = {k: v.replace("{{token}}", token) for k, v in api["headers"].items()}
|
170 |
|
171 |
+
body = json.loads(json.dumps(api["body_template"]))
|
172 |
for k, v in body.items():
|
173 |
if isinstance(v, str) and v.startswith("{{") and v.endswith("}}"):
|
174 |
+
body[k] = s.variables.get(v[2:-2], "")
|
|
|
|
|
175 |
try:
|
176 |
async with httpx.AsyncClient(timeout=api["timeout_seconds"]) as c:
|
177 |
r = await c.request(api["method"], api["url"], headers=headers, json=body)
|
178 |
r.raise_for_status()
|
179 |
api_json = r.json()
|
180 |
+
except Exception:
|
|
|
181 |
return intent_cfg["fallback_error_prompt"]
|
182 |
|
183 |
+
summary = api["response_prompt"].replace(
|
|
|
184 |
"{{api_response}}", json.dumps(api_json, ensure_ascii=False)
|
185 |
)
|
186 |
+
return await spark_generate(summary)
|