Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ INSERT INTO llm_models (platform_id, model_key, model_name) VALUES
-- Azure models
((SELECT id FROM llm_platforms WHERE platform_key = 'azure'), 'gpt-4o-mini', 'GPT-4o-mini'),
((SELECT id FROM llm_platforms WHERE platform_key = 'azure'), 'gpt-4o', 'GPT-4o'),
((SELECT id FROM llm_platforms WHERE platform_key = 'azure'), 'gpt-4.1', 'GPT-4.1'),
-- AWS models
((SELECT id FROM llm_platforms WHERE platform_key = 'aws'), 'anthropic-claude-3.5-sonnet', 'Anthropic Claude 3.5 Sonnet'),
((SELECT id FROM llm_platforms WHERE platform_key = 'aws'), 'anthropic-claude-3.7-sonnet', 'Anthropic Claude 3.7 Sonnet');
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The **BYK-RAG Module** is part of the Burokratt ecosystem, designed to provide *
- Models searchable via dropdown with cache-enabled indicators.

- **Enhanced Security with RSA Encryption**
- LLM credentials encrypted with RSA-2048 asymmetric encryption before storage.
- LLM credentials encrypted with RSA-2048 asymmetric encryption before storage.
- GUI encrypts using public key; CronManager decrypts with private key.
- Additional security layer beyond HashiCorp Vault's encryption.

Expand Down
166 changes: 108 additions & 58 deletions src/llm_orchestration_service.py

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/llm_orchestrator_config/config/llm_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ llm:
temperature: 0.5
deployment_name: "gpt-4o-deployment"

gpt-4.1:
model_type: "chat"
max_tokens: 13107
temperature: 0.6
deployment_name: "gpt-4.1"


# AWS Bedrock Configuration
aws_bedrock:
cache: true # Keep caching enabled (DSPY default)
Expand Down
60 changes: 46 additions & 14 deletions src/llm_orchestrator_config/llm_ochestrator_constants.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
OUT_OF_SCOPE_MESSAGE = (
"I apologize, but I’m unable to provide a complete response because the available "
"context does not sufficiently cover your request. Please try rephrasing or providing more details."
)

TECHNICAL_ISSUE_MESSAGE = (
"Technical issue with response generation\n"
"I apologize, but I’m currently unable to generate a response due to a temporary technical issue. "
"Please try again in a moment."
)
# Multilingual message dictionaries
OUT_OF_SCOPE_MESSAGES = {
"et": "Vabandust, kuid mul pole piisavalt konteksti, et teie küsimusele vastata. Palun püüdke ümber sõnastada või lisage rohkem üksikasju.",
"ru": "Извините, но у меня недостаточно контекста для ответа на ваш вопрос. Пожалуйста, попробуйте переформулировать или предоставить больше деталей.",
"en": "I apologize, but I'm unable to provide a complete response because the available context does not sufficiently cover your request. Please try rephrasing or providing more details.",
}

TECHNICAL_ISSUE_MESSAGES = {
"et": "Tehniline probleem vastuse genereerimisel\nVabandust, kuid ma ei saa praegu vastust genereerida ajutise tehnilise probleemi tõttu. Palun proovige mõne hetke pärast uuesti.",
"ru": "Техническая проблема при генерации ответа\nИзвините, в настоящее время я не могу сгенерировать ответ из-за временной технической проблемы. Пожалуйста, попробуйте еще раз через мгновение.",
"en": "Technical issue with response generation\nI apologize, but I'm currently unable to generate a response due to a temporary technical issue. Please try again in a moment.",
}

INPUT_GUARDRAIL_VIOLATION_MESSAGES = {
"et": "Vabandust, kuid ma ei saa selle taotlusega aidata, kuna see rikub meie kasutustingimusi.",
"ru": "Извините, но я не могу помочь с этим запросом, так как он нарушает нашу политику использования.",
"en": "I apologize, but I'm unable to assist with that request as it violates our usage policies.",
}

OUTPUT_GUARDRAIL_VIOLATION_MESSAGES = {
"et": "Vabandust, kuid ma ei saa vastust anda, kuna see võib rikkuda meie kasutustingimusi.",
"ru": "Извините, но я не могу предоставить ответ, так как он может нарушить нашу политику использования.",
"en": "I apologize, but I'm unable to provide a response as it may violate our usage policies.",
}

# Legacy constants for backward compatibility (English defaults)
OUT_OF_SCOPE_MESSAGE = OUT_OF_SCOPE_MESSAGES["en"]
TECHNICAL_ISSUE_MESSAGE = TECHNICAL_ISSUE_MESSAGES["en"]
INPUT_GUARDRAIL_VIOLATION_MESSAGE = INPUT_GUARDRAIL_VIOLATION_MESSAGES["en"]
OUTPUT_GUARDRAIL_VIOLATION_MESSAGE = OUTPUT_GUARDRAIL_VIOLATION_MESSAGES["en"]

UNKNOWN_SOURCE = "Unknown source"

INPUT_GUARDRAIL_VIOLATION_MESSAGE = "I apologize, but I'm unable to assist with that request as it violates our usage policies."

OUTPUT_GUARDRAIL_VIOLATION_MESSAGE = "I apologize, but I'm unable to provide a response as it may violate our usage policies."

GUARDRAILS_BLOCKED_PHRASES = [
"i'm sorry, i can't respond to that",
"i cannot respond to that",
Expand Down Expand Up @@ -88,6 +104,22 @@

VALIDATION_GENERIC_ERROR = "I apologize, but I couldn't process your request. Please check your input and try again."


# Helper function to get localized messages
def get_localized_message(message_dict: dict, language_code: str = "en") -> str:
"""
Get message in the specified language, fallback to English.

Args:
message_dict: Dictionary with language codes as keys
language_code: Language code ('et', 'ru', 'en')

Returns:
Localized message string
"""
return message_dict.get(language_code, message_dict.get("en", ""))


# Service endpoints
RAG_SEARCH_RESQL = "http://resql:8082/rag-search"
RAG_SEARCH_RUUTER_PUBLIC = "http://ruuter-public:8086/rag-search"
Expand Down
13 changes: 11 additions & 2 deletions src/prompt_refine_manager/prompt_refiner.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ class ConversationHistory(BaseModel):
class PromptRefiner(dspy.Signature):
"""Produce N distinct, concise rewrites of the user's question using chat history.

CRITICAL LANGUAGE RULE:
- The rewrites MUST be in the SAME language as the input question
- Estonian question → Estonian rewrites
- Russian question → Russian rewrites
- English question → English rewrites
- Preserve the natural language of the original question

Constraints:
- Preserve the original intent; don't inject unsupported constraints.
- Resolve pronouns with context when safe; avoid changing semantics.
Expand All @@ -36,11 +43,13 @@ class PromptRefiner(dspy.Signature):
"""

history: str = dspy.InputField(desc="Recent conversation history (turns).")
question: str = dspy.InputField(desc="The user's latest question to refine.")
question: str = dspy.InputField(
desc="The user's latest question to refine. Preserve its language in rewrites."
)
n: int = dspy.InputField(desc="Number of rewrites to produce (N).")

rewrites: list[str] = dspy.OutputField(
desc="Exactly N refined variations of the question, each a single sentence."
desc="Exactly N refined variations of the question in THE SAME LANGUAGE as input, each a single sentence."
)


Expand Down
17 changes: 15 additions & 2 deletions src/response_generator/response_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,27 @@
class ResponseGenerator(dspy.Signature):
"""Produce a grounded answer from the provided context ONLY.

CRITICAL LANGUAGE RULE:
- The answer MUST be in the SAME language as the input question
- Estonian question → Estonian answer
- Russian question → Russian answer
- English question → English answer
- Maintain the natural language flow and grammar of the detected language

Rules:
- Use ONLY the provided context blocks; do not invent facts.
- If the context is insufficient, set questionOutOfLLMScope=true and say so briefly.
- Do not include citations in the 'answer' field.
"""

question: str = dspy.InputField()
question: str = dspy.InputField(
desc="User's question. Answer in the SAME language as this question."
)
context_blocks: List[str] = dspy.InputField()
citations: List[str] = dspy.InputField()
answer: str = dspy.OutputField(desc="Human-friendly answer without citations")
answer: str = dspy.OutputField(
desc="Human-friendly answer in THE SAME LANGUAGE as the question, without citations"
)
questionOutOfLLMScope: bool = dspy.OutputField(
desc="True if context is insufficient to answer"
)
Expand All @@ -40,6 +51,8 @@ class ResponseGenerator(dspy.Signature):
class ScopeChecker(dspy.Signature):
"""Quick check if question can be answered from context.

LANGUAGE NOTE: This is an internal check, language doesn't matter for scope determination.

Rules:
- Return True ONLY if context is completely insufficient
- Return False if context has ANY relevant information
Expand Down
116 changes: 116 additions & 0 deletions src/utils/language_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Language detection utility for multilingual support.

Detects Estonian, Russian, and English based on character patterns and common words.
"""

import re
from typing import Literal
from loguru import logger

LanguageCode = Literal["et", "ru", "en"]


def detect_language(text: str) -> LanguageCode:
"""
Detect language from input text.

Detection Strategy:
1. Check for Cyrillic characters (Russian)
2. Check for Estonian-specific characters
3. Check for Estonian common words
4. Default to English

Args:
text: Input text to analyze

Returns:
Language code: 'et' (Estonian), 'ru' (Russian), 'en' (English)

Examples:
>>> detect_language("Mis on sünnitoetus?")
'et'
>>> detect_language("Что такое пособие?")
'ru'
>>> detect_language("What is the benefit?")
'en'
"""
if not text or not text.strip():
logger.warning(
"Empty text provided for language detection, defaulting to English"
)
return "en"

text_sample = text.strip()[:500] # Use first 500 chars for detection

# Check for Cyrillic characters (Russian) - use percentage-based detection
cyrillic_count = len(re.findall(r"[а-яА-ЯёЁ]", text_sample))
total_alpha = len(re.findall(r"[a-zA-Zа-яА-ЯёЁõäöüšžÕÄÖÜŠŽ]", text_sample))

if (
total_alpha > 0 and cyrillic_count / total_alpha > 0.25
): # 25% Cyrillic threshold
logger.debug(
f"Detected Russian (Cyrillic: {cyrillic_count}/{total_alpha} = {cyrillic_count / total_alpha:.1%})"
)
return "ru"

# Check for Estonian-specific characters (õ, ä, ö, ü, š, ž)
estonian_chars = re.findall(r"[õäöüšž]", text_sample, re.IGNORECASE)
if len(estonian_chars) > 0:
logger.debug(f"Detected Estonian (special chars: {len(estonian_chars)})")
return "et"

# Check for Estonian common words - use distinctive markers to avoid English false positives
estonian_markers = [
"kuidas",
"miks",
"kus",
"millal",
"kes",
"võib",
"olen",
"oled",
"see",
"seda",
"jah",
"või",
"ning",
"siis",
"veel",
"aga",
"kuid",
"nii",
"nagu",
"oli",
"mis",
]

# Tokenize and check for Estonian markers
words = re.findall(r"\b\w+\b", text_sample.lower())
estonian_word_count = sum(1 for word in words if word in estonian_markers)

# Scale threshold based on text length for better accuracy
threshold = 1 if len(words) < 10 else 2
if estonian_word_count >= threshold:
logger.debug(
f"Detected Estonian (marker words: {estonian_word_count}/{len(words)}, threshold: {threshold})"
)
return "et"

# Default to English
logger.debug("Detected English (default)")
return "en"


def get_language_name(language_code: LanguageCode) -> str:
"""
Get human-readable language name from code.

Args:
language_code: ISO 639-1 language code

Returns:
Full language name
"""
language_names = {"et": "Estonian", "ru": "Russian", "en": "English"}
return language_names.get(language_code, "Unknown")
Loading