ciyidogan commited on
Commit
4966ebb
Β·
verified Β·
1 Parent(s): 13a2bd1

Update chat_handler.py

Browse files
Files changed (1) hide show
  1. chat_handler.py +247 -468
chat_handler.py CHANGED
@@ -1,40 +1,28 @@
1
  """
2
- Flare – Chat Handler (v1.7 Β· parameter parsing dΓΌzeltmesi)
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, build_smart_parameter_question_prompt, extract_params_from_question
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
- def get_config():
28
- """Always get fresh config"""
29
- global cfg
30
- cfg = ConfigProvider.get()
31
- return cfg
32
-
33
- # Initialize on module load
34
- cfg = get_config()
35
 
36
- # Global LLM instance
37
- llm_provider: Optional[LLMInterface] = None
 
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 based on config"""
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
- # ───────────────────────── SPARK/LLM CALL ───────────────────────── #
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 = session.get_version_config()
173
  if not version:
174
- raise HTTPException(500, "Version configuration lost")
175
-
176
- # Handle based on state
177
- if session.state == "await_param":
178
- log(f"πŸ”„ Handling parameter followup for missing: {session.awaiting_parameters}")
179
- answer = await _handle_parameter_followup(session, user_input)
180
- else:
181
- log("πŸ†• Handling new message")
182
- answer = await _handle_new_message(session, user_input)
183
 
184
- session.add_turn("assistant", answer)
185
- return ChatResponse(session_id=session.session_id, answer=answer)
186
 
187
- except HTTPException:
188
- raise
189
- except Exception as e:
190
- log(f"❌ Chat error: {e}")
191
- session.reset_flow()
192
- error_msg = "Bir hata oluştu. Lütfen tekrar deneyin."
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
- if session.missing_ask_count >= 3:
302
- session.reset_flow()
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
- # Smart parameter question oluştur
360
- return await _generate_smart_parameter_question(session, intent_config, still_missing)
361
-
362
- # TΓΌm parametreler toplandΔ±
363
- log("βœ… All parameters collected, calling API")
364
- session.state = "call_api"
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
- def _process_parameters(session: Session, intent_config, raw: str) -> bool:
483
- """Process parameter extraction response with flexible parsing"""
 
484
  try:
485
- # Try to parse JSON, handling both with and without #PARAMETERS: prefix
486
- json_str = raw
487
- if raw.startswith("#PARAMETERS:"):
488
- json_str = raw[len("#PARAMETERS:"):]
489
- log(f"πŸ” Found #PARAMETERS: prefix, removing it")
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
- data = json.loads(json_str)
 
 
525
 
526
- extracted = data.get("extracted", [])
527
- log(f"πŸ“¦ Extracted data: {extracted}")
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 spark_generate(session, prompt, json.dumps(api_json))
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()