Spaces:
Sleeping
Sleeping
| """ | |
| LLM integration for natural answer generation. | |
| Supports OpenAI GPT, Anthropic Claude, and local LLMs (Ollama). | |
| """ | |
| import os | |
| import re | |
| import json | |
| from typing import List, Dict, Any, Optional | |
| try: | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| except ImportError: | |
| pass # dotenv is optional | |
| # LLM Provider types | |
| LLM_PROVIDER_OPENAI = "openai" | |
| LLM_PROVIDER_ANTHROPIC = "anthropic" | |
| LLM_PROVIDER_OLLAMA = "ollama" | |
| LLM_PROVIDER_NONE = "none" | |
| # Get provider from environment | |
| LLM_PROVIDER = os.environ.get("LLM_PROVIDER", LLM_PROVIDER_NONE).lower() | |
| class LLMGenerator: | |
| """Generate natural language answers using LLMs.""" | |
| def __init__(self, provider: Optional[str] = None): | |
| """ | |
| Initialize LLM generator. | |
| Args: | |
| provider: LLM provider ('openai', 'anthropic', 'ollama', or None for auto-detect). | |
| """ | |
| self.provider = provider or LLM_PROVIDER | |
| self.client = None | |
| self._initialize_client() | |
| def _initialize_client(self): | |
| """Initialize LLM client based on provider.""" | |
| if self.provider == LLM_PROVIDER_OPENAI: | |
| try: | |
| import openai | |
| api_key = os.environ.get("OPENAI_API_KEY") | |
| if api_key: | |
| self.client = openai.OpenAI(api_key=api_key) | |
| print("✅ OpenAI client initialized") | |
| else: | |
| print("⚠️ OPENAI_API_KEY not found, OpenAI disabled") | |
| except ImportError: | |
| print("⚠️ openai package not installed, install with: pip install openai") | |
| elif self.provider == LLM_PROVIDER_ANTHROPIC: | |
| try: | |
| import anthropic | |
| api_key = os.environ.get("ANTHROPIC_API_KEY") | |
| if api_key: | |
| self.client = anthropic.Anthropic(api_key=api_key) | |
| print("✅ Anthropic client initialized") | |
| else: | |
| print("⚠️ ANTHROPIC_API_KEY not found, Anthropic disabled") | |
| except ImportError: | |
| print("⚠️ anthropic package not installed, install with: pip install anthropic") | |
| elif self.provider == LLM_PROVIDER_OLLAMA: | |
| self.ollama_base_url = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434") | |
| print(f"✅ Ollama configured (base_url: {self.ollama_base_url})") | |
| else: | |
| print("ℹ️ No LLM provider configured, using template-based generation") | |
| def is_available(self) -> bool: | |
| """Check if LLM is available.""" | |
| return self.client is not None or self.provider == LLM_PROVIDER_OLLAMA | |
| def generate_answer( | |
| self, | |
| query: str, | |
| context: Optional[List[Dict[str, Any]]] = None, | |
| documents: Optional[List[Any]] = None | |
| ) -> Optional[str]: | |
| """ | |
| Generate natural language answer from documents. | |
| Args: | |
| query: User query. | |
| context: Optional conversation context. | |
| documents: Retrieved documents. | |
| Returns: | |
| Generated answer or None if LLM not available. | |
| """ | |
| if not self.is_available(): | |
| return None | |
| # Build prompt | |
| prompt = self._build_prompt(query, context, documents) | |
| try: | |
| if self.provider == LLM_PROVIDER_OPENAI: | |
| return self._generate_openai(prompt) | |
| elif self.provider == LLM_PROVIDER_ANTHROPIC: | |
| return self._generate_anthropic(prompt) | |
| elif self.provider == LLM_PROVIDER_OLLAMA: | |
| return self._generate_ollama(prompt) | |
| except Exception as e: | |
| print(f"Error generating answer with LLM: {e}") | |
| return None | |
| def _build_prompt( | |
| self, | |
| query: str, | |
| context: Optional[List[Dict[str, Any]]], | |
| documents: Optional[List[Any]] | |
| ) -> str: | |
| """Build prompt for LLM.""" | |
| prompt_parts = [ | |
| "Bạn là chatbot tư vấn pháp lý của Công an Thừa Thiên Huế.", | |
| "Nhiệm vụ: Trả lời câu hỏi của người dùng dựa trên các văn bản pháp luật và quy định được cung cấp.", | |
| "", | |
| f"Câu hỏi của người dùng: {query}", | |
| "" | |
| ] | |
| if context: | |
| prompt_parts.append("Ngữ cảnh cuộc hội thoại trước đó:") | |
| for msg in context[-3:]: # Last 3 messages | |
| role = "Người dùng" if msg.get("role") == "user" else "Bot" | |
| content = msg.get("content", "") | |
| prompt_parts.append(f"{role}: {content}") | |
| prompt_parts.append("") | |
| if documents: | |
| prompt_parts.append("Các văn bản/quy định liên quan:") | |
| for i, doc in enumerate(documents[:5], 1): | |
| # Extract relevant fields based on document type | |
| doc_text = self._format_document(doc) | |
| prompt_parts.append(f"{i}. {doc_text}") | |
| prompt_parts.append("") | |
| prompt_parts.extend([ | |
| "Yêu cầu QUAN TRỌNG:", | |
| "- CHỈ trả lời dựa trên thông tin trong 'Các văn bản/quy định liên quan' ở trên", | |
| "- KHÔNG được tự tạo hoặc suy đoán thông tin không có trong tài liệu", | |
| "- Nếu thông tin không đủ để trả lời, hãy nói rõ: 'Thông tin trong cơ sở dữ liệu chưa đủ để trả lời câu hỏi này'", | |
| "- Nếu có mức phạt, phải ghi rõ số tiền (ví dụ: 200.000 - 400.000 VNĐ)", | |
| "- Nếu có điều khoản, ghi rõ mã điều (ví dụ: Điều 5, Điều 10)", | |
| "- Nếu có thủ tục, ghi rõ hồ sơ, lệ phí, thời hạn", | |
| "- Trả lời bằng tiếng Việt, ngắn gọn, dễ hiểu", | |
| "", | |
| "Trả lời:" | |
| ]) | |
| return "\n".join(prompt_parts) | |
| def _format_document(self, doc: Any) -> str: | |
| """Format document for prompt.""" | |
| doc_type = type(doc).__name__.lower() | |
| if "fine" in doc_type: | |
| parts = [f"Mức phạt: {getattr(doc, 'name', '')}"] | |
| if hasattr(doc, 'code') and doc.code: | |
| parts.append(f"Mã: {doc.code}") | |
| if hasattr(doc, 'min_fine') and hasattr(doc, 'max_fine'): | |
| if doc.min_fine and doc.max_fine: | |
| parts.append(f"Số tiền: {doc.min_fine:,.0f} - {doc.max_fine:,.0f} VNĐ") | |
| return " | ".join(parts) | |
| elif "procedure" in doc_type: | |
| parts = [f"Thủ tục: {getattr(doc, 'title', '')}"] | |
| if hasattr(doc, 'dossier') and doc.dossier: | |
| parts.append(f"Hồ sơ: {doc.dossier}") | |
| if hasattr(doc, 'fee') and doc.fee: | |
| parts.append(f"Lệ phí: {doc.fee}") | |
| return " | ".join(parts) | |
| elif "office" in doc_type: | |
| parts = [f"Đơn vị: {getattr(doc, 'unit_name', '')}"] | |
| if hasattr(doc, 'address') and doc.address: | |
| parts.append(f"Địa chỉ: {doc.address}") | |
| if hasattr(doc, 'phone') and doc.phone: | |
| parts.append(f"Điện thoại: {doc.phone}") | |
| return " | ".join(parts) | |
| elif "advisory" in doc_type: | |
| parts = [f"Cảnh báo: {getattr(doc, 'title', '')}"] | |
| if hasattr(doc, 'summary') and doc.summary: | |
| parts.append(f"Nội dung: {doc.summary[:200]}") | |
| return " | ".join(parts) | |
| elif "legalsection" in doc_type or "legal" in doc_type: | |
| parts = [] | |
| if hasattr(doc, 'section_code') and doc.section_code: | |
| parts.append(f"Điều khoản: {doc.section_code}") | |
| if hasattr(doc, 'section_title') and doc.section_title: | |
| parts.append(f"Tiêu đề: {doc.section_title}") | |
| if hasattr(doc, 'document') and doc.document: | |
| doc_obj = doc.document | |
| if hasattr(doc_obj, 'title'): | |
| parts.append(f"Văn bản: {doc_obj.title}") | |
| if hasattr(doc_obj, 'code'): | |
| parts.append(f"Mã văn bản: {doc_obj.code}") | |
| if hasattr(doc, 'content') and doc.content: | |
| # Truncate content to 300 chars for prompt | |
| content_short = doc.content[:300] + "..." if len(doc.content) > 300 else doc.content | |
| parts.append(f"Nội dung: {content_short}") | |
| return " | ".join(parts) if parts else str(doc) | |
| return str(doc) | |
| def _generate_openai(self, prompt: str) -> Optional[str]: | |
| """Generate answer using OpenAI.""" | |
| if not self.client: | |
| return None | |
| try: | |
| response = self.client.chat.completions.create( | |
| model=os.environ.get("OPENAI_MODEL", "gpt-3.5-turbo"), | |
| messages=[ | |
| {"role": "system", "content": "Bạn là chatbot tư vấn chuyên nghiệp."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| temperature=0.7, | |
| max_tokens=500 | |
| ) | |
| return response.choices[0].message.content | |
| except Exception as e: | |
| print(f"OpenAI API error: {e}") | |
| return None | |
| def _generate_anthropic(self, prompt: str) -> Optional[str]: | |
| """Generate answer using Anthropic Claude.""" | |
| if not self.client: | |
| return None | |
| try: | |
| message = self.client.messages.create( | |
| model=os.environ.get("ANTHROPIC_MODEL", "claude-3-haiku-20240307"), | |
| max_tokens=500, | |
| messages=[ | |
| {"role": "user", "content": prompt} | |
| ] | |
| ) | |
| return message.content[0].text | |
| except Exception as e: | |
| print(f"Anthropic API error: {e}") | |
| return None | |
| def _generate_ollama(self, prompt: str) -> Optional[str]: | |
| """Generate answer using Ollama (local LLM).""" | |
| try: | |
| import requests | |
| model = os.environ.get("OLLAMA_MODEL", "gemma3:1b") | |
| response = requests.post( | |
| f"{self.ollama_base_url}/api/generate", | |
| json={ | |
| "model": model, | |
| "prompt": prompt, | |
| "stream": False, | |
| "options": { | |
| "temperature": 0.7, | |
| "top_p": 0.9, | |
| "num_predict": 500 | |
| } | |
| }, | |
| timeout=60 | |
| ) | |
| if response.status_code == 200: | |
| return response.json().get("response") | |
| return None | |
| except Exception as e: | |
| print(f"Ollama API error: {e}") | |
| return None | |
| def summarize_context(self, messages: List[Dict[str, Any]], max_length: int = 200) -> str: | |
| """ | |
| Summarize conversation context. | |
| Args: | |
| messages: List of conversation messages. | |
| max_length: Maximum summary length. | |
| Returns: | |
| Summary string. | |
| """ | |
| if not messages: | |
| return "" | |
| # Simple summarization: extract key entities and intents | |
| intents = [] | |
| entities = set() | |
| for msg in messages: | |
| if msg.get("intent"): | |
| intents.append(msg["intent"]) | |
| if msg.get("entities"): | |
| for key, value in msg["entities"].items(): | |
| if isinstance(value, str): | |
| entities.add(value) | |
| elif isinstance(value, list): | |
| entities.update(value) | |
| summary_parts = [] | |
| if intents: | |
| unique_intents = list(set(intents)) | |
| summary_parts.append(f"Chủ đề: {', '.join(unique_intents)}") | |
| if entities: | |
| summary_parts.append(f"Thông tin: {', '.join(list(entities)[:5])}") | |
| summary = ". ".join(summary_parts) | |
| return summary[:max_length] if len(summary) > max_length else summary | |
| def extract_entities_llm(self, query: str) -> Dict[str, Any]: | |
| """ | |
| Extract entities using LLM. | |
| Args: | |
| query: User query. | |
| Returns: | |
| Dictionary of extracted entities. | |
| """ | |
| if not self.is_available(): | |
| return {} | |
| prompt = f""" | |
| Trích xuất các thực thể từ câu hỏi sau: | |
| "{query}" | |
| Các loại thực thể cần tìm: | |
| - fine_code: Mã vi phạm (V001, V002, ...) | |
| - fine_name: Tên vi phạm | |
| - procedure_name: Tên thủ tục | |
| - office_name: Tên đơn vị | |
| Trả lời dưới dạng JSON: {{"fine_code": "...", "fine_name": "...", ...}} | |
| Nếu không có, trả về {{}}. | |
| """ | |
| try: | |
| if self.provider == LLM_PROVIDER_OPENAI: | |
| response = self._generate_openai(prompt) | |
| elif self.provider == LLM_PROVIDER_ANTHROPIC: | |
| response = self._generate_anthropic(prompt) | |
| elif self.provider == LLM_PROVIDER_OLLAMA: | |
| response = self._generate_ollama(prompt) | |
| else: | |
| return {} | |
| if response: | |
| # Try to extract JSON from response | |
| json_match = re.search(r'\{[^}]+\}', response) | |
| if json_match: | |
| return json.loads(json_match.group()) | |
| except Exception as e: | |
| print(f"Error extracting entities with LLM: {e}") | |
| return {} | |
| # Global LLM generator instance | |
| _llm_generator: Optional[LLMGenerator] = None | |
| def get_llm_generator() -> Optional[LLMGenerator]: | |
| """Get or create LLM generator instance.""" | |
| global _llm_generator | |
| if _llm_generator is None: | |
| _llm_generator = LLMGenerator() | |
| return _llm_generator if _llm_generator.is_available() else None | |