VietCat commited on
Commit
19142de
·
1 Parent(s): b4a5075
Files changed (3) hide show
  1. app/llm.py +504 -0
  2. app/main.py +117 -16
  3. app/sheets.py +14 -6
app/llm.py ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Any, Optional, Union
2
+ import httpx
3
+ import json
4
+ from loguru import logger
5
+ from tenacity import retry, stop_after_attempt, wait_exponential
6
+ import os
7
+
8
+ from .utils import timing_decorator_async, timing_decorator_sync
9
+
10
+ class LLMClient:
11
+ """
12
+ Client để tương tác với các mô hình ngôn ngữ lớn (LLM).
13
+ Hỗ trợ nhiều provider: OpenAI, HuggingFace, local models, etc.
14
+ """
15
+
16
+ def __init__(self, provider: str = "openai", **kwargs):
17
+ """
18
+ Khởi tạo LLMClient.
19
+
20
+ Args:
21
+ provider (str): Loại provider ("openai", "huggingface", "local", "custom")
22
+ **kwargs: Các tham số cấu hình khác
23
+ """
24
+ self.provider = provider.lower()
25
+ self._client = httpx.AsyncClient(timeout=60.0)
26
+
27
+ # Cấu hình theo provider
28
+ if self.provider == "openai":
29
+ self._setup_openai(kwargs)
30
+ elif self.provider == "huggingface":
31
+ self._setup_huggingface(kwargs)
32
+ elif self.provider == "local":
33
+ self._setup_local(kwargs)
34
+ elif self.provider == "custom":
35
+ self._setup_custom(kwargs)
36
+ elif self.provider == "hfs":
37
+ self._setup_HFS(kwargs)
38
+ else:
39
+ raise ValueError(f"Unsupported provider: {provider}")
40
+
41
+ def _setup_openai(self, config: Dict[str, Any]):
42
+ """Cấu hình cho OpenAI."""
43
+ self.api_key = config.get("api_key") or os.getenv("OPENAI_API_KEY")
44
+ self.base_url = config.get("base_url", "https://api.openai.com/v1")
45
+ self.model = config.get("model", "gpt-3.5-turbo")
46
+ self.max_tokens = config.get("max_tokens", 1000)
47
+ self.temperature = config.get("temperature", 0.7)
48
+
49
+ if not self.api_key:
50
+ raise ValueError("OpenAI API key is required")
51
+
52
+ def _setup_huggingface(self, config: Dict[str, Any]):
53
+ """Cấu hình cho HuggingFace."""
54
+ self.api_key = config.get("api_key") or os.getenv("HUGGINGFACE_API_KEY")
55
+ self.base_url = config.get("base_url", "https://api-inference.huggingface.co")
56
+ self.model = config.get("model", "microsoft/DialoGPT-medium")
57
+ self.max_tokens = config.get("max_tokens", 1000)
58
+ self.temperature = config.get("temperature", 0.7)
59
+
60
+ if not self.api_key:
61
+ raise ValueError("HuggingFace API key is required")
62
+
63
+ def _setup_local(self, config: Dict[str, Any]):
64
+ """Cấu hình cho local model."""
65
+ self.base_url = config.get("base_url", "http://localhost:8000")
66
+ self.model = config.get("model", "default")
67
+ self.max_tokens = config.get("max_tokens", 1000)
68
+ self.temperature = config.get("temperature", 0.7)
69
+
70
+ def _setup_custom(self, config: Dict[str, Any]):
71
+ """Cấu hình cho custom provider."""
72
+ self.base_url = config.get("base_url")
73
+ self.api_key = config.get("api_key")
74
+ self.model = config.get("model", "default")
75
+ self.max_tokens = config.get("max_tokens", 1000)
76
+ self.temperature = config.get("temperature", 0.7)
77
+
78
+ def _setup_HFS(self, config: Dict[str, Any]):
79
+ """Cấu hình cho custom provider."""
80
+ self.base_url = config.get("base_url")
81
+
82
+ if not self.base_url:
83
+ raise ValueError("Custom provider requires base_url")
84
+
85
+ @timing_decorator_async
86
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10), reraise=True)
87
+ async def generate_text(
88
+ self,
89
+ prompt: str,
90
+ system_prompt: Optional[str] = None,
91
+ **kwargs
92
+ ) -> str:
93
+ """
94
+ Tạo text từ prompt sử dụng LLM.
95
+
96
+ Args:
97
+ prompt (str): Prompt đầu vào
98
+ system_prompt (str, optional): System prompt
99
+ **kwargs: Các tham số bổ sung
100
+
101
+ Returns:
102
+ str: Text được tạo ra
103
+ """
104
+ try:
105
+ if self.provider == "openai":
106
+ return await self._generate_openai(prompt, system_prompt, **kwargs)
107
+ elif self.provider == "huggingface":
108
+ return await self._generate_huggingface(prompt, **kwargs)
109
+ elif self.provider == "local":
110
+ return await self._generate_local(prompt, **kwargs)
111
+ elif self.provider == "custom":
112
+ return await self._generate_custom(prompt, **kwargs)
113
+ elif self.provider == "hfs":
114
+ return await self._generate_hfs(prompt, **kwargs)
115
+ else:
116
+ raise ValueError(f"Unsupported provider: {self.provider}")
117
+
118
+ except Exception as e:
119
+ logger.error(f"Error generating text with {self.provider}: {e}")
120
+ raise
121
+
122
+ async def _generate_openai(self, prompt: str, system_prompt: Optional[str] = None, **kwargs) -> str:
123
+ """Generate text với OpenAI API."""
124
+ messages = []
125
+
126
+ if system_prompt:
127
+ messages.append({"role": "system", "content": system_prompt})
128
+
129
+ messages.append({"role": "user", "content": prompt})
130
+
131
+ payload = {
132
+ "model": kwargs.get("model", self.model),
133
+ "messages": messages,
134
+ "max_tokens": kwargs.get("max_tokens", self.max_tokens),
135
+ "temperature": kwargs.get("temperature", self.temperature),
136
+ "stream": False
137
+ }
138
+
139
+ headers = {
140
+ "Authorization": f"Bearer {self.api_key}",
141
+ "Content-Type": "application/json"
142
+ }
143
+
144
+ response = await self._client.post(
145
+ f"{self.base_url}/chat/completions",
146
+ headers=headers,
147
+ json=payload
148
+ )
149
+ response.raise_for_status()
150
+
151
+ data = response.json()
152
+ return data["choices"][0]["message"]["content"]
153
+
154
+ async def _generate_huggingface(self, prompt: str, **kwargs) -> str:
155
+ """Generate text với HuggingFace API."""
156
+ payload = {
157
+ "inputs": prompt,
158
+ "parameters": {
159
+ "max_new_tokens": kwargs.get("max_tokens", self.max_tokens),
160
+ "temperature": kwargs.get("temperature", self.temperature),
161
+ "return_full_text": False
162
+ }
163
+ }
164
+
165
+ headers = {
166
+ "Authorization": f"Bearer {self.api_key}",
167
+ "Content-Type": "application/json"
168
+ }
169
+
170
+ response = await self._client.post(
171
+ f"{self.base_url}/models/{self.model}",
172
+ headers=headers,
173
+ json=payload
174
+ )
175
+ response.raise_for_status()
176
+
177
+ data = response.json()
178
+ return data[0]["generated_text"]
179
+
180
+ async def _generate_local(self, prompt: str, **kwargs) -> str:
181
+ """Generate text với local model."""
182
+ payload = {
183
+ "prompt": prompt,
184
+ "max_tokens": kwargs.get("max_tokens", self.max_tokens),
185
+ "temperature": kwargs.get("temperature", self.temperature),
186
+ "model": kwargs.get("model", self.model)
187
+ }
188
+
189
+ response = await self._client.post(
190
+ f"{self.base_url}/generate",
191
+ json=payload
192
+ )
193
+ response.raise_for_status()
194
+
195
+ data = response.json()
196
+ return data.get("text", "")
197
+
198
+ async def _generate_custom(self, prompt: str, **kwargs) -> str:
199
+ """Generate text với custom provider."""
200
+ payload = {
201
+ "prompt": prompt,
202
+ "max_tokens": kwargs.get("max_tokens", self.max_tokens),
203
+ "temperature": kwargs.get("temperature", self.temperature),
204
+ "model": kwargs.get("model", self.model)
205
+ }
206
+
207
+ headers = {}
208
+ if self.api_key:
209
+ headers["Authorization"] = f"Bearer {self.api_key}"
210
+
211
+ response = await self._client.post(
212
+ f"{self.base_url}/generate",
213
+ headers=headers,
214
+ json=payload
215
+ )
216
+ response.raise_for_status()
217
+
218
+ data = response.json()
219
+ return data.get("text", "")
220
+
221
+ async def _generate_hfs(self, prompt: str, **kwargs) -> str:
222
+ """Generate text với HFS provider."""
223
+ payload = {
224
+ "prompt": prompt
225
+ }
226
+
227
+ headers = {}
228
+ if self.api_key:
229
+ headers["Authorization"] = f"Bearer {self.api_key}"
230
+
231
+ response = await self._client.post(
232
+ f"{self.base_url}/purechat",
233
+ headers=headers,
234
+ json=payload
235
+ )
236
+ response.raise_for_status()
237
+
238
+ data = response.json()
239
+ return data.get("response", "")
240
+
241
+ @timing_decorator_async
242
+ async def chat(
243
+ self,
244
+ messages: List[Dict[str, str]],
245
+ **kwargs
246
+ ) -> str:
247
+ """
248
+ Chat với LLM sử dụng conversation history.
249
+
250
+ Args:
251
+ messages (List[Dict]): List các message với format [{"role": "user", "content": "..."}]
252
+ **kwargs: Các tham số bổ sung
253
+
254
+ Returns:
255
+ str: Response từ LLM
256
+ """
257
+ if self.provider == "openai":
258
+ return await self._chat_openai(messages, **kwargs)
259
+ else:
260
+ # Với các provider khác, convert messages thành prompt
261
+ prompt = self._messages_to_prompt(messages)
262
+ return await self.generate_text(prompt, **kwargs)
263
+
264
+ async def _chat_openai(self, messages: List[Dict[str, str]], **kwargs) -> str:
265
+ """Chat với OpenAI API."""
266
+ payload = {
267
+ "model": kwargs.get("model", self.model),
268
+ "messages": messages,
269
+ "max_tokens": kwargs.get("max_tokens", self.max_tokens),
270
+ "temperature": kwargs.get("temperature", self.temperature),
271
+ "stream": False
272
+ }
273
+
274
+ headers = {
275
+ "Authorization": f"Bearer {self.api_key}",
276
+ "Content-Type": "application/json"
277
+ }
278
+
279
+ response = await self._client.post(
280
+ f"{self.base_url}/chat/completions",
281
+ headers=headers,
282
+ json=payload
283
+ )
284
+ response.raise_for_status()
285
+
286
+ data = response.json()
287
+ return data["choices"][0]["message"]["content"]
288
+
289
+ def _messages_to_prompt(self, messages: List[Dict[str, str]]) -> str:
290
+ """Convert conversation messages thành prompt string."""
291
+ prompt = ""
292
+ for msg in messages:
293
+ role = msg.get("role", "user")
294
+ content = msg.get("content", "")
295
+
296
+ if role == "system":
297
+ prompt += f"System: {content}\n\n"
298
+ elif role == "user":
299
+ prompt += f"User: {content}\n"
300
+ elif role == "assistant":
301
+ prompt += f"Assistant: {content}\n"
302
+
303
+ prompt += "Assistant: "
304
+ return prompt
305
+
306
+ @timing_decorator_async
307
+ async def classify_text(
308
+ self,
309
+ text: str,
310
+ categories: List[str],
311
+ **kwargs
312
+ ) -> Dict[str, Any]:
313
+ """
314
+ Phân loại text vào các categories.
315
+
316
+ Args:
317
+ text (str): Text cần phân loại
318
+ categories (List[str]): List các categories
319
+ **kwargs: Các tham số bổ sung
320
+
321
+ Returns:
322
+ Dict: Kết quả phân loại
323
+ """
324
+ prompt = f"""
325
+ Phân loại text sau vào một trong các categories: {', '.join(categories)}
326
+
327
+ Text: {text}
328
+
329
+ Trả về kết quả theo format JSON:
330
+ {{
331
+ "category": "tên_category",
332
+ "confidence": 0.95,
333
+ "reasoning": "lý do phân loại"
334
+ }}
335
+ """
336
+
337
+ response = await self.generate_text(prompt, **kwargs)
338
+
339
+ try:
340
+ # Tìm JSON trong response
341
+ import re
342
+ json_match = re.search(r'\{.*\}', response, re.DOTALL)
343
+ if json_match:
344
+ result = json.loads(json_match.group())
345
+ return result
346
+ else:
347
+ return {
348
+ "category": "unknown",
349
+ "confidence": 0.0,
350
+ "reasoning": "Không thể parse JSON response"
351
+ }
352
+ except json.JSONDecodeError:
353
+ return {
354
+ "category": "unknown",
355
+ "confidence": 0.0,
356
+ "reasoning": f"JSON parse error: {response}"
357
+ }
358
+
359
+ @timing_decorator_async
360
+ async def extract_entities(
361
+ self,
362
+ text: str,
363
+ entity_types: Optional[List[str]] = None,
364
+ **kwargs
365
+ ) -> List[Dict[str, Any]]:
366
+ """
367
+ Trích xuất entities từ text.
368
+
369
+ Args:
370
+ text (str): Text cần trích xuất
371
+ entity_types (List[str]): Các loại entity cần tìm
372
+ **kwargs: Các tham số bổ sung
373
+
374
+ Returns:
375
+ List[Dict]: List các entities được tìm thấy
376
+ """
377
+ if entity_types is None:
378
+ entity_types = ["PERSON", "ORGANIZATION", "LOCATION", "MONEY", "DATE"]
379
+
380
+ prompt = f"""
381
+ Trích xuất các entities từ text sau. Tìm các entities thuộc types: {', '.join(entity_types)}
382
+
383
+ Text: {text}
384
+
385
+ Trả về kết quả theo format JSON:
386
+ [
387
+ {{
388
+ "text": "tên entity",
389
+ "type": "loại entity",
390
+ "start": 0,
391
+ "end": 10
392
+ }}
393
+ ]
394
+ """
395
+
396
+ response = await self.generate_text(prompt, **kwargs)
397
+
398
+ try:
399
+ import re
400
+ json_match = re.search(r'\[.*\]', response, re.DOTALL)
401
+ if json_match:
402
+ entities = json.loads(json_match.group())
403
+ return entities
404
+ else:
405
+ return []
406
+ except json.JSONDecodeError:
407
+ logger.error(f"Error parsing entities JSON: {response}")
408
+ return []
409
+
410
+ @timing_decorator_async
411
+ async def analyze(
412
+ self,
413
+ text: str,
414
+ **kwargs
415
+ ) -> List[Dict[str, Any]]:
416
+ """
417
+ Trích xuất entities từ text.
418
+
419
+ Args:
420
+ text (str): Text cần trích xuất
421
+ **kwargs: Các tham số bổ sung
422
+
423
+ Returns:
424
+ List[Dict]: List các entities được tìm thấy
425
+ """
426
+
427
+ prompt = f"""
428
+ Phân tích ngữ nghĩa câu sau: \"{text}\"
429
+
430
+ Trả lời dưới dạng JSON với 3 trường sau:
431
+ {{
432
+ "muc_dich": "...",
433
+ "phuong_tien": "...",
434
+ "hanh_vi_vi_pham": "..."
435
+ }}
436
+
437
+ Ví dụ:
438
+ "Tôi chạy xe hơi không bật đèn vào ban đêm thì có bị sao không?"
439
+ → {{
440
+ "muc_dich": "Hỏi về hậu quả/hình phạt khi không bật đèn xe hơi ban đêm",
441
+ "phuong_tien": "Xe hơi",
442
+ "hanh_vi_vi_pham": "Không bật đèn khi lái xe vào ban đêm"
443
+ }}
444
+
445
+ Câu bạn cần phân tích:
446
+ \"{text}\"
447
+ """.strip()
448
+
449
+ response = await self.generate_text(prompt, **kwargs)
450
+
451
+ try:
452
+ import re
453
+ json_match = re.search(r'\[.*\]', response, re.DOTALL)
454
+ if json_match:
455
+ entities = json.loads(json_match.group())
456
+ return entities
457
+ else:
458
+ return []
459
+ except json.JSONDecodeError:
460
+ logger.error(f"Error parsing entities JSON: {response}")
461
+ return []
462
+
463
+ async def close(self):
464
+ """Đóng client connection."""
465
+ await self._client.aclose()
466
+
467
+
468
+ # Factory function để tạo LLMClient dễ dàng
469
+ def create_llm_client(provider: str = "openai", **kwargs) -> LLMClient:
470
+ """
471
+ Factory function để tạo LLMClient.
472
+
473
+ Args:
474
+ provider (str): Loại provider
475
+ **kwargs: Các tham số cấu hình
476
+
477
+ Returns:
478
+ LLMClient: Instance của LLMClient
479
+ """
480
+ return LLMClient(provider, **kwargs)
481
+
482
+
483
+ # Ví dụ sử dụng
484
+ if __name__ == "__main__":
485
+ import asyncio
486
+
487
+ async def test_llm():
488
+ # Test với OpenAI
489
+ llm = create_llm_client("openai", model="gpt-3.5-turbo")
490
+
491
+ # Generate text
492
+ response = await llm.generate_text("Xin chào, bạn có khỏe không?")
493
+ print(f"Response: {response}")
494
+
495
+ # Chat
496
+ messages = [
497
+ {"role": "user", "content": "Bạn có thể giúp tôi không?"}
498
+ ]
499
+ chat_response = await llm.chat(messages)
500
+ print(f"Chat response: {chat_response}")
501
+
502
+ await llm.close()
503
+
504
+ asyncio.run(test_llm())
app/main.py CHANGED
@@ -16,6 +16,7 @@ from .embedding import EmbeddingClient
16
  from .utils import setup_logging, extract_command, extract_keywords, timing_decorator_async, timing_decorator_sync, ensure_log_dir, validate_config
17
  from .constants import VEHICLE_KEYWORDS, SHEET_RANGE, VEHICLE_KEYWORD_TO_COLUMN
18
  from .health import router as health_router
 
19
 
20
  app = FastAPI(title="WeBot Facebook Messenger API")
21
 
@@ -52,6 +53,12 @@ embedding_client = EmbeddingClient()
52
  # Keywords to look for in messages
53
  VEHICLE_KEYWORDS = ["xe máy", "ô tô", "xe đạp", "xe hơi"]
54
 
 
 
 
 
 
 
55
  logger.info("[STARTUP] Mount health router...")
56
  app.include_router(health_router)
57
 
@@ -60,6 +67,8 @@ validate_config(settings)
60
 
61
  executor = ThreadPoolExecutor(max_workers=4)
62
 
 
 
63
  @app.get("/")
64
  async def root():
65
  """Endpoint root để kiểm tra trạng thái app."""
@@ -171,7 +180,23 @@ async def process_message(message_data: Dict[str, Any]):
171
 
172
  # Extract command and keywords
173
  command, remaining_text = extract_command(message_text)
174
- keywords = extract_keywords(message_text, VEHICLE_KEYWORDS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
  # Get conversation history (run in thread pool)
177
  loop = asyncio.get_event_loop()
@@ -191,6 +216,8 @@ async def process_message(message_data: Dict[str, Any]):
191
  'content': remaining_text,
192
  'attachments': attachments,
193
  'vehicle': ','.join(keywords),
 
 
194
  'is_done': False
195
  }
196
 
@@ -199,12 +226,15 @@ async def process_message(message_data: Dict[str, Any]):
199
  if not command:
200
  if keywords:
201
  # Có thông tin phương tiện
202
- embedding = await embedding_client.create_embedding(message_text)
 
 
 
203
  logger.info(f"[DEBUG] embedding: {embedding[:5]} ... (total {len(embedding)})")
204
  matches = supabase_client.match_documents(embedding, vehicle_keywords=keywords)
205
  logger.info(f"[DEBUG] matches: {matches}")
206
  if matches:
207
- response = format_search_results(matches)
208
  else:
209
  response = "Xin lỗi, tôi không tìm thấy thông tin phù hợp."
210
  log_kwargs['is_done'] = True
@@ -225,16 +255,28 @@ async def process_message(message_data: Dict[str, Any]):
225
  last_command = last_conv['originalcommand'] if last_conv else ''
226
  last_isdone = last_conv['isdone'] if last_conv else False
227
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  if not last_command:
229
  # Lịch sử không có command
230
  if keywords:
231
  # Có thông tin phương tiện
232
- embedding = await embedding_client.create_embedding(message_text)
233
  logger.info(f"[DEBUG] embedding: {embedding[:5]} ... (total {len(embedding)})")
234
  matches = supabase_client.match_documents(embedding, vehicle_keywords=keywords)
235
  logger.info(f"[DEBUG] matches: {matches}")
236
  if matches:
237
- response = format_search_results(matches)
238
  else:
239
  response = "Xin lỗi, tôi không tìm thấy thông tin phù hợp."
240
  log_kwargs['is_done'] = True
@@ -268,36 +310,95 @@ async def process_message(message_data: Dict[str, Any]):
268
  await loop.run_in_executor(executor, lambda: sheets_client.log_conversation(**log_kwargs))
269
  return
270
 
271
- def format_search_results(matches: List[Dict[str, Any]]) -> str:
272
  if not matches:
273
  return "Không tìm thấy kết quả phù hợp."
274
  # Tìm item có similarity cao nhất
275
  top = None
276
- for item in matches:
277
- if not top or (item.get('similarity', 0) > top.get('similarity', 0)):
278
- top = item
279
- result_text = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  if top and (top.get('tieude') or top.get('noidung')):
281
  # Thực hiện hành vi
282
  tieude = (top.get('tieude') or '').strip()
283
  noidung = (top.get('noidung') or '').strip()
284
  hanhvi = (tieude + "\n" + noidung).strip().replace('\n', ' ')
285
- result_text += f"Thực hiện hành vi:\n{hanhvi}"
286
  # Cá nhân bị phạt tiền
287
  if top.get('canhantu') or top.get('canhanden'):
288
- result_text += f"\nCá nhân sẽ bị phạt tiền từ {top.get('canhantu', '')} VNĐ đến {top.get('canhanden', '')} VNĐ"
289
  # Tổ chức bị phạt tiền
290
  if top.get('tochuctu') or top.get('tochucden'):
291
- result_text += f"\nTổ chức sẽ bị phạt tiền từ {top.get('tochuctu', '')} VNĐ đến {top.get('tochucden', '')} VNĐ"
292
  # Hình phạt bổ sung
293
  if top.get('hpbsnoidung'):
294
- result_text += f"\nNgoài việc bị phạt tiền, người vi phạm còn bị {top.get('hpbsnoidung')}"
295
  # Biện pháp khắc phục hậu quả
296
  if top.get('bpkpnoidung'):
297
- result_text += f"\nNgoài ra, người vi phạm còn bị buộc {top.get('bpkpnoidung')}"
298
  else:
299
  result_text = "Không có kết quả phù hợp!"
300
- return result_text.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
  async def create_facebook_post(page_token: str, sender_id: str, history: List[Dict[str, Any]]) -> str:
303
  """
 
16
  from .utils import setup_logging, extract_command, extract_keywords, timing_decorator_async, timing_decorator_sync, ensure_log_dir, validate_config
17
  from .constants import VEHICLE_KEYWORDS, SHEET_RANGE, VEHICLE_KEYWORD_TO_COLUMN
18
  from .health import router as health_router
19
+ from .llm import create_llm_client
20
 
21
  app = FastAPI(title="WeBot Facebook Messenger API")
22
 
 
53
  # Keywords to look for in messages
54
  VEHICLE_KEYWORDS = ["xe máy", "ô tô", "xe đạp", "xe hơi"]
55
 
56
+ # Khởi tạo LLM client (ví dụ dùng HFS, bạn có thể đổi provider tuỳ ý)
57
+ llm_client = create_llm_client(
58
+ provider="hfs",
59
+ base_url="https://vietcat-vietnameseembeddingv2.hf.space"
60
+ )
61
+
62
  logger.info("[STARTUP] Mount health router...")
63
  app.include_router(health_router)
64
 
 
67
 
68
  executor = ThreadPoolExecutor(max_workers=4)
69
 
70
+ message_text = None
71
+
72
  @app.get("/")
73
  async def root():
74
  """Endpoint root để kiểm tra trạng thái app."""
 
180
 
181
  # Extract command and keywords
182
  command, remaining_text = extract_command(message_text)
183
+ # Sử dụng LLM để phân tích message_text và extract keywords, mục đích, hành vi vi phạm
184
+ llm_analysis = await llm_client.analyze(message_text)
185
+ muc_dich = None
186
+ hanh_vi_vi_pham = None
187
+ if isinstance(llm_analysis, dict):
188
+ keywords = [llm_analysis.get('phuong_tien', '').lower()]
189
+ muc_dich = llm_analysis.get('muc_dich')
190
+ hanh_vi_vi_pham = llm_analysis.get('hanh_vi_vi_pham')
191
+ elif isinstance(llm_analysis, list) and len(llm_analysis) > 0:
192
+ keywords = [llm_analysis[0].get('phuong_tien', '').lower()]
193
+ muc_dich = llm_analysis[0].get('muc_dich')
194
+ hanh_vi_vi_pham = llm_analysis[0].get('hanh_vi_vi_pham')
195
+ else:
196
+ keywords = extract_keywords(message_text, VEHICLE_KEYWORDS)
197
+ hanh_vi_vi_pham = message_text.replace(keywords, "")
198
+
199
+ logger.info(f"[DEBUG] Phương tiện: {keywords} - Hành vi: {hanh_vi_vi_pham} - Mục đích: {muc_dich}")
200
 
201
  # Get conversation history (run in thread pool)
202
  loop = asyncio.get_event_loop()
 
216
  'content': remaining_text,
217
  'attachments': attachments,
218
  'vehicle': ','.join(keywords),
219
+ 'action': hanh_vi_vi_pham,
220
+ 'purpose': muc_dich,
221
  'is_done': False
222
  }
223
 
 
226
  if not command:
227
  if keywords:
228
  # Có thông tin phương tiện
229
+ if hanh_vi_vi_pham:
230
+ embedding = await embedding_client.create_embedding(hanh_vi_vi_pham)
231
+ else:
232
+ embedding = await embedding_client.create_embedding(message_text)
233
  logger.info(f"[DEBUG] embedding: {embedding[:5]} ... (total {len(embedding)})")
234
  matches = supabase_client.match_documents(embedding, vehicle_keywords=keywords)
235
  logger.info(f"[DEBUG] matches: {matches}")
236
  if matches:
237
+ response = await format_search_results(matches)
238
  else:
239
  response = "Xin lỗi, tôi không tìm thấy thông tin phù hợp."
240
  log_kwargs['is_done'] = True
 
255
  last_command = last_conv['originalcommand'] if last_conv else ''
256
  last_isdone = last_conv['isdone'] if last_conv else False
257
 
258
+ # --- Cập nhật log_kwargs theo lịch sử ---
259
+ log_kwargs_old = log_kwargs.copy()
260
+ log_kwargs_overwritten = {}
261
+ for key in log_kwargs.keys():
262
+ history_val = last_conv.get(key) if last_conv else None
263
+ current_val = log_kwargs[key]
264
+ if history_val not in [None, '', [], {}]:
265
+ if current_val not in [None, '', [], {}]:
266
+ log_kwargs_overwritten[key] = current_val # Lưu giá trị cũ để xử lý sau
267
+ log_kwargs[key] = history_val # Ưu tiên giá trị từ lịch sử
268
+ # --- END cập nhật log_kwargs ---
269
+
270
  if not last_command:
271
  # Lịch sử không có command
272
  if keywords:
273
  # Có thông tin phương tiện
274
+ embedding = await embedding_client.create_embedding(log_kwargs['action'])
275
  logger.info(f"[DEBUG] embedding: {embedding[:5]} ... (total {len(embedding)})")
276
  matches = supabase_client.match_documents(embedding, vehicle_keywords=keywords)
277
  logger.info(f"[DEBUG] matches: {matches}")
278
  if matches:
279
+ response = await format_search_results(matches)
280
  else:
281
  response = "Xin lỗi, tôi không tìm thấy thông tin phù hợp."
282
  log_kwargs['is_done'] = True
 
310
  await loop.run_in_executor(executor, lambda: sheets_client.log_conversation(**log_kwargs))
311
  return
312
 
313
+ async def format_search_results(matches: List[Dict[str, Any]]) -> str:
314
  if not matches:
315
  return "Không tìm thấy kết quả phù hợp."
316
  # Tìm item có similarity cao nhất
317
  top = None
318
+ top_result_text = ""
319
+ full_result_text = ""
320
+
321
+ for i, match in enumerate(matches, 1):
322
+ if not top or (match.get('similarity', 0) > top.get('similarity', 0)):
323
+ top = match
324
+
325
+ # Chuẩn bị context cho LLM: liệt kê tất cả các item với chú thích rõ ràng
326
+ full_result_text += f"Đoạn {i}:\n"
327
+ # Thực hiện hành vi
328
+ tieude = (top.get('tieude') or '').strip()
329
+ noidung = (top.get('noidung') or '').strip()
330
+ hanhvi = (tieude + "\n" + noidung).strip().replace('\n', ' ')
331
+ full_result_text += f"Thực hiện hành vi:\n{hanhvi}"
332
+ # Cá nhân bị phạt tiền
333
+ if top.get('canhantu') or top.get('canhanden'):
334
+ full_result_text += f"\nCá nhân sẽ bị phạt tiền từ {top.get('canhantu', '')} VNĐ đến {top.get('canhanden', '')} VNĐ"
335
+ # Tổ chức bị phạt tiền
336
+ if top.get('tochuctu') or top.get('tochucden'):
337
+ full_result_text += f"\nTổ chức sẽ bị phạt tiền từ {top.get('tochuctu', '')} VNĐ đến {top.get('tochucden', '')} VNĐ"
338
+ # Hình phạt bổ sung
339
+ if top.get('hpbsnoidung'):
340
+ full_result_text += f"\nNgoài việc bị phạt tiền, người vi phạm còn bị {top.get('hpbsnoidung')}"
341
+ # Biện pháp khắc phục hậu quả
342
+ if top.get('bpkpnoidung'):
343
+ full_result_text += f"\nNgoài ra, người vi phạm còn bị buộc {top.get('bpkpnoidung')}"
344
+
345
  if top and (top.get('tieude') or top.get('noidung')):
346
  # Thực hiện hành vi
347
  tieude = (top.get('tieude') or '').strip()
348
  noidung = (top.get('noidung') or '').strip()
349
  hanhvi = (tieude + "\n" + noidung).strip().replace('\n', ' ')
350
+ top_result_text += f"Thực hiện hành vi:\n{hanhvi}"
351
  # Cá nhân bị phạt tiền
352
  if top.get('canhantu') or top.get('canhanden'):
353
+ top_result_text += f"\nCá nhân sẽ bị phạt tiền từ {top.get('canhantu', '')} VNĐ đến {top.get('canhanden', '')} VNĐ"
354
  # Tổ chức bị phạt tiền
355
  if top.get('tochuctu') or top.get('tochucden'):
356
+ top_result_text += f"\nTổ chức sẽ bị phạt tiền từ {top.get('tochuctu', '')} VNĐ đến {top.get('tochucden', '')} VNĐ"
357
  # Hình phạt bổ sung
358
  if top.get('hpbsnoidung'):
359
+ top_result_text += f"\nNgoài việc bị phạt tiền, người vi phạm còn bị {top.get('hpbsnoidung')}"
360
  # Biện pháp khắc phục hậu quả
361
  if top.get('bpkpnoidung'):
362
+ top_result_text += f"\nNgoài ra, người vi phạm còn bị buộc {top.get('bpkpnoidung')}"
363
  else:
364
  result_text = "Không có kết quả phù hợp!"
365
+
366
+ # Prompt cho LLM
367
+ prompt = (
368
+ "Bạn là một trợ lý AI có kiến thức pháp luật, hãy trả lời câu hỏi dựa trên các đoạn luật sau. "
369
+ "Chỉ sử dụng thông tin có trong các đoạn, không tự đoán.\n"
370
+ f"Các đoạn luật liên quan:\n{full_result_text}"
371
+ "\nHãy trả lời ngắn gọn, dễ hiểu, trích dẫn rõ ràng thông tin từ các đoạn luật nếu cần."
372
+ f"Câu hỏi của người dùng: {message_text}\n"
373
+ )
374
+
375
+ logger.info(f"[DEBUG] prompt:\n {prompt}")
376
+
377
+ # Gọi LLM để sinh câu trả lời, fallback nếu lỗi
378
+ try:
379
+ answer = await llm_client.generate_text(prompt)
380
+ if answer and answer.strip():
381
+ return answer.strip()
382
+ except Exception as e:
383
+ logger.error(f"LLM không sẵn sàng: {e}\n{traceback.format_exc()}")
384
+ # Fallback: trả về tổng hợp các đoạn luật như cũ
385
+ fallback = "Tóm tắt các đoạn luật liên quan:\n\n"
386
+ for i, match in enumerate(matches, 1):
387
+ fallback += f"Đoạn {i}:\n"
388
+ tieude = (match.get('tieude') or '').strip()
389
+ noidung = (match.get('noidung') or '').strip()
390
+ if tieude or noidung:
391
+ fallback += f" - Hành vi: {(tieude + ' ' + noidung).strip()}\n"
392
+ if match.get('canhantu') or match.get('canhanden'):
393
+ fallback += f" - Cá nhân bị phạt tiền từ {match.get('canhantu', '')} VNĐ đến {match.get('canhanden', '')} VNĐ\n"
394
+ if match.get('tochuctu') or match.get('tochucden'):
395
+ fallback += f" - Tổ chức bị phạt tiền từ {match.get('tochuctu', '')} VNĐ đến {match.get('tochucden', '')} VNĐ\n"
396
+ if match.get('hpbsnoidung'):
397
+ fallback += f" - Hình phạt bổ sung: {match.get('hpbsnoidung')}\n"
398
+ if match.get('bpkpnoidung'):
399
+ fallback += f" - Biện pháp khắc phục hậu quả: {match.get('bpkpnoidung')}\n"
400
+ fallback += "\n"
401
+ return fallback.strip()
402
 
403
  async def create_facebook_post(page_token: str, sender_id: str, history: List[Dict[str, Any]]) -> str:
404
  """
app/sheets.py CHANGED
@@ -90,8 +90,8 @@ class SheetsClient:
90
  history = []
91
 
92
  for row in values:
93
- # Bổ sung cột rỗng cho đủ 9 cột
94
- row = row + [""] * (9 - len(row))
95
  if row[4] == user_id and row[5] == page_id and row[8].lower() == 'false':
96
  history.append({
97
  'conversation_id': row[0],
@@ -102,7 +102,9 @@ class SheetsClient:
102
  'page_id': row[5],
103
  'originaltext': row[6],
104
  'originalvehicle': row[7],
105
- 'isdone': row[8].lower() == 'true'
 
 
106
  })
107
 
108
  return history
@@ -121,6 +123,8 @@ class SheetsClient:
121
  content: str = "",
122
  attachments: Optional[List[str]] = None,
123
  vehicle: str = "",
 
 
124
  is_done: bool = False
125
  ) -> bool:
126
  """
@@ -148,6 +152,8 @@ class SheetsClient:
148
  page_id,
149
  message,
150
  vehicle,
 
 
151
  str(is_done).lower()
152
  ]]
153
 
@@ -182,8 +188,8 @@ class SheetsClient:
182
  if row_index is not None:
183
  # Lấy dữ liệu dòng hiện tại
184
  current_row = values[row_index]
185
- # Đảm bảo đủ 9 cột
186
- while len(current_row) < 9:
187
  current_row.append("")
188
  # Tạo dòng mới với giá trị mới nếu có, giữ nguyên nếu không
189
  new_row = [
@@ -195,7 +201,9 @@ class SheetsClient:
195
  page_id if page_id else current_row[5],
196
  message if message else current_row[6],
197
  vehicle if vehicle else current_row[7],
198
- str(is_done).lower() if is_done is not None else current_row[8]
 
 
199
  ]
200
  update_range = f"{SHEET_RANGE.split('!')[0]}!A{row_index + 1}"
201
  body = {
 
90
  history = []
91
 
92
  for row in values:
93
+ # Bổ sung cột rỗng cho đủ 11 cột
94
+ row = row + [""] * (11 - len(row))
95
  if row[4] == user_id and row[5] == page_id and row[8].lower() == 'false':
96
  history.append({
97
  'conversation_id': row[0],
 
102
  'page_id': row[5],
103
  'originaltext': row[6],
104
  'originalvehicle': row[7],
105
+ 'originalaction': row[8],
106
+ 'originalpurpose': row[9],
107
+ 'isdone': row[10].lower() == 'true'
108
  })
109
 
110
  return history
 
123
  content: str = "",
124
  attachments: Optional[List[str]] = None,
125
  vehicle: str = "",
126
+ action: str = "",
127
+ purpose: str = "",
128
  is_done: bool = False
129
  ) -> bool:
130
  """
 
152
  page_id,
153
  message,
154
  vehicle,
155
+ action,
156
+ purpose,
157
  str(is_done).lower()
158
  ]]
159
 
 
188
  if row_index is not None:
189
  # Lấy dữ liệu dòng hiện tại
190
  current_row = values[row_index]
191
+ # Đảm bảo đủ 11 cột
192
+ while len(current_row) < 11:
193
  current_row.append("")
194
  # Tạo dòng mới với giá trị mới nếu có, giữ nguyên nếu không
195
  new_row = [
 
201
  page_id if page_id else current_row[5],
202
  message if message else current_row[6],
203
  vehicle if vehicle else current_row[7],
204
+ action if action else current_row[8],
205
+ purpose if purpose else current_row[9],
206
+ str(is_done).lower() if is_done is not None else current_row[10]
207
  ]
208
  update_range = f"{SHEET_RANGE.split('!')[0]}!A{row_index + 1}"
209
  body = {