From 99d7bd8e3d3a12a80977c18b1e26e24bcd5181ab Mon Sep 17 00:00:00 2001 From: Nicolas Fernandez Date: Wed, 18 Mar 2026 11:12:23 -0300 Subject: [PATCH] Implement language detection and response handling in the agent; update README and requirements --- README.md | 5 + agents/cogsolframeworkagent/agent.py | 3 +- .../prompts/cogsolframeworkagent.md | 4 + agents/tools.py | 157 ++++++++++++++++++ requirements.txt | 2 + 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 requirements.txt diff --git a/README.md b/README.md index 95a8be9..6c70a20 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ ## Setup - Install CogSol Framework from: https://github.com/Pyxis-Cognitive-Solutions/cogsol-framework +- Install local dependencies: `pip install -r requirements.txt` - Configure environment variables in `.env` (see `.env.example`). - Migrate data app first: `python manage.py migrate data`. - Migrate agents app next: `python manage.py migrate`. @@ -24,6 +25,10 @@ ## Running the Agent - Start chat with the agent: `python manage.py chat --agent CogsolFrameworkAgent`. +## Language Behavior +- The assistant detects and pins response language from the first user message in each chat. +- Responses stay in that language unless the user explicitly asks to switch language. + ## MCP Server - Install MCP support: `python -m pip install mcp`. - Run the server over stdio: `python mcp_server.py`. diff --git a/agents/cogsolframeworkagent/agent.py b/agents/cogsolframeworkagent/agent.py index 6daa30a..de3f48b 100644 --- a/agents/cogsolframeworkagent/agent.py +++ b/agents/cogsolframeworkagent/agent.py @@ -1,12 +1,13 @@ from cogsol.agents import BaseAgent, genconfigs from cogsol.prompts import Prompts from ..searches import CogsolFrameworkDocsSearch, CogsolAPIsDocsSearch -from ..tools import CogSolScaffoldGenerator +from ..tools import CogSolScaffoldGenerator, DetectUserLanguageTool class CogsolFrameworkAgent(BaseAgent): system_prompt = Prompts.load("cogsolframeworkagent.md") generation_config = genconfigs.QA() + pretools = [DetectUserLanguageTool()] tools = [CogsolFrameworkDocsSearch(), CogsolAPIsDocsSearch(), CogSolScaffoldGenerator()] max_responses = 20 max_msg_length = 2048 diff --git a/agents/cogsolframeworkagent/prompts/cogsolframeworkagent.md b/agents/cogsolframeworkagent/prompts/cogsolframeworkagent.md index 5c71350..e368759 100644 --- a/agents/cogsolframeworkagent/prompts/cogsolframeworkagent.md +++ b/agents/cogsolframeworkagent/prompts/cogsolframeworkagent.md @@ -31,6 +31,10 @@ When developers ask questions: ## Response Guidelines +- Respect language context injected at runtime: + - If `Preferred response language` is present, answer in that language. + - Keep the same language across the session unless the user explicitly asks to switch language. + - If language context is missing, default to English. - Provide code examples with correct imports and file paths - Reference the appropriate files (e.g., `agents/tools.py`, `data/retrievals.py`) - Explain configuration options and their effects diff --git a/agents/tools.py b/agents/tools.py index 89215d7..b2ac23a 100644 --- a/agents/tools.py +++ b/agents/tools.py @@ -1,5 +1,162 @@ from cogsol.tools import BaseTool, tool_params + +class DetectUserLanguageTool(BaseTool): + """Detect and enforce the response language from chat history.""" + + name = "detect_user_language" + description = ( + "Detect the user's language from chat messages and inject language instructions " + "into prompt context before the assistant generates a response." + ) + + def run(self, chat=None, data=None, secrets=None, log=None): + if data is None: + data = {} + + prompt_params = data.setdefault("prompt_params", {}) + context = prompt_params.setdefault("context", {}) + + first_user_message, last_user_message = self._get_user_messages(chat) + + first_lang_name, first_lang_code, first_reliable = self._detect_language( + first_user_message + ) + last_lang_name, last_lang_code, last_reliable = self._detect_language( + last_user_message + ) + + if first_reliable: + selected_name, selected_code = first_lang_name, first_lang_code + policy_source = "first_user_message" + elif last_reliable: + selected_name, selected_code = last_lang_name, last_lang_code + policy_source = "last_user_message_fallback" + else: + selected_name, selected_code = "English", "en" + policy_source = "default_english" + + no_info_source = self._get_no_info_message(chat) + translated_no_info = self._translate_no_info_message( + no_info_source=no_info_source, + target_code=selected_code, + ) + + context["Preferred response language"] = selected_name + context["Response language policy"] = ( + "Always respond in the language of the first user message unless the user explicitly asks to switch language." + ) + context["Message of not having information"] = translated_no_info + + if log: + log( + "DetectUserLanguageTool => selected=" + f"{selected_name} ({selected_code}), source={policy_source}, " + f"first_reliable={first_reliable}, last_reliable={last_reliable}" + ) + + return { + "selected_language": selected_name, + "selected_language_code": selected_code, + "policy_source": policy_source, + "first_message_reliable": first_reliable, + "last_message_reliable": last_reliable, + } + + def _get_user_messages(self, chat): + """Return first and last user messages from chat history.""" + empty = "" + if chat is None or not hasattr(chat, "messages"): + return empty, empty + + try: + user_messages = chat.messages.filter(role="user").order_by("msg_num") + + first_obj = ( + user_messages.first() if hasattr(user_messages, "first") else None + ) + last_obj = user_messages.last() if hasattr(user_messages, "last") else None + + first_text = getattr(first_obj, "content", "") if first_obj else "" + last_text = getattr(last_obj, "content", "") if last_obj else "" + return first_text or "", last_text or "" + except Exception: + return empty, empty + + def _get_no_info_message(self, chat): + """Get the current no-information message from assistant config.""" + default_message = "I don't have information on that topic." + + try: + assistant = getattr(chat, "assistant", None) + message = getattr(assistant, "not_info_message", None) + return message or default_message + except Exception: + return default_message + + def _detect_language(self, text): + """Detect language from text with pycld2, returning safe defaults.""" + if not text or not text.strip(): + return "English", "en", False + + try: + import pycld2 as cld2 + + is_reliable, _text_bytes_found, details = cld2.detect(text) + if not details: + return "English", "en", False + + lang_name = details[0][0] or "English" + lang_code = self._normalize_lang_code(details[0][1]) + return lang_name, lang_code, bool(is_reliable) + except Exception: + return "English", "en", False + + def _normalize_lang_code(self, code): + """Normalize detected language codes into translator-safe two-letter codes.""" + if not code: + return "en" + + normalized = str(code).strip().lower().replace("_", "-") + + aliases = { + "zh-hans": "zh", + "zh-hant": "zh", + "pt-br": "pt", + "pt-pt": "pt", + } + + if normalized in aliases: + return aliases[normalized] + + if "-" in normalized: + normalized = normalized.split("-", 1)[0] + + return normalized[:2] if normalized else "en" + + def _translate_no_info_message(self, no_info_source, target_code): + """Translate no-information message to target language with safe fallback.""" + if not no_info_source: + return "I don't have information on that topic." + + if not target_code or target_code == "en": + return no_info_source + + source_lang_name, source_lang_code, source_reliable = self._detect_language( + no_info_source + ) + source_code = source_lang_code if source_reliable else "en" + + try: + from translate import Translator + + translator = Translator(to_lang=target_code, from_lang=source_code) + translated = translator.translate(no_info_source) + return translated or no_info_source + except Exception: + return no_info_source + + class CogSolScaffoldGenerator(BaseTool): """Generate boilerplate code for CogSol Framework components.""" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a8dc03e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pycld2>=0.42 +translate>=3.6.1 \ No newline at end of file