Spaces:
Building
Building
Update chat_handler.py
Browse files- chat_handler.py +247 -468
chat_handler.py
CHANGED
@@ -1,40 +1,28 @@
|
|
1 |
"""
|
2 |
-
Flare β Chat Handler
|
3 |
==========================================
|
4 |
"""
|
5 |
|
6 |
-
import os
|
7 |
-
import re, json, sys, httpx
|
8 |
from datetime import datetime
|
9 |
-
from typing import Dict, List, Optional
|
10 |
from fastapi import APIRouter, HTTPException, Header
|
11 |
from pydantic import BaseModel
|
12 |
import requests
|
13 |
|
14 |
-
from prompt_builder import build_intent_prompt, build_parameter_prompt
|
15 |
from utils import log
|
16 |
from api_executor import call_api as execute_api
|
|
|
17 |
from validation_engine import validate
|
18 |
from session import session_store, Session
|
19 |
-
from llm_interface import LLMInterface, SparkLLM, GPT4oLLM
|
20 |
-
from config_provider import ConfigProvider
|
21 |
-
from llm_factory import LLMFactory
|
22 |
-
|
23 |
-
# βββββββββββββββββββββββββ CONFIG βββββββββββββββββββββββββ #
|
24 |
-
# Global config reference
|
25 |
-
cfg = None
|
26 |
|
27 |
-
|
28 |
-
|
29 |
-
global cfg
|
30 |
-
cfg = ConfigProvider.get()
|
31 |
-
return cfg
|
32 |
-
|
33 |
-
# Initialize on module load
|
34 |
-
cfg = get_config()
|
35 |
|
36 |
-
#
|
37 |
-
|
|
|
38 |
|
39 |
# βββββββββββββββββββββββββ HELPERS βββββββββββββββββββββββββ #
|
40 |
def _trim_response(raw: str) -> str:
|
@@ -67,480 +55,82 @@ def _safe_intent_parse(raw: str) -> tuple[str, str]:
|
|
67 |
|
68 |
# βββββββββββββββββββββββββ LLM SETUP βββββββββββββββββββββββββ #
|
69 |
def setup_llm_provider():
|
70 |
-
"""Initialize LLM provider
|
71 |
global llm_provider
|
72 |
|
73 |
try:
|
|
|
74 |
llm_provider = LLMFactory.create_provider()
|
75 |
log("β
LLM provider initialized successfully")
|
76 |
except Exception as e:
|
77 |
log(f"β Failed to initialize LLM provider: {e}")
|
78 |
raise
|
79 |
|
80 |
-
# βββββββββββββββββββββββββ
|
81 |
async def llm_generate(s: Session, prompt: str, user_msg: str) -> str:
|
82 |
"""Call LLM provider with proper error handling"""
|
|
|
|
|
|
|
|
|
|
|
83 |
try:
|
84 |
-
# Get conversation context
|
85 |
-
context = [
|
86 |
-
{"role": msg["role"], "content": msg["content"]}
|
87 |
-
for msg in s.chat_history[-10:] # Last 10 messages
|
88 |
-
]
|
89 |
-
|
90 |
-
# Generate response
|
91 |
-
raw_response = await llm_provider.generate(
|
92 |
-
system_prompt=prompt,
|
93 |
-
user_input=user_msg,
|
94 |
-
context=context
|
95 |
-
)
|
96 |
-
|
97 |
-
log(f"π₯ LLM response length: {len(raw_response)}")
|
98 |
-
return raw_response
|
99 |
-
|
100 |
-
except httpx.TimeoutException:
|
101 |
-
log("β±οΈ LLM timeout - returning fallback")
|
102 |
-
return "Δ°steΔiniz zaman aΕΔ±mΔ±na uΔradΔ±. LΓΌtfen tekrar deneyin."
|
103 |
-
except Exception as e:
|
104 |
-
log(f"β LLM error: {str(e)}")
|
105 |
-
return "Bir hata oluΕtu. LΓΌtfen daha sonra tekrar deneyin."
|
106 |
-
|
107 |
-
# βββββββββββββββββββββββββ ALLOWED INTENTS βββββββββββββββββββββββββ #
|
108 |
-
ALLOWED_INTENTS = {"flight-booking", "flight-info", "booking-cancel"}
|
109 |
-
|
110 |
-
# βββββββββββββββββββββββββ FASTAPI βββββββββββββββββββββββββ #
|
111 |
-
router = APIRouter()
|
112 |
-
|
113 |
-
@router.get("/")
|
114 |
-
def health():
|
115 |
-
return {"status": "ok", "sessions": len(session_store._sessions)}
|
116 |
-
|
117 |
-
class StartRequest(BaseModel):
|
118 |
-
project_name: str
|
119 |
-
|
120 |
-
class ChatRequest(BaseModel):
|
121 |
-
user_input: str
|
122 |
-
|
123 |
-
class ChatResponse(BaseModel):
|
124 |
-
session_id: str
|
125 |
-
answer: str
|
126 |
-
|
127 |
-
@router.post("/start_session", response_model=ChatResponse)
|
128 |
-
async def start_session(req: StartRequest):
|
129 |
-
"""Create new session"""
|
130 |
-
try:
|
131 |
-
# Validate project exists
|
132 |
-
project = next((p for p in cfg.projects if p.name == req.project_name and p.enabled), None)
|
133 |
-
if not project:
|
134 |
-
raise HTTPException(404, f"Project '{req.project_name}' not found or disabled")
|
135 |
-
|
136 |
-
# Find published version
|
137 |
-
version = next((v for v in project.versions if v.published), None)
|
138 |
-
if not version:
|
139 |
-
raise HTTPException(404, f"No published version for project '{req.project_name}'")
|
140 |
-
|
141 |
-
# Create session with version config
|
142 |
-
session = session_store.create_session(req.project_name, version)
|
143 |
-
greeting = "HoΕ geldiniz! Size nasΔ±l yardΔ±mcΔ± olabilirim?"
|
144 |
-
session.add_turn("assistant", greeting)
|
145 |
-
|
146 |
-
return ChatResponse(session_id=session.session_id, answer=greeting)
|
147 |
-
|
148 |
-
except Exception as e:
|
149 |
-
log(f"β Error creating session: {e}")
|
150 |
-
raise HTTPException(500, str(e))
|
151 |
-
|
152 |
-
@router.post("/chat", response_model=ChatResponse)
|
153 |
-
async def chat(body: ChatRequest, x_session_id: str = Header(...)):
|
154 |
-
"""Process chat message"""
|
155 |
-
try:
|
156 |
-
# Get session
|
157 |
-
session = session_store.get_session(x_session_id)
|
158 |
-
if not session:
|
159 |
-
raise HTTPException(404, "Session not found")
|
160 |
-
|
161 |
-
user_input = body.user_input.strip()
|
162 |
-
if not user_input:
|
163 |
-
raise HTTPException(400, "Empty message")
|
164 |
-
|
165 |
-
log(f"π¬ User input: {user_input}")
|
166 |
-
log(f"π Session state: {session.state}, last_intent: {session.last_intent}")
|
167 |
-
log(f"π Session version: {session.version_number}")
|
168 |
-
|
169 |
-
session.add_turn("user", user_input)
|
170 |
-
|
171 |
# Get version config from session
|
172 |
-
version =
|
173 |
if not version:
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
log("π Handling new message")
|
182 |
-
answer = await _handle_new_message(session, user_input)
|
183 |
|
184 |
-
session.
|
185 |
-
|
186 |
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
session.add_turn("assistant", error_msg)
|
194 |
-
return ChatResponse(session_id=x_session_id, answer=error_msg)
|
195 |
-
|
196 |
-
# βββββββββββββββββββββββββ MESSAGE HANDLERS βββββββββββββββββββββββββ #
|
197 |
-
async def _handle_new_message(session: Session, user_input: str) -> str:
|
198 |
-
"""Handle new message (not parameter followup)"""
|
199 |
-
|
200 |
-
# Get version config from session
|
201 |
-
version = session.get_version_config()
|
202 |
-
if not version:
|
203 |
-
log("β Version config not found")
|
204 |
-
return "Bir hata oluΕtu. LΓΌtfen tekrar deneyin."
|
205 |
-
|
206 |
-
# Build intent detection prompt
|
207 |
-
prompt = build_intent_prompt(
|
208 |
-
version.general_prompt,
|
209 |
-
session.chat_history,
|
210 |
-
user_input,
|
211 |
-
version.intents
|
212 |
-
)
|
213 |
-
|
214 |
-
# Get LLM response
|
215 |
-
raw = await spark_generate(session, prompt, user_input)
|
216 |
-
|
217 |
-
# Empty response fallback
|
218 |
-
if not raw:
|
219 |
-
log("β οΈ Empty response from LLM")
|
220 |
-
return "ΓzgΓΌnΓΌm, mesajΔ±nΔ±zΔ± anlayamadΔ±m. LΓΌtfen tekrar dener misiniz?"
|
221 |
-
|
222 |
-
# Check for intent
|
223 |
-
if not raw.startswith("#DETECTED_INTENT"):
|
224 |
-
# Small talk response
|
225 |
-
log("π¬ No intent detected, returning small talk")
|
226 |
-
return _trim_response(raw)
|
227 |
-
|
228 |
-
# Parse intent
|
229 |
-
intent_name, tail = _safe_intent_parse(raw)
|
230 |
-
|
231 |
-
# Validate intent
|
232 |
-
if intent_name not in ALLOWED_INTENTS:
|
233 |
-
log(f"β οΈ Invalid intent: {intent_name}")
|
234 |
-
return _trim_response(tail) if tail else "Size nasΔ±l yardΔ±mcΔ± olabilirim?"
|
235 |
-
|
236 |
-
# Short message guard (less than 3 words usually means incomplete request)
|
237 |
-
if len(user_input.split()) < 3 and intent_name != "flight-info":
|
238 |
-
log(f"β οΈ Message too short ({len(user_input.split())} words) for intent {intent_name}")
|
239 |
-
return _trim_response(tail) if tail else "LΓΌtfen talebinizi biraz daha detaylandΔ±rΔ±r mΔ±sΔ±nΔ±z?"
|
240 |
-
|
241 |
-
# Find intent config
|
242 |
-
intent_config = next((i for i in version.intents if i.name == intent_name), None)
|
243 |
-
if not intent_config:
|
244 |
-
log(f"β Intent config not found for: {intent_name}")
|
245 |
-
return "ΓzgΓΌnΓΌm, bu iΕlemi gerΓ§ekleΕtiremiyorum."
|
246 |
-
|
247 |
-
# Set intent in session
|
248 |
-
session.last_intent = intent_name
|
249 |
-
log(f"β
Intent set: {intent_name}")
|
250 |
-
|
251 |
-
# Log intent parameters
|
252 |
-
log(f"π Intent parameters: {[p.name for p in intent_config.parameters]}")
|
253 |
-
|
254 |
-
# Extract parameters
|
255 |
-
return await _extract_parameters(session, intent_config, user_input)
|
256 |
-
|
257 |
-
async def _handle_parameter_followup(session: Session, user_input: str) -> str:
|
258 |
-
"""Handle parameter collection followup"""
|
259 |
-
if not session.last_intent:
|
260 |
-
log("β οΈ No last intent in session")
|
261 |
-
session.reset_flow()
|
262 |
-
return "ΓzgΓΌnΓΌm, hangi iΕlem iΓ§in bilgi istediΔimi unuttum. BaΕtan baΕlayalΔ±m."
|
263 |
-
|
264 |
-
# Get version config from session
|
265 |
-
version = session.get_version_config()
|
266 |
-
if not version:
|
267 |
-
log("β Version config not found")
|
268 |
-
session.reset_flow()
|
269 |
-
return "Bir hata oluΕtu. LΓΌtfen tekrar deneyin."
|
270 |
-
|
271 |
-
# Get intent config
|
272 |
-
intent_config = next((i for i in version.intents if i.name == session.last_intent), None)
|
273 |
-
if not intent_config:
|
274 |
-
log(f"β Intent config not found for: {session.last_intent}")
|
275 |
-
session.reset_flow()
|
276 |
-
return "Bir hata oluΕtu. LΓΌtfen tekrar deneyin."
|
277 |
-
|
278 |
-
# Smart parameter collection
|
279 |
-
if cfg.global_config.parameter_collection_config.smart_grouping:
|
280 |
-
return await _handle_smart_parameter_collection(session, intent_config, user_input)
|
281 |
-
else:
|
282 |
-
return await _handle_simple_parameter_collection(session, intent_config, user_input)
|
283 |
-
|
284 |
-
async def _handle_simple_parameter_collection(session: Session, intent_config, user_input: str) -> str:
|
285 |
-
"""Original simple parameter collection logic"""
|
286 |
-
# Try to extract missing parameters
|
287 |
-
missing = session.awaiting_parameters
|
288 |
-
log(f"π Trying to extract missing params: {missing}")
|
289 |
-
|
290 |
-
prompt = build_parameter_prompt(intent_config, missing, user_input, session.chat_history, intent_config.locale)
|
291 |
-
raw = await spark_generate(session, prompt, user_input)
|
292 |
-
|
293 |
-
# Try parsing with or without #PARAMETERS: prefix
|
294 |
-
success = _process_parameters(session, intent_config, raw)
|
295 |
-
|
296 |
-
if not success:
|
297 |
-
# Increment miss count
|
298 |
-
session.missing_ask_count += 1
|
299 |
-
log(f"β οΈ No parameters extracted, miss count: {session.missing_ask_count}")
|
300 |
|
301 |
-
|
302 |
-
|
303 |
-
return "ΓzgΓΌnΓΌm, istediΔiniz bilgileri anlayamadΔ±m. BaΕka bir konuda yardΔ±mcΔ± olabilir miyim?"
|
304 |
-
return "ΓzgΓΌnΓΌm, anlayamadΔ±m. LΓΌtfen tekrar sΓΆyler misiniz?"
|
305 |
-
|
306 |
-
# Check if we have all required parameters
|
307 |
-
missing = _get_missing_parameters(session, intent_config)
|
308 |
-
log(f"π Still missing params: {missing}")
|
309 |
-
|
310 |
-
if missing:
|
311 |
-
session.awaiting_parameters = missing
|
312 |
-
param = next(p for p in intent_config.parameters if p.name == missing[0])
|
313 |
-
return f"{param.caption} bilgisini alabilir miyim?"
|
314 |
-
|
315 |
-
# All parameters collected, call API
|
316 |
-
log("β
All parameters collected, calling API")
|
317 |
-
session.state = "call_api"
|
318 |
-
return await _execute_api_call(session, intent_config)
|
319 |
-
|
320 |
-
async def _handle_smart_parameter_collection(session: Session, intent_config, user_input: str) -> str:
|
321 |
-
"""Smart parameter collection with grouping and retry logic"""
|
322 |
-
|
323 |
-
# Try to extract missing parameters
|
324 |
-
missing = session.awaiting_parameters
|
325 |
-
log(f"π Trying to extract missing params: {missing}")
|
326 |
-
|
327 |
-
prompt = build_parameter_prompt(intent_config, missing, user_input, session.chat_history, intent_config.locale)
|
328 |
-
raw = await spark_generate(session, prompt, user_input)
|
329 |
-
|
330 |
-
# Try parsing with or without #PARAMETERS: prefix
|
331 |
-
success = _process_parameters(session, intent_config, raw)
|
332 |
-
|
333 |
-
# Hangi parametreler hala eksik?
|
334 |
-
still_missing = _get_missing_parameters(session, intent_config)
|
335 |
-
|
336 |
-
# Sorulan ama cevaplanmayan parametreleri belirle
|
337 |
-
asked_but_not_answered = []
|
338 |
-
for param in session.awaiting_parameters:
|
339 |
-
if param in still_missing:
|
340 |
-
asked_but_not_answered.append(param)
|
341 |
-
|
342 |
-
# CevaplanmayanlarΔ± session'a kaydet
|
343 |
-
if asked_but_not_answered:
|
344 |
-
session.mark_parameters_unanswered(asked_but_not_answered)
|
345 |
-
log(f"β Parameters not answered: {asked_but_not_answered}")
|
346 |
-
|
347 |
-
# CevaplananlarΔ± iΕaretle
|
348 |
-
for param in session.awaiting_parameters:
|
349 |
-
if param not in still_missing:
|
350 |
-
session.mark_parameter_answered(param)
|
351 |
-
log(f"β
Parameter answered: {param}")
|
352 |
-
|
353 |
-
if still_missing:
|
354 |
-
# Maksimum deneme kontrolΓΌ
|
355 |
-
if session.missing_ask_count >= 3:
|
356 |
-
session.reset_flow()
|
357 |
-
return "ΓzgΓΌnΓΌm, istediΔiniz bilgileri anlayamadΔ±m. BaΕka bir konuda yardΔ±mcΔ± olabilir miyim?"
|
358 |
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
return await _execute_api_call(session, intent_config)
|
366 |
-
|
367 |
-
async def _generate_smart_parameter_question(session: Session, intent_config, missing_params: List[str]) -> str:
|
368 |
-
"""Generate smart parameter collection question"""
|
369 |
-
|
370 |
-
# KaΓ§ parametre soracaΔΔ±mΔ±zΔ± belirle
|
371 |
-
max_params = cfg.global_config.parameter_collection_config.max_params_per_question
|
372 |
-
|
373 |
-
# Γncelik sΔ±rasΔ±na gΓΆre parametreleri seΓ§
|
374 |
-
params_to_ask = []
|
375 |
-
|
376 |
-
# Γnce daha ΓΆnce sorulmamΔ±Ε parametreler
|
377 |
-
for param in missing_params:
|
378 |
-
if session.get_parameter_ask_count(param) == 0:
|
379 |
-
params_to_ask.append(param)
|
380 |
-
if len(params_to_ask) >= max_params:
|
381 |
-
break
|
382 |
-
|
383 |
-
# Hala yer varsa, daha ΓΆnce sorulmuΕ ama cevaplanmamΔ±Ε parametreler
|
384 |
-
if len(params_to_ask) < max_params and cfg.global_config.parameter_collection_config.retry_unanswered:
|
385 |
-
for param in session.unanswered_parameters:
|
386 |
-
if param in missing_params and param not in params_to_ask:
|
387 |
-
params_to_ask.append(param)
|
388 |
-
if len(params_to_ask) >= max_params:
|
389 |
-
break
|
390 |
-
|
391 |
-
# Hala yer varsa, kalan parametreler
|
392 |
-
if len(params_to_ask) < max_params:
|
393 |
-
for param in missing_params:
|
394 |
-
if param not in params_to_ask:
|
395 |
-
params_to_ask.append(param)
|
396 |
-
if len(params_to_ask) >= max_params:
|
397 |
-
break
|
398 |
-
|
399 |
-
# Parametreleri session'a kaydet
|
400 |
-
session.record_parameter_question(params_to_ask)
|
401 |
-
session.awaiting_parameters = params_to_ask
|
402 |
-
session.missing_ask_count += 1
|
403 |
-
|
404 |
-
# Build smart question prompt
|
405 |
-
collected_params = {
|
406 |
-
p.name: session.variables.get(p.variable_name, "")
|
407 |
-
for p in intent_config.parameters
|
408 |
-
if p.variable_name in session.variables
|
409 |
-
}
|
410 |
-
|
411 |
-
question_prompt = build_smart_parameter_question_prompt(
|
412 |
-
intent_config,
|
413 |
-
params_to_ask,
|
414 |
-
session.chat_history,
|
415 |
-
collected_params,
|
416 |
-
session.unanswered_parameters,
|
417 |
-
cfg.global_config.parameter_collection_config.collection_prompt
|
418 |
-
)
|
419 |
-
|
420 |
-
# Generate natural question
|
421 |
-
question = await spark_generate(session, question_prompt, "")
|
422 |
-
|
423 |
-
# Clean up the response
|
424 |
-
question = _trim_response(question)
|
425 |
-
|
426 |
-
log(f"π€ Generated smart question for {params_to_ask}: {question}")
|
427 |
-
|
428 |
-
return question
|
429 |
-
|
430 |
-
# βββββββββββββββββββββββββ PARAMETER HANDLING βββββββββββββββββββββββββ #
|
431 |
-
async def _extract_parameters(session: Session, intent_config, user_input: str) -> str:
|
432 |
-
"""Extract parameters from user input"""
|
433 |
-
missing = _get_missing_parameters(session, intent_config)
|
434 |
-
log(f"π Missing parameters: {missing}")
|
435 |
-
|
436 |
-
if not missing:
|
437 |
-
# All parameters already available
|
438 |
-
log("β
All parameters already available")
|
439 |
-
return await _execute_api_call(session, intent_config)
|
440 |
-
|
441 |
-
# Build parameter extraction prompt
|
442 |
-
prompt = build_parameter_prompt(intent_config, missing, user_input, session.chat_history)
|
443 |
-
raw = await spark_generate(session, prompt, user_input)
|
444 |
-
|
445 |
-
# Try processing with flexible parsing
|
446 |
-
success = _process_parameters(session, intent_config, raw)
|
447 |
-
|
448 |
-
if success:
|
449 |
-
missing = _get_missing_parameters(session, intent_config)
|
450 |
-
log(f"π After extraction, missing: {missing}")
|
451 |
-
else:
|
452 |
-
log("β οΈ Failed to extract parameters from response")
|
453 |
-
|
454 |
-
if missing:
|
455 |
-
# Smart parameter collection
|
456 |
-
if cfg.global_config.parameter_collection_config.smart_grouping:
|
457 |
-
# Reset parameter tracking for new intent
|
458 |
-
session.reset_parameter_tracking()
|
459 |
-
return await _generate_smart_parameter_question(session, intent_config, missing)
|
460 |
-
else:
|
461 |
-
# Simple parameter collection
|
462 |
-
session.state = "await_param"
|
463 |
-
session.awaiting_parameters = missing
|
464 |
-
session.missing_ask_count = 0
|
465 |
-
param = next(p for p in intent_config.parameters if p.name == missing[0])
|
466 |
-
log(f"β Asking for parameter: {param.name} ({param.caption})")
|
467 |
-
return f"{param.caption} bilgisini alabilir miyim?"
|
468 |
-
|
469 |
-
# All parameters collected
|
470 |
-
log("β
All parameters collected after extraction")
|
471 |
-
return await _execute_api_call(session, intent_config)
|
472 |
-
|
473 |
-
def _get_missing_parameters(session: Session, intent_config) -> List[str]:
|
474 |
-
"""Get list of missing required parameters"""
|
475 |
-
missing = [
|
476 |
-
p.name for p in intent_config.parameters
|
477 |
-
if p.required and p.variable_name not in session.variables
|
478 |
-
]
|
479 |
-
log(f"π Session variables: {list(session.variables.keys())}")
|
480 |
-
return missing
|
481 |
|
482 |
-
|
483 |
-
|
|
|
484 |
try:
|
485 |
-
#
|
486 |
-
|
487 |
-
if
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
# Clean up any trailing content after JSON
|
492 |
-
# Find the closing brace for the JSON object
|
493 |
-
brace_count = 0
|
494 |
-
json_end = -1
|
495 |
-
in_string = False
|
496 |
-
escape_next = False
|
497 |
-
|
498 |
-
for i, char in enumerate(json_str):
|
499 |
-
if escape_next:
|
500 |
-
escape_next = False
|
501 |
-
continue
|
502 |
-
|
503 |
-
if char == '\\':
|
504 |
-
escape_next = True
|
505 |
-
continue
|
506 |
-
|
507 |
-
if char == '"' and not escape_next:
|
508 |
-
in_string = not in_string
|
509 |
-
continue
|
510 |
-
|
511 |
-
if not in_string:
|
512 |
-
if char == '{':
|
513 |
-
brace_count += 1
|
514 |
-
elif char == '}':
|
515 |
-
brace_count -= 1
|
516 |
-
if brace_count == 0:
|
517 |
-
json_end = i + 1
|
518 |
-
break
|
519 |
-
|
520 |
-
if json_end > 0:
|
521 |
-
json_str = json_str[:json_end]
|
522 |
-
log(f"π Cleaned JSON string: {json_str[:200]}")
|
523 |
|
524 |
-
|
|
|
|
|
525 |
|
526 |
-
|
527 |
-
|
528 |
|
529 |
any_valid = False
|
530 |
-
|
531 |
-
for param_data in extracted:
|
532 |
-
param_name = param_data.get("name")
|
533 |
-
param_value = param_data.get("value")
|
534 |
-
|
535 |
-
if not param_name or not param_value:
|
536 |
-
log(f"β οΈ Invalid param data: {param_data}")
|
537 |
-
continue
|
538 |
-
|
539 |
# Find parameter config
|
540 |
param_config = next(
|
541 |
(p for p in intent_config.parameters if p.name == param_name),
|
542 |
None
|
543 |
)
|
|
|
544 |
if not param_config:
|
545 |
log(f"β οΈ Parameter config not found for: {param_name}")
|
546 |
continue
|
@@ -591,7 +181,7 @@ async def _execute_api_call(session: Session, intent_config) -> str:
|
|
591 |
"{{api_response}}",
|
592 |
json.dumps(api_json, ensure_ascii=False)
|
593 |
)
|
594 |
-
human_response = await
|
595 |
session.reset_flow()
|
596 |
return human_response if human_response else f"Δ°Εlem sonucu: {api_json}"
|
597 |
else:
|
@@ -607,5 +197,194 @@ async def _execute_api_call(session: Session, intent_config) -> str:
|
|
607 |
session.reset_flow()
|
608 |
return intent_config.fallback_error_prompt or "Δ°Εlem sΔ±rasΔ±nda bir hata oluΕtu."
|
609 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
610 |
# Initialize LLM on module load
|
611 |
setup_llm_provider()
|
|
|
1 |
"""
|
2 |
+
Flare β Chat Handler (Refactored with LLM Factory)
|
3 |
==========================================
|
4 |
"""
|
5 |
|
6 |
+
import re, json, sys, httpx, os
|
|
|
7 |
from datetime import datetime
|
8 |
+
from typing import Dict, List, Optional, Any
|
9 |
from fastapi import APIRouter, HTTPException, Header
|
10 |
from pydantic import BaseModel
|
11 |
import requests
|
12 |
|
13 |
+
from prompt_builder import build_intent_prompt, build_parameter_prompt
|
14 |
from utils import log
|
15 |
from api_executor import call_api as execute_api
|
16 |
+
from config_provider import ConfigProvider
|
17 |
from validation_engine import validate
|
18 |
from session import session_store, Session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
|
20 |
+
# Initialize router
|
21 |
+
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
|
23 |
+
# βββββββββββββββββββββββββ GLOBAL VARS βββββββββββββββββββββββββ #
|
24 |
+
cfg = ConfigProvider.get()
|
25 |
+
llm_provider = None
|
26 |
|
27 |
# βββββββββββββββββββββββββ HELPERS βββββββββββββββββββββββββ #
|
28 |
def _trim_response(raw: str) -> str:
|
|
|
55 |
|
56 |
# βββββββββββββββββββββββββ LLM SETUP βββββββββββββββββββββββββ #
|
57 |
def setup_llm_provider():
|
58 |
+
"""Initialize LLM provider using factory pattern"""
|
59 |
global llm_provider
|
60 |
|
61 |
try:
|
62 |
+
from llm_factory import LLMFactory
|
63 |
llm_provider = LLMFactory.create_provider()
|
64 |
log("β
LLM provider initialized successfully")
|
65 |
except Exception as e:
|
66 |
log(f"β Failed to initialize LLM provider: {e}")
|
67 |
raise
|
68 |
|
69 |
+
# βββββββββββββββββββββββββ LLM GENERATION βββββββββββββββββββββββββ #
|
70 |
async def llm_generate(s: Session, prompt: str, user_msg: str) -> str:
|
71 |
"""Call LLM provider with proper error handling"""
|
72 |
+
global llm_provider
|
73 |
+
|
74 |
+
if llm_provider is None:
|
75 |
+
setup_llm_provider()
|
76 |
+
|
77 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
# Get version config from session
|
79 |
+
version = s.get_version_config()
|
80 |
if not version:
|
81 |
+
# Fallback: get from project config
|
82 |
+
project = next((p for p in cfg.projects if p.name == s.project_name), None)
|
83 |
+
if not project:
|
84 |
+
raise ValueError(f"Project not found: {s.project_name}")
|
85 |
+
version = next((v for v in project.versions if v.published), None)
|
86 |
+
if not version:
|
87 |
+
raise ValueError("No published version found")
|
|
|
|
|
88 |
|
89 |
+
log(f"π Calling LLM for session {s.session_id[:8]}...")
|
90 |
+
log(f"π Prompt preview (first 200 chars): {prompt[:200]}...")
|
91 |
|
92 |
+
# Call the configured LLM provider
|
93 |
+
raw = await llm_provider.generate(
|
94 |
+
user_input=user_msg,
|
95 |
+
system_prompt=prompt,
|
96 |
+
context=s.chat_history[-10:] if s.chat_history else []
|
97 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
|
99 |
+
log(f"πͺ LLM raw response: {raw[:100]}...")
|
100 |
+
return raw
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
|
102 |
+
except requests.exceptions.Timeout:
|
103 |
+
log(f"β±οΈ LLM timeout for session {s.session_id[:8]}")
|
104 |
+
raise HTTPException(status_code=504, detail="LLM request timed out")
|
105 |
+
except Exception as e:
|
106 |
+
log(f"β LLM error: {e}")
|
107 |
+
raise HTTPException(status_code=500, detail=f"LLM error: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
|
109 |
+
# βββββββββββββββββββββββββ PARAMETER EXTRACTION βββββββββββββββββββββββββ #
|
110 |
+
def _extract_parameters_from_response(raw: str, session: Session, intent_config) -> bool:
|
111 |
+
"""Extract parameters from the LLM response"""
|
112 |
try:
|
113 |
+
# Look for JSON block in response
|
114 |
+
json_match = re.search(r'```json\s*(.*?)\s*```', raw, re.DOTALL)
|
115 |
+
if not json_match:
|
116 |
+
# Try to find JSON without code block
|
117 |
+
json_match = re.search(r'\{[^}]+\}', raw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
118 |
|
119 |
+
if not json_match:
|
120 |
+
log("β No JSON found in response")
|
121 |
+
return False
|
122 |
|
123 |
+
json_str = json_match.group(1) if '```' in raw else json_match.group(0)
|
124 |
+
params = json.loads(json_str)
|
125 |
|
126 |
any_valid = False
|
127 |
+
for param_name, param_value in params.items():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
128 |
# Find parameter config
|
129 |
param_config = next(
|
130 |
(p for p in intent_config.parameters if p.name == param_name),
|
131 |
None
|
132 |
)
|
133 |
+
|
134 |
if not param_config:
|
135 |
log(f"β οΈ Parameter config not found for: {param_name}")
|
136 |
continue
|
|
|
181 |
"{{api_response}}",
|
182 |
json.dumps(api_json, ensure_ascii=False)
|
183 |
)
|
184 |
+
human_response = await llm_generate(session, prompt, json.dumps(api_json))
|
185 |
session.reset_flow()
|
186 |
return human_response if human_response else f"Δ°Εlem sonucu: {api_json}"
|
187 |
else:
|
|
|
197 |
session.reset_flow()
|
198 |
return intent_config.fallback_error_prompt or "Δ°Εlem sΔ±rasΔ±nda bir hata oluΕtu."
|
199 |
|
200 |
+
# βββββββββββββββββββββββββ REQUEST MODELS βββββββββββββββββββββββββ #
|
201 |
+
class SessionRequest(BaseModel):
|
202 |
+
project_name: str
|
203 |
+
test_mode: bool = False
|
204 |
+
|
205 |
+
class ChatRequest(BaseModel):
|
206 |
+
message: str
|
207 |
+
|
208 |
+
# βββββββββββββββββββββββββ API ENDPOINTS βββββββββββββββββββββββββ #
|
209 |
+
@router.post("/start_session")
|
210 |
+
async def start_session(req: SessionRequest):
|
211 |
+
"""Initialize a new chat session"""
|
212 |
+
try:
|
213 |
+
# Verify project exists and has published version
|
214 |
+
project = next((p for p in cfg.projects if p.name == req.project_name), None)
|
215 |
+
if not project:
|
216 |
+
raise HTTPException(status_code=404, detail=f"Project '{req.project_name}' not found")
|
217 |
+
|
218 |
+
if not project.enabled:
|
219 |
+
raise HTTPException(status_code=400, detail=f"Project '{req.project_name}' is disabled")
|
220 |
+
|
221 |
+
# Find published version
|
222 |
+
published_version = next((v for v in project.versions if v.published), None)
|
223 |
+
if not published_version:
|
224 |
+
raise HTTPException(status_code=400, detail=f"Project '{req.project_name}' has no published version")
|
225 |
+
|
226 |
+
# Create session
|
227 |
+
session = Session(
|
228 |
+
project_name=req.project_name,
|
229 |
+
version_id=published_version.id,
|
230 |
+
test_mode=req.test_mode
|
231 |
+
)
|
232 |
+
|
233 |
+
# Store version config in session
|
234 |
+
session.version_config = published_version
|
235 |
+
|
236 |
+
# Store session
|
237 |
+
session_store.add(session)
|
238 |
+
|
239 |
+
log(f"π New session created: {session.session_id[:8]}... for project '{req.project_name}' v{published_version.id}")
|
240 |
+
|
241 |
+
return {
|
242 |
+
"session_id": session.session_id,
|
243 |
+
"project_name": req.project_name,
|
244 |
+
"version": published_version.id,
|
245 |
+
"greeting": "Merhaba! Size nasΔ±l yardΔ±mcΔ± olabilirim?"
|
246 |
+
}
|
247 |
+
|
248 |
+
except HTTPException:
|
249 |
+
raise
|
250 |
+
except Exception as e:
|
251 |
+
log(f"β Session creation error: {e}")
|
252 |
+
raise HTTPException(status_code=500, detail=str(e))
|
253 |
+
|
254 |
+
@router.post("/chat")
|
255 |
+
async def chat(req: ChatRequest, x_session_id: str = Header(...)):
|
256 |
+
"""Process chat message"""
|
257 |
+
try:
|
258 |
+
# Get session
|
259 |
+
session = session_store.get(x_session_id)
|
260 |
+
if not session:
|
261 |
+
raise HTTPException(status_code=404, detail="Session not found or expired")
|
262 |
+
|
263 |
+
# Add user message to history
|
264 |
+
session.add_message("user", req.message)
|
265 |
+
log(f"π¬ User [{session.session_id[:8]}...]: {req.message}")
|
266 |
+
|
267 |
+
# Get project and version config
|
268 |
+
project = next((p for p in cfg.projects if p.name == session.project_name), None)
|
269 |
+
if not project:
|
270 |
+
raise HTTPException(status_code=404, detail=f"Project '{session.project_name}' not found")
|
271 |
+
|
272 |
+
version = session.get_version_config()
|
273 |
+
if not version:
|
274 |
+
raise HTTPException(status_code=400, detail="Version config not found in session")
|
275 |
+
|
276 |
+
# Process based on current state
|
277 |
+
if session.state == "idle":
|
278 |
+
# Build intent detection prompt
|
279 |
+
prompt = build_intent_prompt(version, session.chat_history, project.default_locale)
|
280 |
+
raw = await llm_generate(session, prompt, req.message)
|
281 |
+
|
282 |
+
# Check for intent
|
283 |
+
intent_name, tail = _safe_intent_parse(raw)
|
284 |
+
|
285 |
+
if intent_name:
|
286 |
+
# Find intent config
|
287 |
+
intent_config = next((i for i in version.intents if i.name == intent_name), None)
|
288 |
+
|
289 |
+
if intent_config:
|
290 |
+
session.current_intent = intent_name
|
291 |
+
session.intent_config = intent_config
|
292 |
+
session.state = "collect_params"
|
293 |
+
log(f"π― Intent detected: {intent_name}")
|
294 |
+
|
295 |
+
# Check if parameters were already extracted
|
296 |
+
if tail and _extract_parameters_from_response(tail, session, intent_config):
|
297 |
+
log("π¦ Some parameters extracted from initial response")
|
298 |
+
|
299 |
+
# Check what parameters are missing
|
300 |
+
missing_params = [
|
301 |
+
p for p in intent_config.parameters
|
302 |
+
if p.required and p.variable_name not in session.variables
|
303 |
+
]
|
304 |
+
|
305 |
+
if not missing_params:
|
306 |
+
# All required parameters collected, execute API
|
307 |
+
response = await _execute_api_call(session, intent_config)
|
308 |
+
session.add_message("assistant", response)
|
309 |
+
return {"response": response, "intent": intent_name, "state": "completed"}
|
310 |
+
else:
|
311 |
+
# Need to collect more parameters
|
312 |
+
param_prompt = build_parameter_prompt(
|
313 |
+
intent_config,
|
314 |
+
session.variables,
|
315 |
+
session.chat_history,
|
316 |
+
project.default_locale
|
317 |
+
)
|
318 |
+
param_question = await llm_generate(session, param_prompt, req.message)
|
319 |
+
clean_question = _trim_response(param_question)
|
320 |
+
session.add_message("assistant", clean_question)
|
321 |
+
return {"response": clean_question, "intent": intent_name, "state": "collecting_params"}
|
322 |
+
else:
|
323 |
+
log(f"β οΈ Unknown intent: {intent_name}")
|
324 |
+
|
325 |
+
# No intent detected, return general response
|
326 |
+
clean_response = _trim_response(raw)
|
327 |
+
session.add_message("assistant", clean_response)
|
328 |
+
return {"response": clean_response, "state": "idle"}
|
329 |
+
|
330 |
+
elif session.state == "collect_params":
|
331 |
+
# Continue parameter collection
|
332 |
+
intent_config = session.intent_config
|
333 |
+
|
334 |
+
# Try to extract parameters from user message
|
335 |
+
param_prompt = f"""
|
336 |
+
Extract parameters from user message: "{req.message}"
|
337 |
+
|
338 |
+
Expected parameters:
|
339 |
+
{json.dumps([{
|
340 |
+
'name': p.name,
|
341 |
+
'type': p.type,
|
342 |
+
'required': p.required,
|
343 |
+
'extraction_prompt': p.extraction_prompt
|
344 |
+
} for p in intent_config.parameters if p.variable_name not in session.variables], ensure_ascii=False)}
|
345 |
+
|
346 |
+
Return as JSON object with parameter names as keys.
|
347 |
+
"""
|
348 |
+
|
349 |
+
raw = await llm_generate(session, param_prompt, req.message)
|
350 |
+
_extract_parameters_from_response(raw, session, intent_config)
|
351 |
+
|
352 |
+
# Check what parameters are still missing
|
353 |
+
missing_params = [
|
354 |
+
p for p in intent_config.parameters
|
355 |
+
if p.required and p.variable_name not in session.variables
|
356 |
+
]
|
357 |
+
|
358 |
+
if not missing_params:
|
359 |
+
# All parameters collected, execute API
|
360 |
+
response = await _execute_api_call(session, intent_config)
|
361 |
+
session.add_message("assistant", response)
|
362 |
+
return {"response": response, "intent": session.current_intent, "state": "completed"}
|
363 |
+
else:
|
364 |
+
# Still need more parameters
|
365 |
+
param_prompt = build_parameter_prompt(
|
366 |
+
intent_config,
|
367 |
+
session.variables,
|
368 |
+
session.chat_history,
|
369 |
+
project.default_locale
|
370 |
+
)
|
371 |
+
param_question = await llm_generate(session, param_prompt, req.message)
|
372 |
+
clean_question = _trim_response(param_question)
|
373 |
+
session.add_message("assistant", clean_question)
|
374 |
+
return {"response": clean_question, "intent": session.current_intent, "state": "collecting_params"}
|
375 |
+
|
376 |
+
else:
|
377 |
+
# Unknown state, reset
|
378 |
+
session.reset_flow()
|
379 |
+
return {"response": "Bir hata oluΕtu, lΓΌtfen tekrar deneyin.", "state": "error"}
|
380 |
+
|
381 |
+
except HTTPException:
|
382 |
+
raise
|
383 |
+
except Exception as e:
|
384 |
+
log(f"β Chat error: {e}")
|
385 |
+
import traceback
|
386 |
+
traceback.print_exc()
|
387 |
+
raise HTTPException(status_code=500, detail=str(e))
|
388 |
+
|
389 |
# Initialize LLM on module load
|
390 |
setup_llm_provider()
|