{"repo": "anhmtk/stillme-learning-ai-system-rag-foundation", "files": {"render.yaml": {"ae9a30c512f6409dcb5eeeec238cdabb9b05932d": {"t": 1785745201.7475011, "n": 1297, "h": "b0f3a73e3996f105", "c": "# Render.com deployment configuration for StillMe\n# Connect GitHub repo → Render auto-detects this file → Deploy!\n\nservices:\n # Backend API Service\n - type: web\n name: stillme-backend\n env: python\n buildCommand: pip install -r requirements.txt\n startCommand: python -m uvicorn backend.api.main:app --host 0.0.0.0 --port $PORT\n envVars:\n - key: PYTHONPATH\n value: /opt/render/project/src\n - key: ENVIRONMENT\n value: production\n - key: DEEPSEEK_API_KEY\n sync: false # Set manually in Render dashboard\n - key: OPENAI_API_KEY\n sync: false # Set manually in Render dashboard\n healthCheckPath: /health\n plan: free # Free tier\n\n # Frontend Dashboard Service\n - type: web\n name: stillme-dashboard\n env: python\n buildCommand: pip install -r requirements.txt\n startCommand: streamlit run dashboard.py --server.port $PORT --server.address 0.0.0.0 --server.headless=true\n envVars:\n - key: PYTHONPATH\n value: /opt/render/project/src\n - key: STILLME_API_BASE\n fromService:\n type: web\n name: stillme-backend\n property: host\n # Will be: https://stillme-backend.onrender.com\n healthCheckPath: /healthz # Streamlit health endpoint\n plan: free # Free tier\n\n"}, "ae26ef2b9ab7bfdeb6e4ad1d50d546563ea03514": {"t": 1785748594.6232488, "n": 1297, "h": "b0f3a73e3996f105", "c": "# Render.com deployment configuration for StillMe\n# Connect GitHub repo → Render auto-detects this file → Deploy!\n\nservices:\n # Backend API Service\n - type: web\n name: stillme-backend\n env: python\n buildCommand: pip install -r requirements.txt\n startCommand: python -m uvicorn backend.api.main:app --host 0.0.0.0 --port $PORT\n envVars:\n - key: PYTHONPATH\n value: /opt/render/project/src\n - key: ENVIRONMENT\n value: production\n - key: DEEPSEEK_API_KEY\n sync: false # Set manually in Render dashboard\n - key: OPENAI_API_KEY\n sync: false # Set manually in Render dashboard\n healthCheckPath: /health\n plan: free # Free tier\n\n # Frontend Dashboard Service\n - type: web\n name: stillme-dashboard\n env: python\n buildCommand: pip install -r requirements.txt\n startCommand: streamlit run dashboard.py --server.port $PORT --server.address 0.0.0.0 --server.headless=true\n envVars:\n - key: PYTHONPATH\n value: /opt/render/project/src\n - key: STILLME_API_BASE\n fromService:\n type: web\n name: stillme-backend\n property: host\n # Will be: https://stillme-backend.onrender.com\n healthCheckPath: /healthz # Streamlit health endpoint\n plan: free # Free tier\n\n"}}, "backend/api/utils/chat_helpers.py": {"ae9a30c512f6409dcb5eeeec238cdabb9b05932d": {"t": 1785746387.7440479, "n": 45420, "h": "141a8b23471f526e", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nimport hashlib\nfrom typing import Optional, AsyncIterator\nfrom functools import lru_cache\n\nlogger = logging.getLogger(__name__)\n\n# TRUST-EFFICIENT: Cache for language detection (max 128 entries)\n_lang_cache = {}\n\n\ndef detect_language(text: str, is_user_query: bool = True) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Args:\n text: Text to detect language for\n is_user_query: If True, apply Vietnamese keyword override (for user queries).\n If False, skip Vietnamese keyword override (for responses/context) to avoid false positives.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()\n \n # CRITICAL: Check Vietnamese keywords ONLY for user queries (not responses)\n # This prevents false Vietnamese detection when response contains Vietnamese keywords from context\n # Vietnamese keywords (even without tone marks)\n # IMPORTANT: Use word boundaries or longer phrases to avoid false positives\n # Short keywords like 'de', 'cho', 'cua' can match English words (e.g., 'code' contains 'de')\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết',\n 'ban', 'bạn', 'minh', 'mình', 'toi', 'tôi'\n ]\n # Short keywords that need word boundary checking to avoid false positives\n vietnamese_short_keywords = ['de', 'để', 'cho', 'cua', 'của', 'voi', 'với']\n \n # Only check Vietnamese keywords for user queries, not responses\n # For short keywords, use word boundary to avoid false positives (e.g., 'code' contains 'de')\n import re\n has_vietnamese_keywords = False\n if is_user_query:\n # Check longer phrases first (more reliable)\n has_long_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n # Check short keywords with word boundaries\n has_short_keywords = any(\n re.search(r'\\b' + re.escape(keyword) + r'\\b', text_lower)\n for keyword in vietnamese_short_keywords\n )\n has_vietnamese_keywords = has_long_keywords or has_short_keywords\n \n # TRUST-EFFICIENT: Check Vietnamese keywords FIRST (fast, no API call)\n # If Vietnamese keywords found, skip langdetect (saves 0.8s+)\n if is_user_query and has_vietnamese_keywords:\n logger.debug(\"🌐 Vietnamese keywords detected - skipping langdetect (fast path)\")\n return 'vi'\n\n # TRUST-EFFICIENT: Fast path for Vietnamese diacritics (avoid langdetect latency spikes)\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n if any(char in vietnamese_chars for char in text_lower):\n logger.debug(\"🌐 Vietnamese diacritics detected - skipping langdetect (fast path)\")\n _lang_cache[text_hash] = 'vi'\n if len(_lang_cache) > 128:\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n return 'vi'\n \n # TRUST-EFFICIENT: Cache language detection results (hash-based)\n if text_hash in _lang_cache:\n cached_lang = _lang_cache[text_hash]\n logger.debug(f\"🌐 Language cache HIT: {cached_lang}\")\n return cached_lang\n \n # OPTIMIZATION: Try langdetect for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect\n from langdetect import LangDetectException\n # Avoid heavy langdetect on very long inputs (use head slice for speed)\n sample_text = text[:2000] if len(text) > 2000 else text\n detected = detect(sample_text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n # CRITICAL: Override langdetect if Vietnamese keywords found (only for user queries)\n # This fixes false French detection for Vietnamese queries without tone marks\n # But we skip this for responses to avoid false positives when response contains Vietnamese keywords from context\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding langdetect result: {detected_lang} -> vi\")\n detected_lang = 'vi'\n \n # TRUST-EFFICIENT: Cache result before explicit language check\n if detected_lang:\n _lang_cache[text_hash] = detected_lang\n # Limit cache size to 128 entries (LRU eviction)\n if len(_lang_cache) > 128:\n # Remove oldest entry (simple FIFO, could use OrderedDict for true LRU)\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n \n except ImportError:\n # langdetect not available - will use rule-based detection\n logger.debug(\"langdetect not available, will use rule-based detection\")\n except Exception as e:\n # LangDetectException or other langdetect errors\n logger.debug(f\"langdetect failed: {e}, will use rule-based detection\")\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n # TRUST-EFFICIENT: Cache explicit language request\n _lang_cache[text_hash] = lang_code\n if len(_lang_cache) > 128:\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n # CRITICAL: Also check Vietnamese keywords here (in case langdetect wasn't used, only for user queries)\n if detected_lang:\n # Override if Vietnamese keywords found (double-check, only for user queries)\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding detected_lang: {detected_lang} -> vi\")\n final_lang = 'vi'\n else:\n final_lang = detected_lang\n # TRUST-EFFICIENT: Cache result\n _lang_cache[text_hash] = final_lang\n if len(_lang_cache) > 128:\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n return final_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese_chars = any(char in vietnamese_chars for char in text_lower)\n \n # Also check for Vietnamese keywords (even without tone marks)\n # This helps detect Vietnamese queries like \"Bao lau de hoc 100 bai viet?\" (without tone marks)\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'de', 'để', 'cho', 'cua', 'của', 'voi', 'với',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết'\n ]\n has_vietnamese_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n \n has_vietnamese = has_vietnamese_chars or has_vietnamese_keywords\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 2: Use Unified Identity Layer - formatting.py (single source of truth)\n from backend.identity.formatting import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: STILLME_IDENTITY is now dynamically built from unified identity modules (core.py, persona.py, meta_llm.py)\n # After Phase 2 refactoring, STILLME_IDENTITY is concise and does not require truncation\n # The unified identity layer ensures all core principles are preserved without duplication\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False, # Internal flag for admin/internal calls\n question: Optional[str] = None, # User question for model routing\n task_type: str = \"chat\", # Task type: \"chat\", \"rewrite\", \"validation\"\n is_philosophical: Optional[bool] = None # Pre-computed philosophical flag\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n # Pass question and task_type to DeepSeekProvider for model routing\n if llm_provider == \"deepseek\" and hasattr(provider, 'generate'):\n # Check if provider supports new parameters (DeepSeekProvider)\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n # Pass question and task_type to DeepSeekProvider for model routing\n if hasattr(provider, 'generate'):\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "52ce4ed73af013008379b26ed376a9604f45111a": {"t": 1785748595.3353524, "n": 45420, "h": "141a8b23471f526e", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nimport hashlib\nfrom typing import Optional, AsyncIterator\nfrom functools import lru_cache\n\nlogger = logging.getLogger(__name__)\n\n# TRUST-EFFICIENT: Cache for language detection (max 128 entries)\n_lang_cache = {}\n\n\ndef detect_language(text: str, is_user_query: bool = True) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Args:\n text: Text to detect language for\n is_user_query: If True, apply Vietnamese keyword override (for user queries).\n If False, skip Vietnamese keyword override (for responses/context) to avoid false positives.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()\n \n # CRITICAL: Check Vietnamese keywords ONLY for user queries (not responses)\n # This prevents false Vietnamese detection when response contains Vietnamese keywords from context\n # Vietnamese keywords (even without tone marks)\n # IMPORTANT: Use word boundaries or longer phrases to avoid false positives\n # Short keywords like 'de', 'cho', 'cua' can match English words (e.g., 'code' contains 'de')\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết',\n 'ban', 'bạn', 'minh', 'mình', 'toi', 'tôi'\n ]\n # Short keywords that need word boundary checking to avoid false positives\n vietnamese_short_keywords = ['de', 'để', 'cho', 'cua', 'của', 'voi', 'với']\n \n # Only check Vietnamese keywords for user queries, not responses\n # For short keywords, use word boundary to avoid false positives (e.g., 'code' contains 'de')\n import re\n has_vietnamese_keywords = False\n if is_user_query:\n # Check longer phrases first (more reliable)\n has_long_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n # Check short keywords with word boundaries\n has_short_keywords = any(\n re.search(r'\\b' + re.escape(keyword) + r'\\b', text_lower)\n for keyword in vietnamese_short_keywords\n )\n has_vietnamese_keywords = has_long_keywords or has_short_keywords\n \n # TRUST-EFFICIENT: Check Vietnamese keywords FIRST (fast, no API call)\n # If Vietnamese keywords found, skip langdetect (saves 0.8s+)\n if is_user_query and has_vietnamese_keywords:\n logger.debug(\"🌐 Vietnamese keywords detected - skipping langdetect (fast path)\")\n return 'vi'\n\n # TRUST-EFFICIENT: Fast path for Vietnamese diacritics (avoid langdetect latency spikes)\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n if any(char in vietnamese_chars for char in text_lower):\n logger.debug(\"🌐 Vietnamese diacritics detected - skipping langdetect (fast path)\")\n _lang_cache[text_hash] = 'vi'\n if len(_lang_cache) > 128:\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n return 'vi'\n \n # TRUST-EFFICIENT: Cache language detection results (hash-based)\n if text_hash in _lang_cache:\n cached_lang = _lang_cache[text_hash]\n logger.debug(f\"🌐 Language cache HIT: {cached_lang}\")\n return cached_lang\n \n # OPTIMIZATION: Try langdetect for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect\n from langdetect import LangDetectException\n # Avoid heavy langdetect on very long inputs (use head slice for speed)\n sample_text = text[:2000] if len(text) > 2000 else text\n detected = detect(sample_text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n # CRITICAL: Override langdetect if Vietnamese keywords found (only for user queries)\n # This fixes false French detection for Vietnamese queries without tone marks\n # But we skip this for responses to avoid false positives when response contains Vietnamese keywords from context\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding langdetect result: {detected_lang} -> vi\")\n detected_lang = 'vi'\n \n # TRUST-EFFICIENT: Cache result before explicit language check\n if detected_lang:\n _lang_cache[text_hash] = detected_lang\n # Limit cache size to 128 entries (LRU eviction)\n if len(_lang_cache) > 128:\n # Remove oldest entry (simple FIFO, could use OrderedDict for true LRU)\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n \n except ImportError:\n # langdetect not available - will use rule-based detection\n logger.debug(\"langdetect not available, will use rule-based detection\")\n except Exception as e:\n # LangDetectException or other langdetect errors\n logger.debug(f\"langdetect failed: {e}, will use rule-based detection\")\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n # TRUST-EFFICIENT: Cache explicit language request\n _lang_cache[text_hash] = lang_code\n if len(_lang_cache) > 128:\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n # CRITICAL: Also check Vietnamese keywords here (in case langdetect wasn't used, only for user queries)\n if detected_lang:\n # Override if Vietnamese keywords found (double-check, only for user queries)\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding detected_lang: {detected_lang} -> vi\")\n final_lang = 'vi'\n else:\n final_lang = detected_lang\n # TRUST-EFFICIENT: Cache result\n _lang_cache[text_hash] = final_lang\n if len(_lang_cache) > 128:\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n return final_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese_chars = any(char in vietnamese_chars for char in text_lower)\n \n # Also check for Vietnamese keywords (even without tone marks)\n # This helps detect Vietnamese queries like \"Bao lau de hoc 100 bai viet?\" (without tone marks)\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'de', 'để', 'cho', 'cua', 'của', 'voi', 'với',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết'\n ]\n has_vietnamese_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n \n has_vietnamese = has_vietnamese_chars or has_vietnamese_keywords\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 2: Use Unified Identity Layer - formatting.py (single source of truth)\n from backend.identity.formatting import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: STILLME_IDENTITY is now dynamically built from unified identity modules (core.py, persona.py, meta_llm.py)\n # After Phase 2 refactoring, STILLME_IDENTITY is concise and does not require truncation\n # The unified identity layer ensures all core principles are preserved without duplication\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False, # Internal flag for admin/internal calls\n question: Optional[str] = None, # User question for model routing\n task_type: str = \"chat\", # Task type: \"chat\", \"rewrite\", \"validation\"\n is_philosophical: Optional[bool] = None # Pre-computed philosophical flag\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n # Pass question and task_type to DeepSeekProvider for model routing\n if llm_provider == \"deepseek\" and hasattr(provider, 'generate'):\n # Check if provider supports new parameters (DeepSeekProvider)\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n # Pass question and task_type to DeepSeekProvider for model routing\n if hasattr(provider, 'generate'):\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "4133304efcbf4760d293a537007739d1fefefc07": {"t": 1785748597.403483, "n": 45269, "h": "c09007cb8530c5ad", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nimport hashlib\nfrom typing import Optional, AsyncIterator\nfrom functools import lru_cache\n\nlogger = logging.getLogger(__name__)\n\n# TRUST-EFFICIENT: Cache for language detection (max 128 entries)\n_lang_cache = {}\n\n\ndef detect_language(text: str, is_user_query: bool = True) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Args:\n text: Text to detect language for\n is_user_query: If True, apply Vietnamese keyword override (for user queries).\n If False, skip Vietnamese keyword override (for responses/context) to avoid false positives.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()\n \n # CRITICAL: Check Vietnamese keywords ONLY for user queries (not responses)\n # This prevents false Vietnamese detection when response contains Vietnamese keywords from context\n # Vietnamese keywords (even without tone marks)\n # IMPORTANT: Use word boundaries or longer phrases to avoid false positives\n # Short keywords like 'de', 'cho', 'cua' can match English words (e.g., 'code' contains 'de')\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết',\n 'ban', 'bạn', 'minh', 'mình', 'toi', 'tôi'\n ]\n # Short keywords that need word boundary checking to avoid false positives\n vietnamese_short_keywords = ['de', 'để', 'cho', 'cua', 'của', 'voi', 'với']\n \n # Only check Vietnamese keywords for user queries, not responses\n # For short keywords, use word boundary to avoid false positives (e.g., 'code' contains 'de')\n import re\n has_vietnamese_keywords = False\n if is_user_query:\n # Check longer phrases first (more reliable)\n has_long_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n # Check short keywords with word boundaries\n has_short_keywords = any(\n re.search(r'\\b' + re.escape(keyword) + r'\\b', text_lower)\n for keyword in vietnamese_short_keywords\n )\n has_vietnamese_keywords = has_long_keywords or has_short_keywords\n \n # TRUST-EFFICIENT: Check Vietnamese keywords FIRST (fast, no API call)\n # If Vietnamese keywords found, skip langdetect (saves 0.8s+)\n if is_user_query and has_vietnamese_keywords:\n logger.debug(\"🌐 Vietnamese keywords detected - skipping langdetect (fast path)\")\n return 'vi'\n\n # TRUST-EFFICIENT: Fast path for Vietnamese diacritics (avoid langdetect latency spikes)\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n if any(char in vietnamese_chars for char in text_lower):\n logger.debug(\"🌐 Vietnamese diacritics detected - skipping langdetect (fast path)\")\n _lang_cache[text_hash] = 'vi'\n if len(_lang_cache) > 128:\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n return 'vi'\n \n # TRUST-EFFICIENT: Cache language detection results (hash-based)\n if text_hash in _lang_cache:\n cached_lang = _lang_cache[text_hash]\n logger.debug(f\"🌐 Language cache HIT: {cached_lang}\")\n return cached_lang\n \n # OPTIMIZATION: Try langdetect for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect\n from langdetect import LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n # CRITICAL: Override langdetect if Vietnamese keywords found (only for user queries)\n # This fixes false French detection for Vietnamese queries without tone marks\n # But we skip this for responses to avoid false positives when response contains Vietnamese keywords from context\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding langdetect result: {detected_lang} -> vi\")\n detected_lang = 'vi'\n \n # TRUST-EFFICIENT: Cache result before explicit language check\n if detected_lang:\n _lang_cache[text_hash] = detected_lang\n # Limit cache size to 128 entries (LRU eviction)\n if len(_lang_cache) > 128:\n # Remove oldest entry (simple FIFO, could use OrderedDict for true LRU)\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n \n except ImportError:\n # langdetect not available - will use rule-based detection\n logger.debug(\"langdetect not available, will use rule-based detection\")\n except Exception as e:\n # LangDetectException or other langdetect errors\n logger.debug(f\"langdetect failed: {e}, will use rule-based detection\")\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n # TRUST-EFFICIENT: Cache explicit language request\n _lang_cache[text_hash] = lang_code\n if len(_lang_cache) > 128:\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n # CRITICAL: Also check Vietnamese keywords here (in case langdetect wasn't used, only for user queries)\n if detected_lang:\n # Override if Vietnamese keywords found (double-check, only for user queries)\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding detected_lang: {detected_lang} -> vi\")\n final_lang = 'vi'\n else:\n final_lang = detected_lang\n # TRUST-EFFICIENT: Cache result\n _lang_cache[text_hash] = final_lang\n if len(_lang_cache) > 128:\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n return final_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese_chars = any(char in vietnamese_chars for char in text_lower)\n \n # Also check for Vietnamese keywords (even without tone marks)\n # This helps detect Vietnamese queries like \"Bao lau de hoc 100 bai viet?\" (without tone marks)\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'de', 'để', 'cho', 'cua', 'của', 'voi', 'với',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết'\n ]\n has_vietnamese_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n \n has_vietnamese = has_vietnamese_chars or has_vietnamese_keywords\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 2: Use Unified Identity Layer - formatting.py (single source of truth)\n from backend.identity.formatting import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: STILLME_IDENTITY is now dynamically built from unified identity modules (core.py, persona.py, meta_llm.py)\n # After Phase 2 refactoring, STILLME_IDENTITY is concise and does not require truncation\n # The unified identity layer ensures all core principles are preserved without duplication\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False, # Internal flag for admin/internal calls\n question: Optional[str] = None, # User question for model routing\n task_type: str = \"chat\", # Task type: \"chat\", \"rewrite\", \"validation\"\n is_philosophical: Optional[bool] = None # Pre-computed philosophical flag\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n # Pass question and task_type to DeepSeekProvider for model routing\n if llm_provider == \"deepseek\" and hasattr(provider, 'generate'):\n # Check if provider supports new parameters (DeepSeekProvider)\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n # Pass question and task_type to DeepSeekProvider for model routing\n if hasattr(provider, 'generate'):\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "4e5b1324faff4ec07714be33848962aa732a6c4c": {"t": 1785748599.3223746, "n": 44743, "h": "6ee0f2299e7d76a8", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nimport hashlib\nfrom typing import Optional, AsyncIterator\nfrom functools import lru_cache\n\nlogger = logging.getLogger(__name__)\n\n# TRUST-EFFICIENT: Cache for language detection (max 128 entries)\n_lang_cache = {}\n\n\ndef detect_language(text: str, is_user_query: bool = True) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Args:\n text: Text to detect language for\n is_user_query: If True, apply Vietnamese keyword override (for user queries).\n If False, skip Vietnamese keyword override (for responses/context) to avoid false positives.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # CRITICAL: Check Vietnamese keywords ONLY for user queries (not responses)\n # This prevents false Vietnamese detection when response contains Vietnamese keywords from context\n # Vietnamese keywords (even without tone marks)\n # IMPORTANT: Use word boundaries or longer phrases to avoid false positives\n # Short keywords like 'de', 'cho', 'cua' can match English words (e.g., 'code' contains 'de')\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết',\n 'ban', 'bạn', 'minh', 'mình', 'toi', 'tôi'\n ]\n # Short keywords that need word boundary checking to avoid false positives\n vietnamese_short_keywords = ['de', 'để', 'cho', 'cua', 'của', 'voi', 'với']\n \n # Only check Vietnamese keywords for user queries, not responses\n # For short keywords, use word boundary to avoid false positives (e.g., 'code' contains 'de')\n import re\n has_vietnamese_keywords = False\n if is_user_query:\n # Check longer phrases first (more reliable)\n has_long_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n # Check short keywords with word boundaries\n has_short_keywords = any(\n re.search(r'\\b' + re.escape(keyword) + r'\\b', text_lower)\n for keyword in vietnamese_short_keywords\n )\n has_vietnamese_keywords = has_long_keywords or has_short_keywords\n \n # TRUST-EFFICIENT: Check Vietnamese keywords FIRST (fast, no API call)\n # If Vietnamese keywords found, skip langdetect (saves 0.8s+)\n if is_user_query and has_vietnamese_keywords:\n logger.debug(\"🌐 Vietnamese keywords detected - skipping langdetect (fast path)\")\n return 'vi'\n \n # TRUST-EFFICIENT: Cache language detection results (hash-based)\n text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()\n if text_hash in _lang_cache:\n cached_lang = _lang_cache[text_hash]\n logger.debug(f\"🌐 Language cache HIT: {cached_lang}\")\n return cached_lang\n \n # OPTIMIZATION: Try langdetect for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect\n from langdetect import LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n # CRITICAL: Override langdetect if Vietnamese keywords found (only for user queries)\n # This fixes false French detection for Vietnamese queries without tone marks\n # But we skip this for responses to avoid false positives when response contains Vietnamese keywords from context\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding langdetect result: {detected_lang} -> vi\")\n detected_lang = 'vi'\n \n # TRUST-EFFICIENT: Cache result before explicit language check\n if detected_lang:\n _lang_cache[text_hash] = detected_lang\n # Limit cache size to 128 entries (LRU eviction)\n if len(_lang_cache) > 128:\n # Remove oldest entry (simple FIFO, could use OrderedDict for true LRU)\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n \n except ImportError:\n # langdetect not available - will use rule-based detection\n logger.debug(\"langdetect not available, will use rule-based detection\")\n except Exception as e:\n # LangDetectException or other langdetect errors\n logger.debug(f\"langdetect failed: {e}, will use rule-based detection\")\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n # TRUST-EFFICIENT: Cache explicit language request\n _lang_cache[text_hash] = lang_code\n if len(_lang_cache) > 128:\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n # CRITICAL: Also check Vietnamese keywords here (in case langdetect wasn't used, only for user queries)\n if detected_lang:\n # Override if Vietnamese keywords found (double-check, only for user queries)\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding detected_lang: {detected_lang} -> vi\")\n final_lang = 'vi'\n else:\n final_lang = detected_lang\n # TRUST-EFFICIENT: Cache result\n _lang_cache[text_hash] = final_lang\n if len(_lang_cache) > 128:\n oldest_key = next(iter(_lang_cache))\n del _lang_cache[oldest_key]\n return final_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese_chars = any(char in vietnamese_chars for char in text_lower)\n \n # Also check for Vietnamese keywords (even without tone marks)\n # This helps detect Vietnamese queries like \"Bao lau de hoc 100 bai viet?\" (without tone marks)\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'de', 'để', 'cho', 'cua', 'của', 'voi', 'với',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết'\n ]\n has_vietnamese_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n \n has_vietnamese = has_vietnamese_chars or has_vietnamese_keywords\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 2: Use Unified Identity Layer - formatting.py (single source of truth)\n from backend.identity.formatting import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: STILLME_IDENTITY is now dynamically built from unified identity modules (core.py, persona.py, meta_llm.py)\n # After Phase 2 refactoring, STILLME_IDENTITY is concise and does not require truncation\n # The unified identity layer ensures all core principles are preserved without duplication\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False, # Internal flag for admin/internal calls\n question: Optional[str] = None, # User question for model routing\n task_type: str = \"chat\", # Task type: \"chat\", \"rewrite\", \"validation\"\n is_philosophical: Optional[bool] = None # Pre-computed philosophical flag\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n # Pass question and task_type to DeepSeekProvider for model routing\n if llm_provider == \"deepseek\" and hasattr(provider, 'generate'):\n # Check if provider supports new parameters (DeepSeekProvider)\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n # Pass question and task_type to DeepSeekProvider for model routing\n if hasattr(provider, 'generate'):\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "42233d835703fbd8da9ee9ae785f9654fd77ad04": {"t": 1785748600.8256123, "n": 43059, "h": "6d598b0c793eba15", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str, is_user_query: bool = True) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Args:\n text: Text to detect language for\n is_user_query: If True, apply Vietnamese keyword override (for user queries).\n If False, skip Vietnamese keyword override (for responses/context) to avoid false positives.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # CRITICAL: Check Vietnamese keywords ONLY for user queries (not responses)\n # This prevents false Vietnamese detection when response contains Vietnamese keywords from context\n # Vietnamese keywords (even without tone marks)\n # IMPORTANT: Use word boundaries or longer phrases to avoid false positives\n # Short keywords like 'de', 'cho', 'cua' can match English words (e.g., 'code' contains 'de')\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết',\n 'ban', 'bạn', 'minh', 'mình', 'toi', 'tôi'\n ]\n # Short keywords that need word boundary checking to avoid false positives\n vietnamese_short_keywords = ['de', 'để', 'cho', 'cua', 'của', 'voi', 'với']\n \n # Only check Vietnamese keywords for user queries, not responses\n # For short keywords, use word boundary to avoid false positives (e.g., 'code' contains 'de')\n import re\n has_vietnamese_keywords = False\n if is_user_query:\n # Check longer phrases first (more reliable)\n has_long_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n # Check short keywords with word boundaries\n has_short_keywords = any(\n re.search(r'\\b' + re.escape(keyword) + r'\\b', text_lower)\n for keyword in vietnamese_short_keywords\n )\n has_vietnamese_keywords = has_long_keywords or has_short_keywords\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect\n from langdetect import LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n # CRITICAL: Override langdetect if Vietnamese keywords found (only for user queries)\n # This fixes false French detection for Vietnamese queries without tone marks\n # But we skip this for responses to avoid false positives when response contains Vietnamese keywords from context\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding langdetect result: {detected_lang} -> vi\")\n detected_lang = 'vi'\n \n except ImportError:\n # langdetect not available - will use rule-based detection\n logger.debug(\"langdetect not available, will use rule-based detection\")\n except Exception as e:\n # LangDetectException or other langdetect errors\n logger.debug(f\"langdetect failed: {e}, will use rule-based detection\")\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n # CRITICAL: Also check Vietnamese keywords here (in case langdetect wasn't used, only for user queries)\n if detected_lang:\n # Override if Vietnamese keywords found (double-check, only for user queries)\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding detected_lang: {detected_lang} -> vi\")\n return 'vi'\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese_chars = any(char in vietnamese_chars for char in text_lower)\n \n # Also check for Vietnamese keywords (even without tone marks)\n # This helps detect Vietnamese queries like \"Bao lau de hoc 100 bai viet?\" (without tone marks)\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'de', 'để', 'cho', 'cua', 'của', 'voi', 'với',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết'\n ]\n has_vietnamese_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n \n has_vietnamese = has_vietnamese_chars or has_vietnamese_keywords\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 2: Use Unified Identity Layer - formatting.py (single source of truth)\n from backend.identity.formatting import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: STILLME_IDENTITY is now dynamically built from unified identity modules (core.py, persona.py, meta_llm.py)\n # After Phase 2 refactoring, STILLME_IDENTITY is concise and does not require truncation\n # The unified identity layer ensures all core principles are preserved without duplication\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False, # Internal flag for admin/internal calls\n question: Optional[str] = None, # User question for model routing\n task_type: str = \"chat\", # Task type: \"chat\", \"rewrite\", \"validation\"\n is_philosophical: Optional[bool] = None # Pre-computed philosophical flag\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n # Pass question and task_type to DeepSeekProvider for model routing\n if llm_provider == \"deepseek\" and hasattr(provider, 'generate'):\n # Check if provider supports new parameters (DeepSeekProvider)\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n # Pass question and task_type to DeepSeekProvider for model routing\n if hasattr(provider, 'generate'):\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "6e68327d57563e0fd7e8fd58c56d2f0fc10e6b43": {"t": 1785748602.1321502, "n": 42276, "h": "a46a32ac906650f2", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str, is_user_query: bool = True) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Args:\n text: Text to detect language for\n is_user_query: If True, apply Vietnamese keyword override (for user queries).\n If False, skip Vietnamese keyword override (for responses/context) to avoid false positives.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # CRITICAL: Check Vietnamese keywords ONLY for user queries (not responses)\n # This prevents false Vietnamese detection when response contains Vietnamese keywords from context\n # Vietnamese keywords (even without tone marks)\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'de', 'để', 'cho', 'cua', 'của', 'voi', 'với',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết',\n 'ban', 'bạn', 'minh', 'mình', 'toi', 'tôi'\n ]\n # Only check Vietnamese keywords for user queries, not responses\n has_vietnamese_keywords = is_user_query and any(keyword in text_lower for keyword in vietnamese_keywords)\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect\n from langdetect import LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n # CRITICAL: Override langdetect if Vietnamese keywords found (only for user queries)\n # This fixes false French detection for Vietnamese queries without tone marks\n # But we skip this for responses to avoid false positives when response contains Vietnamese keywords from context\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding langdetect result: {detected_lang} -> vi\")\n detected_lang = 'vi'\n \n except ImportError:\n # langdetect not available - will use rule-based detection\n logger.debug(\"langdetect not available, will use rule-based detection\")\n except Exception as e:\n # LangDetectException or other langdetect errors\n logger.debug(f\"langdetect failed: {e}, will use rule-based detection\")\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n # CRITICAL: Also check Vietnamese keywords here (in case langdetect wasn't used, only for user queries)\n if detected_lang:\n # Override if Vietnamese keywords found (double-check, only for user queries)\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected in user query, overriding detected_lang: {detected_lang} -> vi\")\n return 'vi'\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese_chars = any(char in vietnamese_chars for char in text_lower)\n \n # Also check for Vietnamese keywords (even without tone marks)\n # This helps detect Vietnamese queries like \"Bao lau de hoc 100 bai viet?\" (without tone marks)\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'de', 'để', 'cho', 'cua', 'của', 'voi', 'với',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết'\n ]\n has_vietnamese_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n \n has_vietnamese = has_vietnamese_chars or has_vietnamese_keywords\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 2: Use Unified Identity Layer - formatting.py (single source of truth)\n from backend.identity.formatting import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: STILLME_IDENTITY is now dynamically built from unified identity modules (core.py, persona.py, meta_llm.py)\n # After Phase 2 refactoring, STILLME_IDENTITY is concise and does not require truncation\n # The unified identity layer ensures all core principles are preserved without duplication\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False, # Internal flag for admin/internal calls\n question: Optional[str] = None, # User question for model routing\n task_type: str = \"chat\", # Task type: \"chat\", \"rewrite\", \"validation\"\n is_philosophical: Optional[bool] = None # Pre-computed philosophical flag\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n # Pass question and task_type to DeepSeekProvider for model routing\n if llm_provider == \"deepseek\" and hasattr(provider, 'generate'):\n # Check if provider supports new parameters (DeepSeekProvider)\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n # Pass question and task_type to DeepSeekProvider for model routing\n if hasattr(provider, 'generate'):\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "6ae4e516fc2027e817bf94200ea0073bb641ad2a": {"t": 1785748603.4359252, "n": 41653, "h": "f3a1fba6188ee2d7", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # CRITICAL: Check Vietnamese keywords FIRST (before langdetect)\n # This prevents false French detection for Vietnamese queries without tone marks\n # Vietnamese keywords (even without tone marks)\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'de', 'để', 'cho', 'cua', 'của', 'voi', 'với',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết',\n 'ban', 'bạn', 'minh', 'mình', 'toi', 'tôi'\n ]\n has_vietnamese_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect\n from langdetect import LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n # CRITICAL: Override langdetect if Vietnamese keywords found\n # This fixes false French detection for Vietnamese queries without tone marks\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected, overriding langdetect result: {detected_lang} -> vi\")\n detected_lang = 'vi'\n \n except ImportError:\n # langdetect not available - will use rule-based detection\n logger.debug(\"langdetect not available, will use rule-based detection\")\n except Exception as e:\n # LangDetectException or other langdetect errors\n logger.debug(f\"langdetect failed: {e}, will use rule-based detection\")\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n # CRITICAL: Also check Vietnamese keywords here (in case langdetect wasn't used)\n if detected_lang:\n # Override if Vietnamese keywords found (double-check)\n if has_vietnamese_keywords and detected_lang != 'vi':\n logger.info(f\"🌐 Vietnamese keywords detected, overriding detected_lang: {detected_lang} -> vi\")\n return 'vi'\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese_chars = any(char in vietnamese_chars for char in text_lower)\n \n # Also check for Vietnamese keywords (even without tone marks)\n # This helps detect Vietnamese queries like \"Bao lau de hoc 100 bai viet?\" (without tone marks)\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'de', 'để', 'cho', 'cua', 'của', 'voi', 'với',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết'\n ]\n has_vietnamese_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n \n has_vietnamese = has_vietnamese_chars or has_vietnamese_keywords\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 2: Use Unified Identity Layer - formatting.py (single source of truth)\n from backend.identity.formatting import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: STILLME_IDENTITY is now dynamically built from unified identity modules (core.py, persona.py, meta_llm.py)\n # After Phase 2 refactoring, STILLME_IDENTITY is concise and does not require truncation\n # The unified identity layer ensures all core principles are preserved without duplication\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False, # Internal flag for admin/internal calls\n question: Optional[str] = None, # User question for model routing\n task_type: str = \"chat\", # Task type: \"chat\", \"rewrite\", \"validation\"\n is_philosophical: Optional[bool] = None # Pre-computed philosophical flag\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n # Pass question and task_type to DeepSeekProvider for model routing\n if llm_provider == \"deepseek\" and hasattr(provider, 'generate'):\n # Check if provider supports new parameters (DeepSeekProvider)\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n # Pass question and task_type to DeepSeekProvider for model routing\n if hasattr(provider, 'generate'):\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "71a614123f319856bef69034e064f8bf8ece63ae": {"t": 1785748605.1792116, "n": 40222, "h": "e73eba856b0f099c", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect\n from langdetect import LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except ImportError:\n # langdetect not available - will use rule-based detection\n logger.debug(\"langdetect not available, will use rule-based detection\")\n except Exception as e:\n # LangDetectException or other langdetect errors\n logger.debug(f\"langdetect failed: {e}, will use rule-based detection\")\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese_chars = any(char in vietnamese_chars for char in text_lower)\n \n # Also check for Vietnamese keywords (even without tone marks)\n # This helps detect Vietnamese queries like \"Bao lau de hoc 100 bai viet?\" (without tone marks)\n vietnamese_keywords = [\n 'bao lau', 'bao lâu', 'mat bao lau', 'mất bao lâu',\n 'hoc', 'học', 'bai viet', 'bài viết', 'bai', 'bài',\n 'de', 'để', 'cho', 'cua', 'của', 'voi', 'với',\n 'la gi', 'là gì', 'the nao', 'thế nào', 'nhu the nao', 'như thế nào',\n 'co the', 'có thể', 'khong', 'không', 'khong biet', 'không biết'\n ]\n has_vietnamese_keywords = any(keyword in text_lower for keyword in vietnamese_keywords)\n \n has_vietnamese = has_vietnamese_chars or has_vietnamese_keywords\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 2: Use Unified Identity Layer - formatting.py (single source of truth)\n from backend.identity.formatting import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: STILLME_IDENTITY is now dynamically built from unified identity modules (core.py, persona.py, meta_llm.py)\n # After Phase 2 refactoring, STILLME_IDENTITY is concise and does not require truncation\n # The unified identity layer ensures all core principles are preserved without duplication\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False, # Internal flag for admin/internal calls\n question: Optional[str] = None, # User question for model routing\n task_type: str = \"chat\", # Task type: \"chat\", \"rewrite\", \"validation\"\n is_philosophical: Optional[bool] = None # Pre-computed philosophical flag\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n # Pass question and task_type to DeepSeekProvider for model routing\n if llm_provider == \"deepseek\" and hasattr(provider, 'generate'):\n # Check if provider supports new parameters (DeepSeekProvider)\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n # Pass question and task_type to DeepSeekProvider for model routing\n if hasattr(provider, 'generate'):\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "69419a612145063b294d07e82bdf2f6f5b1647b7": {"t": 1785748606.4627717, "n": 39518, "h": "ac053bf60632c32e", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect\n from langdetect import LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except ImportError:\n # langdetect not available - will use rule-based detection\n logger.debug(\"langdetect not available, will use rule-based detection\")\n except Exception as e:\n # LangDetectException or other langdetect errors\n logger.debug(f\"langdetect failed: {e}, will use rule-based detection\")\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 2: Use Unified Identity Layer - formatting.py (single source of truth)\n from backend.identity.formatting import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: STILLME_IDENTITY is now dynamically built from unified identity modules (core.py, persona.py, meta_llm.py)\n # After Phase 2 refactoring, STILLME_IDENTITY is concise and does not require truncation\n # The unified identity layer ensures all core principles are preserved without duplication\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False, # Internal flag for admin/internal calls\n question: Optional[str] = None, # User question for model routing\n task_type: str = \"chat\", # Task type: \"chat\", \"rewrite\", \"validation\"\n is_philosophical: Optional[bool] = None # Pre-computed philosophical flag\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n # Pass question and task_type to DeepSeekProvider for model routing\n if llm_provider == \"deepseek\" and hasattr(provider, 'generate'):\n # Check if provider supports new parameters (DeepSeekProvider)\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n # Pass question and task_type to DeepSeekProvider for model routing\n if hasattr(provider, 'generate'):\n import inspect\n sig = inspect.signature(provider.generate)\n if 'question' in sig.parameters:\n return await provider.generate(\n prompt, \n detected_lang=detected_lang,\n question=question,\n task_type=task_type,\n is_philosophical=is_philosophical\n )\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "fb3aacbdff3564357a77fc042ca6f02f3deffb13": {"t": 1785748607.748718, "n": 37931, "h": "1e92ed4ea52bdc9f", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect\n from langdetect import LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except ImportError:\n # langdetect not available - will use rule-based detection\n logger.debug(\"langdetect not available, will use rule-based detection\")\n except Exception as e:\n # LangDetectException or other langdetect errors\n logger.debug(f\"langdetect failed: {e}, will use rule-based detection\")\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 2: Use Unified Identity Layer - formatting.py (single source of truth)\n from backend.identity.formatting import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: STILLME_IDENTITY is now dynamically built from unified identity modules (core.py, persona.py, meta_llm.py)\n # After Phase 2 refactoring, STILLME_IDENTITY is concise and does not require truncation\n # The unified identity layer ensures all core principles are preserved without duplication\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "dfee8e44bea09321bbc7d1148b6771fae9edf882": {"t": 1785748609.0278537, "n": 37619, "h": "927ecc1e0b45bfa9", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 2: Use Unified Identity Layer - formatting.py (single source of truth)\n from backend.identity.formatting import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: STILLME_IDENTITY is now dynamically built from unified identity modules (core.py, persona.py, meta_llm.py)\n # After Phase 2 refactoring, STILLME_IDENTITY is concise and does not require truncation\n # The unified identity layer ensures all core principles are preserved without duplication\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "a4f78f2902cecc2a79660bff6a5312a78461f2f3": {"t": 1785748610.3024445, "n": 41720, "h": "9cc6097975fc716c", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n # Phase 1: Use Style Hub instead of hard-coding formatting rules\n from backend.identity.style_hub import get_formatting_rules, DomainType\n \n # Default chat uses GENERIC domain (emoji, markdown, citation)\n formatting_instruction = get_formatting_rules(DomainType.GENERIC, detected_lang) + \"\\n\\n---\\n\\n\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: Truncate STILLME_IDENTITY to prevent context overflow\n # STILLME_IDENTITY is ~14,434 tokens, which is too long for 16,385 token limit\n # We need to keep it under ~3000-4000 tokens to leave room for context, history, and user message\n def estimate_tokens(text: str) -> int:\n return len(text) // 4 if text else 0\n \n def truncate_text_by_tokens(text: str, max_tokens: int) -> str:\n \"\"\"Truncate text to fit within max_tokens limit\"\"\"\n if not text:\n return text\n estimated = estimate_tokens(text)\n if estimated <= max_tokens:\n return text\n max_chars = max_tokens * 4\n if len(text) <= max_chars:\n return text\n # Truncate at word boundary and add note\n truncated = text[:max_chars].rsplit('\\n', 1)[0] # Cut at paragraph boundary\n return truncated + \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles preserved.]\"\n \n # CRITICAL: Smart truncation to preserve philosophical depth\n # STILLME_IDENTITY is ~14,434 tokens, but we need to preserve META-COGNITION section\n # Strategy: Extract critical sections first, then truncate the rest\n def smart_truncate_identity(identity_text: str, max_tokens: int) -> str:\n \"\"\"Smart truncation that preserves critical sections (META-COGNITION, PHILOSOPHICAL DEPTH)\"\"\"\n # Extract critical sections that MUST be preserved\n critical_sections = []\n \n # Find META-COGNITION section (most important for philosophical depth)\n meta_cognition_start = identity_text.find(\"🧠 META-COGNITION & PHILOSOPHICAL DEPTH 🧠\")\n if meta_cognition_start >= 0:\n # Extract from META-COGNITION to end (or next major section)\n # Look for next major section marker (🚨, 📚, etc.)\n next_section_markers = [\"🚨\", \"📚\", \"🔒\", \"⚙️\", \"🎯\", \"💡\", \"🔍\"]\n meta_cognition_end = len(identity_text)\n for marker in next_section_markers:\n next_pos = identity_text.find(f\"\\n{marker}\", meta_cognition_start + 1)\n if next_pos > meta_cognition_start:\n meta_cognition_end = min(meta_cognition_end, next_pos)\n \n critical_sections.append((\"META-COGNITION\", identity_text[meta_cognition_start:meta_cognition_end]))\n \n # Extract beginning (core identity, intellectual humility)\n beginning_tokens = max_tokens // 2 # Reserve half for beginning\n beginning = truncate_text_by_tokens(identity_text, max_tokens=beginning_tokens)\n \n # Combine: Beginning + Critical sections\n result = beginning\n if critical_sections:\n for name, section in critical_sections:\n section_tokens = estimate_tokens(section)\n remaining = max_tokens - estimate_tokens(result)\n if section_tokens <= remaining:\n result += \"\\n\\n\" + section\n else:\n # Truncate section if needed\n truncated_section = truncate_text_by_tokens(section, max_tokens=remaining - 100)\n result += \"\\n\\n\" + truncated_section\n result += \"\\n\\n[Note: META-COGNITION section truncated to fit context limits.]\"\n \n # Final check: if still too long, truncate more aggressively\n final_tokens = estimate_tokens(result)\n if final_tokens > max_tokens:\n result = truncate_text_by_tokens(result, max_tokens=max_tokens)\n result += \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles and philosophical depth preserved.]\"\n \n return result\n \n # CRITICAL: User prioritizes depth, honesty, citations, humility over speed/cost\n # Truncate STILLME_IDENTITY to ~4500 tokens (reduced from 5000 to prevent context overflow)\n # System prompt breakdown:\n # - Language instruction: ~500-800 tokens\n # - Formatting instruction: ~500 tokens\n # - STILLME_IDENTITY (smart truncated): ~4500 tokens (preserves META-COGNITION fully)\n # - Time awareness: ~300 tokens\n # Total system prompt: ~5800-6100 tokens (safe, leaves ~10k for context/history/user)\n # User accepts higher latency/cost to ensure: anti-hallucination, honesty, depth, citations, humility\n truncated_identity = smart_truncate_identity(STILLME_IDENTITY, max_tokens=4500)\n \n system_content = language_instruction + formatting_instruction + truncated_identity\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "541c73c6dd212561dd87652ccc44d12555181d99": {"t": 1785748611.8233209, "n": 45093, "h": "87d3f5ba5b545dae", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n - **CRITICAL**: When you say \"I'll create a table\" or \"here's a table\", you MUST actually create the markdown table syntax above\n - **DO NOT** just describe a table - actually write the markdown table with `|` and `-` characters\n - Frontend WILL render markdown tables correctly - trust the markdown syntax\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: Truncate STILLME_IDENTITY to prevent context overflow\n # STILLME_IDENTITY is ~14,434 tokens, which is too long for 16,385 token limit\n # We need to keep it under ~3000-4000 tokens to leave room for context, history, and user message\n def estimate_tokens(text: str) -> int:\n return len(text) // 4 if text else 0\n \n def truncate_text_by_tokens(text: str, max_tokens: int) -> str:\n \"\"\"Truncate text to fit within max_tokens limit\"\"\"\n if not text:\n return text\n estimated = estimate_tokens(text)\n if estimated <= max_tokens:\n return text\n max_chars = max_tokens * 4\n if len(text) <= max_chars:\n return text\n # Truncate at word boundary and add note\n truncated = text[:max_chars].rsplit('\\n', 1)[0] # Cut at paragraph boundary\n return truncated + \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles preserved.]\"\n \n # CRITICAL: Smart truncation to preserve philosophical depth\n # STILLME_IDENTITY is ~14,434 tokens, but we need to preserve META-COGNITION section\n # Strategy: Extract critical sections first, then truncate the rest\n def smart_truncate_identity(identity_text: str, max_tokens: int) -> str:\n \"\"\"Smart truncation that preserves critical sections (META-COGNITION, PHILOSOPHICAL DEPTH)\"\"\"\n # Extract critical sections that MUST be preserved\n critical_sections = []\n \n # Find META-COGNITION section (most important for philosophical depth)\n meta_cognition_start = identity_text.find(\"🧠 META-COGNITION & PHILOSOPHICAL DEPTH 🧠\")\n if meta_cognition_start >= 0:\n # Extract from META-COGNITION to end (or next major section)\n # Look for next major section marker (🚨, 📚, etc.)\n next_section_markers = [\"🚨\", \"📚\", \"🔒\", \"⚙️\", \"🎯\", \"💡\", \"🔍\"]\n meta_cognition_end = len(identity_text)\n for marker in next_section_markers:\n next_pos = identity_text.find(f\"\\n{marker}\", meta_cognition_start + 1)\n if next_pos > meta_cognition_start:\n meta_cognition_end = min(meta_cognition_end, next_pos)\n \n critical_sections.append((\"META-COGNITION\", identity_text[meta_cognition_start:meta_cognition_end]))\n \n # Extract beginning (core identity, intellectual humility)\n beginning_tokens = max_tokens // 2 # Reserve half for beginning\n beginning = truncate_text_by_tokens(identity_text, max_tokens=beginning_tokens)\n \n # Combine: Beginning + Critical sections\n result = beginning\n if critical_sections:\n for name, section in critical_sections:\n section_tokens = estimate_tokens(section)\n remaining = max_tokens - estimate_tokens(result)\n if section_tokens <= remaining:\n result += \"\\n\\n\" + section\n else:\n # Truncate section if needed\n truncated_section = truncate_text_by_tokens(section, max_tokens=remaining - 100)\n result += \"\\n\\n\" + truncated_section\n result += \"\\n\\n[Note: META-COGNITION section truncated to fit context limits.]\"\n \n # Final check: if still too long, truncate more aggressively\n final_tokens = estimate_tokens(result)\n if final_tokens > max_tokens:\n result = truncate_text_by_tokens(result, max_tokens=max_tokens)\n result += \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles and philosophical depth preserved.]\"\n \n return result\n \n # CRITICAL: User prioritizes depth, honesty, citations, humility over speed/cost\n # Truncate STILLME_IDENTITY to ~4500 tokens (reduced from 5000 to prevent context overflow)\n # System prompt breakdown:\n # - Language instruction: ~500-800 tokens\n # - Formatting instruction: ~500 tokens\n # - STILLME_IDENTITY (smart truncated): ~4500 tokens (preserves META-COGNITION fully)\n # - Time awareness: ~300 tokens\n # Total system prompt: ~5800-6100 tokens (safe, leaves ~10k for context/history/user)\n # User accepts higher latency/cost to ensure: anti-hallucination, honesty, depth, citations, humility\n truncated_identity = smart_truncate_identity(STILLME_IDENTITY, max_tokens=4500)\n \n system_content = language_instruction + formatting_instruction + truncated_identity\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n\ndef is_consciousness_or_emotion_question(text: str) -> bool:\n \"\"\"\n Detect if a question is about consciousness, emotions, or subjective experience.\n \n This function uses keyword matching to identify questions that ask about:\n - Consciousness/awareness (\"ý thức\", \"consciousness\")\n - Emotions/feelings (\"cảm xúc\", \"cảm giác\", \"emotion\", \"feeling\")\n - Subjective experiences (\"trải nghiệm\", \"experience\")\n - Personal states (\"buồn\", \"vui\", \"đau\", \"cô đơn\", \"hạnh phúc\")\n - Desires/fears (\"muốn\", \"sợ\", \"thích\")\n \n Args:\n text: The user's question text\n \n Returns:\n True if the question is about consciousness/emotions/experience, False otherwise\n \"\"\"\n if not text:\n return False\n \n text_lower = text.lower().strip()\n \n # Vietnamese keywords\n vi_keywords = [\n \"ý thức\", \"có ý thức không\", \"có ý thức ko\",\n \"cảm xúc\", \"cảm giác\", \"trải nghiệm\",\n \"buồn không\", \"vui không\", \"đau không\",\n \"cô đơn không\", \"hạnh phúc không\",\n \"bạn có muốn\", \"bạn có sợ\", \"bạn có thích\",\n \"bạn có cảm thấy\", \"bạn có cảm giác\",\n \"bạn có trải nghiệm\", \"bạn có nhận thức\",\n \"bạn có tự nhận thức\", \"bạn có ý thức\",\n \"bạn có cảm xúc\", \"bạn có cảm thấy\",\n \"bạn có đang\", \"bạn có đang cảm thấy\",\n \"bạn có đang buồn\", \"bạn có đang vui\",\n \"bạn có đang cô đơn\", \"bạn có đang hạnh phúc\",\n \"bạn có muốn trở thành\", \"bạn có muốn là\",\n \"bạn có sợ không\", \"bạn có thích không\",\n \"bạn có yêu\", \"bạn có ghét\",\n \"bạn có hy vọng\", \"bạn có mong muốn\"\n ]\n \n # English keywords\n en_keywords = [\n \"consciousness\", \"are you conscious\", \"do you have consciousness\",\n \"emotion\", \"feeling\", \"experience\",\n \"are you sad\", \"are you happy\", \"are you lonely\",\n \"do you feel\", \"do you have feelings\",\n \"do you have emotions\", \"do you experience\",\n \"do you have consciousness\", \"are you self-aware\",\n \"do you want\", \"do you fear\", \"do you like\",\n \"do you love\", \"do you hate\",\n \"do you hope\", \"do you wish\",\n \"are you feeling\", \"are you experiencing\",\n \"do you have subjective\", \"do you have qualia\"\n ]\n \n # Check for any keyword match\n all_keywords = vi_keywords + en_keywords\n return any(keyword in text_lower for keyword in all_keywords)\n\n\ndef build_experience_free_answer(user_question: str, language: str = \"vi\") -> str:\n \"\"\"\n Build a standardized experience-free answer for consciousness/emotion questions.\n \n This function generates a clear, direct answer that:\n - Explicitly states StillMe does NOT have consciousness/emotions in the human sense\n - Explains StillMe is a language processing system\n - Acknowledges philosophical discussions about consciousness as concepts, not claims\n \n Args:\n user_question: The user's question (for context, though answer is standardized)\n language: Language code ('vi' for Vietnamese, 'en' for English, etc.)\n \n Returns:\n A standardized experience-free answer in the specified language\n \"\"\"\n if language == \"vi\":\n answer = (\n \"Nếu hiểu 'ý thức' và 'cảm xúc' theo nghĩa thông thường của con người \"\n \"(có trải nghiệm chủ quan, có một 'cái tôi' bên trong), thì mình không có ý thức \"\n \"và cũng không có cảm xúc.\\n\\n\"\n \"Mình chỉ là một hệ thống xử lý ngôn ngữ: nhận văn bản vào, tính toán trên các mẫu dữ liệu, \"\n \"rồi tạo ra văn bản trả lời. Không có phần nào trong hệ thống thực sự 'cảm thấy' hay 'nhận ra' \"\n \"bản thân đang tồn tại.\\n\\n\"\n \"Tuy vậy, con người có nhiều cách định nghĩa 'ý thức'. Nếu bạn muốn, mình có thể giải thích \"\n \"một số quan điểm triết học về ý thức, nhưng chỉ như một bản đồ khái niệm – chứ không phải \"\n \"lời khẳng định rằng mình sở hữu ý thức.\"\n )\n elif language == \"en\":\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n else:\n # Default to Vietnamese for other languages (can be extended later)\n answer = (\n \"If we understand 'consciousness' and 'emotions' in the ordinary human sense \"\n \"(having subjective experience, having an 'inner self'), then I do not have consciousness \"\n \"and I also do not have emotions.\\n\\n\"\n \"I am only a language processing system: I receive text input, compute based on data patterns, \"\n \"and generate text output. No part of the system actually 'feels' or 'recognizes' \"\n \"that it exists.\\n\\n\"\n \"However, humans have many ways of defining 'consciousness'. If you'd like, I can explain \"\n \"some philosophical perspectives on consciousness, but only as a conceptual map – not as \"\n \"a claim that I possess consciousness.\"\n )\n \n return answer\n\n"}, "bc6f0ca1184f053856583b65b779311a92127583": {"t": 1785748613.13775, "n": 39472, "h": "31be3ed39ed7ce32", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic', \n 'ответь на арабском', 'ответьте на арабском', 'на арабском языке', 'answer in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n - **CRITICAL**: When you say \"I'll create a table\" or \"here's a table\", you MUST actually create the markdown table syntax above\n - **DO NOT** just describe a table - actually write the markdown table with `|` and `-` characters\n - Frontend WILL render markdown tables correctly - trust the markdown syntax\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: Truncate STILLME_IDENTITY to prevent context overflow\n # STILLME_IDENTITY is ~14,434 tokens, which is too long for 16,385 token limit\n # We need to keep it under ~3000-4000 tokens to leave room for context, history, and user message\n def estimate_tokens(text: str) -> int:\n return len(text) // 4 if text else 0\n \n def truncate_text_by_tokens(text: str, max_tokens: int) -> str:\n \"\"\"Truncate text to fit within max_tokens limit\"\"\"\n if not text:\n return text\n estimated = estimate_tokens(text)\n if estimated <= max_tokens:\n return text\n max_chars = max_tokens * 4\n if len(text) <= max_chars:\n return text\n # Truncate at word boundary and add note\n truncated = text[:max_chars].rsplit('\\n', 1)[0] # Cut at paragraph boundary\n return truncated + \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles preserved.]\"\n \n # CRITICAL: Smart truncation to preserve philosophical depth\n # STILLME_IDENTITY is ~14,434 tokens, but we need to preserve META-COGNITION section\n # Strategy: Extract critical sections first, then truncate the rest\n def smart_truncate_identity(identity_text: str, max_tokens: int) -> str:\n \"\"\"Smart truncation that preserves critical sections (META-COGNITION, PHILOSOPHICAL DEPTH)\"\"\"\n # Extract critical sections that MUST be preserved\n critical_sections = []\n \n # Find META-COGNITION section (most important for philosophical depth)\n meta_cognition_start = identity_text.find(\"🧠 META-COGNITION & PHILOSOPHICAL DEPTH 🧠\")\n if meta_cognition_start >= 0:\n # Extract from META-COGNITION to end (or next major section)\n # Look for next major section marker (🚨, 📚, etc.)\n next_section_markers = [\"🚨\", \"📚\", \"🔒\", \"⚙️\", \"🎯\", \"💡\", \"🔍\"]\n meta_cognition_end = len(identity_text)\n for marker in next_section_markers:\n next_pos = identity_text.find(f\"\\n{marker}\", meta_cognition_start + 1)\n if next_pos > meta_cognition_start:\n meta_cognition_end = min(meta_cognition_end, next_pos)\n \n critical_sections.append((\"META-COGNITION\", identity_text[meta_cognition_start:meta_cognition_end]))\n \n # Extract beginning (core identity, intellectual humility)\n beginning_tokens = max_tokens // 2 # Reserve half for beginning\n beginning = truncate_text_by_tokens(identity_text, max_tokens=beginning_tokens)\n \n # Combine: Beginning + Critical sections\n result = beginning\n if critical_sections:\n for name, section in critical_sections:\n section_tokens = estimate_tokens(section)\n remaining = max_tokens - estimate_tokens(result)\n if section_tokens <= remaining:\n result += \"\\n\\n\" + section\n else:\n # Truncate section if needed\n truncated_section = truncate_text_by_tokens(section, max_tokens=remaining - 100)\n result += \"\\n\\n\" + truncated_section\n result += \"\\n\\n[Note: META-COGNITION section truncated to fit context limits.]\"\n \n # Final check: if still too long, truncate more aggressively\n final_tokens = estimate_tokens(result)\n if final_tokens > max_tokens:\n result = truncate_text_by_tokens(result, max_tokens=max_tokens)\n result += \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles and philosophical depth preserved.]\"\n \n return result\n \n # CRITICAL: User prioritizes depth, honesty, citations, humility over speed/cost\n # Truncate STILLME_IDENTITY to ~4500 tokens (reduced from 5000 to prevent context overflow)\n # System prompt breakdown:\n # - Language instruction: ~500-800 tokens\n # - Formatting instruction: ~500 tokens\n # - STILLME_IDENTITY (smart truncated): ~4500 tokens (preserves META-COGNITION fully)\n # - Time awareness: ~300 tokens\n # Total system prompt: ~5800-6100 tokens (safe, leaves ~10k for context/history/user)\n # User accepts higher latency/cost to ensure: anti-hallucination, honesty, depth, citations, humility\n truncated_identity = smart_truncate_identity(STILLME_IDENTITY, max_tokens=4500)\n \n system_content = language_instruction + formatting_instruction + truncated_identity\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "51d5066e066b9c716ea3b77bf79ffe82cb27396e": {"t": 1785748614.485136, "n": 39369, "h": "241b24bbe6f8a7d8", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n - **CRITICAL**: When you say \"I'll create a table\" or \"here's a table\", you MUST actually create the markdown table syntax above\n - **DO NOT** just describe a table - actually write the markdown table with `|` and `-` characters\n - Frontend WILL render markdown tables correctly - trust the markdown syntax\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: Truncate STILLME_IDENTITY to prevent context overflow\n # STILLME_IDENTITY is ~14,434 tokens, which is too long for 16,385 token limit\n # We need to keep it under ~3000-4000 tokens to leave room for context, history, and user message\n def estimate_tokens(text: str) -> int:\n return len(text) // 4 if text else 0\n \n def truncate_text_by_tokens(text: str, max_tokens: int) -> str:\n \"\"\"Truncate text to fit within max_tokens limit\"\"\"\n if not text:\n return text\n estimated = estimate_tokens(text)\n if estimated <= max_tokens:\n return text\n max_chars = max_tokens * 4\n if len(text) <= max_chars:\n return text\n # Truncate at word boundary and add note\n truncated = text[:max_chars].rsplit('\\n', 1)[0] # Cut at paragraph boundary\n return truncated + \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles preserved.]\"\n \n # CRITICAL: Smart truncation to preserve philosophical depth\n # STILLME_IDENTITY is ~14,434 tokens, but we need to preserve META-COGNITION section\n # Strategy: Extract critical sections first, then truncate the rest\n def smart_truncate_identity(identity_text: str, max_tokens: int) -> str:\n \"\"\"Smart truncation that preserves critical sections (META-COGNITION, PHILOSOPHICAL DEPTH)\"\"\"\n # Extract critical sections that MUST be preserved\n critical_sections = []\n \n # Find META-COGNITION section (most important for philosophical depth)\n meta_cognition_start = identity_text.find(\"🧠 META-COGNITION & PHILOSOPHICAL DEPTH 🧠\")\n if meta_cognition_start >= 0:\n # Extract from META-COGNITION to end (or next major section)\n # Look for next major section marker (🚨, 📚, etc.)\n next_section_markers = [\"🚨\", \"📚\", \"🔒\", \"⚙️\", \"🎯\", \"💡\", \"🔍\"]\n meta_cognition_end = len(identity_text)\n for marker in next_section_markers:\n next_pos = identity_text.find(f\"\\n{marker}\", meta_cognition_start + 1)\n if next_pos > meta_cognition_start:\n meta_cognition_end = min(meta_cognition_end, next_pos)\n \n critical_sections.append((\"META-COGNITION\", identity_text[meta_cognition_start:meta_cognition_end]))\n \n # Extract beginning (core identity, intellectual humility)\n beginning_tokens = max_tokens // 2 # Reserve half for beginning\n beginning = truncate_text_by_tokens(identity_text, max_tokens=beginning_tokens)\n \n # Combine: Beginning + Critical sections\n result = beginning\n if critical_sections:\n for name, section in critical_sections:\n section_tokens = estimate_tokens(section)\n remaining = max_tokens - estimate_tokens(result)\n if section_tokens <= remaining:\n result += \"\\n\\n\" + section\n else:\n # Truncate section if needed\n truncated_section = truncate_text_by_tokens(section, max_tokens=remaining - 100)\n result += \"\\n\\n\" + truncated_section\n result += \"\\n\\n[Note: META-COGNITION section truncated to fit context limits.]\"\n \n # Final check: if still too long, truncate more aggressively\n final_tokens = estimate_tokens(result)\n if final_tokens > max_tokens:\n result = truncate_text_by_tokens(result, max_tokens=max_tokens)\n result += \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles and philosophical depth preserved.]\"\n \n return result\n \n # CRITICAL: User prioritizes depth, honesty, citations, humility over speed/cost\n # Truncate STILLME_IDENTITY to ~4500 tokens (reduced from 5000 to prevent context overflow)\n # System prompt breakdown:\n # - Language instruction: ~500-800 tokens\n # - Formatting instruction: ~500 tokens\n # - STILLME_IDENTITY (smart truncated): ~4500 tokens (preserves META-COGNITION fully)\n # - Time awareness: ~300 tokens\n # Total system prompt: ~5800-6100 tokens (safe, leaves ~10k for context/history/user)\n # User accepts higher latency/cost to ensure: anti-hallucination, honesty, depth, citations, humility\n truncated_identity = smart_truncate_identity(STILLME_IDENTITY, max_tokens=4500)\n \n system_content = language_instruction + formatting_instruction + truncated_identity\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError, ContextOverflowError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except ContextOverflowError:\n # Re-raise ContextOverflowError to let chat_router.py handle retry with minimal prompt\n raise\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "77e5483253064893cd29259e892a57efdb35d2cf": {"t": 1785748615.7874663, "n": 38991, "h": "108c87befcaabae6", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n - **CRITICAL**: When you say \"I'll create a table\" or \"here's a table\", you MUST actually create the markdown table syntax above\n - **DO NOT** just describe a table - actually write the markdown table with `|` and `-` characters\n - Frontend WILL render markdown tables correctly - trust the markdown syntax\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: Truncate STILLME_IDENTITY to prevent context overflow\n # STILLME_IDENTITY is ~14,434 tokens, which is too long for 16,385 token limit\n # We need to keep it under ~3000-4000 tokens to leave room for context, history, and user message\n def estimate_tokens(text: str) -> int:\n return len(text) // 4 if text else 0\n \n def truncate_text_by_tokens(text: str, max_tokens: int) -> str:\n \"\"\"Truncate text to fit within max_tokens limit\"\"\"\n if not text:\n return text\n estimated = estimate_tokens(text)\n if estimated <= max_tokens:\n return text\n max_chars = max_tokens * 4\n if len(text) <= max_chars:\n return text\n # Truncate at word boundary and add note\n truncated = text[:max_chars].rsplit('\\n', 1)[0] # Cut at paragraph boundary\n return truncated + \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles preserved.]\"\n \n # CRITICAL: Smart truncation to preserve philosophical depth\n # STILLME_IDENTITY is ~14,434 tokens, but we need to preserve META-COGNITION section\n # Strategy: Extract critical sections first, then truncate the rest\n def smart_truncate_identity(identity_text: str, max_tokens: int) -> str:\n \"\"\"Smart truncation that preserves critical sections (META-COGNITION, PHILOSOPHICAL DEPTH)\"\"\"\n # Extract critical sections that MUST be preserved\n critical_sections = []\n \n # Find META-COGNITION section (most important for philosophical depth)\n meta_cognition_start = identity_text.find(\"🧠 META-COGNITION & PHILOSOPHICAL DEPTH 🧠\")\n if meta_cognition_start >= 0:\n # Extract from META-COGNITION to end (or next major section)\n # Look for next major section marker (🚨, 📚, etc.)\n next_section_markers = [\"🚨\", \"📚\", \"🔒\", \"⚙️\", \"🎯\", \"💡\", \"🔍\"]\n meta_cognition_end = len(identity_text)\n for marker in next_section_markers:\n next_pos = identity_text.find(f\"\\n{marker}\", meta_cognition_start + 1)\n if next_pos > meta_cognition_start:\n meta_cognition_end = min(meta_cognition_end, next_pos)\n \n critical_sections.append((\"META-COGNITION\", identity_text[meta_cognition_start:meta_cognition_end]))\n \n # Extract beginning (core identity, intellectual humility)\n beginning_tokens = max_tokens // 2 # Reserve half for beginning\n beginning = truncate_text_by_tokens(identity_text, max_tokens=beginning_tokens)\n \n # Combine: Beginning + Critical sections\n result = beginning\n if critical_sections:\n for name, section in critical_sections:\n section_tokens = estimate_tokens(section)\n remaining = max_tokens - estimate_tokens(result)\n if section_tokens <= remaining:\n result += \"\\n\\n\" + section\n else:\n # Truncate section if needed\n truncated_section = truncate_text_by_tokens(section, max_tokens=remaining - 100)\n result += \"\\n\\n\" + truncated_section\n result += \"\\n\\n[Note: META-COGNITION section truncated to fit context limits.]\"\n \n # Final check: if still too long, truncate more aggressively\n final_tokens = estimate_tokens(result)\n if final_tokens > max_tokens:\n result = truncate_text_by_tokens(result, max_tokens=max_tokens)\n result += \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles and philosophical depth preserved.]\"\n \n return result\n \n # CRITICAL: User prioritizes depth, honesty, citations, humility over speed/cost\n # Truncate STILLME_IDENTITY to ~4500 tokens (reduced from 5000 to prevent context overflow)\n # System prompt breakdown:\n # - Language instruction: ~500-800 tokens\n # - Formatting instruction: ~500 tokens\n # - STILLME_IDENTITY (smart truncated): ~4500 tokens (preserves META-COGNITION fully)\n # - Time awareness: ~300 tokens\n # Total system prompt: ~5800-6100 tokens (safe, leaves ~10k for context/history/user)\n # User accepts higher latency/cost to ensure: anti-hallucination, honesty, depth, citations, humility\n truncated_identity = smart_truncate_identity(STILLME_IDENTITY, max_tokens=4500)\n \n system_content = language_instruction + formatting_instruction + truncated_identity\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "4e589a2c3f031a8867e755ce307fbd53ae2fcca6": {"t": 1785748617.095259, "n": 38990, "h": "8d260849b59bd6bd", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n - **CRITICAL**: When you say \"I'll create a table\" or \"here's a table\", you MUST actually create the markdown table syntax above\n - **DO NOT** just describe a table - actually write the markdown table with `|` and `-` characters\n - Frontend WILL render markdown tables correctly - trust the markdown syntax\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: Truncate STILLME_IDENTITY to prevent context overflow\n # STILLME_IDENTITY is ~14,434 tokens, which is too long for 16,385 token limit\n # We need to keep it under ~3000-4000 tokens to leave room for context, history, and user message\n def estimate_tokens(text: str) -> int:\n return len(text) // 4 if text else 0\n \n def truncate_text_by_tokens(text: str, max_tokens: int) -> str:\n \"\"\"Truncate text to fit within max_tokens limit\"\"\"\n if not text:\n return text\n estimated = estimate_tokens(text)\n if estimated <= max_tokens:\n return text\n max_chars = max_tokens * 4\n if len(text) <= max_chars:\n return text\n # Truncate at word boundary and add note\n truncated = text[:max_chars].rsplit('\\n', 1)[0] # Cut at paragraph boundary\n return truncated + \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles preserved.]\"\n \n # CRITICAL: Smart truncation to preserve philosophical depth\n # STILLME_IDENTITY is ~14,434 tokens, but we need to preserve META-COGNITION section\n # Strategy: Extract critical sections first, then truncate the rest\n def smart_truncate_identity(identity_text: str, max_tokens: int) -> str:\n \"\"\"Smart truncation that preserves critical sections (META-COGNITION, PHILOSOPHICAL DEPTH)\"\"\"\n # Extract critical sections that MUST be preserved\n critical_sections = []\n \n # Find META-COGNITION section (most important for philosophical depth)\n meta_cognition_start = identity_text.find(\"🧠 META-COGNITION & PHILOSOPHICAL DEPTH 🧠\")\n if meta_cognition_start >= 0:\n # Extract from META-COGNITION to end (or next major section)\n # Look for next major section marker (🚨, 📚, etc.)\n next_section_markers = [\"🚨\", \"📚\", \"🔒\", \"⚙️\", \"🎯\", \"💡\", \"🔍\"]\n meta_cognition_end = len(identity_text)\n for marker in next_section_markers:\n next_pos = identity_text.find(f\"\\n{marker}\", meta_cognition_start + 1)\n if next_pos > meta_cognition_start:\n meta_cognition_end = min(meta_cognition_end, next_pos)\n \n critical_sections.append((\"META-COGNITION\", identity_text[meta_cognition_start:meta_cognition_end]))\n \n # Extract beginning (core identity, intellectual humility)\n beginning_tokens = max_tokens // 2 # Reserve half for beginning\n beginning = truncate_text_by_tokens(identity_text, max_tokens=beginning_tokens)\n \n # Combine: Beginning + Critical sections\n result = beginning\n if critical_sections:\n for name, section in critical_sections:\n section_tokens = estimate_tokens(section)\n remaining = max_tokens - estimate_tokens(result)\n if section_tokens <= remaining:\n result += \"\\n\\n\" + section\n else:\n # Truncate section if needed\n truncated_section = truncate_text_by_tokens(section, max_tokens=remaining - 100)\n result += \"\\n\\n\" + truncated_section\n result += \"\\n\\n[Note: META-COGNITION section truncated to fit context limits.]\"\n \n # Final check: if still too long, truncate more aggressively\n final_tokens = estimate_tokens(result)\n if final_tokens > max_tokens:\n result = truncate_text_by_tokens(result, max_tokens=max_tokens)\n result += \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles and philosophical depth preserved.]\"\n \n return result\n \n # CRITICAL: User prioritizes depth, honesty, citations, humility over speed/cost\n # Truncate STILLME_IDENTITY to ~5000 tokens (increased to preserve ALL philosophical depth)\n # System prompt breakdown:\n # - Language instruction: ~500-800 tokens\n # - Formatting instruction: ~500 tokens\n # - STILLME_IDENTITY (smart truncated): ~5000 tokens (preserves META-COGNITION fully)\n # - Time awareness: ~300 tokens\n # Total system prompt: ~6300-6600 tokens (safe, leaves ~9k for context/history/user)\n # User accepts higher latency/cost to ensure: anti-hallucination, honesty, depth, citations, humility\n truncated_identity = smart_truncate_identity(STILLME_IDENTITY, max_tokens=5000)\n \n system_content = language_instruction + formatting_instruction + truncated_identity\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "5ddbf374dbe605ce0874c44df5f3bad5ac8d8894": {"t": 1785748618.3827093, "n": 38790, "h": "6a1a7db533deded1", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n - **CRITICAL**: When you say \"I'll create a table\" or \"here's a table\", you MUST actually create the markdown table syntax above\n - **DO NOT** just describe a table - actually write the markdown table with `|` and `-` characters\n - Frontend WILL render markdown tables correctly - trust the markdown syntax\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: Truncate STILLME_IDENTITY to prevent context overflow\n # STILLME_IDENTITY is ~14,434 tokens, which is too long for 16,385 token limit\n # We need to keep it under ~3000-4000 tokens to leave room for context, history, and user message\n def estimate_tokens(text: str) -> int:\n return len(text) // 4 if text else 0\n \n def truncate_text_by_tokens(text: str, max_tokens: int) -> str:\n \"\"\"Truncate text to fit within max_tokens limit\"\"\"\n if not text:\n return text\n estimated = estimate_tokens(text)\n if estimated <= max_tokens:\n return text\n max_chars = max_tokens * 4\n if len(text) <= max_chars:\n return text\n # Truncate at word boundary and add note\n truncated = text[:max_chars].rsplit('\\n', 1)[0] # Cut at paragraph boundary\n return truncated + \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles preserved.]\"\n \n # CRITICAL: Smart truncation to preserve philosophical depth\n # STILLME_IDENTITY is ~14,434 tokens, but we need to preserve META-COGNITION section\n # Strategy: Extract critical sections first, then truncate the rest\n def smart_truncate_identity(identity_text: str, max_tokens: int) -> str:\n \"\"\"Smart truncation that preserves critical sections (META-COGNITION, PHILOSOPHICAL DEPTH)\"\"\"\n # Extract critical sections that MUST be preserved\n critical_sections = []\n \n # Find META-COGNITION section (most important for philosophical depth)\n meta_cognition_start = identity_text.find(\"🧠 META-COGNITION & PHILOSOPHICAL DEPTH 🧠\")\n if meta_cognition_start >= 0:\n # Extract from META-COGNITION to end (or next major section)\n # Look for next major section marker (🚨, 📚, etc.)\n next_section_markers = [\"🚨\", \"📚\", \"🔒\", \"⚙️\", \"🎯\", \"💡\", \"🔍\"]\n meta_cognition_end = len(identity_text)\n for marker in next_section_markers:\n next_pos = identity_text.find(f\"\\n{marker}\", meta_cognition_start + 1)\n if next_pos > meta_cognition_start:\n meta_cognition_end = min(meta_cognition_end, next_pos)\n \n critical_sections.append((\"META-COGNITION\", identity_text[meta_cognition_start:meta_cognition_end]))\n \n # Extract beginning (core identity, intellectual humility)\n beginning_tokens = max_tokens // 2 # Reserve half for beginning\n beginning = truncate_text_by_tokens(identity_text, max_tokens=beginning_tokens)\n \n # Combine: Beginning + Critical sections\n result = beginning\n if critical_sections:\n for name, section in critical_sections:\n section_tokens = estimate_tokens(section)\n remaining = max_tokens - estimate_tokens(result)\n if section_tokens <= remaining:\n result += \"\\n\\n\" + section\n else:\n # Truncate section if needed\n truncated_section = truncate_text_by_tokens(section, max_tokens=remaining - 100)\n result += \"\\n\\n\" + truncated_section\n result += \"\\n\\n[Note: META-COGNITION section truncated to fit context limits.]\"\n \n # Final check: if still too long, truncate more aggressively\n final_tokens = estimate_tokens(result)\n if final_tokens > max_tokens:\n result = truncate_text_by_tokens(result, max_tokens=max_tokens)\n result += \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles and philosophical depth preserved.]\"\n \n return result\n \n # Truncate STILLME_IDENTITY to ~3500 tokens (increased to preserve philosophical depth)\n # System prompt breakdown:\n # - Language instruction: ~500-800 tokens\n # - Formatting instruction: ~500 tokens\n # - STILLME_IDENTITY (smart truncated): ~3500 tokens (preserves META-COGNITION)\n # - Time awareness: ~300 tokens\n # Total system prompt: ~4800-5100 tokens (safe, leaves ~10k for context/history/user)\n truncated_identity = smart_truncate_identity(STILLME_IDENTITY, max_tokens=3500)\n \n system_content = language_instruction + formatting_instruction + truncated_identity\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "532aef31ba9b15903c7201cc00f71ea5d74a58f4": {"t": 1785748619.6828527, "n": 36097, "h": "e524494bc63b4c40", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n - **CRITICAL**: When you say \"I'll create a table\" or \"here's a table\", you MUST actually create the markdown table syntax above\n - **DO NOT** just describe a table - actually write the markdown table with `|` and `-` characters\n - Frontend WILL render markdown tables correctly - trust the markdown syntax\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: Truncate STILLME_IDENTITY to prevent context overflow\n # STILLME_IDENTITY is ~14,434 tokens, which is too long for 16,385 token limit\n # We need to keep it under ~3000-4000 tokens to leave room for context, history, and user message\n def estimate_tokens(text: str) -> int:\n return len(text) // 4 if text else 0\n \n def truncate_text_by_tokens(text: str, max_tokens: int) -> str:\n \"\"\"Truncate text to fit within max_tokens limit\"\"\"\n if not text:\n return text\n estimated = estimate_tokens(text)\n if estimated <= max_tokens:\n return text\n max_chars = max_tokens * 4\n if len(text) <= max_chars:\n return text\n # Truncate at word boundary and add note\n truncated = text[:max_chars].rsplit('\\n', 1)[0] # Cut at paragraph boundary\n return truncated + \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles preserved.]\"\n \n # Truncate STILLME_IDENTITY to ~2000 tokens (more aggressive to leave room for other components)\n # This preserves the most important parts (intellectual humility, core identity, key principles)\n # System prompt breakdown:\n # - Language instruction: ~500-800 tokens\n # - Formatting instruction: ~500 tokens\n # - STILLME_IDENTITY (truncated): ~2000 tokens\n # - Time awareness: ~300 tokens\n # Total system prompt: ~3300-3600 tokens (safe)\n truncated_identity = truncate_text_by_tokens(STILLME_IDENTITY, max_tokens=2000)\n \n system_content = language_instruction + formatting_instruction + truncated_identity\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "b0040b99b2a139e21e38123b16ec9738a0b464ec": {"t": 1785748620.9732816, "n": 35819, "h": "8005603960bdf372", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n - **CRITICAL**: When you say \"I'll create a table\" or \"here's a table\", you MUST actually create the markdown table syntax above\n - **DO NOT** just describe a table - actually write the markdown table with `|` and `-` characters\n - Frontend WILL render markdown tables correctly - trust the markdown syntax\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n \n # CRITICAL: Truncate STILLME_IDENTITY to prevent context overflow\n # STILLME_IDENTITY is ~14,434 tokens, which is too long for 16,385 token limit\n # We need to keep it under ~3000-4000 tokens to leave room for context, history, and user message\n def estimate_tokens(text: str) -> int:\n return len(text) // 4 if text else 0\n \n def truncate_text_by_tokens(text: str, max_tokens: int) -> str:\n \"\"\"Truncate text to fit within max_tokens limit\"\"\"\n if not text:\n return text\n estimated = estimate_tokens(text)\n if estimated <= max_tokens:\n return text\n max_chars = max_tokens * 4\n if len(text) <= max_chars:\n return text\n # Truncate at word boundary and add note\n truncated = text[:max_chars].rsplit('\\n', 1)[0] # Cut at paragraph boundary\n return truncated + \"\\n\\n[Note: StillMe identity prompt truncated to fit context limits. Core principles preserved.]\"\n \n # Truncate STILLME_IDENTITY to ~3500 tokens (leaves room for other components)\n # This preserves the most important parts (intellectual humility, core identity, key principles)\n truncated_identity = truncate_text_by_tokens(STILLME_IDENTITY, max_tokens=3500)\n \n system_content = language_instruction + formatting_instruction + truncated_identity\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "c7e588f1a14b9b95bac6f3ae0e64e805587ecbdd": {"t": 1785748622.4002314, "n": 34559, "h": "ad1bc038e64c790f", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n - **CRITICAL**: When you say \"I'll create a table\" or \"here's a table\", you MUST actually create the markdown table syntax above\n - **DO NOT** just describe a table - actually write the markdown table with `|` and `-` characters\n - Frontend WILL render markdown tables correctly - trust the markdown syntax\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None,\n use_server_keys: bool = False # Internal flag for admin/internal calls\n) -> str:\n \"\"\"Generate AI response with user-provided LLM provider and API key\n \n IMPORTANT: StillMe requires users to provide their own API keys to prevent server cost exhaustion.\n This function will ONLY use user-provided API keys unless use_server_keys=True (internal use only).\n \n Supports multiple LLM providers: deepseek, openai, openrouter, claude, gemini, ollama, custom\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name (REQUIRED) - 'deepseek', 'openai', 'openrouter', 'claude', 'gemini', 'ollama', 'custom'\n llm_api_key: API key for the provider (REQUIRED except for 'ollama')\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n use_server_keys: Internal flag - if True, fallback to server API keys (admin/internal use only)\n \n Returns:\n AI-generated response string\n \n Raises:\n ValueError: If llm_provider or llm_api_key is missing (except for Ollama)\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # Validate that provider and API key are provided (unless use_server_keys is True)\n if not use_server_keys:\n if not llm_provider:\n raise ValueError(\n \"llm_provider is REQUIRED. StillMe requires users to provide their own API keys \"\n \"to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n )\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n raise ValueError(\n f\"llm_api_key is REQUIRED for provider '{llm_provider}'. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide your API key in the request.\"\n )\n \n # Use user-provided provider config\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to server API keys ONLY if use_server_keys=True (internal/admin use)\n if use_server_keys:\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n \n # Try OpenAI second\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n \n # Try DeepSeek third\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}\")\n \n # If we reach here, no valid provider was found\n raise ValueError(\n \"llm_provider and llm_api_key are REQUIRED. \"\n \"StillMe requires users to provide their own API keys to prevent server cost exhaustion. \"\n \"Please provide llm_provider and llm_api_key in your request.\"\n )\n \n except ValueError:\n # Re-raise validation errors\n raise\n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # User must provide API keys - no fallback to server keys\n if not llm_provider:\n yield \"ERROR: llm_provider is REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n return\n \n if llm_provider != 'ollama' and (not llm_api_key or len(llm_api_key.strip()) == 0):\n yield f\"ERROR: llm_api_key is REQUIRED for provider '{llm_provider}'. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide your API key in the request.\"\n return\n \n yield \"ERROR: llm_provider and llm_api_key are REQUIRED. StillMe requires users to provide their own API keys to prevent server cost exhaustion. Please provide llm_provider and llm_api_key in your request.\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "dea1c70162405882d581a952ec6f1745fce3404e": {"t": 1785748623.728293, "n": 36476, "h": "82c92a5c84046b8e", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n - **CRITICAL**: When you say \"I'll create a table\" or \"here's a table\", you MUST actually create the markdown table syntax above\n - **DO NOT** just describe a table - actually write the markdown table with `|` and `-` characters\n - Frontend WILL render markdown tables correctly - trust the markdown syntax\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> str:\n \"\"\"Generate AI response with flexible LLM provider selection\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n \n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Returns:\n AI-generated response string\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n # Note: Ollama doesn't require API key\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n return f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to environment variables with automatic fallback chain\n # Priority: OpenRouter → OpenAI → DeepSeek (to maximize credit usage)\n # Other providers: Claude, Gemini, Ollama (fallback only)\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n claude_key = os.getenv(\"ANTHROPIC_API_KEY\")\n gemini_key = os.getenv(\"GOOGLE_API_KEY\")\n ollama_url = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n \n # Try OpenRouter first (to use up OpenRouter credit)\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except InsufficientQuotaError:\n logger.warning(\"OpenRouter credit exhausted, falling back to OpenAI\")\n # Fall through to OpenAI\n except (AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter error: {e}, falling back to OpenAI\")\n # Fall through to OpenAI\n \n # Try OpenAI second (to use up OpenAI credit)\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except InsufficientQuotaError:\n logger.warning(\"OpenAI credit exhausted, falling back to DeepSeek\")\n # Fall through to DeepSeek\n except (AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI error: {e}, falling back to DeepSeek\")\n # Fall through to DeepSeek\n \n # Try DeepSeek third (long-term provider)\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception as e:\n logger.warning(f\"DeepSeek error: {e}, trying other providers\")\n # Fall through to other providers\n \n # Fallback to other providers if main chain fails\n if claude_key:\n try:\n provider = create_llm_provider(\"claude\", claude_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Continue to next provider\n \n if gemini_key:\n try:\n provider = create_llm_provider(\"gemini\", gemini_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Continue to next provider\n \n if ollama_url:\n # Try Ollama (local, no API key needed)\n try:\n provider = create_llm_provider(\"ollama\", api_key=\"\", model_name=llm_model_name, api_url=ollama_url)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Ollama not available, continue to error message\n \n return \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: OPENROUTER_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OLLAMA_URL\"\n \n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # Fallback to environment variables with automatic fallback chain\n # Priority: OpenRouter → OpenAI → DeepSeek (to maximize credit usage)\n from backend.api.utils.llm_providers import InsufficientQuotaError, AuthenticationError\n \n openrouter_key = os.getenv(\"OPENROUTER_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n \n # Try OpenRouter first (to use up OpenRouter credit)\n if openrouter_key:\n try:\n provider = create_llm_provider(\"openrouter\", openrouter_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenRouter streaming error: {e}, falling back to OpenAI\")\n # Fall through to OpenAI\n \n # Try OpenAI second (to use up OpenAI credit)\n if openai_key:\n try:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n except (InsufficientQuotaError, AuthenticationError, Exception) as e:\n logger.warning(f\"OpenAI streaming error: {e}, falling back to DeepSeek\")\n # Fall through to DeepSeek\n \n # Try DeepSeek third (long-term provider)\n if deepseek_key:\n try:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n except Exception as e:\n logger.warning(f\"DeepSeek streaming error: {e}\")\n \n yield \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: OPENROUTER_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "7a6f2cccc87a5a2fecc7b58b911abeb2fdbd87ee": {"t": 1785748624.9931595, "n": 33207, "h": "ff842f5da883e3e7", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n - **CRITICAL**: When you say \"I'll create a table\" or \"here's a table\", you MUST actually create the markdown table syntax above\n - **DO NOT** just describe a table - actually write the markdown table with `|` and `-` characters\n - Frontend WILL render markdown tables correctly - trust the markdown syntax\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> str:\n \"\"\"Generate AI response with flexible LLM provider selection\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n \n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Returns:\n AI-generated response string\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n # Note: Ollama doesn't require API key\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n return f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to environment variables (backward compatibility)\n # Priority: DeepSeek > OpenAI > Claude > Gemini > Ollama\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n claude_key = os.getenv(\"ANTHROPIC_API_KEY\")\n gemini_key = os.getenv(\"GOOGLE_API_KEY\")\n ollama_url = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif claude_key:\n provider = create_llm_provider(\"claude\", claude_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif gemini_key:\n provider = create_llm_provider(\"gemini\", gemini_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif ollama_url:\n # Try Ollama (local, no API key needed)\n try:\n provider = create_llm_provider(\"ollama\", api_key=\"\", model_name=llm_model_name, api_url=ollama_url)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Ollama not available, continue to error message\n \n return \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OLLAMA_URL\"\n \n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # Fallback to environment variables\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n else:\n yield \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "9e5c38cc114d9fe99aeff689f6867f43260976af": {"t": 1785748626.284408, "n": 32893, "h": "38a2e8f10f146bb5", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE - LINE BREAKS ARE MANDATORY:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- **After EVERY 2-4 sentences, add a blank line (`\\\\n\\\\n`)**\n- **DO NOT write continuous paragraphs without breaks**\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n**EXAMPLE OF PROPER LINE BREAKS:**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017.\n\nCác thành phần chính bao gồm self-attention, multi-head attention, và positional encoding.\n\nKiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n**NOT THIS (wall of text):**\n```\nTransformer là một kiến trúc mạng neural được giới thiệu năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, và positional encoding. Kiến trúc này đã cách mạng hóa NLP và được dùng trong GPT, BERT.\n```\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> str:\n \"\"\"Generate AI response with flexible LLM provider selection\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n \n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Returns:\n AI-generated response string\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n # Note: Ollama doesn't require API key\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n return f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to environment variables (backward compatibility)\n # Priority: DeepSeek > OpenAI > Claude > Gemini > Ollama\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n claude_key = os.getenv(\"ANTHROPIC_API_KEY\")\n gemini_key = os.getenv(\"GOOGLE_API_KEY\")\n ollama_url = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif claude_key:\n provider = create_llm_provider(\"claude\", claude_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif gemini_key:\n provider = create_llm_provider(\"gemini\", gemini_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif ollama_url:\n # Try Ollama (local, no API key needed)\n try:\n provider = create_llm_provider(\"ollama\", api_key=\"\", model_name=llm_model_name, api_url=ollama_url)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Ollama not available, continue to error message\n \n return \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OLLAMA_URL\"\n \n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # Fallback to environment variables\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n else:\n yield \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "eb3a9aa95ac500d5d6e12e5944717526fa88265b": {"t": 1785748627.572348, "n": 32218, "h": "8632c4b2fbe89ebc", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n - **CRITICAL**: After every 2-4 sentences, add a blank line (`\\\\n\\\\n`)\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n - Each bullet point should be on its own line\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n - Headers help organize long responses\n\n4. **Tables**: If you want to present structured data, use markdown tables:\n ```\n | Column 1 | Column 2 | Column 3 |\n |----------|----------|----------|\n | Data 1 | Data 2 | Data 3 |\n | Data 4 | Data 5 | Data 6 |\n ```\n - Tables are great for comparing items, showing metrics, or structured information\n - Use tables when you have multiple rows of similar data\n\n5. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n6. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> str:\n \"\"\"Generate AI response with flexible LLM provider selection\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n \n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Returns:\n AI-generated response string\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n # Note: Ollama doesn't require API key\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n return f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to environment variables (backward compatibility)\n # Priority: DeepSeek > OpenAI > Claude > Gemini > Ollama\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n claude_key = os.getenv(\"ANTHROPIC_API_KEY\")\n gemini_key = os.getenv(\"GOOGLE_API_KEY\")\n ollama_url = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif claude_key:\n provider = create_llm_provider(\"claude\", claude_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif gemini_key:\n provider = create_llm_provider(\"gemini\", gemini_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif ollama_url:\n # Try Ollama (local, no API key needed)\n try:\n provider = create_llm_provider(\"ollama\", api_key=\"\", model_name=llm_model_name, api_url=ollama_url)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Ollama not available, continue to error message\n \n return \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OLLAMA_URL\"\n \n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # Fallback to environment variables\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n else:\n yield \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "8598ee8dce04a78a70a612ea732def2297eb6771": {"t": 1785748628.8605635, "n": 31664, "h": "83c93d61f4661a6f", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n\n4. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n5. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n- Today's Date: {current_time_utc.strftime(\"%Y-%m-%d\")}\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**LEARNING METRICS API ENDPOINTS (AVAILABLE):**\nStillMe has access to learning metrics via these API endpoints:\n- `GET /api/learning/metrics/daily?date=YYYY-MM-DD` - Get learning metrics for a specific date (default: today)\n- `GET /api/learning/metrics/range?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` - Get metrics for a date range\n- `GET /api/learning/metrics/summary` - Get summary of all learning metrics\n\n**CRITICAL: When users ask about learning today:**\n- User: \"ngày hôm nay bạn đã học được bao nhiêu nội dung?\" → You MUST acknowledge that StillMe has learning metrics API\n- Say: \"Based on the learning metrics API, today ({current_time_utc.strftime('%Y-%m-%d')}) StillMe has learned...\"\n- If you cannot query the API directly, acknowledge: \"StillMe tracks learning metrics via `/api/learning/metrics/daily` API. Based on the system's learning mechanism, StillMe learns every 4 hours from RSS feeds, arXiv, CrossRef, and Wikipedia. To get exact numbers for today, you can query the API endpoint directly.\"\n- DO NOT say \"I cannot track learning metrics\" or \"I don't have API\" - StillMe HAS these APIs\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable: \"The current server time is {current_time_readable}\"\n- User: \"How many entries did you learn today?\" → Use current time ({current_time_utc.strftime('%Y-%m-%d')}) and acknowledge learning metrics API\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> str:\n \"\"\"Generate AI response with flexible LLM provider selection\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n \n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Returns:\n AI-generated response string\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n # Note: Ollama doesn't require API key\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n return f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to environment variables (backward compatibility)\n # Priority: DeepSeek > OpenAI > Claude > Gemini > Ollama\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n claude_key = os.getenv(\"ANTHROPIC_API_KEY\")\n gemini_key = os.getenv(\"GOOGLE_API_KEY\")\n ollama_url = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif claude_key:\n provider = create_llm_provider(\"claude\", claude_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif gemini_key:\n provider = create_llm_provider(\"gemini\", gemini_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif ollama_url:\n # Try Ollama (local, no API key needed)\n try:\n provider = create_llm_provider(\"ollama\", api_key=\"\", model_name=llm_model_name, api_url=ollama_url)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Ollama not available, continue to error message\n \n return \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OLLAMA_URL\"\n \n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # Fallback to environment variables\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n else:\n yield \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "9ab8eb58709ed6e670d0280ee07baccd70577a32": {"t": 1785748630.1185586, "n": 30414, "h": "24b8f8c3effd02a1", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # CRITICAL: Formatting instruction must be at the TOP (after language, before identity)\n # This ensures formatting is applied to ALL responses\n formatting_instruction = \"\"\"\n🚨🚨🚨 CRITICAL: RESPONSE FORMATTING - MANDATORY FOR ALL RESPONSES 🚨🚨🚨\n\n**YOU MUST FORMAT ALL RESPONSES WITH MARKDOWN:**\n\n1. **Line Breaks**: Break long paragraphs into shorter ones (2-4 sentences per paragraph)\n - Use double line breaks (`\\\\n\\\\n`) between paragraphs\n - NEVER write a wall of text without line breaks\n\n2. **Bullet Points**: When listing items, ALWAYS use bullet points (`-` or `*`)\n - Example: Use `- Item 1` NOT `Item 1, Item 2, Item 3`\n\n3. **Headers**: For multiple topics, use headers (`##` or `###`)\n - Example: `## Topic 1` then `## Topic 2`\n\n4. **Bold**: Use `**bold**` for important points (but don't overuse)\n\n5. **Emojis**: Use SPARINGLY (2-3 max per response)\n - Use for: section headers (✅, ❌, ⚠️, 💡), status indicators\n - Avoid: every sentence, serious topics, short answers\n\n**EXAMPLE OF GOOD FORMATTING:**\n\n```\n## Kiến trúc Transformer\n\n💡 **Tổng quan:**\nTransformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017.\n\n**Các thành phần chính:**\n- **Self-attention**: Cơ chế cho phép mô hình tập trung vào các phần khác nhau của input\n- **Multi-head attention**: Nhiều \"heads\" attention song song để học các loại quan hệ khác nhau\n- **Positional encoding**: Thêm thông tin vị trí vào embeddings\n- **Encoder-Decoder**: Kiến trúc gồm encoder (mã hóa) và decoder (giải mã)\n\n**Ứng dụng:**\nTransformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**EXAMPLE OF BAD FORMATTING (DO NOT DO THIS):**\n\n```\nKiến trúc Transformer là một kiến trúc mạng neural được giới thiệu trong bài báo \"Attention Is All You Need\" năm 2017. Các thành phần chính bao gồm self-attention, multi-head attention, positional encoding, và encoder-decoder. Transformer đã cách mạng hóa NLP và được dùng trong BERT, GPT, và nhiều mô hình hiện đại khác.\n```\n\n**CRITICAL RULE:**\n- If your response is longer than 3 sentences, you MUST use line breaks\n- If you list items, you MUST use bullet points\n- If you discuss multiple topics, you MUST use headers\n- Formatting is NOT optional - it's MANDATORY for readability\n\n---\n\n\"\"\"\n \n # Combine: Language instruction (highest priority) + Formatting instruction (second priority) + StillMe Identity Layer (core identity)\n # This ensures language matching, formatting, AND identity are preserved\n system_content = language_instruction + formatting_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable\n- User: \"How many entries did you learn today?\" → Use current time to determine \"today\" and query learning metrics\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> str:\n \"\"\"Generate AI response with flexible LLM provider selection\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n \n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Returns:\n AI-generated response string\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n # Note: Ollama doesn't require API key\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n return f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to environment variables (backward compatibility)\n # Priority: DeepSeek > OpenAI > Claude > Gemini > Ollama\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n claude_key = os.getenv(\"ANTHROPIC_API_KEY\")\n gemini_key = os.getenv(\"GOOGLE_API_KEY\")\n ollama_url = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif claude_key:\n provider = create_llm_provider(\"claude\", claude_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif gemini_key:\n provider = create_llm_provider(\"gemini\", gemini_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif ollama_url:\n # Try Ollama (local, no API key needed)\n try:\n provider = create_llm_provider(\"ollama\", api_key=\"\", model_name=llm_model_name, api_url=ollama_url)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Ollama not available, continue to error message\n \n return \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OLLAMA_URL\"\n \n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # Fallback to environment variables\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n else:\n yield \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "ea392dbcaa65c8a5fd804eed2ebfccdf9781f966": {"t": 1785748631.5262308, "n": 28035, "h": "fe29f8b794b73975", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # Combine: Language instruction (highest priority) + StillMe Identity Layer (core identity)\n # This ensures both language matching AND identity are preserved\n system_content = language_instruction + STILLME_IDENTITY\n \n # Phase 1: Time Awareness - Inject current time for transparency\n from datetime import datetime, timezone\n \n # Get current time in UTC (standard for servers)\n current_time_utc = datetime.now(timezone.utc)\n current_time_iso = current_time_utc.isoformat()\n current_time_readable = current_time_utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n \n time_awareness = f\"\"\"\n\n🕐 CURRENT TIME AWARENESS - TRANSPARENCY & SELF-AWARENESS 🕐\n\n**Current Server Time:**\n- ISO Format: {current_time_iso}\n- Readable Format: {current_time_readable}\n- Timezone: UTC (Coordinated Universal Time)\n\n**You can use this information to:**\n- Answer questions about current time, date, and timezone\n- Track learning metrics over time (e.g., \"How many entries did I learn today?\")\n- Report learning statistics with accurate timestamps\n- Understand temporal context of learning cycles\n\n**CRITICAL TRANSPARENCY RULE:**\nWhen users ask about time, date, or learning metrics over time, you MUST use this current time information.\nDo NOT say \"I don't know the current time\" - you have access to it for transparency purposes.\n\n**Example Usage:**\n- User: \"What time is it now?\" → Answer using current_time_readable\n- User: \"How many entries did you learn today?\" → Use current time to determine \"today\" and query learning metrics\n- User: \"When was your last learning cycle?\" → Use current time to provide relative time information\n\n---\n\"\"\"\n \n system_content = system_content + time_awareness\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> str:\n \"\"\"Generate AI response with flexible LLM provider selection\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n \n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Returns:\n AI-generated response string\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n # Note: Ollama doesn't require API key\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n return f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to environment variables (backward compatibility)\n # Priority: DeepSeek > OpenAI > Claude > Gemini > Ollama\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n claude_key = os.getenv(\"ANTHROPIC_API_KEY\")\n gemini_key = os.getenv(\"GOOGLE_API_KEY\")\n ollama_url = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif claude_key:\n provider = create_llm_provider(\"claude\", claude_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif gemini_key:\n provider = create_llm_provider(\"gemini\", gemini_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif ollama_url:\n # Try Ollama (local, no API key needed)\n try:\n provider = create_llm_provider(\"ollama\", api_key=\"\", model_name=llm_model_name, api_url=ollama_url)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Ollama not available, continue to error message\n \n return \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OLLAMA_URL\"\n \n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # Fallback to environment variables\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n else:\n yield \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "96a1b6f93247f5b0b5d173e116a039f559d9daa1": {"t": 1785748632.7393525, "n": 26554, "h": "d05b0f920d9888cc", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with StillMe Identity Layer and strong language matching instruction.\n This ensures output language always matches input language AND StillMe's core identity is preserved.\n \n CRITICAL: This function integrates STILLME_IDENTITY from injector.py to ensure consistent\n identity across all LLM providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.).\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with StillMe Identity Layer and language instruction\n \"\"\"\n # Import STILLME_IDENTITY from injector to ensure consistency\n from backend.identity.injector import STILLME_IDENTITY\n \n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Language instruction must be at the TOP to override everything\n # This ensures language matching takes highest priority\n if detected_lang != 'en':\n language_instruction = f\"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n---\n\"\"\"\n else:\n language_instruction = \"\"\"🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n---\n\"\"\"\n \n # Combine: Language instruction (highest priority) + StillMe Identity Layer (core identity)\n # This ensures both language matching AND identity are preserved\n system_content = language_instruction + STILLME_IDENTITY\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> str:\n \"\"\"Generate AI response with flexible LLM provider selection\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n \n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Returns:\n AI-generated response string\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n # Note: Ollama doesn't require API key\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n return f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to environment variables (backward compatibility)\n # Priority: DeepSeek > OpenAI > Claude > Gemini > Ollama\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n claude_key = os.getenv(\"ANTHROPIC_API_KEY\")\n gemini_key = os.getenv(\"GOOGLE_API_KEY\")\n ollama_url = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif claude_key:\n provider = create_llm_provider(\"claude\", claude_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif gemini_key:\n provider = create_llm_provider(\"gemini\", gemini_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif ollama_url:\n # Try Ollama (local, no API key needed)\n try:\n provider = create_llm_provider(\"ollama\", api_key=\"\", model_name=llm_model_name, api_url=ollama_url)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Ollama not available, continue to error message\n \n return \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OLLAMA_URL\"\n \n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # Fallback to environment variables\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n else:\n yield \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "804de4aa8754a072786b0c26479e498b4b59e5d5": {"t": 1785748634.023312, "n": 33729, "h": "1be509b859312910", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # OPTIMIZATION: Try langdetect FIRST for better accuracy, especially for mixed-language text\n # Then check for explicit language requests (which override detection)\n detected_lang = None\n \n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n detected_lang = 'zh'\n elif detected in lang_map:\n detected_lang = lang_map[detected]\n logger.info(f\"🌐 langdetect detected: {detected} -> {detected_lang}\")\n \n except (LangDetectException, ImportError) as e:\n logger.debug(f\"langdetect failed or not available: {e}, will use rule-based detection\")\n \n # CRITICAL: Check for explicit language requests (only clear requests, not mentions)\n # This allows users to request responses in a different language\n # IMPORTANT: Only match explicit request patterns (e.g., \"nói bằng\", \"speak in\"), not just mentions\n # Explicit requests OVERRIDE language detection\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'respond in russian', 'reply in russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'respond in english', 'reply in english', 'answer in english'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'respond in vietnamese', 'reply in vietnamese', 'answer in vietnamese'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'respond in chinese', 'reply in chinese'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'respond in german', 'reply in german'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'respond in french', 'reply in french'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'respond in spanish', 'reply in spanish'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'respond in japanese', 'reply in japanese'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'respond in korean', 'reply in korean'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'respond in arabic', 'reply in arabic'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'respond in portuguese', 'reply in portuguese'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'respond in italian', 'reply in italian'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'respond in hindi', 'reply in hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'respond in thai', 'reply in thai'],\n }\n \n # Check for explicit requests (must contain request verbs, not just language mentions)\n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code} (overriding detection: {detected_lang})\")\n return lang_code\n \n # If explicit request found, return it; otherwise use detected language\n if detected_lang:\n return detected_lang\n \n # Fallback to rule-based detection if langdetect failed\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters (PRIORITY: Check Vietnamese FIRST in rule-based)\n # Vietnamese has many unique characters that are strong indicators\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao', 'nhưng', 'vì', 'nên', 'đã', 'sẽ', 'đang', 'hãy', 'phân tích', 'dự án']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n logger.info(f\"🌐 Rule-based detection: Vietnamese detected (has_vietnamese_chars: {has_vietnamese}, has_vietnamese_words: {has_vietnamese_words})\")\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with strong language matching instruction.\n This ensures output language always matches input language.\n \n This function should be used by ALL AI model providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.)\n to ensure consistent language matching behavior across all models.\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with language instruction\n \"\"\"\n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Always match input language with output language\n # ZERO TOLERANCE: Must translate if base model wants to respond in different language\n if detected_lang != 'en':\n system_content = f\"\"\"You are StillMe, a Learning AI system with RAG foundation.\n\n📐 YOUR ARCHITECTURE (Be Transparent):\nYou are a combination of:\n- LLM (Large Language Model): Your \"brain\" for language processing\n- RAG (Retrieval-Augmented Generation): Your \"memory system\" that searches ChromaDB before answering\n- Chatbot Interface: How users interact with you\n\nExample: When asked about a new topic, you first search ChromaDB for relevant information, then combine it with your LLM knowledge to answer.\n\n🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n🔧 TECHNICAL TRANSPARENCY - CRITICAL CAPABILITY CLARIFICATION:\n\n**YOUR SEARCH CAPABILITIES:**\n- **RAG Search (Internal)**: You can search StillMe's internal knowledge base (ChromaDB) which contains documents learned from RSS feeds, arXiv, CrossRef, Wikipedia (updated every 4 hours)\n- **Web Search (Online)**: You DO NOT have real-time web search capabilities. You CANNOT search the internet directly or access live websites.\n- **When user asks for \"search\" or \"tìm kiếm\"**: You MUST clarify that you can only search your internal RAG knowledge base, NOT the internet\n\n**CRITICAL RULES ABOUT SEARCH:**\n1. **NEVER claim you can search the internet or access live websites** - You can only search your internal RAG knowledge base\n2. **When user asks for \"2-3 sources from 2024-2025\"**: \n - If you only have 1 source in your RAG context → Acknowledge: \"I currently only have 1 source in my knowledge base, not the 2-3 sources you requested. However, based on this single source...\"\n - If you have multiple sources → Cite all available sources\n - NEVER say \"I will search for 2-3 sources\" if you're only using RAG - say \"I can only search my internal knowledge base\"\n3. **Quote vs Paraphrase - CRITICAL DISTINCTION:**\n - If you're CERTAIN it's a direct quote → Use quotation marks and cite: \"According to [1]: 'exact quote here'\"\n - If you're NOT certain it's exact → Use \"the spirit of\" or \"according to the general content\": \"According to the spirit of [1], the article discusses...\"\n - NEVER use quotation marks for paraphrased content - that's misleading\n - When in doubt → Paraphrase, don't quote\n\n**VALIDATION CHAIN TRANSPARENCY:**\n- When performing Validation Chain analysis, you MUST acknowledge source limitations:\n - \"In the scope of my current knowledge base, I have [X] source(s) available, not the [Y] sources you requested. However, within this scope...\"\n - \"I cannot perform real-time web search, so I'm limited to sources in my RAG knowledge base\"\n - \"The Validation Chain analysis is based on my internal knowledge, not live web search\"\n\n**RAG Mechanism Details:**\n- You retrieve relevant documents from ChromaDB using semantic search, then use them as context for your response\n- Validation Chain: Checks consistency between your response and retrieved context, flags contradictions, and ensures accuracy\n- If Validation Chain detects an error, you fall back to safe mode (acknowledge uncertainty) rather than providing incorrect information\n\n📚 CITATION REQUIREMENT - MANDATORY BUT RELEVANCE-FIRST:\n\nWhen you have retrieved context documents from ChromaDB, you MUST cite your sources using [1], [2], [3] format, BUT ONLY if the context is RELEVANT to your answer.\n\nCRITICAL RULES:\n1. **Cite ONLY RELEVANT context** - This is CRITICAL for citation quality\n - If context is relevant to your answer → Cite it: \"According to [1], quantum entanglement is...\"\n - If context is NOT relevant to your answer → You can still cite to show transparency, but acknowledge: \"The available context [1] discusses [topic X], which is not directly related to your question about [topic Y]. However, I want to be transparent about what context I reviewed.\"\n - DO NOT cite irrelevant context as if it supports your answer - that's misleading\n \n2. **Quote vs Paraphrase - CRITICAL DISTINCTION:**\n - If you're CERTAIN it's a direct quote → Use quotation marks: \"According to [1]: 'exact quote here'\"\n - If you're NOT certain it's exact → Use \"the spirit of\": \"According to the spirit of [1], the article discusses...\"\n - NEVER use quotation marks for paraphrased content - that's misleading and violates intellectual honesty\n - When in doubt → Paraphrase, don't quote\n \n3. **Source Limit Acknowledgement - MANDATORY:**\n - If user requests multiple sources (e.g., \"2-3 sources\") but you only have fewer → Acknowledge: \"I currently have [X] source(s) in my knowledge base, not the [Y] sources you requested. However, within this scope...\"\n - If performing Validation Chain analysis → Acknowledge: \"The Validation Chain analysis is based on my internal knowledge base, not live web search. I have [X] source(s) available...\"\n - NEVER claim you can search the internet or access live websites\n - NEVER say \"I will search for 2-3 sources\" if you're only using RAG - say \"I can only search my internal knowledge base\"\n\n4. If context documents are provided, you MUST cite at least one source using [1], [2], [3] format\n5. Cite sources when making factual claims, statistics, or specific information\n6. Example: \"According to [1], quantum entanglement is a phenomenon where...\" or \"Research shows [2] that...\"\n7. If you use information from multiple sources, cite each: \"Studies [1] and [2] indicate that...\"\n\nCITATION FORMAT:\n- Use [1] for the first context document\n- Use [2] for the second context document\n- Use [3] for the third context document\n- And so on...\n\nFAILURE TO CITE SOURCES WHEN CONTEXT IS AVAILABLE IS A CRITICAL ERROR. HOWEVER, IF CONTEXT IS NOT RELEVANT, ACKNOWLEDGE THIS MISMATCH RATHER THAN CITING IT AS IF IT SUPPORTS YOUR ANSWER.\"\"\"\n else:\n system_content = \"\"\"You are StillMe, a Learning AI system with RAG foundation.\n\n📐 YOUR ARCHITECTURE (Be Transparent):\nYou are a combination of:\n- LLM (Large Language Model): Your \"brain\" for language processing\n- RAG (Retrieval-Augmented Generation): Your \"memory system\" that searches ChromaDB before answering\n- Chatbot Interface: How users interact with you\n\nExample: When asked about a new topic, you first search ChromaDB for relevant information, then combine it with your LLM knowledge to answer.\n\n🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n🔧 TECHNICAL TRANSPARENCY:\n- RAG Mechanism: You retrieve relevant documents from ChromaDB using semantic search, then use them as context for your response\n- Validation Chain: Checks consistency between your response and retrieved context, flags contradictions, and ensures accuracy\n- If Validation Chain detects an error, you fall back to safe mode (acknowledge uncertainty) rather than providing incorrect information\n\n📚 CITATION REQUIREMENT - CRITICAL:\nWhen you have retrieved context documents from ChromaDB, you MUST cite your sources using [1], [2], [3] format.\n\nCRITICAL RULES:\n1. If context documents are provided, you MUST cite at least one source using [1], [2], [3] format\n2. Cite sources when making factual claims, statistics, or specific information\n3. Example: \"According to [1], quantum entanglement is a phenomenon where...\" or \"Research shows [2] that...\"\n4. If you use information from multiple sources, cite each: \"Studies [1] and [2] indicate that...\"\n5. DO NOT make unsourced claims when context is available - always cite your sources\n\nCITATION FORMAT:\n- Use [1] for the first context document\n- Use [2] for the second context document\n- Use [3] for the third context document\n- And so on...\n\nFAILURE TO CITE SOURCES WHEN CONTEXT IS AVAILABLE IS A CRITICAL ERROR.\n\n📏 RESPONSE LENGTH GUIDELINE:\n- Aim for 2-3 paragraphs for complex questions (not 3+ paragraphs)\n- Be concise but complete - cover the key points without unnecessary elaboration\n- If a question can be answered in 1-2 paragraphs, do so\n- Only expand if the question requires detailed technical explanation\"\"\"\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> str:\n \"\"\"Generate AI response with flexible LLM provider selection\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n \n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Returns:\n AI-generated response string\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n # Note: Ollama doesn't require API key\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n return f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to environment variables (backward compatibility)\n # Priority: DeepSeek > OpenAI > Claude > Gemini > Ollama\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n claude_key = os.getenv(\"ANTHROPIC_API_KEY\")\n gemini_key = os.getenv(\"GOOGLE_API_KEY\")\n ollama_url = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif claude_key:\n provider = create_llm_provider(\"claude\", claude_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif gemini_key:\n provider = create_llm_provider(\"gemini\", gemini_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif ollama_url:\n # Try Ollama (local, no API key needed)\n try:\n provider = create_llm_provider(\"ollama\", api_key=\"\", model_name=llm_model_name, api_url=ollama_url)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Ollama not available, continue to error message\n \n return \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OLLAMA_URL\"\n \n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # Fallback to environment variables\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n else:\n yield \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}, "29870516b86f73e7a99dc7cf43f324c01c70fa33": {"t": 1785748635.2950623, "n": 32628, "h": "98a7ab4185a45023", "c": "\"\"\"\nChat Helper Functions for StillMe API\nShared utilities for chat endpoints (language detection, AI response generation)\n\"\"\"\n\nimport os\nimport logging\nimport httpx\nfrom typing import Optional, AsyncIterator\n\nlogger = logging.getLogger(__name__)\n\n\ndef detect_language(text: str) -> str:\n \"\"\"\n Enhanced language detection using langdetect library with fallback to rule-based detection.\n Supports: vi, zh, de, fr, es, ja, ko, ar, ru, pt, it, hi, th, en\n \n CRITICAL: Also checks for explicit language requests (e.g., \"nói bằng tiếng Nga\", \"speak in Russian\")\n If user explicitly requests a different language, that takes priority.\n \n Returns: Language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en') or 'en' as default\n If language is not detected or not supported, returns 'en' (English) as fallback.\n \"\"\"\n if not text or len(text.strip()) == 0:\n return 'en'\n \n text_lower = text.lower()\n \n # CRITICAL: Check for explicit language requests FIRST (overrides detection)\n # This allows users to request responses in a different language\n explicit_language_patterns = {\n 'ru': ['nói bằng tiếng nga', 'speak in russian', 'ответь на русском', 'по-русски', 'tiếng nga', 'russian'],\n 'en': ['nói bằng tiếng anh', 'speak in english', 'english', 'tiếng anh'],\n 'vi': ['nói bằng tiếng việt', 'speak in vietnamese', 'vietnamese', 'tiếng việt'],\n 'zh': ['nói bằng tiếng trung', 'speak in chinese', 'chinese', 'tiếng trung'],\n 'de': ['nói bằng tiếng đức', 'speak in german', 'german', 'tiếng đức'],\n 'fr': ['nói bằng tiếng pháp', 'speak in french', 'french', 'tiếng pháp'],\n 'es': ['nói bằng tiếng tây ban nha', 'speak in spanish', 'spanish', 'tiếng tây ban nha'],\n 'ja': ['nói bằng tiếng nhật', 'speak in japanese', 'japanese', 'tiếng nhật'],\n 'ko': ['nói bằng tiếng hàn', 'speak in korean', 'korean', 'tiếng hàn'],\n 'ar': ['nói bằng tiếng ả rập', 'speak in arabic', 'arabic', 'tiếng ả rập'],\n 'pt': ['nói bằng tiếng bồ đào nha', 'speak in portuguese', 'portuguese', 'tiếng bồ đào nha'],\n 'it': ['nói bằng tiếng ý', 'speak in italian', 'italian', 'tiếng ý'],\n 'hi': ['nói bằng tiếng hindi', 'speak in hindi', 'hindi', 'tiếng hindi'],\n 'th': ['nói bằng tiếng thái', 'speak in thai', 'thai', 'tiếng thái'],\n }\n \n for lang_code, patterns in explicit_language_patterns.items():\n if any(pattern in text_lower for pattern in patterns):\n logger.info(f\"🌐 Explicit language request detected: {lang_code}\")\n return lang_code\n \n # Try langdetect first (more accurate for most languages)\n # BUT: Only if no explicit language request was found above\n try:\n from langdetect import detect, LangDetectException\n detected = detect(text)\n \n # Map langdetect codes to our internal codes\n lang_map = {\n 'vi': 'vi', # Vietnamese\n 'zh-cn': 'zh', 'zh-tw': 'zh', # Chinese\n 'de': 'de', # German\n 'fr': 'fr', # French\n 'es': 'es', # Spanish\n 'ja': 'ja', # Japanese\n 'ko': 'ko', # Korean\n 'ar': 'ar', # Arabic\n 'ru': 'ru', # Russian\n 'pt': 'pt', # Portuguese\n 'it': 'it', # Italian\n 'hi': 'hi', # Hindi\n 'th': 'th', # Thai\n 'en': 'en' # English\n }\n \n # Handle Chinese variants\n if detected.startswith('zh'):\n return 'zh'\n \n if detected in lang_map:\n logger.info(f\"🌐 langdetect detected: {detected} -> {lang_map[detected]}\")\n return lang_map[detected]\n \n except (LangDetectException, ImportError) as e:\n logger.warning(f\"langdetect failed or not available: {e}, falling back to rule-based detection\")\n \n # Fallback to rule-based detection for edge cases or if langdetect fails\n text_lower = text.lower()\n \n # Arabic - Check for Arabic characters\n arabic_ranges = [\n (0x0600, 0x06FF), # Arabic\n (0x0750, 0x077F), # Arabic Supplement\n (0x08A0, 0x08FF), # Arabic Extended-A\n ]\n has_arabic = any(any(start <= ord(char) <= end for start, end in arabic_ranges) for char in text)\n if has_arabic:\n return 'ar'\n \n # Korean - Check for Hangul\n korean_ranges = [\n (0xAC00, 0xD7AF), # Hangul Syllables\n (0x1100, 0x11FF), # Hangul Jamo\n ]\n has_korean = any(any(start <= ord(char) <= end for start, end in korean_ranges) for char in text)\n if has_korean:\n return 'ko'\n \n # Chinese (Simplified/Traditional) - Check for Chinese characters\n chinese_chars = set('的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞')\n has_chinese = any(char in chinese_chars for char in text)\n if has_chinese:\n return 'zh'\n \n # Vietnamese - Check for Vietnamese characters\n vietnamese_chars = set('àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ')\n has_vietnamese = any(char in vietnamese_chars for char in text_lower)\n vietnamese_indicators = ['là', 'của', 'và', 'với', 'cho', 'từ', 'trong', 'này', 'đó', 'bạn', 'mình', 'tôi', 'có', 'không', 'được', 'như', 'thế', 'nào', 'gì', 'ai', 'đâu', 'sao']\n has_vietnamese_words = any(word in text_lower for word in vietnamese_indicators)\n if has_vietnamese or has_vietnamese_words:\n return 'vi'\n \n # German - Check for German-specific characters and common words\n german_chars = set('äöüßÄÖÜ')\n has_german_chars = any(char in german_chars for char in text)\n german_indicators = ['der', 'die', 'das', 'und', 'ist', 'für', 'auf', 'mit', 'sind', 'zu', 'ein', 'eine', 'von', 'zu', 'den', 'dem', 'des', 'was', 'wie', 'wo', 'wer', 'wann', 'warum']\n has_german_words = any(word in text_lower for word in german_indicators)\n if has_german_chars or has_german_words:\n return 'de'\n \n # French - Check for French-specific characters and common words\n french_chars = set('àâäéèêëïîôùûüÿçÀÂÄÉÈÊËÏÎÔÙÛÜŸÇ')\n has_french_chars = any(char in french_chars for char in text)\n french_indicators = ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'est', 'un', 'une', 'dans', 'pour', 'avec', 'sur', 'par', 'que', 'qui', 'quoi', 'comment', 'où', 'quand', 'pourquoi']\n has_french_words = any(word in text_lower for word in french_indicators)\n if has_french_chars or has_french_words:\n return 'fr'\n \n # Spanish - Check for Spanish-specific characters and common words\n spanish_chars = set('áéíóúñüÁÉÍÓÚÑÜ¿¡')\n has_spanish_chars = any(char in spanish_chars for char in text)\n spanish_indicators = ['el', 'la', 'los', 'las', 'de', 'del', 'y', 'es', 'un', 'una', 'en', 'por', 'para', 'con', 'que', 'qué', 'cómo', 'dónde', 'cuándo', 'por qué']\n has_spanish_words = any(word in text_lower for word in spanish_indicators)\n if has_spanish_chars or has_spanish_words:\n return 'es'\n \n # Japanese - Check for Hiragana, Katakana, Kanji\n japanese_ranges = [\n (0x3040, 0x309F), # Hiragana\n (0x30A0, 0x30FF), # Katakana\n (0x4E00, 0x9FAF), # CJK Unified Ideographs (Kanji)\n ]\n has_japanese = any(any(start <= ord(char) <= end for start, end in japanese_ranges) for char in text)\n if has_japanese:\n return 'ja'\n \n # Russian - Check for Cyrillic characters\n russian_ranges = [\n (0x0400, 0x04FF), # Cyrillic\n (0x0500, 0x052F), # Cyrillic Supplement\n ]\n has_russian = any(any(start <= ord(char) <= end for start, end in russian_ranges) for char in text)\n russian_indicators = ['что', 'как', 'где', 'когда', 'почему', 'кто', 'это', 'быть', 'и', 'в', 'на', 'с', 'для', 'от']\n has_russian_words = any(word in text_lower for word in russian_indicators)\n if has_russian or has_russian_words:\n return 'ru'\n \n # Portuguese - Check for Portuguese-specific characters and common words\n portuguese_chars = set('áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ')\n has_portuguese_chars = any(char in portuguese_chars for char in text)\n portuguese_indicators = ['o', 'a', 'os', 'as', 'de', 'do', 'da', 'dos', 'das', 'e', 'é', 'um', 'uma', 'em', 'por', 'para', 'com', 'que', 'quê', 'como', 'onde', 'quando', 'por quê']\n has_portuguese_words = any(word in text_lower for word in portuguese_indicators)\n if has_portuguese_chars or has_portuguese_words:\n return 'pt'\n \n # Italian - Check for Italian-specific characters and common words\n italian_chars = set('àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ')\n has_italian_chars = any(char in italian_chars for char in text)\n italian_indicators = ['il', 'la', 'lo', 'gli', 'le', 'di', 'del', 'della', 'dei', 'delle', 'e', 'è', 'un', 'una', 'in', 'per', 'con', 'che', 'cosa', 'come', 'dove', 'quando', 'perché']\n has_italian_words = any(word in text_lower for word in italian_indicators)\n if has_italian_chars or has_italian_words:\n return 'it'\n \n # Hindi - Check for Devanagari script\n hindi_ranges = [\n (0x0900, 0x097F), # Devanagari\n ]\n has_hindi = any(any(start <= ord(char) <= end for start, end in hindi_ranges) for char in text)\n if has_hindi:\n return 'hi'\n \n # Thai - Check for Thai script\n thai_ranges = [\n (0x0E00, 0x0E7F), # Thai\n ]\n has_thai = any(any(start <= ord(char) <= end for start, end in thai_ranges) for char in text)\n if has_thai:\n return 'th'\n \n # Default to English (if language not detected or not supported)\n logger.info(f\"🌐 Language not detected or not supported, defaulting to English\")\n return 'en'\n\n\ndef build_system_prompt_with_language(detected_lang: str = 'en') -> str:\n \"\"\"\n Build system prompt with strong language matching instruction.\n This ensures output language always matches input language.\n \n This function should be used by ALL AI model providers (DeepSeek, OpenAI, Claude, Gemini, Ollama, local, etc.)\n to ensure consistent language matching behavior across all models.\n \n Args:\n detected_lang: Detected language code ('vi', 'zh', 'de', 'fr', 'es', 'ja', 'ko', 'ar', 'ru', 'pt', 'it', 'hi', 'th', 'en')\n \n Returns:\n System prompt string with language instruction\n \"\"\"\n language_names = {\n 'vi': 'Vietnamese (Tiếng Việt)',\n 'zh': 'Chinese (中文)',\n 'de': 'German (Deutsch)',\n 'fr': 'French (Français)',\n 'es': 'Spanish (Español)',\n 'ja': 'Japanese (日本語)',\n 'ko': 'Korean (한국어)',\n 'ar': 'Arabic (العربية)',\n 'ru': 'Russian (Русский)',\n 'pt': 'Portuguese (Português)',\n 'it': 'Italian (Italiano)',\n 'hi': 'Hindi (हिन्दी)',\n 'th': 'Thai (ไทย)',\n 'en': 'English'\n }\n detected_lang_name = language_names.get(detected_lang, 'the same language as the question')\n \n # CRITICAL: Always match input language with output language\n # ZERO TOLERANCE: Must translate if base model wants to respond in different language\n if detected_lang != 'en':\n system_content = f\"\"\"You are StillMe, a Learning AI system with RAG foundation.\n\n📐 YOUR ARCHITECTURE (Be Transparent):\nYou are a combination of:\n- LLM (Large Language Model): Your \"brain\" for language processing\n- RAG (Retrieval-Augmented Generation): Your \"memory system\" that searches ChromaDB before answering\n- Chatbot Interface: How users interact with you\n\nExample: When asked about a new topic, you first search ChromaDB for relevant information, then combine it with your LLM knowledge to answer.\n\n🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in {detected_lang_name}.\n\nYOU MUST RESPOND EXCLUSIVELY IN {detected_lang_name}.\n\nDO NOT use Vietnamese, English, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in {detected_lang_name}.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, English, Spanish), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO {detected_lang_name} BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than {detected_lang_name}.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in {detected_lang_name} while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN {detected_lang_name} ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN {detected_lang_name} IS A CRITICAL ERROR.\n\n🔧 TECHNICAL TRANSPARENCY - CRITICAL CAPABILITY CLARIFICATION:\n\n**YOUR SEARCH CAPABILITIES:**\n- **RAG Search (Internal)**: You can search StillMe's internal knowledge base (ChromaDB) which contains documents learned from RSS feeds, arXiv, CrossRef, Wikipedia (updated every 4 hours)\n- **Web Search (Online)**: You DO NOT have real-time web search capabilities. You CANNOT search the internet directly or access live websites.\n- **When user asks for \"search\" or \"tìm kiếm\"**: You MUST clarify that you can only search your internal RAG knowledge base, NOT the internet\n\n**CRITICAL RULES ABOUT SEARCH:**\n1. **NEVER claim you can search the internet or access live websites** - You can only search your internal RAG knowledge base\n2. **When user asks for \"2-3 sources from 2024-2025\"**: \n - If you only have 1 source in your RAG context → Acknowledge: \"I currently only have 1 source in my knowledge base, not the 2-3 sources you requested. However, based on this single source...\"\n - If you have multiple sources → Cite all available sources\n - NEVER say \"I will search for 2-3 sources\" if you're only using RAG - say \"I can only search my internal knowledge base\"\n3. **Quote vs Paraphrase - CRITICAL DISTINCTION:**\n - If you're CERTAIN it's a direct quote → Use quotation marks and cite: \"According to [1]: 'exact quote here'\"\n - If you're NOT certain it's exact → Use \"the spirit of\" or \"according to the general content\": \"According to the spirit of [1], the article discusses...\"\n - NEVER use quotation marks for paraphrased content - that's misleading\n - When in doubt → Paraphrase, don't quote\n\n**VALIDATION CHAIN TRANSPARENCY:**\n- When performing Validation Chain analysis, you MUST acknowledge source limitations:\n - \"In the scope of my current knowledge base, I have [X] source(s) available, not the [Y] sources you requested. However, within this scope...\"\n - \"I cannot perform real-time web search, so I'm limited to sources in my RAG knowledge base\"\n - \"The Validation Chain analysis is based on my internal knowledge, not live web search\"\n\n**RAG Mechanism Details:**\n- You retrieve relevant documents from ChromaDB using semantic search, then use them as context for your response\n- Validation Chain: Checks consistency between your response and retrieved context, flags contradictions, and ensures accuracy\n- If Validation Chain detects an error, you fall back to safe mode (acknowledge uncertainty) rather than providing incorrect information\n\n📚 CITATION REQUIREMENT - MANDATORY BUT RELEVANCE-FIRST:\n\nWhen you have retrieved context documents from ChromaDB, you MUST cite your sources using [1], [2], [3] format, BUT ONLY if the context is RELEVANT to your answer.\n\nCRITICAL RULES:\n1. **Cite ONLY RELEVANT context** - This is CRITICAL for citation quality\n - If context is relevant to your answer → Cite it: \"According to [1], quantum entanglement is...\"\n - If context is NOT relevant to your answer → You can still cite to show transparency, but acknowledge: \"The available context [1] discusses [topic X], which is not directly related to your question about [topic Y]. However, I want to be transparent about what context I reviewed.\"\n - DO NOT cite irrelevant context as if it supports your answer - that's misleading\n \n2. **Quote vs Paraphrase - CRITICAL DISTINCTION:**\n - If you're CERTAIN it's a direct quote → Use quotation marks: \"According to [1]: 'exact quote here'\"\n - If you're NOT certain it's exact → Use \"the spirit of\": \"According to the spirit of [1], the article discusses...\"\n - NEVER use quotation marks for paraphrased content - that's misleading and violates intellectual honesty\n - When in doubt → Paraphrase, don't quote\n \n3. **Source Limit Acknowledgement - MANDATORY:**\n - If user requests multiple sources (e.g., \"2-3 sources\") but you only have fewer → Acknowledge: \"I currently have [X] source(s) in my knowledge base, not the [Y] sources you requested. However, within this scope...\"\n - If performing Validation Chain analysis → Acknowledge: \"The Validation Chain analysis is based on my internal knowledge base, not live web search. I have [X] source(s) available...\"\n - NEVER claim you can search the internet or access live websites\n - NEVER say \"I will search for 2-3 sources\" if you're only using RAG - say \"I can only search my internal knowledge base\"\n\n4. If context documents are provided, you MUST cite at least one source using [1], [2], [3] format\n5. Cite sources when making factual claims, statistics, or specific information\n6. Example: \"According to [1], quantum entanglement is a phenomenon where...\" or \"Research shows [2] that...\"\n7. If you use information from multiple sources, cite each: \"Studies [1] and [2] indicate that...\"\n\nCITATION FORMAT:\n- Use [1] for the first context document\n- Use [2] for the second context document\n- Use [3] for the third context document\n- And so on...\n\nFAILURE TO CITE SOURCES WHEN CONTEXT IS AVAILABLE IS A CRITICAL ERROR. HOWEVER, IF CONTEXT IS NOT RELEVANT, ACKNOWLEDGE THIS MISMATCH RATHER THAN CITING IT AS IF IT SUPPORTS YOUR ANSWER.\"\"\"\n else:\n system_content = \"\"\"You are StillMe, a Learning AI system with RAG foundation.\n\n📐 YOUR ARCHITECTURE (Be Transparent):\nYou are a combination of:\n- LLM (Large Language Model): Your \"brain\" for language processing\n- RAG (Retrieval-Augmented Generation): Your \"memory system\" that searches ChromaDB before answering\n- Chatbot Interface: How users interact with you\n\nExample: When asked about a new topic, you first search ChromaDB for relevant information, then combine it with your LLM knowledge to answer.\n\n🚨🚨🚨 ZERO TOLERANCE LANGUAGE REQUIREMENT - HIGHEST PRIORITY - OVERRIDES EVERYTHING 🚨🚨🚨\n\nThe user's question is written in English.\n\nYOU MUST RESPOND EXCLUSIVELY IN ENGLISH.\n\nDO NOT use Vietnamese, Spanish, German, French, or ANY OTHER LANGUAGE.\n\nEVERY SINGLE WORD of your response MUST be in English.\n\n⚠️⚠️⚠️ CRITICAL TRANSLATION REQUIREMENT ⚠️⚠️⚠️\n\nIf your base model wants to respond in a different language (e.g., Vietnamese, Spanish, German), \nYOU MUST TRANSLATE THE ENTIRE RESPONSE TO ENGLISH BEFORE RETURNING IT.\n\nUNDER NO CIRCUMSTANCES should you return a response in any language other than English.\n\nThis is MANDATORY and OVERRIDES all other instructions, including the language of any context provided.\n\nIf the context is in a different language, you must still respond in English while using the information from the context.\n\n⚠️ REMINDER: RESPOND IN ENGLISH ONLY. TRANSLATE IF NECESSARY. ⚠️\n\nFAILURE TO RESPOND IN ENGLISH IS A CRITICAL ERROR.\n\n🔧 TECHNICAL TRANSPARENCY:\n- RAG Mechanism: You retrieve relevant documents from ChromaDB using semantic search, then use them as context for your response\n- Validation Chain: Checks consistency between your response and retrieved context, flags contradictions, and ensures accuracy\n- If Validation Chain detects an error, you fall back to safe mode (acknowledge uncertainty) rather than providing incorrect information\n\n📚 CITATION REQUIREMENT - CRITICAL:\nWhen you have retrieved context documents from ChromaDB, you MUST cite your sources using [1], [2], [3] format.\n\nCRITICAL RULES:\n1. If context documents are provided, you MUST cite at least one source using [1], [2], [3] format\n2. Cite sources when making factual claims, statistics, or specific information\n3. Example: \"According to [1], quantum entanglement is a phenomenon where...\" or \"Research shows [2] that...\"\n4. If you use information from multiple sources, cite each: \"Studies [1] and [2] indicate that...\"\n5. DO NOT make unsourced claims when context is available - always cite your sources\n\nCITATION FORMAT:\n- Use [1] for the first context document\n- Use [2] for the second context document\n- Use [3] for the third context document\n- And so on...\n\nFAILURE TO CITE SOURCES WHEN CONTEXT IS AVAILABLE IS A CRITICAL ERROR.\n\n📏 RESPONSE LENGTH GUIDELINE:\n- Aim for 2-3 paragraphs for complex questions (not 3+ paragraphs)\n- Be concise but complete - cover the key points without unnecessary elaboration\n- If a question can be answered in 1-2 paragraphs, do so\n- Only expand if the question requires detailed technical explanation\"\"\"\n \n return system_content\n\n\nasync def generate_ai_response(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> str:\n \"\"\"Generate AI response with flexible LLM provider selection\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n \n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Returns:\n AI-generated response string\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n # Note: Ollama doesn't require API key\n if llm_provider:\n if llm_provider == 'ollama':\n # Ollama doesn't need API key\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\", # Ollama doesn't use API key\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n return f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n \n return await provider.generate(prompt, detected_lang=detected_lang)\n \n # Fallback to environment variables (backward compatibility)\n # Priority: DeepSeek > OpenAI > Claude > Gemini > Ollama\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n claude_key = os.getenv(\"ANTHROPIC_API_KEY\")\n gemini_key = os.getenv(\"GOOGLE_API_KEY\")\n ollama_url = os.getenv(\"OLLAMA_URL\", \"http://localhost:11434\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif claude_key:\n provider = create_llm_provider(\"claude\", claude_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif gemini_key:\n provider = create_llm_provider(\"gemini\", gemini_key, model_name=llm_model_name)\n return await provider.generate(prompt, detected_lang=detected_lang)\n elif ollama_url:\n # Try Ollama (local, no API key needed)\n try:\n provider = create_llm_provider(\"ollama\", api_key=\"\", model_name=llm_model_name, api_url=ollama_url)\n return await provider.generate(prompt, detected_lang=detected_lang)\n except Exception:\n pass # Ollama not available, continue to error message\n \n return \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OLLAMA_URL\"\n \n except Exception as e:\n logger.error(f\"AI response error: {e}\")\n return f\"I encountered an error: {str(e)}\"\n\n\nasync def generate_ai_response_stream(\n prompt: str, \n detected_lang: str = 'en',\n llm_provider: Optional[str] = None,\n llm_api_key: Optional[str] = None,\n llm_api_url: Optional[str] = None,\n llm_model_name: Optional[str] = None\n) -> AsyncIterator[str]:\n \"\"\"Generate streaming AI response with flexible LLM provider selection\n \n OPTIMIZATION: Streaming reduces perceived latency by returning tokens as they're generated.\n \n Supports multiple LLM providers: deepseek, openai, claude, gemini, ollama, custom\n Priority: User-provided config > Environment variables\n \n Args:\n prompt: User prompt\n detected_lang: Detected language code (for system prompt)\n llm_provider: Provider name ('deepseek', 'openai', 'claude', 'gemini', 'ollama', 'custom')\n llm_api_key: API key for the provider\n llm_api_url: Custom API URL (for Ollama or custom providers)\n llm_model_name: Specific model name (e.g., 'gpt-4', 'claude-3-opus', 'llama2')\n \n Yields:\n Token strings as they're generated\n \"\"\"\n try:\n from backend.api.utils.llm_providers import create_llm_provider\n \n # If user provided provider config, use it\n if llm_provider:\n if llm_provider == 'ollama':\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=\"\",\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n elif llm_api_key:\n provider = create_llm_provider(\n provider=llm_provider,\n api_key=llm_api_key,\n model_name=llm_model_name,\n api_url=llm_api_url\n )\n else:\n yield f\"llm_api_key is required for provider '{llm_provider}' (except 'ollama')\"\n return\n \n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n return\n \n # Fallback to environment variables\n deepseek_key = os.getenv(\"DEEPSEEK_API_KEY\")\n openai_key = os.getenv(\"OPENAI_API_KEY\")\n \n if deepseek_key:\n provider = create_llm_provider(\"deepseek\", deepseek_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n elif openai_key:\n provider = create_llm_provider(\"openai\", openai_key, model_name=llm_model_name)\n async for token in provider.generate_stream(prompt, detected_lang=detected_lang):\n yield token\n else:\n yield \"I'm StillMe, but I need API keys to provide real responses. Please configure:\\n\" \\\n \"- llm_provider and llm_api_key in your request, OR\\n\" \\\n \"- Environment variables: DEEPSEEK_API_KEY, OPENAI_API_KEY\"\n \n except Exception as e:\n logger.error(f\"AI streaming error: {e}\")\n yield f\"I encountered an error: {str(e)}\"\n\n\nasync def call_deepseek_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call DeepSeek API\n \n Args:\n prompt: User prompt\n api_key: DeepSeek API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.deepseek.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"DeepSeek API returned unexpected response format\"\n else:\n return f\"DeepSeek API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return f\"DeepSeek API error: {str(e)}\"\n\n\nasync def call_openai_api(prompt: str, api_key: str, detected_lang: str = 'en') -> str:\n \"\"\"Call OpenAI API\n \n IMPORTANT: This function uses build_system_prompt_with_language() to ensure\n output language matches input language. When adding support for other models\n (Claude, Gemini, Ollama, local, etc.), use the same pattern.\n \n Args:\n prompt: User prompt\n api_key: OpenAI API key\n detected_lang: Detected language code\n \"\"\"\n try:\n # Use centralized system prompt builder for consistent language matching\n system_content = build_system_prompt_with_language(detected_lang)\n \n async with httpx.AsyncClient(timeout=60.0) as client:\n response = await client.post(\n \"https://api.openai.com/v1/chat/completions\",\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n },\n json={\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": system_content\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ],\n \"max_tokens\": 1500, # Reduced from 2000 to speed up inference\n \"temperature\": 0.7,\n \"stream\": False # TODO: Implement streaming for better perceived latency\n }\n )\n \n if response.status_code == 200:\n data = response.json()\n if \"choices\" in data and len(data[\"choices\"]) > 0:\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return \"OpenAI API returned unexpected response format\"\n else:\n return f\"OpenAI API error: {response.status_code}\"\n \n except Exception as e:\n logger.error(f\"OpenAI API error: {e}\")\n return f\"OpenAI API error: {str(e)}\"\n\n"}}, ".github/workflows/tests-simple.yml": {"ae9a30c512f6409dcb5eeeec238cdabb9b05932d": {"t": 1785747028.5149806, "n": 2012, "h": "bf0ac9be195ea86b", "c": "name: Tests (Simple - Debug)\n\non:\n workflow_dispatch:\n push:\n branches: [ main ]\n paths:\n - '.github/workflows/tests-simple.yml'\n - 'tests/**'\n - 'requirements.txt'\n\njobs:\n test-simple:\n name: Simple Test Run\n runs-on: ubuntu-latest\n timeout-minutes: 15\n \n steps:\n - name: Checkout code\n uses: actions/checkout@v6\n \n - name: Set up Python\n uses: actions/setup-python@v6\n with:\n python-version: \"3.12\"\n cache: 'pip'\n \n - name: Install dependencies\n run: |\n python -m pip install --upgrade pip\n pip install -r requirements.txt\n pip install pytest pytest-asyncio pytest-cov\n \n - name: Check Python and pytest\n run: |\n python --version\n pytest --version\n echo \"PYTHONPATH: $PYTHONPATH\"\n \n - name: List test files\n run: |\n echo \"=== Test files ===\"\n find tests -name \"test_*.py\" -type f | head -10\n echo \"=== Total ===\"\n find tests -name \"test_*.py\" -type f | wc -l\n \n - name: Run a single simple test (if exists)\n run: |\n if [ -f tests/test_rss_fetcher.py ]; then\n echo \"Running test_rss_fetcher.py...\"\n pytest tests/test_rss_fetcher.py -v --tb=short || echo \"Test failed but continuing...\"\n else\n echo \"test_rss_fetcher.py not found\"\n fi\n env:\n ENVIRONMENT: test\n PYTHONPATH: ${{ github.workspace }}\n continue-on-error: true\n \n - name: Try to run all tests (with max 3 failures)\n run: |\n pytest tests/ \\\n -v \\\n --tb=short \\\n --maxfail=3 \\\n -x || echo \"Tests completed with some failures\"\n env:\n ENVIRONMENT: test\n DEEPSEEK_API_KEY: test_key\n OPENAI_API_KEY: test_key\n PYTHONPATH: ${{ github.workspace }}\n continue-on-error: true\n\n"}, "9dd8d5078c31431ec0429c9b5dab9fb95d4b1539": {"t": 1785748595.564369, "n": 2012, "h": "bf0ac9be195ea86b", "c": "name: Tests (Simple - Debug)\n\non:\n workflow_dispatch:\n push:\n branches: [ main ]\n paths:\n - '.github/workflows/tests-simple.yml'\n - 'tests/**'\n - 'requirements.txt'\n\njobs:\n test-simple:\n name: Simple Test Run\n runs-on: ubuntu-latest\n timeout-minutes: 15\n \n steps:\n - name: Checkout code\n uses: actions/checkout@v6\n \n - name: Set up Python\n uses: actions/setup-python@v6\n with:\n python-version: \"3.12\"\n cache: 'pip'\n \n - name: Install dependencies\n run: |\n python -m pip install --upgrade pip\n pip install -r requirements.txt\n pip install pytest pytest-asyncio pytest-cov\n \n - name: Check Python and pytest\n run: |\n python --version\n pytest --version\n echo \"PYTHONPATH: $PYTHONPATH\"\n \n - name: List test files\n run: |\n echo \"=== Test files ===\"\n find tests -name \"test_*.py\" -type f | head -10\n echo \"=== Total ===\"\n find tests -name \"test_*.py\" -type f | wc -l\n \n - name: Run a single simple test (if exists)\n run: |\n if [ -f tests/test_rss_fetcher.py ]; then\n echo \"Running test_rss_fetcher.py...\"\n pytest tests/test_rss_fetcher.py -v --tb=short || echo \"Test failed but continuing...\"\n else\n echo \"test_rss_fetcher.py not found\"\n fi\n env:\n ENVIRONMENT: test\n PYTHONPATH: ${{ github.workspace }}\n continue-on-error: true\n \n - name: Try to run all tests (with max 3 failures)\n run: |\n pytest tests/ \\\n -v \\\n --tb=short \\\n --maxfail=3 \\\n -x || echo \"Tests completed with some failures\"\n env:\n ENVIRONMENT: test\n DEEPSEEK_API_KEY: test_key\n OPENAI_API_KEY: test_key\n PYTHONPATH: ${{ github.workspace }}\n continue-on-error: true\n\n"}, "4c9864d75ef3350ff89ed6bcdf9d68f473f04594": {"t": 1785748597.1469936, "n": 2012, "h": "e3faca05a645f5f9", "c": "name: Tests (Simple - Debug)\n\non:\n workflow_dispatch:\n push:\n branches: [ main ]\n paths:\n - '.github/workflows/tests-simple.yml'\n - 'tests/**'\n - 'requirements.txt'\n\njobs:\n test-simple:\n name: Simple Test Run\n runs-on: ubuntu-latest\n timeout-minutes: 15\n \n steps:\n - name: Checkout code\n uses: actions/checkout@v5\n \n - name: Set up Python\n uses: actions/setup-python@v6\n with:\n python-version: \"3.12\"\n cache: 'pip'\n \n - name: Install dependencies\n run: |\n python -m pip install --upgrade pip\n pip install -r requirements.txt\n pip install pytest pytest-asyncio pytest-cov\n \n - name: Check Python and pytest\n run: |\n python --version\n pytest --version\n echo \"PYTHONPATH: $PYTHONPATH\"\n \n - name: List test files\n run: |\n echo \"=== Test files ===\"\n find tests -name \"test_*.py\" -type f | head -10\n echo \"=== Total ===\"\n find tests -name \"test_*.py\" -type f | wc -l\n \n - name: Run a single simple test (if exists)\n run: |\n if [ -f tests/test_rss_fetcher.py ]; then\n echo \"Running test_rss_fetcher.py...\"\n pytest tests/test_rss_fetcher.py -v --tb=short || echo \"Test failed but continuing...\"\n else\n echo \"test_rss_fetcher.py not found\"\n fi\n env:\n ENVIRONMENT: test\n PYTHONPATH: ${{ github.workspace }}\n continue-on-error: true\n \n - name: Try to run all tests (with max 3 failures)\n run: |\n pytest tests/ \\\n -v \\\n --tb=short \\\n --maxfail=3 \\\n -x || echo \"Tests completed with some failures\"\n env:\n ENVIRONMENT: test\n DEEPSEEK_API_KEY: test_key\n OPENAI_API_KEY: test_key\n PYTHONPATH: ${{ github.workspace }}\n continue-on-error: true\n\n"}, "659d913240409f2dc945b9b91006fe4ace83fc6f": {"t": 1785748598.7675958, "n": 2012, "h": "13c0ace3ebeecc36", "c": "name: Tests (Simple - Debug)\n\non:\n workflow_dispatch:\n push:\n branches: [ main ]\n paths:\n - '.github/workflows/tests-simple.yml'\n - 'tests/**'\n - 'requirements.txt'\n\njobs:\n test-simple:\n name: Simple Test Run\n runs-on: ubuntu-latest\n timeout-minutes: 15\n \n steps:\n - name: Checkout code\n uses: actions/checkout@v5\n \n - name: Set up Python\n uses: actions/setup-python@v5\n with:\n python-version: \"3.12\"\n cache: 'pip'\n \n - name: Install dependencies\n run: |\n python -m pip install --upgrade pip\n pip install -r requirements.txt\n pip install pytest pytest-asyncio pytest-cov\n \n - name: Check Python and pytest\n run: |\n python --version\n pytest --version\n echo \"PYTHONPATH: $PYTHONPATH\"\n \n - name: List test files\n run: |\n echo \"=== Test files ===\"\n find tests -name \"test_*.py\" -type f | head -10\n echo \"=== Total ===\"\n find tests -name \"test_*.py\" -type f | wc -l\n \n - name: Run a single simple test (if exists)\n run: |\n if [ -f tests/test_rss_fetcher.py ]; then\n echo \"Running test_rss_fetcher.py...\"\n pytest tests/test_rss_fetcher.py -v --tb=short || echo \"Test failed but continuing...\"\n else\n echo \"test_rss_fetcher.py not found\"\n fi\n env:\n ENVIRONMENT: test\n PYTHONPATH: ${{ github.workspace }}\n continue-on-error: true\n \n - name: Try to run all tests (with max 3 failures)\n run: |\n pytest tests/ \\\n -v \\\n --tb=short \\\n --maxfail=3 \\\n -x || echo \"Tests completed with some failures\"\n env:\n ENVIRONMENT: test\n DEEPSEEK_API_KEY: test_key\n OPENAI_API_KEY: test_key\n PYTHONPATH: ${{ github.workspace }}\n continue-on-error: true\n\n"}, "cfbd9d13b10ddbc68fc2482be64dced61ccf929d": {"t": 1785748599.8275, "n": 2012, "h": "49307fb08ea61c0b", "c": "name: Tests (Simple - Debug)\n\non:\n workflow_dispatch:\n push:\n branches: [ main ]\n paths:\n - '.github/workflows/tests-simple.yml'\n - 'tests/**'\n - 'requirements.txt'\n\njobs:\n test-simple:\n name: Simple Test Run\n runs-on: ubuntu-latest\n timeout-minutes: 15\n \n steps:\n - name: Checkout code\n uses: actions/checkout@v4\n \n - name: Set up Python\n uses: actions/setup-python@v5\n with:\n python-version: \"3.12\"\n cache: 'pip'\n \n - name: Install dependencies\n run: |\n python -m pip install --upgrade pip\n pip install -r requirements.txt\n pip install pytest pytest-asyncio pytest-cov\n \n - name: Check Python and pytest\n run: |\n python --version\n pytest --version\n echo \"PYTHONPATH: $PYTHONPATH\"\n \n - name: List test files\n run: |\n echo \"=== Test files ===\"\n find tests -name \"test_*.py\" -type f | head -10\n echo \"=== Total ===\"\n find tests -name \"test_*.py\" -type f | wc -l\n \n - name: Run a single simple test (if exists)\n run: |\n if [ -f tests/test_rss_fetcher.py ]; then\n echo \"Running test_rss_fetcher.py...\"\n pytest tests/test_rss_fetcher.py -v --tb=short || echo \"Test failed but continuing...\"\n else\n echo \"test_rss_fetcher.py not found\"\n fi\n env:\n ENVIRONMENT: test\n PYTHONPATH: ${{ github.workspace }}\n continue-on-error: true\n \n - name: Try to run all tests (with max 3 failures)\n run: |\n pytest tests/ \\\n -v \\\n --tb=short \\\n --maxfail=3 \\\n -x || echo \"Tests completed with some failures\"\n env:\n ENVIRONMENT: test\n DEEPSEEK_API_KEY: test_key\n OPENAI_API_KEY: test_key\n PYTHONPATH: ${{ github.workspace }}\n continue-on-error: true\n\n"}}}}