import os import logging import json import asyncio import time import random import re from app.config import GEMINI_API_KEY, GEMINI_MODEL try: import google.genai as genai try: from google.genai import errors as genai_errors except Exception: genai_errors = None except Exception: genai = None genai_errors = None logging.warning("[keywords_service] google.genai module not found; keyword extraction disabled") try: from google.api_core.exceptions import GoogleAPIError except Exception: GoogleAPIError = Exception gemini_client = None if not genai: logging.warning("[keywords_service] google.genai not available, keyword extraction will be disabled") elif not GEMINI_API_KEY: logging.warning("[keywords_service] GEMINI_API_KEY is not set, keyword extraction will be disabled") else: try: gemini_client = genai.Client(api_key=GEMINI_API_KEY) logging.info(f"[keywords_service] Initialized google.genai client with model={GEMINI_MODEL}") except Exception as e: logging.exception(f"[keywords_service] Failed to init google.genai client: {e}") gemini_client = None async def extract_title_and_keywords(text: str) -> tuple[str | None, list[str]]: if not text or not text.strip(): return None, [] if not gemini_client and not genai: # AI not available → safe fallback return None, [] prompt = f""" Bạn là một hệ thống Xử lý Hậu kỳ NLP (NLP Post-Processing) Tiếng Việt. Nhiệm vụ: 1. Sinh **tiêu đề (title)** ngắn gọn phản ánh đúng chủ đề chính của văn bản: - Độ dài tối đa **10 từ** - Mang tính mô tả, trung tính, phù hợp làm tiêu đề ghi chú (note) - KHÔNG giật tít, KHÔNG suy diễn quá mức 2. Rút trích các **từ khóa quan trọng** phản ánh đúng **chủ đề và nội dung chính** của văn bản. - Mỗi từ khóa dài từ **1–4 từ**. - Ưu tiên danh từ, cụm danh từ, thuật ngữ, khái niệm chính. - Loại bỏ từ chung chung, từ đệm, từ cảm thán, từ lặp nghĩa. - KHÔNG diễn giải, KHÔNG tóm tắt, KHÔNG chuẩn hóa lại văn bản. - KHÔNG tạo từ khóa không xuất hiện hoặc không suy luận hợp lý từ văn bản. Quy tắc: - Số lượng từ khóa: 3–10 (tùy độ dài và nội dung văn bản). - Giữ nguyên chữ thường/hoa theo cách viết phổ biến. - KHÔNG trùng lặp từ khóa. - KHÔNG sắp xếp theo bảng chữ cái; ưu tiên theo mức độ quan trọng. Văn bản đầu vào: \"\"\"{text}\"\"\" YÊU CẦU ĐẦU RA: - Chỉ trả về **JSON hợp lệ** - KHÔNG giải thích - KHÔNG markdown - KHÔNG thêm trường khác ngoài schema dưới đây Cấu trúc JSON bắt buộc: {{ "title": "Tiêu đề ngắn gọn", "keywords": ["Từ khóa 1", "Từ khóa 2", "..."] }} """ loop = asyncio.get_event_loop() MAX_RETRIES = 3 BASE_DELAY = 1.0 def call(): last_exc = None for attempt in range(1, MAX_RETRIES + 1): try: if gemini_client: resp = gemini_client.models.generate_content( model=GEMINI_MODEL, contents=prompt, ) return getattr(resp, "text", "") or "" else: model = genai.GenerativeModel(GEMINI_MODEL) if genai else None if model: resp = model.generate_content(prompt) return getattr(resp, "text", "") or "" return "" except Exception as e: last_exc = e is_server_error = False try: if genai_errors and isinstance(e, genai_errors.ServerError): is_server_error = True except Exception: pass msg = str(e) if "503" in msg or "UNAVAILABLE" in msg or is_server_error: if attempt < MAX_RETRIES: delay = BASE_DELAY * (2 ** (attempt - 1)) delay = delay + random.uniform(0, 0.5 * delay) logging.warning(f"[keywords_service] model overloaded (attempt {attempt}/{MAX_RETRIES}), retrying after {delay:.2f}s") time.sleep(delay) continue logging.exception(f"[keywords_service] extract_keywords call failed on attempt {attempt}: {e}") break if last_exc: raise last_exc return "" try: raw = await loop.run_in_executor(None, call) title, keywords = _parse_response(raw) return title, keywords except GoogleAPIError as e: logging.error("[title_keywords_service] Gemini API error: %s", e) except Exception as e: logging.exception("[title_keywords_service] extract failed: %s", e) return None, [] def _parse_response(raw: str) -> tuple[str | None, list[str]]: if not raw: return None, [] raw = raw.strip() # Try extracting JSON block start = raw.find("{") end = raw.rfind("}") if start != -1 and end != -1 and end > start: raw_json = raw[start : end + 1] else: raw_json = raw try: parsed = json.loads(raw_json) except Exception as e: logging.warning( "[title_keywords_service] Failed to parse JSON: %s | raw=%r", e, raw[:300], ) return None, [] title = parsed.get("title") keywords = parsed.get("keywords") # Validate title if not isinstance(title, str) or not title.strip(): title = None else: title = title.strip() # Validate keywords if not isinstance(keywords, list): keywords = [] else: keywords = [ k.strip() for k in keywords if isinstance(k, str) and k.strip() ] return title, keywords