Spaces:
Paused
Paused
Update chat_handler.py
Browse files- chat_handler.py +226 -267
chat_handler.py
CHANGED
|
@@ -18,9 +18,20 @@ 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 locale_manager import LocaleManager
|
| 22 |
|
| 23 |
-
# βββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
# Global LLM instance
|
| 25 |
llm_provider: Optional[LLMInterface] = None
|
| 26 |
|
|
@@ -52,83 +63,93 @@ def _safe_intent_parse(raw: str) -> tuple[str, str]:
|
|
| 52 |
tail = raw[m.end():]
|
| 53 |
log(f"π― Parsed intent: {name}")
|
| 54 |
return name, tail
|
| 55 |
-
|
| 56 |
-
# βββββββββββββββββββββββββ
|
| 57 |
-
def
|
| 58 |
-
"""Initialize LLM provider based on
|
| 59 |
global llm_provider
|
| 60 |
|
| 61 |
-
cfg = ConfigProvider.get()
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
if not api_key:
|
| 69 |
-
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
model
|
| 72 |
llm_provider = GPT4oLLM(api_key, model)
|
| 73 |
-
log(f"β
Initialized {model} provider")
|
| 74 |
else:
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
if not spark_token:
|
| 78 |
-
raise ValueError("Spark token not configured")
|
| 79 |
-
|
| 80 |
-
spark_endpoint = str(cfg.global_config.spark_endpoint)
|
| 81 |
-
llm_provider = SparkLLM(spark_endpoint, spark_token)
|
| 82 |
-
log("β
Initialized Spark provider")
|
| 83 |
-
|
| 84 |
-
# βββββββββββββββββββββββββ SPARK βββββββββββββββββββββββββ #
|
| 85 |
-
def _get_spark_token() -> Optional[str]:
|
| 86 |
-
"""Get Spark token based on work_mode"""
|
| 87 |
-
cfg = ConfigProvider.get()
|
| 88 |
-
|
| 89 |
-
work_mode = cfg.global_config.work_mode
|
| 90 |
-
|
| 91 |
-
if cfg.global_config.is_cloud_mode():
|
| 92 |
-
token = os.getenv("SPARK_TOKEN")
|
| 93 |
-
if not token and not cfg.global_config.is_gpt_mode():
|
| 94 |
-
# GPT modlarΔ± iΓ§in SPARK_TOKEN gerekmez
|
| 95 |
-
log("β SPARK_TOKEN not found in cloud Secrets!")
|
| 96 |
-
return token
|
| 97 |
-
else:
|
| 98 |
-
# On-premise mode - use .env file
|
| 99 |
-
from dotenv import load_dotenv
|
| 100 |
-
load_dotenv()
|
| 101 |
-
return os.getenv("SPARK_TOKEN")
|
| 102 |
|
|
|
|
| 103 |
async def spark_generate(s: Session, prompt: str, user_msg: str) -> str:
|
| 104 |
-
"""Call LLM provider with proper error handling"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
try:
|
| 106 |
-
#
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
# Use the abstract interface
|
| 113 |
-
raw = await llm_provider.generate(prompt, user_msg, s.chat_history)
|
| 114 |
log(f"πͺ LLM raw response: {raw[:120]!r}")
|
| 115 |
return raw
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
| 117 |
except Exception as e:
|
| 118 |
log(f"β LLM error: {e}")
|
| 119 |
raise
|
| 120 |
|
|
|
|
|
|
|
|
|
|
| 121 |
# βββββββββββββββββββββββββ FASTAPI βββββββββββββββββββββββββ #
|
| 122 |
router = APIRouter()
|
| 123 |
|
| 124 |
-
@router.get("/
|
| 125 |
-
def
|
| 126 |
-
"""
|
| 127 |
-
return {
|
| 128 |
-
"status": "ok",
|
| 129 |
-
"sessions": len(session_store._sessions),
|
| 130 |
-
"timestamp": datetime.now().isoformat()
|
| 131 |
-
}
|
| 132 |
|
| 133 |
class StartRequest(BaseModel):
|
| 134 |
project_name: str
|
|
@@ -144,19 +165,17 @@ class ChatResponse(BaseModel):
|
|
| 144 |
async def start_session(req: StartRequest):
|
| 145 |
"""Create new session"""
|
| 146 |
try:
|
| 147 |
-
cfg = ConfigProvider.get()
|
| 148 |
-
|
| 149 |
# Validate project exists
|
| 150 |
project = next((p for p in cfg.projects if p.name == req.project_name and p.enabled), None)
|
| 151 |
if not project:
|
| 152 |
raise HTTPException(404, f"Project '{req.project_name}' not found or disabled")
|
| 153 |
-
|
| 154 |
-
#
|
| 155 |
version = next((v for v in project.versions if v.published), None)
|
| 156 |
if not version:
|
| 157 |
-
raise HTTPException(404, f"No published version
|
| 158 |
-
|
| 159 |
-
# Create session
|
| 160 |
session = session_store.create_session(req.project_name, version)
|
| 161 |
greeting = "HoΕ geldiniz! Size nasΔ±l yardΔ±mcΔ± olabilirim?"
|
| 162 |
session.add_turn("assistant", greeting)
|
|
@@ -194,10 +213,10 @@ async def chat(body: ChatRequest, x_session_id: str = Header(...)):
|
|
| 194 |
# Handle based on state
|
| 195 |
if session.state == "await_param":
|
| 196 |
log(f"π Handling parameter followup for missing: {session.awaiting_parameters}")
|
| 197 |
-
answer = await _handle_parameter_followup(session, user_input)
|
| 198 |
else:
|
| 199 |
log("π Handling new message")
|
| 200 |
-
answer = await _handle_new_message(session, user_input)
|
| 201 |
|
| 202 |
session.add_turn("assistant", answer)
|
| 203 |
return ChatResponse(session_id=session.session_id, answer=answer)
|
|
@@ -226,8 +245,7 @@ async def _handle_new_message(session: Session, user_input: str) -> str:
|
|
| 226 |
version.general_prompt,
|
| 227 |
session.chat_history,
|
| 228 |
user_input,
|
| 229 |
-
version.intents
|
| 230 |
-
session.project_name
|
| 231 |
)
|
| 232 |
|
| 233 |
# Get LLM response
|
|
@@ -247,10 +265,9 @@ async def _handle_new_message(session: Session, user_input: str) -> str:
|
|
| 247 |
# Parse intent
|
| 248 |
intent_name, tail = _safe_intent_parse(raw)
|
| 249 |
|
| 250 |
-
# Validate intent
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
log(f"β οΈ Invalid intent: {intent_name} (valid: {valid_intents})")
|
| 254 |
return _trim_response(tail) if tail else "Size nasΔ±l yardΔ±mcΔ± olabilirim?"
|
| 255 |
|
| 256 |
# Short message guard (less than 3 words usually means incomplete request)
|
|
@@ -274,8 +291,8 @@ async def _handle_new_message(session: Session, user_input: str) -> str:
|
|
| 274 |
# Extract parameters
|
| 275 |
return await _extract_parameters(session, intent_config, user_input)
|
| 276 |
|
| 277 |
-
async def _handle_parameter_followup(session: Session, user_input: str
|
| 278 |
-
"""Handle parameter collection followup
|
| 279 |
if not session.last_intent:
|
| 280 |
log("β οΈ No last intent in session")
|
| 281 |
session.reset_flow()
|
|
@@ -295,6 +312,51 @@ async def _handle_parameter_followup(session: Session, user_input: str, version)
|
|
| 295 |
session.reset_flow()
|
| 296 |
return "Bir hata oluΕtu. LΓΌtfen tekrar deneyin."
|
| 297 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
# Try to extract missing parameters
|
| 299 |
missing = session.awaiting_parameters
|
| 300 |
log(f"π Trying to extract missing params: {missing}")
|
|
@@ -331,40 +393,80 @@ async def _handle_parameter_followup(session: Session, user_input: str, version)
|
|
| 331 |
session.reset_flow()
|
| 332 |
return "ΓzgΓΌnΓΌm, istediΔiniz bilgileri anlayamadΔ±m. BaΕka bir konuda yardΔ±mcΔ± olabilir miyim?"
|
| 333 |
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
# AkΔ±llΔ± soru ΓΌret
|
| 337 |
-
question = await _generate_smart_parameter_question(
|
| 338 |
-
session, intent_config, still_missing, version
|
| 339 |
-
)
|
| 340 |
-
|
| 341 |
-
# Sorulan parametreleri tahmin et ve kaydet
|
| 342 |
-
params_in_question = extract_params_from_question(question, still_missing, intent_config)
|
| 343 |
-
session.record_parameter_question(params_in_question)
|
| 344 |
-
session.awaiting_parameters = params_in_question
|
| 345 |
-
|
| 346 |
-
log(f"π€ Smart question generated for params: {params_in_question}")
|
| 347 |
-
return question
|
| 348 |
|
| 349 |
-
#
|
| 350 |
log("β
All parameters collected, calling API")
|
| 351 |
session.state = "call_api"
|
| 352 |
return await _execute_api_call(session, intent_config)
|
| 353 |
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
-
#
|
| 365 |
-
|
| 366 |
-
session.reset_parameter_tracking()
|
| 367 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
missing = _get_missing_parameters(session, intent_config)
|
| 369 |
log(f"π Missing parameters: {missing}")
|
| 370 |
|
|
@@ -374,7 +476,7 @@ async def _extract_parameters(session: Session, intent_config, user_input: str)
|
|
| 374 |
return await _execute_api_call(session, intent_config)
|
| 375 |
|
| 376 |
# Build parameter extraction prompt
|
| 377 |
-
prompt = build_parameter_prompt(intent_config, missing, user_input, session.chat_history
|
| 378 |
raw = await spark_generate(session, prompt, user_input)
|
| 379 |
|
| 380 |
# Try processing with flexible parsing
|
|
@@ -387,133 +489,24 @@ async def _extract_parameters(session: Session, intent_config, user_input: str)
|
|
| 387 |
log("β οΈ Failed to extract parameters from response")
|
| 388 |
|
| 389 |
if missing:
|
| 390 |
-
#
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
log(f"π€ Smart question generated for initial params: {params_in_question}")
|
| 405 |
-
return question
|
| 406 |
|
| 407 |
# All parameters collected
|
| 408 |
log("β
All parameters collected after extraction")
|
| 409 |
return await _execute_api_call(session, intent_config)
|
| 410 |
|
| 411 |
-
async def _generate_smart_parameter_question(
|
| 412 |
-
session: Session,
|
| 413 |
-
intent_config,
|
| 414 |
-
missing_params: List[str],
|
| 415 |
-
version
|
| 416 |
-
) -> str:
|
| 417 |
-
"""LLM kullanarak doΔal parametre sorusu ΓΌret"""
|
| 418 |
-
cfg = ConfigProvider.get()
|
| 419 |
-
|
| 420 |
-
# Config'i al
|
| 421 |
-
collection_config = cfg.global_config.parameter_collection_config
|
| 422 |
-
|
| 423 |
-
# Γncelik sΔ±ralamasΔ±: ΓΆnce cevaplanmayanlar
|
| 424 |
-
prioritized_params = []
|
| 425 |
-
|
| 426 |
-
# 1. Daha ΓΆnce sorulup cevaplanmayanlar
|
| 427 |
-
for param in session.unanswered_parameters:
|
| 428 |
-
if param in missing_params:
|
| 429 |
-
prioritized_params.append(param)
|
| 430 |
-
log(f"π Priority param (unanswered): {param}")
|
| 431 |
-
|
| 432 |
-
# 2. HiΓ§ sorulmamΔ±Εlar
|
| 433 |
-
for param in missing_params:
|
| 434 |
-
if param not in prioritized_params:
|
| 435 |
-
prioritized_params.append(param)
|
| 436 |
-
log(f"β Additional param (not asked): {param}")
|
| 437 |
-
|
| 438 |
-
# Maksimum parametre sayΔ±sΔ±nΔ± belirle
|
| 439 |
-
max_params = min(
|
| 440 |
-
len(prioritized_params),
|
| 441 |
-
collection_config.max_params_per_question
|
| 442 |
-
)
|
| 443 |
-
params_to_ask = prioritized_params[:max_params]
|
| 444 |
-
|
| 445 |
-
log(f"π Params to ask in this round: {params_to_ask}")
|
| 446 |
-
|
| 447 |
-
# Proje dilini belirle
|
| 448 |
-
project = next((p for p in cfg.projects if p.name == session.project_name), None)
|
| 449 |
-
if project and hasattr(project, 'default_language'):
|
| 450 |
-
# default_language locale code'dur (tr-TR, en-US vb.)
|
| 451 |
-
project_locale_code = project.default_language
|
| 452 |
-
# Locale'den dil adΔ±nΔ± al
|
| 453 |
-
locale_data = LocaleManager.get_locale(project_locale_code)
|
| 454 |
-
project_language = locale_data.get('name', 'Turkish')
|
| 455 |
-
else:
|
| 456 |
-
project_locale_code = 'tr-TR'
|
| 457 |
-
project_language = 'Turkish'
|
| 458 |
-
|
| 459 |
-
log(f"π Project locale: {project_locale_code}, language: {project_language}")
|
| 460 |
-
|
| 461 |
-
# Prompt oluΕtur
|
| 462 |
-
prompt = build_smart_parameter_question_prompt(
|
| 463 |
-
collection_config,
|
| 464 |
-
intent_config,
|
| 465 |
-
params_to_ask, # Sadece bu turda sorulacak parametreler
|
| 466 |
-
session,
|
| 467 |
-
project_language,
|
| 468 |
-
intent_config.locale # Locale code'u da gΓΆnder
|
| 469 |
-
)
|
| 470 |
-
|
| 471 |
-
# LLM'den soru ΓΌret
|
| 472 |
-
response = await spark_generate(session, prompt, "")
|
| 473 |
-
|
| 474 |
-
# GΓΌvenlik: EΔer LLM boΕ veya hatalΔ± response verirse fallback
|
| 475 |
-
if not response or len(response.strip()) < 10:
|
| 476 |
-
log("β οΈ Empty or invalid response from LLM, using fallback")
|
| 477 |
-
# En yΓΌksek ΓΆncelikli parametre iΓ§in fallback soru
|
| 478 |
-
param = params_to_ask[0]
|
| 479 |
-
param_config = next(p for p in intent_config.parameters if p.name == param)
|
| 480 |
-
|
| 481 |
-
# Intent'in locale'ini kullan
|
| 482 |
-
intent_locale_code = getattr(intent_config, 'locale', project_locale_code)
|
| 483 |
-
locale_data = LocaleManager.get_locale(intent_locale_code)
|
| 484 |
-
|
| 485 |
-
# Parametrenin kaΓ§ kez sorulduΔuna gΓΆre farklΔ± fallback
|
| 486 |
-
ask_count = session.get_parameter_ask_count(param)
|
| 487 |
-
fallback_questions = locale_data.get('parameter_collection', {}).get('fallback_questions', {})
|
| 488 |
-
|
| 489 |
-
# Caption'Δ± kΓΌΓ§ΓΌk harfe Γ§evir (TΓΌrkΓ§e iΓ§in ΓΆzel)
|
| 490 |
-
caption = param_config.caption
|
| 491 |
-
if intent_locale_code.startswith('tr'):
|
| 492 |
-
caption_lower = caption.lower()
|
| 493 |
-
else:
|
| 494 |
-
caption_lower = caption
|
| 495 |
-
|
| 496 |
-
if ask_count == 0:
|
| 497 |
-
template = fallback_questions.get('first_ask', '{caption} bilgisini alabilir miyim?')
|
| 498 |
-
return template.replace('{caption}', caption)
|
| 499 |
-
elif ask_count == 1:
|
| 500 |
-
template = fallback_questions.get('second_ask', 'LΓΌtfen {caption} bilgisini paylaΕΔ±r mΔ±sΔ±nΔ±z?')
|
| 501 |
-
return template.replace('{caption}', caption_lower)
|
| 502 |
-
else:
|
| 503 |
-
template = fallback_questions.get('third_ask', 'Devam edebilmem iΓ§in {caption} bilgisine ihtiyacΔ±m var.')
|
| 504 |
-
return template.replace('{caption}', caption_lower)
|
| 505 |
-
|
| 506 |
-
# Response'u temizle
|
| 507 |
-
clean_response = response.strip()
|
| 508 |
-
|
| 509 |
-
# EΔer response yanlΔ±ΕlΔ±kla baΕka Εeyler iΓ§eriyorsa temizle
|
| 510 |
-
if "#" in clean_response or "assistant:" in clean_response.lower():
|
| 511 |
-
# Δ°lk satΔ±rΔ± al
|
| 512 |
-
clean_response = clean_response.split('\n')[0].strip()
|
| 513 |
-
|
| 514 |
-
log(f"π¬ Generated smart question: {clean_response[:100]}...")
|
| 515 |
-
return clean_response
|
| 516 |
-
|
| 517 |
def _get_missing_parameters(session: Session, intent_config) -> List[str]:
|
| 518 |
"""Get list of missing required parameters"""
|
| 519 |
missing = [
|
|
@@ -588,16 +581,6 @@ def _process_parameters(session: Session, intent_config, raw: str) -> bool:
|
|
| 588 |
if not param_config:
|
| 589 |
log(f"β οΈ Parameter config not found for: {param_name}")
|
| 590 |
continue
|
| 591 |
-
|
| 592 |
-
# Date tipi iΓ§in ΓΆzel kontrol
|
| 593 |
-
if param_config.type == "date":
|
| 594 |
-
try:
|
| 595 |
-
# ISO format kontrolΓΌ
|
| 596 |
-
from datetime import datetime
|
| 597 |
-
datetime.strptime(str(param_value), "%Y-%m-%d")
|
| 598 |
-
except ValueError:
|
| 599 |
-
log(f"β Invalid date format for {param_name}: {param_value}")
|
| 600 |
-
continue
|
| 601 |
|
| 602 |
# Validate parameter
|
| 603 |
if validate(str(param_value), param_config):
|
|
@@ -612,42 +595,17 @@ def _process_parameters(session: Session, intent_config, raw: str) -> bool:
|
|
| 612 |
except json.JSONDecodeError as e:
|
| 613 |
log(f"β JSON parsing error: {e}")
|
| 614 |
log(f"β Failed to parse: {raw[:200]}")
|
| 615 |
-
|
| 616 |
-
# Fallback: Try to extract simple values from user input
|
| 617 |
-
# This is especially useful for single parameter responses
|
| 618 |
-
if session.state == "await_param" and len(session.awaiting_parameters) > 0:
|
| 619 |
-
# Get the first missing parameter
|
| 620 |
-
first_missing = session.awaiting_parameters[0]
|
| 621 |
-
param_config = next(
|
| 622 |
-
(p for p in intent_config.parameters if p.name == first_missing),
|
| 623 |
-
None
|
| 624 |
-
)
|
| 625 |
-
|
| 626 |
-
if param_config and session.chat_history:
|
| 627 |
-
# Get the last user input
|
| 628 |
-
last_user_input = session.chat_history[-1].get("content", "").strip()
|
| 629 |
-
|
| 630 |
-
# For simple inputs like city names, try direct assignment
|
| 631 |
-
if param_config.type in ["str", "string"] and len(last_user_input.split()) <= 3:
|
| 632 |
-
if validate(last_user_input, param_config):
|
| 633 |
-
session.variables[param_config.variable_name] = last_user_input
|
| 634 |
-
log(f"β
Fallback extraction: {first_missing}={last_user_input}")
|
| 635 |
-
return True
|
| 636 |
-
|
| 637 |
return False
|
| 638 |
except Exception as e:
|
| 639 |
log(f"β Parameter processing error: {e}")
|
| 640 |
return False
|
| 641 |
-
|
| 642 |
# βββββββββββββββββββββββββ API EXECUTION βββββββββββββββββββββββββ #
|
| 643 |
async def _execute_api_call(session: Session, intent_config) -> str:
|
| 644 |
"""Execute API call and return humanized response"""
|
| 645 |
try:
|
| 646 |
session.state = "call_api"
|
| 647 |
api_name = intent_config.action
|
| 648 |
-
|
| 649 |
-
cfg = ConfigProvider.get()
|
| 650 |
-
|
| 651 |
api_config = cfg.get_api(api_name)
|
| 652 |
|
| 653 |
if not api_config:
|
|
@@ -671,10 +629,8 @@ async def _execute_api_call(session: Session, intent_config) -> str:
|
|
| 671 |
json.dumps(api_json, ensure_ascii=False)
|
| 672 |
)
|
| 673 |
human_response = await spark_generate(session, prompt, json.dumps(api_json))
|
| 674 |
-
# Trim response to remove any trailing "assistant" artifacts
|
| 675 |
-
trimmed_response = _trim_response(human_response)
|
| 676 |
session.reset_flow()
|
| 677 |
-
return
|
| 678 |
else:
|
| 679 |
session.reset_flow()
|
| 680 |
return f"Δ°Εlem tamamlandΔ±: {api_json}"
|
|
@@ -686,4 +642,7 @@ async def _execute_api_call(session: Session, intent_config) -> str:
|
|
| 686 |
except Exception as e:
|
| 687 |
log(f"β API call error: {e}")
|
| 688 |
session.reset_flow()
|
| 689 |
-
return intent_config.fallback_error_prompt or "Δ°Εlem sΔ±rasΔ±nda bir hata oluΕtu."
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
from session import session_store, Session
|
| 19 |
from llm_interface import LLMInterface, SparkLLM, GPT4oLLM
|
| 20 |
from config_provider import ConfigProvider
|
|
|
|
| 21 |
|
| 22 |
+
# βββββββββββββββββββββββββ CONFIG βββββββββββββββββββββββββ #
|
| 23 |
+
# Global config reference
|
| 24 |
+
cfg = None
|
| 25 |
+
|
| 26 |
+
def get_config():
|
| 27 |
+
"""Always get fresh config"""
|
| 28 |
+
global cfg
|
| 29 |
+
cfg = ConfigProvider.get()
|
| 30 |
+
return cfg
|
| 31 |
+
|
| 32 |
+
# Initialize on module load
|
| 33 |
+
cfg = get_config()
|
| 34 |
+
|
| 35 |
# Global LLM instance
|
| 36 |
llm_provider: Optional[LLMInterface] = None
|
| 37 |
|
|
|
|
| 63 |
tail = raw[m.end():]
|
| 64 |
log(f"π― Parsed intent: {name}")
|
| 65 |
return name, tail
|
| 66 |
+
|
| 67 |
+
# βββββββββββββββββββββββββ LLM SETUP βββββββββββββββββββββββββ #
|
| 68 |
+
def setup_llm_provider():
|
| 69 |
+
"""Initialize LLM provider based on internal_prompt config"""
|
| 70 |
global llm_provider
|
| 71 |
|
| 72 |
+
cfg = ConfigProvider.get() # Her zaman gΓΌncel config'i al
|
| 73 |
+
internal_prompt = cfg.global_config.internal_prompt
|
| 74 |
+
if not internal_prompt:
|
| 75 |
+
log("β οΈ No internal_prompt configured, using default Spark")
|
| 76 |
+
llm_provider = SparkLLM(cfg)
|
| 77 |
+
return
|
| 78 |
+
|
| 79 |
+
# Parse internal prompt format: "provider:model"
|
| 80 |
+
parts = internal_prompt.split(":", 1)
|
| 81 |
+
if len(parts) != 2:
|
| 82 |
+
log(f"β οΈ Invalid internal_prompt format: {internal_prompt}, using Spark")
|
| 83 |
+
llm_provider = SparkLLM(cfg)
|
| 84 |
+
return
|
| 85 |
+
|
| 86 |
+
provider, model = parts[0].lower(), parts[1]
|
| 87 |
+
|
| 88 |
+
if provider == "openai":
|
| 89 |
+
# Get API key from environment
|
| 90 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 91 |
if not api_key:
|
| 92 |
+
log("β OPENAI_API_KEY not found in environment")
|
| 93 |
+
llm_provider = SparkLLM(cfg)
|
| 94 |
+
return
|
| 95 |
|
| 96 |
+
log(f"π€ Using OpenAI with model: {model}")
|
| 97 |
llm_provider = GPT4oLLM(api_key, model)
|
|
|
|
| 98 |
else:
|
| 99 |
+
log(f"β οΈ Unknown provider: {provider}, using Spark")
|
| 100 |
+
llm_provider = SparkLLM(cfg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
# βββββββββββββββββββββββββ SPARK/LLM CALL βββββββββββββββββββββββββ #
|
| 103 |
async def spark_generate(s: Session, prompt: str, user_msg: str) -> str:
|
| 104 |
+
"""Call LLM (Spark or configured provider) with proper error handling"""
|
| 105 |
+
global llm_provider
|
| 106 |
+
|
| 107 |
+
if llm_provider is None:
|
| 108 |
+
setup_llm_provider()
|
| 109 |
+
|
| 110 |
try:
|
| 111 |
+
# Get version config from session
|
| 112 |
+
version = s.get_version_config()
|
| 113 |
+
if not version:
|
| 114 |
+
# Fallback: get from project config
|
| 115 |
+
project = next((p for p in cfg.projects if p.name == s.project_name), None)
|
| 116 |
+
if not project:
|
| 117 |
+
raise ValueError(f"Project not found: {s.project_name}")
|
| 118 |
+
version = next((v for v in project.versions if v.published), None)
|
| 119 |
+
if not version:
|
| 120 |
+
raise ValueError("No published version found")
|
| 121 |
+
|
| 122 |
+
log(f"π Calling LLM for session {s.session_id[:8]}...")
|
| 123 |
+
log(f"π Prompt preview (first 200 chars): {prompt[:200]}...")
|
| 124 |
+
|
| 125 |
+
# Call the configured LLM provider
|
| 126 |
+
raw = await llm_provider.generate(
|
| 127 |
+
project_name=s.project_name,
|
| 128 |
+
user_input=user_msg,
|
| 129 |
+
system_prompt=prompt,
|
| 130 |
+
context=s.chat_history[-10:],
|
| 131 |
+
version_config=version
|
| 132 |
+
)
|
| 133 |
|
|
|
|
|
|
|
| 134 |
log(f"πͺ LLM raw response: {raw[:120]!r}")
|
| 135 |
return raw
|
| 136 |
+
|
| 137 |
+
except httpx.TimeoutException:
|
| 138 |
+
log(f"β±οΈ LLM timeout for session {s.session_id[:8]}")
|
| 139 |
+
raise
|
| 140 |
except Exception as e:
|
| 141 |
log(f"β LLM error: {e}")
|
| 142 |
raise
|
| 143 |
|
| 144 |
+
# βββββββββββββββββββββββββ ALLOWED INTENTS βββββββββββββββββββββββββ #
|
| 145 |
+
ALLOWED_INTENTS = {"flight-booking", "flight-info", "booking-cancel"}
|
| 146 |
+
|
| 147 |
# βββββββββββββββββββββββββ FASTAPI βββββββββββββββββββββββββ #
|
| 148 |
router = APIRouter()
|
| 149 |
|
| 150 |
+
@router.get("/")
|
| 151 |
+
def health():
|
| 152 |
+
return {"status": "ok", "sessions": len(session_store._sessions)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
class StartRequest(BaseModel):
|
| 155 |
project_name: str
|
|
|
|
| 165 |
async def start_session(req: StartRequest):
|
| 166 |
"""Create new session"""
|
| 167 |
try:
|
|
|
|
|
|
|
| 168 |
# Validate project exists
|
| 169 |
project = next((p for p in cfg.projects if p.name == req.project_name and p.enabled), None)
|
| 170 |
if not project:
|
| 171 |
raise HTTPException(404, f"Project '{req.project_name}' not found or disabled")
|
| 172 |
+
|
| 173 |
+
# Find published version
|
| 174 |
version = next((v for v in project.versions if v.published), None)
|
| 175 |
if not version:
|
| 176 |
+
raise HTTPException(404, f"No published version for project '{req.project_name}'")
|
| 177 |
+
|
| 178 |
+
# Create session with version config
|
| 179 |
session = session_store.create_session(req.project_name, version)
|
| 180 |
greeting = "HoΕ geldiniz! Size nasΔ±l yardΔ±mcΔ± olabilirim?"
|
| 181 |
session.add_turn("assistant", greeting)
|
|
|
|
| 213 |
# Handle based on state
|
| 214 |
if session.state == "await_param":
|
| 215 |
log(f"π Handling parameter followup for missing: {session.awaiting_parameters}")
|
| 216 |
+
answer = await _handle_parameter_followup(session, user_input)
|
| 217 |
else:
|
| 218 |
log("π Handling new message")
|
| 219 |
+
answer = await _handle_new_message(session, user_input)
|
| 220 |
|
| 221 |
session.add_turn("assistant", answer)
|
| 222 |
return ChatResponse(session_id=session.session_id, answer=answer)
|
|
|
|
| 245 |
version.general_prompt,
|
| 246 |
session.chat_history,
|
| 247 |
user_input,
|
| 248 |
+
version.intents
|
|
|
|
| 249 |
)
|
| 250 |
|
| 251 |
# Get LLM response
|
|
|
|
| 265 |
# Parse intent
|
| 266 |
intent_name, tail = _safe_intent_parse(raw)
|
| 267 |
|
| 268 |
+
# Validate intent
|
| 269 |
+
if intent_name not in ALLOWED_INTENTS:
|
| 270 |
+
log(f"β οΈ Invalid intent: {intent_name}")
|
|
|
|
| 271 |
return _trim_response(tail) if tail else "Size nasΔ±l yardΔ±mcΔ± olabilirim?"
|
| 272 |
|
| 273 |
# Short message guard (less than 3 words usually means incomplete request)
|
|
|
|
| 291 |
# Extract parameters
|
| 292 |
return await _extract_parameters(session, intent_config, user_input)
|
| 293 |
|
| 294 |
+
async def _handle_parameter_followup(session: Session, user_input: str) -> str:
|
| 295 |
+
"""Handle parameter collection followup"""
|
| 296 |
if not session.last_intent:
|
| 297 |
log("β οΈ No last intent in session")
|
| 298 |
session.reset_flow()
|
|
|
|
| 312 |
session.reset_flow()
|
| 313 |
return "Bir hata oluΕtu. LΓΌtfen tekrar deneyin."
|
| 314 |
|
| 315 |
+
# Smart parameter collection
|
| 316 |
+
if cfg.global_config.parameter_collection_config.smart_grouping:
|
| 317 |
+
return await _handle_smart_parameter_collection(session, intent_config, user_input)
|
| 318 |
+
else:
|
| 319 |
+
return await _handle_simple_parameter_collection(session, intent_config, user_input)
|
| 320 |
+
|
| 321 |
+
async def _handle_simple_parameter_collection(session: Session, intent_config, user_input: str) -> str:
|
| 322 |
+
"""Original simple parameter collection logic"""
|
| 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 |
+
if not success:
|
| 334 |
+
# Increment miss count
|
| 335 |
+
session.missing_ask_count += 1
|
| 336 |
+
log(f"β οΈ No parameters extracted, miss count: {session.missing_ask_count}")
|
| 337 |
+
|
| 338 |
+
if session.missing_ask_count >= 3:
|
| 339 |
+
session.reset_flow()
|
| 340 |
+
return "ΓzgΓΌnΓΌm, istediΔiniz bilgileri anlayamadΔ±m. BaΕka bir konuda yardΔ±mcΔ± olabilir miyim?"
|
| 341 |
+
return "ΓzgΓΌnΓΌm, anlayamadΔ±m. LΓΌtfen tekrar sΓΆyler misiniz?"
|
| 342 |
+
|
| 343 |
+
# Check if we have all required parameters
|
| 344 |
+
missing = _get_missing_parameters(session, intent_config)
|
| 345 |
+
log(f"π Still missing params: {missing}")
|
| 346 |
+
|
| 347 |
+
if missing:
|
| 348 |
+
session.awaiting_parameters = missing
|
| 349 |
+
param = next(p for p in intent_config.parameters if p.name == missing[0])
|
| 350 |
+
return f"{param.caption} bilgisini alabilir miyim?"
|
| 351 |
+
|
| 352 |
+
# All parameters collected, call API
|
| 353 |
+
log("β
All parameters collected, calling API")
|
| 354 |
+
session.state = "call_api"
|
| 355 |
+
return await _execute_api_call(session, intent_config)
|
| 356 |
+
|
| 357 |
+
async def _handle_smart_parameter_collection(session: Session, intent_config, user_input: str) -> str:
|
| 358 |
+
"""Smart parameter collection with grouping and retry logic"""
|
| 359 |
+
|
| 360 |
# Try to extract missing parameters
|
| 361 |
missing = session.awaiting_parameters
|
| 362 |
log(f"π Trying to extract missing params: {missing}")
|
|
|
|
| 393 |
session.reset_flow()
|
| 394 |
return "ΓzgΓΌnΓΌm, istediΔiniz bilgileri anlayamadΔ±m. BaΕka bir konuda yardΔ±mcΔ± olabilir miyim?"
|
| 395 |
|
| 396 |
+
# Smart parameter question oluΕtur
|
| 397 |
+
return await _generate_smart_parameter_question(session, intent_config, still_missing)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
| 399 |
+
# TΓΌm parametreler toplandΔ±
|
| 400 |
log("β
All parameters collected, calling API")
|
| 401 |
session.state = "call_api"
|
| 402 |
return await _execute_api_call(session, intent_config)
|
| 403 |
|
| 404 |
+
async def _generate_smart_parameter_question(session: Session, intent_config, missing_params: List[str]) -> str:
|
| 405 |
+
"""Generate smart parameter collection question"""
|
| 406 |
+
|
| 407 |
+
# KaΓ§ parametre soracaΔΔ±mΔ±zΔ± belirle
|
| 408 |
+
max_params = cfg.global_config.parameter_collection_config.max_params_per_question
|
| 409 |
+
|
| 410 |
+
# Γncelik sΔ±rasΔ±na gΓΆre parametreleri seΓ§
|
| 411 |
+
params_to_ask = []
|
| 412 |
+
|
| 413 |
+
# Γnce daha ΓΆnce sorulmamΔ±Ε parametreler
|
| 414 |
+
for param in missing_params:
|
| 415 |
+
if session.get_parameter_ask_count(param) == 0:
|
| 416 |
+
params_to_ask.append(param)
|
| 417 |
+
if len(params_to_ask) >= max_params:
|
| 418 |
+
break
|
| 419 |
+
|
| 420 |
+
# Hala yer varsa, daha ΓΆnce sorulmuΕ ama cevaplanmamΔ±Ε parametreler
|
| 421 |
+
if len(params_to_ask) < max_params and cfg.global_config.parameter_collection_config.retry_unanswered:
|
| 422 |
+
for param in session.unanswered_parameters:
|
| 423 |
+
if param in missing_params and param not in params_to_ask:
|
| 424 |
+
params_to_ask.append(param)
|
| 425 |
+
if len(params_to_ask) >= max_params:
|
| 426 |
+
break
|
| 427 |
+
|
| 428 |
+
# Hala yer varsa, kalan parametreler
|
| 429 |
+
if len(params_to_ask) < max_params:
|
| 430 |
+
for param in missing_params:
|
| 431 |
+
if param not in params_to_ask:
|
| 432 |
+
params_to_ask.append(param)
|
| 433 |
+
if len(params_to_ask) >= max_params:
|
| 434 |
+
break
|
| 435 |
+
|
| 436 |
+
# Parametreleri session'a kaydet
|
| 437 |
+
session.record_parameter_question(params_to_ask)
|
| 438 |
+
session.awaiting_parameters = params_to_ask
|
| 439 |
+
session.missing_ask_count += 1
|
| 440 |
+
|
| 441 |
+
# Build smart question prompt
|
| 442 |
+
collected_params = {
|
| 443 |
+
p.name: session.variables.get(p.variable_name, "")
|
| 444 |
+
for p in intent_config.parameters
|
| 445 |
+
if p.variable_name in session.variables
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
question_prompt = build_smart_parameter_question_prompt(
|
| 449 |
+
intent_config,
|
| 450 |
+
params_to_ask,
|
| 451 |
+
session.chat_history,
|
| 452 |
+
collected_params,
|
| 453 |
+
session.unanswered_parameters,
|
| 454 |
+
cfg.global_config.parameter_collection_config.collection_prompt
|
| 455 |
+
)
|
| 456 |
|
| 457 |
+
# Generate natural question
|
| 458 |
+
question = await spark_generate(session, question_prompt, "")
|
|
|
|
| 459 |
|
| 460 |
+
# Clean up the response
|
| 461 |
+
question = _trim_response(question)
|
| 462 |
+
|
| 463 |
+
log(f"π€ Generated smart question for {params_to_ask}: {question}")
|
| 464 |
+
|
| 465 |
+
return question
|
| 466 |
+
|
| 467 |
+
# βββββββββββββββββββββββββ PARAMETER HANDLING βββββββββββββββββββββββββ #
|
| 468 |
+
async def _extract_parameters(session: Session, intent_config, user_input: str) -> str:
|
| 469 |
+
"""Extract parameters from user input"""
|
| 470 |
missing = _get_missing_parameters(session, intent_config)
|
| 471 |
log(f"π Missing parameters: {missing}")
|
| 472 |
|
|
|
|
| 476 |
return await _execute_api_call(session, intent_config)
|
| 477 |
|
| 478 |
# Build parameter extraction prompt
|
| 479 |
+
prompt = build_parameter_prompt(intent_config, missing, user_input, session.chat_history)
|
| 480 |
raw = await spark_generate(session, prompt, user_input)
|
| 481 |
|
| 482 |
# Try processing with flexible parsing
|
|
|
|
| 489 |
log("β οΈ Failed to extract parameters from response")
|
| 490 |
|
| 491 |
if missing:
|
| 492 |
+
# Smart parameter collection
|
| 493 |
+
if cfg.global_config.parameter_collection_config.smart_grouping:
|
| 494 |
+
# Reset parameter tracking for new intent
|
| 495 |
+
session.reset_parameter_tracking()
|
| 496 |
+
return await _generate_smart_parameter_question(session, intent_config, missing)
|
| 497 |
+
else:
|
| 498 |
+
# Simple parameter collection
|
| 499 |
+
session.state = "await_param"
|
| 500 |
+
session.awaiting_parameters = missing
|
| 501 |
+
session.missing_ask_count = 0
|
| 502 |
+
param = next(p for p in intent_config.parameters if p.name == missing[0])
|
| 503 |
+
log(f"β Asking for parameter: {param.name} ({param.caption})")
|
| 504 |
+
return f"{param.caption} bilgisini alabilir miyim?"
|
|
|
|
|
|
|
|
|
|
| 505 |
|
| 506 |
# All parameters collected
|
| 507 |
log("β
All parameters collected after extraction")
|
| 508 |
return await _execute_api_call(session, intent_config)
|
| 509 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
def _get_missing_parameters(session: Session, intent_config) -> List[str]:
|
| 511 |
"""Get list of missing required parameters"""
|
| 512 |
missing = [
|
|
|
|
| 581 |
if not param_config:
|
| 582 |
log(f"β οΈ Parameter config not found for: {param_name}")
|
| 583 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
|
| 585 |
# Validate parameter
|
| 586 |
if validate(str(param_value), param_config):
|
|
|
|
| 595 |
except json.JSONDecodeError as e:
|
| 596 |
log(f"β JSON parsing error: {e}")
|
| 597 |
log(f"β Failed to parse: {raw[:200]}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 598 |
return False
|
| 599 |
except Exception as e:
|
| 600 |
log(f"β Parameter processing error: {e}")
|
| 601 |
return False
|
| 602 |
+
|
| 603 |
# βββββββββββββββββββββββββ API EXECUTION βββββββββββββββββββββββββ #
|
| 604 |
async def _execute_api_call(session: Session, intent_config) -> str:
|
| 605 |
"""Execute API call and return humanized response"""
|
| 606 |
try:
|
| 607 |
session.state = "call_api"
|
| 608 |
api_name = intent_config.action
|
|
|
|
|
|
|
|
|
|
| 609 |
api_config = cfg.get_api(api_name)
|
| 610 |
|
| 611 |
if not api_config:
|
|
|
|
| 629 |
json.dumps(api_json, ensure_ascii=False)
|
| 630 |
)
|
| 631 |
human_response = await spark_generate(session, prompt, json.dumps(api_json))
|
|
|
|
|
|
|
| 632 |
session.reset_flow()
|
| 633 |
+
return human_response if human_response else f"Δ°Εlem sonucu: {api_json}"
|
| 634 |
else:
|
| 635 |
session.reset_flow()
|
| 636 |
return f"Δ°Εlem tamamlandΔ±: {api_json}"
|
|
|
|
| 642 |
except Exception as e:
|
| 643 |
log(f"β API call error: {e}")
|
| 644 |
session.reset_flow()
|
| 645 |
+
return intent_config.fallback_error_prompt or "Δ°Εlem sΔ±rasΔ±nda bir hata oluΕtu."
|
| 646 |
+
|
| 647 |
+
# Initialize LLM on module load
|
| 648 |
+
setup_llm_provider()
|