ciyidogan commited on
Commit
fcd13b0
·
verified ·
1 Parent(s): 090cb24

Delete llm_manager.py

Browse files
Files changed (1) hide show
  1. llm_manager.py +0 -689
llm_manager.py DELETED
@@ -1,689 +0,0 @@
1
- """
2
- LLM Manager for Flare
3
- ====================
4
- Manages LLM interactions per session with stateless approach
5
- """
6
- import asyncio
7
- from typing import Dict, Optional, Any, List
8
- from datetime import datetime
9
- import traceback
10
- from dataclasses import dataclass, field
11
- import json
12
-
13
- from event_bus import EventBus, Event, EventType, publish_error
14
- from resource_manager import ResourceManager, ResourceType
15
- from session import Session
16
- from llm.llm_factory import LLMFactory
17
- from llm.llm_interface import LLMInterface
18
- from llm.prompt_builder import build_intent_prompt, build_parameter_prompt
19
- from utils.logger import log_info, log_error, log_debug, log_warning
20
- from config.config_provider import ConfigProvider
21
-
22
-
23
- @dataclass
24
- class LLMJob:
25
- """LLM processing job"""
26
- job_id: str
27
- session_id: str
28
- input_text: str
29
- job_type: str # "intent_detection", "parameter_collection", "response_generation"
30
- created_at: datetime = field(default_factory=datetime.utcnow)
31
- completed_at: Optional[datetime] = None
32
- response_text: Optional[str] = None
33
- detected_intent: Optional[str] = None
34
- error: Optional[str] = None
35
- metadata: Dict[str, Any] = field(default_factory=dict)
36
-
37
- def complete(self, response_text: str, intent: Optional[str] = None):
38
- """Mark job as completed"""
39
- self.response_text = response_text
40
- self.detected_intent = intent
41
- self.completed_at = datetime.utcnow()
42
-
43
- def fail(self, error: str):
44
- """Mark job as failed"""
45
- self.error = error
46
- self.completed_at = datetime.utcnow()
47
-
48
-
49
- @dataclass
50
- class LLMSession:
51
- """LLM session wrapper"""
52
- session_id: str
53
- session: Session
54
- llm_instance: LLMInterface
55
- active_job: Optional[LLMJob] = None
56
- job_history: List[LLMJob] = field(default_factory=list)
57
- created_at: datetime = field(default_factory=datetime.utcnow)
58
- last_activity: datetime = field(default_factory=datetime.utcnow)
59
- total_jobs = 0
60
- total_tokens = 0
61
-
62
- def update_activity(self):
63
- """Update last activity timestamp"""
64
- self.last_activity = datetime.utcnow()
65
-
66
-
67
- class LLMManager:
68
- """Manages LLM interactions with stateless approach"""
69
-
70
- def __init__(self, event_bus: EventBus, resource_manager: ResourceManager):
71
- self.event_bus = event_bus
72
- self.resource_manager = resource_manager
73
- self.llm_sessions: Dict[str, LLMSession] = {}
74
- self.config = ConfigProvider.get()
75
- self._setup_event_handlers()
76
- self._setup_resource_pool()
77
-
78
- def _setup_event_handlers(self):
79
- """Subscribe to LLM-related events"""
80
- self.event_bus.subscribe(EventType.LLM_PROCESSING_STARTED, self._handle_llm_processing)
81
- self.event_bus.subscribe(EventType.SESSION_ENDED, self._handle_session_ended)
82
-
83
- def _setup_resource_pool(self):
84
- """Setup LLM instance pool"""
85
- self.resource_manager.register_pool(
86
- resource_type=ResourceType.LLM_CONTEXT,
87
- factory=self._create_llm_instance,
88
- max_idle=2, # Lower pool size for LLM
89
- max_age_seconds=900 # 15 minutes
90
- )
91
-
92
- async def _create_llm_instance(self) -> LLMInterface:
93
- """Factory for creating LLM instances"""
94
- try:
95
- llm_instance = LLMFactory.create_provider()
96
- if not llm_instance:
97
- raise ValueError("Failed to create LLM instance")
98
-
99
- log_debug("🤖 Created new LLM instance")
100
- return llm_instance
101
-
102
- except Exception as e:
103
- log_error(f"❌ Failed to create LLM instance", error=str(e))
104
- raise
105
-
106
- async def _handle_llm_processing(self, event: Event):
107
- """Handle LLM processing request"""
108
- session_id = event.session_id
109
- input_text = event.data.get("text", "")
110
-
111
- if not input_text:
112
- log_warning(f"⚠️ Empty text for LLM", session_id=session_id)
113
- return
114
-
115
- try:
116
- log_info(
117
- f"🤖 Starting LLM processing",
118
- session_id=session_id,
119
- text_length=len(input_text)
120
- )
121
-
122
- # Get or create LLM session
123
- llm_session = await self._get_or_create_session(session_id)
124
- if not llm_session:
125
- raise ValueError("Failed to create LLM session")
126
-
127
- # Determine job type based on session state
128
- job_type = self._determine_job_type(llm_session.session)
129
-
130
- # Create job
131
- job_id = f"{session_id}_{llm_session.total_jobs}"
132
- job = LLMJob(
133
- job_id=job_id,
134
- session_id=session_id,
135
- input_text=input_text,
136
- job_type=job_type,
137
- metadata={
138
- "session_state": llm_session.session.state,
139
- "current_intent": llm_session.session.current_intent
140
- }
141
- )
142
-
143
- llm_session.active_job = job
144
- llm_session.total_jobs += 1
145
- llm_session.update_activity()
146
-
147
- # Process based on job type
148
- if job_type == "intent_detection":
149
- await self._process_intent_detection(llm_session, job)
150
- elif job_type == "parameter_collection":
151
- await self._process_parameter_collection(llm_session, job)
152
- else:
153
- await self._process_response_generation(llm_session, job)
154
-
155
- except Exception as e:
156
- log_error(
157
- f"❌ Failed to process LLM request",
158
- session_id=session_id,
159
- error=str(e),
160
- traceback=traceback.format_exc()
161
- )
162
-
163
- # Publish error event
164
- await publish_error(
165
- session_id=session_id,
166
- error_type="llm_error",
167
- error_message=f"LLM processing failed: {str(e)}"
168
- )
169
-
170
- async def _get_or_create_session(self, session_id: str) -> Optional[LLMSession]:
171
- """Get or create LLM session"""
172
- if session_id in self.llm_sessions:
173
- return self.llm_sessions[session_id]
174
-
175
- # Get session from store
176
- from session import session_store
177
- session = session_store.get_session(session_id)
178
- if not session:
179
- log_error(f"❌ Session not found", session_id=session_id)
180
- return None
181
-
182
- # Acquire LLM instance from pool
183
- resource_id = f"llm_{session_id}"
184
- llm_instance = await self.resource_manager.acquire(
185
- resource_id=resource_id,
186
- session_id=session_id,
187
- resource_type=ResourceType.LLM_CONTEXT,
188
- cleanup_callback=self._cleanup_llm_instance
189
- )
190
-
191
- # Create LLM session
192
- llm_session = LLMSession(
193
- session_id=session_id,
194
- session=session,
195
- llm_instance=llm_instance
196
- )
197
-
198
- self.llm_sessions[session_id] = llm_session
199
- return llm_session
200
-
201
- def _determine_job_type(self, session: Session) -> str:
202
- """Determine job type based on session state"""
203
- if session.state == "idle":
204
- return "intent_detection"
205
- elif session.state == "collect_params":
206
- return "parameter_collection"
207
- else:
208
- return "response_generation"
209
-
210
- async def _process_intent_detection(self, llm_session: LLMSession, job: LLMJob):
211
- """Process intent detection"""
212
- try:
213
- session = llm_session.session
214
-
215
- # Get project and version config
216
- project = next((p for p in self.config.projects if p.name == session.project_name), None)
217
- if not project:
218
- raise ValueError(f"Project not found: {session.project_name}")
219
-
220
- version = session.get_version_config()
221
- if not version:
222
- raise ValueError("Version config not found")
223
-
224
- # Build intent detection prompt
225
- prompt = build_intent_prompt(
226
- version=version,
227
- conversation=session.chat_history,
228
- project_locale=project.default_locale
229
- )
230
-
231
- log_debug(
232
- f"📝 Intent detection prompt built",
233
- session_id=job.session_id,
234
- prompt_length=len(prompt)
235
- )
236
-
237
- # Call LLM
238
- response = await llm_session.llm_instance.generate(
239
- system_prompt=prompt,
240
- user_input=job.input_text,
241
- context=session.chat_history[-10:] # Last 10 messages
242
- )
243
-
244
- # Parse intent
245
- intent_name, response_text = self._parse_intent_response(response)
246
-
247
- if intent_name:
248
- # Find intent config
249
- intent_config = next((i for i in version.intents if i.name == intent_name), None)
250
-
251
- if intent_config:
252
- # Update session
253
- session.current_intent = intent_name
254
- session.set_intent_config(intent_config)
255
- session.state = "collect_params"
256
-
257
- log_info(
258
- f"🎯 Intent detected",
259
- session_id=job.session_id,
260
- intent=intent_name
261
- )
262
-
263
- # Check if we need to collect parameters
264
- missing_params = [
265
- p.name for p in intent_config.parameters
266
- if p.required and p.variable_name not in session.variables
267
- ]
268
-
269
- if not missing_params:
270
- # All parameters ready, execute action
271
- await self._execute_intent_action(llm_session, intent_config)
272
- return
273
- else:
274
- # Need to collect parameters
275
- await self._request_parameter_collection(llm_session, intent_config, missing_params)
276
- return
277
-
278
- # No intent detected, use response as is
279
- response_text = self._clean_response(response)
280
- job.complete(response_text, intent_name)
281
-
282
- # Publish response
283
- await self._publish_response(job)
284
-
285
- except Exception as e:
286
- job.fail(str(e))
287
- raise
288
-
289
- async def _process_parameter_collection(self, llm_session: LLMSession, job: LLMJob):
290
- """Process parameter collection"""
291
- try:
292
- session = llm_session.session
293
- intent_config = session.get_intent_config()
294
-
295
- if not intent_config:
296
- raise ValueError("No intent config in session")
297
-
298
- # Extract parameters from user input
299
- extracted_params = await self._extract_parameters(
300
- llm_session,
301
- job.input_text,
302
- intent_config,
303
- session.variables
304
- )
305
-
306
- # Update session variables
307
- for param_name, param_value in extracted_params.items():
308
- param_config = next(
309
- (p for p in intent_config.parameters if p.name == param_name),
310
- None
311
- )
312
- if param_config:
313
- session.variables[param_config.variable_name] = str(param_value)
314
-
315
- # Check what parameters are still missing
316
- missing_params = [
317
- p.name for p in intent_config.parameters
318
- if p.required and p.variable_name not in session.variables
319
- ]
320
-
321
- if not missing_params:
322
- # All parameters collected, execute action
323
- await self._execute_intent_action(llm_session, intent_config)
324
- else:
325
- # Still need more parameters
326
- await self._request_parameter_collection(llm_session, intent_config, missing_params)
327
-
328
- except Exception as e:
329
- job.fail(str(e))
330
- raise
331
-
332
- async def _process_response_generation(self, llm_session: LLMSession, job: LLMJob):
333
- """Process general response generation"""
334
- try:
335
- session = llm_session.session
336
-
337
- # Get version config
338
- version = session.get_version_config()
339
- if not version:
340
- raise ValueError("Version config not found")
341
-
342
- # Use general prompt
343
- prompt = version.general_prompt
344
-
345
- # Generate response
346
- response = await llm_session.llm_instance.generate(
347
- system_prompt=prompt,
348
- user_input=job.input_text,
349
- context=session.chat_history[-10:]
350
- )
351
-
352
- response_text = self._clean_response(response)
353
- job.complete(response_text)
354
-
355
- # Publish response
356
- await self._publish_response(job)
357
-
358
- except Exception as e:
359
- job.fail(str(e))
360
- raise
361
-
362
- async def _extract_parameters(self,
363
- llm_session: LLMSession,
364
- user_input: str,
365
- intent_config: Any,
366
- existing_params: Dict[str, str]) -> Dict[str, Any]:
367
- """Extract parameters from user input"""
368
- # Build extraction prompt
369
- param_info = []
370
- for param in intent_config.parameters:
371
- if param.variable_name not in existing_params:
372
- param_info.append({
373
- 'name': param.name,
374
- 'type': param.type,
375
- 'required': param.required,
376
- 'extraction_prompt': param.extraction_prompt
377
- })
378
-
379
- prompt = f"""
380
- Extract parameters from user message: "{user_input}"
381
-
382
- Expected parameters:
383
- {json.dumps(param_info, ensure_ascii=False)}
384
-
385
- Return as JSON object with parameter names as keys.
386
- """
387
-
388
- # Call LLM
389
- response = await llm_session.llm_instance.generate(
390
- system_prompt=prompt,
391
- user_input=user_input,
392
- context=[]
393
- )
394
-
395
- # Parse JSON response
396
- try:
397
- # Look for JSON block in response
398
- import re
399
- json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL)
400
- if not json_match:
401
- json_match = re.search(r'\{[^}]+\}', response)
402
-
403
- if json_match:
404
- json_str = json_match.group(1) if '```' in response else json_match.group(0)
405
- return json.loads(json_str)
406
- except:
407
- pass
408
-
409
- return {}
410
-
411
- async def _request_parameter_collection(self,
412
- llm_session: LLMSession,
413
- intent_config: Any,
414
- missing_params: List[str]):
415
- """Request parameter collection from user"""
416
- session = llm_session.session
417
-
418
- # Get project config
419
- project = next((p for p in self.config.projects if p.name == session.project_name), None)
420
- if not project:
421
- return
422
-
423
- version = session.get_version_config()
424
- if not version:
425
- return
426
-
427
- # Get parameter collection config
428
- collection_config = self.config.global_config.llm_provider.settings.get("parameter_collection_config", {})
429
- max_params = collection_config.get("max_params_per_question", 2)
430
-
431
- # Decide which parameters to ask
432
- params_to_ask = missing_params[:max_params]
433
-
434
- # Build parameter collection prompt
435
- prompt = build_parameter_prompt(
436
- version=version,
437
- intent_config=intent_config,
438
- chat_history=session.chat_history,
439
- collected_params=session.variables,
440
- missing_params=missing_params,
441
- params_to_ask=params_to_ask,
442
- max_params=max_params,
443
- project_locale=project.default_locale,
444
- unanswered_params=session.unanswered_parameters
445
- )
446
-
447
- # Generate question
448
- response = await llm_session.llm_instance.generate(
449
- system_prompt=prompt,
450
- user_input="",
451
- context=session.chat_history[-5:]
452
- )
453
-
454
- response_text = self._clean_response(response)
455
-
456
- # Create a job for the response
457
- job = LLMJob(
458
- job_id=f"{session.session_id}_param_request",
459
- session_id=session.session_id,
460
- input_text="",
461
- job_type="parameter_request",
462
- response_text=response_text
463
- )
464
-
465
- await self._publish_response(job)
466
-
467
- async def _execute_intent_action(self, llm_session: LLMSession, intent_config: Any):
468
- """Execute intent action (API call)"""
469
- session = llm_session.session
470
-
471
- try:
472
- # Get API config
473
- api_name = intent_config.action
474
- api_config = self.config.get_api(api_name)
475
-
476
- if not api_config:
477
- raise ValueError(f"API config not found: {api_name}")
478
-
479
- log_info(
480
- f"📡 Executing intent action",
481
- session_id=session.session_id,
482
- api_name=api_name,
483
- variables=session.variables
484
- )
485
-
486
- # Execute API call
487
- from api_executor import call_api
488
- response = call_api(api_config, session)
489
- api_json = response.json()
490
-
491
- log_info(f"✅ API response received", session_id=session.session_id)
492
-
493
- # Humanize response if prompt exists
494
- if api_config.response_prompt:
495
- prompt = api_config.response_prompt.replace(
496
- "{{api_response}}",
497
- json.dumps(api_json, ensure_ascii=False)
498
- )
499
-
500
- human_response = await llm_session.llm_instance.generate(
501
- system_prompt=prompt,
502
- user_input=json.dumps(api_json),
503
- context=[]
504
- )
505
-
506
- response_text = self._clean_response(human_response)
507
- else:
508
- response_text = f"İşlem tamamlandı: {api_json}"
509
-
510
- # Reset session flow
511
- session.reset_flow()
512
-
513
- # Create job for response
514
- job = LLMJob(
515
- job_id=f"{session.session_id}_action_result",
516
- session_id=session.session_id,
517
- input_text="",
518
- job_type="action_result",
519
- response_text=response_text
520
- )
521
-
522
- await self._publish_response(job)
523
-
524
- except Exception as e:
525
- log_error(
526
- f"❌ API execution failed",
527
- session_id=session.session_id,
528
- error=str(e)
529
- )
530
-
531
- # Reset flow
532
- session.reset_flow()
533
-
534
- # Send error response
535
- error_response = self._get_user_friendly_error("api_error", {"api_name": api_name})
536
-
537
- job = LLMJob(
538
- job_id=f"{session.session_id}_error",
539
- session_id=session.session_id,
540
- input_text="",
541
- job_type="error",
542
- response_text=error_response
543
- )
544
-
545
- await self._publish_response(job)
546
-
547
- async def _publish_response(self, job: LLMJob):
548
- """Publish LLM response"""
549
- # Update job history
550
- llm_session = self.llm_sessions.get(job.session_id)
551
- if llm_session:
552
- llm_session.job_history.append(job)
553
- # Keep only last 20 jobs
554
- if len(llm_session.job_history) > 20:
555
- llm_session.job_history.pop(0)
556
-
557
- # Publish event
558
- await self.event_bus.publish(Event(
559
- type=EventType.LLM_RESPONSE_READY,
560
- session_id=job.session_id,
561
- data={
562
- "text": job.response_text,
563
- "intent": job.detected_intent,
564
- "job_type": job.job_type
565
- }
566
- ))
567
-
568
- log_info(
569
- f"✅ LLM response published",
570
- session_id=job.session_id,
571
- response_length=len(job.response_text) if job.response_text else 0
572
- )
573
-
574
- def _parse_intent_response(self, response: str) -> tuple[str, str]:
575
- """Parse intent from LLM response"""
576
- import re
577
-
578
- # Look for intent pattern
579
- match = re.search(r"#DETECTED_INTENT:\s*([A-Za-z0-9_-]+)", response)
580
- if not match:
581
- return "", response
582
-
583
- intent_name = match.group(1)
584
-
585
- # Remove 'assistant' suffix if exists
586
- if intent_name.endswith("assistant"):
587
- intent_name = intent_name[:-9]
588
-
589
- # Get remaining text after intent
590
- remaining_text = response[match.end():]
591
-
592
- return intent_name, remaining_text
593
-
594
- def _clean_response(self, response: str) -> str:
595
- """Clean LLM response"""
596
- # Remove everything after the first logical assistant block or intent tag
597
- for stop in ["#DETECTED_INTENT", "⚠️", "\nassistant", "assistant\n", "assistant"]:
598
- idx = response.find(stop)
599
- if idx != -1:
600
- response = response[:idx]
601
-
602
- # Normalize common greetings
603
- import re
604
- response = re.sub(r"Hoş[\s-]?geldin(iz)?", "Hoş geldiniz", response, flags=re.IGNORECASE)
605
-
606
- return response.strip()
607
-
608
- def _get_user_friendly_error(self, error_type: str, context: dict = None) -> str:
609
- """Get user-friendly error messages"""
610
- error_messages = {
611
- "session_not_found": "Oturumunuz bulunamadı. Lütfen yeni bir konuşma başlatın.",
612
- "project_not_found": "Proje konfigürasyonu bulunamadı. Lütfen yönetici ile iletişime geçin.",
613
- "version_not_found": "Proje versiyonu bulunamadı. Lütfen geçerli bir versiyon seçin.",
614
- "intent_not_found": "Üzgünüm, ne yapmak istediğinizi anlayamadım. Lütfen daha açık bir şekilde belirtir misiniz?",
615
- "api_timeout": "İşlem zaman aşımına uğradı. Lütfen tekrar deneyin.",
616
- "api_error": "İşlem sırasında bir hata oluştu. Lütfen daha sonra tekrar deneyin.",
617
- "parameter_validation": "Girdiğiniz bilgide bir hata var. Lütfen kontrol edip tekrar deneyin.",
618
- "llm_error": "Sistem yanıt veremedi. Lütfen biraz sonra tekrar deneyin.",
619
- "llm_timeout": "Sistem meşgul. Lütfen birkaç saniye bekleyip tekrar deneyin.",
620
- "session_expired": "Oturumunuz zaman aşımına uğradı. Lütfen yeni bir konuşma başlatın.",
621
- "rate_limit": "Çok fazla istek gönderdiniz. Lütfen biraz bekleyin.",
622
- "internal_error": "Beklenmeyen bir hata oluştu. Lütfen yönetici ile iletişime geçin."
623
- }
624
-
625
- message = error_messages.get(error_type, error_messages["internal_error"])
626
-
627
- # Add context if available
628
- if context:
629
- if error_type == "api_error" and "api_name" in context:
630
- message = f"{context['api_name']} servisi için {message}"
631
-
632
- return message
633
-
634
- async def _handle_session_ended(self, event: Event):
635
- """Clean up LLM resources when session ends"""
636
- session_id = event.session_id
637
- await self._cleanup_session(session_id)
638
-
639
- async def _cleanup_session(self, session_id: str):
640
- """Clean up LLM session"""
641
- llm_session = self.llm_sessions.pop(session_id, None)
642
- if not llm_session:
643
- return
644
-
645
- try:
646
- # Release resource
647
- resource_id = f"llm_{session_id}"
648
- await self.resource_manager.release(resource_id, delay_seconds=180) # 3 minutes
649
-
650
- log_info(
651
- f"🧹 LLM session cleaned up",
652
- session_id=session_id,
653
- total_jobs=llm_session.total_jobs,
654
- job_history_size=len(llm_session.job_history)
655
- )
656
-
657
- except Exception as e:
658
- log_error(
659
- f"❌ Error cleaning up LLM session",
660
- session_id=session_id,
661
- error=str(e)
662
- )
663
-
664
- async def _cleanup_llm_instance(self, llm_instance: LLMInterface):
665
- """Cleanup callback for LLM instance"""
666
- try:
667
- # LLM instances typically don't need special cleanup
668
- log_debug("🧹 LLM instance cleaned up")
669
-
670
- except Exception as e:
671
- log_error(f"❌ Error cleaning up LLM instance", error=str(e))
672
-
673
- def get_stats(self) -> Dict[str, Any]:
674
- """Get LLM manager statistics"""
675
- session_stats = {}
676
- for session_id, llm_session in self.llm_sessions.items():
677
- session_stats[session_id] = {
678
- "active_job": llm_session.active_job.job_id if llm_session.active_job else None,
679
- "total_jobs": llm_session.total_jobs,
680
- "job_history_size": len(llm_session.job_history),
681
- "uptime_seconds": (datetime.utcnow() - llm_session.created_at).total_seconds(),
682
- "last_activity": llm_session.last_activity.isoformat()
683
- }
684
-
685
- return {
686
- "active_sessions": len(self.llm_sessions),
687
- "total_active_jobs": sum(1 for s in self.llm_sessions.values() if s.active_job),
688
- "sessions": session_stats
689
- }