✨(tools) add basic translate tool
This commit is contained in:
@@ -13,6 +13,7 @@ and this project adheres to
|
||||
- ✨(waffle) hide the waffle if not fr theme
|
||||
- ✨(front) allow pasting an attachment from clipboard
|
||||
- ✨(array) temporarily adjust array
|
||||
- ✨(tools) add basic translate tool
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Build the translation agent."""
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from .base import BaseAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass(init=False)
|
||||
class TranslationAgent(BaseAgent):
|
||||
"""Create a Pydantic AI translation Agent instance with the configured settings"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the agent with the configured model."""
|
||||
super().__init__(
|
||||
model_hrid=settings.LLM_SUMMARIZATION_MODEL_HRID,
|
||||
output_type=str,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -142,6 +142,7 @@ from chat.mcp_servers import get_mcp_servers
|
||||
from chat.tools.document_generic_search_rag import add_document_rag_search_tool_from_setting
|
||||
from chat.tools.document_search_rag import add_document_rag_search_tool
|
||||
from chat.tools.document_summarize import document_summarize
|
||||
from chat.tools.document_translate import document_translate
|
||||
from chat.vercel_ai_sdk.core import events_v4, events_v5
|
||||
from chat.vercel_ai_sdk.encoder import CURRENT_EVENT_ENCODER_VERSION, EventEncoder
|
||||
|
||||
@@ -667,6 +668,16 @@ class AIAgentService: # pylint: disable=too-many-instance-attributes
|
||||
"You may add a follow-up question after the summary if needed."
|
||||
)
|
||||
|
||||
@self.conversation_agent.instructions
|
||||
def translation_system_prompt() -> str:
|
||||
return (
|
||||
"When you receive a result from the translation tool, you MUST return it "
|
||||
"directly to the user without any modification, paraphrasing, or additional "
|
||||
"summarization. "
|
||||
"The tool already produces complete translations that should be presented "
|
||||
"verbatim."
|
||||
)
|
||||
|
||||
# Inform the model (system-level) that documents are attached and available
|
||||
@self.conversation_agent.instructions
|
||||
def attached_documents_note() -> str:
|
||||
@@ -682,6 +693,12 @@ class AIAgentService: # pylint: disable=too-many-instance-attributes
|
||||
"""Wrap the document_summarize tool to provide context and add the tool."""
|
||||
return await document_summarize(ctx, *args, **kwargs)
|
||||
|
||||
@self.conversation_agent.tool(name="translate", retries=2)
|
||||
@functools.wraps(document_translate)
|
||||
async def translate(ctx: RunContext, *args, **kwargs) -> ToolReturn:
|
||||
"""Wrap the document_translate tool to provide context and add the tool."""
|
||||
return await document_translate(ctx, *args, **kwargs)
|
||||
|
||||
async def _handle_input_documents(
|
||||
self,
|
||||
input_documents: List[BinaryContent | DocumentUrl],
|
||||
|
||||
@@ -15,6 +15,7 @@ import core.urls
|
||||
import chat.views
|
||||
import conversations.urls
|
||||
from chat.agents.summarize import SummarizationAgent
|
||||
from chat.agents.translate import TranslationAgent
|
||||
from chat.clients.pydantic_ai import AIAgentService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -91,6 +92,34 @@ def mock_summarization_agent_fixture():
|
||||
yield _mock_agent
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_translation_agent")
|
||||
def mock_translation_agent_fixture():
|
||||
"""Fixture to mock TranslationAgent with a custom model."""
|
||||
|
||||
@contextmanager
|
||||
def _mock_agent(model):
|
||||
"""Context manager to mock TranslationAgent with a custom model."""
|
||||
with ExitStack() as stack:
|
||||
|
||||
class TranslationAgentMock(TranslationAgent):
|
||||
"""Mocked TranslationAgent to override the model."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
logger.info("Overriding TranslationAgent model with %s", model)
|
||||
self._model = model # pylint: disable=protected-access
|
||||
|
||||
stack.enter_context(
|
||||
patch("chat.agents.translate.TranslationAgent", new=TranslationAgentMock)
|
||||
)
|
||||
stack.enter_context(
|
||||
patch("chat.tools.document_translate.TranslationAgent", new=TranslationAgentMock)
|
||||
)
|
||||
yield
|
||||
|
||||
yield _mock_agent
|
||||
|
||||
|
||||
PIXEL_PNG = (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00"
|
||||
b"\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\xf8\xff\xff?\x00\x05\xfe\x02\xfe"
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
"""Tests for document_translate functionality."""
|
||||
|
||||
import io
|
||||
from unittest import mock
|
||||
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
import pytest
|
||||
from pydantic_ai import ModelResponse, RunContext, TextPart
|
||||
from pydantic_ai.exceptions import ModelRetry
|
||||
from pydantic_ai.models.function import FunctionModel
|
||||
from pydantic_ai.usage import RunUsage
|
||||
|
||||
from chat.llm_configuration import LLModel, LLMProvider
|
||||
from chat.tools.document_translate import document_translate
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fixture_translation_agent_config(settings):
|
||||
"""Fixture to set used settings for agent configuration."""
|
||||
settings.TRANSLATION_MAX_CHARS = 100_000
|
||||
settings.LLM_CONFIGURATIONS = {
|
||||
settings.LLM_SUMMARIZATION_MODEL_HRID: LLModel(
|
||||
hrid="mistral-model",
|
||||
model_name="mistral-7b-instruct-v0.1",
|
||||
human_readable_name="Mistral 7B Instruct",
|
||||
profile=None,
|
||||
provider=LLMProvider(
|
||||
hrid="mistral",
|
||||
kind="mistral",
|
||||
base_url="https://api.mistral.ai/v1",
|
||||
api_key="testkey",
|
||||
),
|
||||
is_active=True,
|
||||
system_prompt="direct",
|
||||
tools=[],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="mocked_context")
|
||||
def fixture_mocked_context():
|
||||
"""Fixture for a mocked RunContext."""
|
||||
mock_ctx = mock.Mock(spec=RunContext)
|
||||
mock_ctx.usage = RunUsage(input_tokens=0, output_tokens=0)
|
||||
mock_ctx.max_retries = 2
|
||||
mock_ctx.retries = {}
|
||||
return mock_ctx
|
||||
|
||||
|
||||
def _mock_attachments_queryset(attachment):
|
||||
"""Create a mock queryset that chains .filter().order_by().afirst() returning the attachment."""
|
||||
mock_qs = mock.Mock()
|
||||
mock_qs.order_by.return_value = mock_qs
|
||||
mock_qs.afirst = mock.AsyncMock(return_value=attachment)
|
||||
|
||||
mock_attachments = mock.Mock()
|
||||
mock_attachments.filter.return_value = mock_qs
|
||||
return mock_attachments
|
||||
|
||||
|
||||
def mocked_translation(_messages, _info=None):
|
||||
"""Mocked translation response."""
|
||||
return ModelResponse(parts=[TextPart(content="Ceci est une traduction du document.")])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_single_document(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate with a single document."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "test_doc.txt"
|
||||
mock_attachment.file_name = "test_doc.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "This is a test document. " * 20
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
def mocked_translate_full(_message, _info=None):
|
||||
"""Mocked translation for full flow."""
|
||||
return ModelResponse(parts=[TextPart(content="Ceci est un document de test.")])
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translate_full)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert "Ceci est un document de test." in result.return_value
|
||||
assert result.metadata["sources"] == {"test_doc.txt"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_uses_last_document(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate uses the last uploaded document."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "latest_doc.txt"
|
||||
mock_attachment.file_name = "latest_doc.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "Content of the latest document."
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translation)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert result.metadata["sources"] == {"latest_doc.txt"}
|
||||
# Verify order_by was called with -created_at
|
||||
mock_conversation.attachments.filter.return_value.order_by.assert_called_once_with(
|
||||
"-created_at"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_with_custom_instructions(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate with custom instructions."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "test.txt"
|
||||
mock_attachment.file_name = "test.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "Test content " * 20
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
captured_prompts = []
|
||||
|
||||
def mocked_translate_with_instructions(messages, _info=None):
|
||||
"""Mocked translation that captures prompt."""
|
||||
messages_text = messages[0].parts[-1].content
|
||||
captured_prompts.append(messages_text)
|
||||
return ModelResponse(parts=[TextPart(content="Traduction formelle")])
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translate_with_instructions)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions="Use formal tone"
|
||||
)
|
||||
|
||||
assert result.return_value is not None
|
||||
assert len(captured_prompts) == 1
|
||||
assert "Use formal tone" in captured_prompts[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("target_language", [None, ""])
|
||||
async def test_document_translate_no_target_language(
|
||||
target_language, mocked_context, mock_translation_agent
|
||||
):
|
||||
"""Test document_translate asks the user for language when target_language is not specified."""
|
||||
mocked_context.deps = mock.Mock()
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translation)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language=target_language, instructions=None
|
||||
)
|
||||
|
||||
assert "target language is not specified" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_no_text_attachments(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate returns error message when no text documents found."""
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(None)
|
||||
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translation)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert "No text documents found in the conversation" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_error_reading_document(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate handles errors when reading documents."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "test.txt"
|
||||
mock_attachment.file_name = "test.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
with mock.patch.object(default_storage, "open", side_effect=IOError("File read error")):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translation)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert "An unexpected error occurred during document translation" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_error_during_translation(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate handles ModelRetry during translation."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "test.txt"
|
||||
mock_attachment.file_name = "test.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "Test content " * 20
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
def mocked_translate_error(_messages, _info=None):
|
||||
"""Mocked translation that raises an error."""
|
||||
raise ValueError("Translation error")
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translate_error)):
|
||||
with pytest.raises(ModelRetry):
|
||||
await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_too_large(settings, mocked_context, mock_translation_agent):
|
||||
"""Test document_translate rejects documents exceeding max chars."""
|
||||
settings.TRANSLATION_MAX_CHARS = 100 # Very small limit
|
||||
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "large_doc.txt"
|
||||
mock_attachment.file_name = "large_doc.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "This is a word. " * 100 # Much larger than 100 chars
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translation)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert "too large to translate" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_empty_result(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate raises ModelRetry when translation produces empty result."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "test.txt"
|
||||
mock_attachment.file_name = "test.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "Test content " * 20
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
def mocked_empty_translation(_messages, _info=None):
|
||||
"""Mocked translation that returns empty."""
|
||||
return ModelResponse(parts=[TextPart(content=" ")])
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_empty_translation)):
|
||||
with pytest.raises(ModelRetry) as exc_info:
|
||||
await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert "produced an empty result" in str(exc_info.value)
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Translation tool used for uploaded documents."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from pydantic_ai import RunContext
|
||||
from pydantic_ai.exceptions import ModelRetry
|
||||
from pydantic_ai.messages import ToolReturn
|
||||
|
||||
from chat.agents.translate import TranslationAgent
|
||||
from chat.tools.document_summarize import read_document_content
|
||||
from chat.tools.exceptions import ModelCannotRetry
|
||||
from chat.tools.utils import last_model_retry_soft_fail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@last_model_retry_soft_fail
|
||||
async def document_translate(
|
||||
ctx: RunContext,
|
||||
*,
|
||||
target_language: str | None = None,
|
||||
instructions: str | None = None,
|
||||
) -> ToolReturn:
|
||||
"""
|
||||
Translate the full content of uploaded documents into the specified target language.
|
||||
Preserve the original markdown formatting unless the instructions say otherwise.
|
||||
Return this translation directly to the user WITHOUT any modification
|
||||
or additional summarization.
|
||||
The translation is already complete and MUST be presented as-is in the final response.
|
||||
|
||||
If target_language isn't specified or unknown, the target language should be asked
|
||||
to the user.
|
||||
|
||||
Instructions are optional but should reflect the user's request.
|
||||
|
||||
Examples:
|
||||
"Translate this doc to English" -> target_language = "English", instructions = ""
|
||||
"Translate to Spanish, in formal tone" -> target_language = "Spanish",
|
||||
instructions = "Use formal tone"
|
||||
"Traduis ce document en français" -> target_language = "French", instructions = ""
|
||||
"Translate to German, keep technical terms in English" -> target_language = "German",
|
||||
instructions = "Keep technical terms in English"
|
||||
"Translate this" -> ask the user: "Which language would you like the document
|
||||
translated into?"
|
||||
|
||||
Args:
|
||||
target_language (str | None): The language to translate the document into.
|
||||
If None, ask the user.
|
||||
instructions (str | None): Optional instructions for the translation style or preferences
|
||||
"""
|
||||
try:
|
||||
if not target_language:
|
||||
raise ModelCannotRetry(
|
||||
"The target language is not specified. "
|
||||
"You must ask the user which language they want the document translated into."
|
||||
)
|
||||
instructions_hint = (
|
||||
f"Follow these instructions: {instructions.strip()}" if instructions else ""
|
||||
)
|
||||
translation_agent = TranslationAgent()
|
||||
|
||||
# Get the last uploaded text document
|
||||
last_attachment = await (
|
||||
ctx.deps.conversation.attachments.filter(
|
||||
content_type__startswith="text/",
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.afirst()
|
||||
)
|
||||
|
||||
if not last_attachment:
|
||||
raise ModelCannotRetry(
|
||||
"No text documents found in the conversation. "
|
||||
"You must explain this to the user and ask them to provide documents."
|
||||
)
|
||||
|
||||
doc_name, content = await read_document_content(last_attachment)
|
||||
|
||||
# Check content size against the configured limit
|
||||
max_chars = settings.TRANSLATION_MAX_CHARS
|
||||
|
||||
if len(content) > max_chars:
|
||||
raise ModelCannotRetry(
|
||||
f"The document is too large to translate ({len(content):,} characters, "
|
||||
f"limit is {max_chars:,}). "
|
||||
"You must explain this to the user, without providing numerical details. "
|
||||
"Suggest them to reduce the document size by summarizing it or "
|
||||
"by splitting it into smaller parts. "
|
||||
"Also offer them to summarize the document in the target language instead, "
|
||||
"which can be a good alternative to translation for large documents."
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[translate] translating '%s', %s chars, target_language='%s', instructions='%s'",
|
||||
doc_name,
|
||||
len(content),
|
||||
target_language,
|
||||
instructions_hint,
|
||||
)
|
||||
|
||||
# Translate the document directly
|
||||
translate_prompt = (
|
||||
f"You are an agent specializing in text translation. "
|
||||
f"Translate the following document to {target_language}. "
|
||||
f"Preserve all markdown formatting exactly as-is. "
|
||||
f"{instructions_hint}\n\n"
|
||||
f"'''\n{content}\n'''\n\n"
|
||||
f"Respond directly with the translated text only, no commentary."
|
||||
)
|
||||
|
||||
logger.debug("[translate] prompt for '%s'=> %s", doc_name, translate_prompt[:100] + "...")
|
||||
|
||||
try:
|
||||
resp = await translation_agent.run(translate_prompt, usage=ctx.usage)
|
||||
except Exception as exc:
|
||||
logger.warning("Error during translation of '%s': %s", doc_name, exc, exc_info=True)
|
||||
raise ModelRetry(f"An error occurred while translating document '{doc_name}'.") from exc
|
||||
|
||||
translated_text = (resp.output or "").strip()
|
||||
if not translated_text:
|
||||
raise ModelRetry(f"The translation of '{doc_name}' produced an empty result.")
|
||||
|
||||
logger.debug("[translate] final translation length: %s chars", len(translated_text))
|
||||
|
||||
return ToolReturn(
|
||||
return_value=translated_text,
|
||||
metadata={"sources": {doc_name}},
|
||||
)
|
||||
|
||||
except (ModelCannotRetry, ModelRetry):
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.exception("Unexpected error in document_translate: %s", exc)
|
||||
raise ModelCannotRetry(
|
||||
f"An unexpected error occurred during document translation: {type(exc).__name__}. "
|
||||
"You must explain this to the user and not try to answer based on your knowledge."
|
||||
) from exc
|
||||
@@ -883,6 +883,13 @@ USER QUESTION:
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Translation
|
||||
TRANSLATION_MAX_CHARS = values.PositiveIntegerValue(
|
||||
default=100_000, # ~100k characters, roughly half a 128k context window
|
||||
environ_name="TRANSLATION_MAX_CHARS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Tavily API
|
||||
TAVILY_API_KEY = values.Value(
|
||||
None, # Tavily API key is not set by default
|
||||
|
||||
@@ -227,6 +227,21 @@ const MessageItemComponent: React.FC<MessageItemProps> = ({
|
||||
return tool?.toolInvocation;
|
||||
}, [toolInvocationParts]);
|
||||
|
||||
const activeToolInvocationDisplayName = React.useMemo(() => {
|
||||
if (!activeToolInvocation) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (activeToolInvocation.toolName === 'summarize') {
|
||||
return t('Summarizing...');
|
||||
}
|
||||
if (activeToolInvocation.toolName === 'translate') {
|
||||
return t('Translation in progress...');
|
||||
}
|
||||
|
||||
return t('Search...');
|
||||
}, [activeToolInvocation, t]);
|
||||
|
||||
// Memoize the streaming content split to avoid recreating components in JSX
|
||||
const { completedBlocks, pending } = React.useMemo(() => {
|
||||
// When not streaming, everything is completed as a single block array
|
||||
@@ -361,9 +376,7 @@ const MessageItemComponent: React.FC<MessageItemProps> = ({
|
||||
>
|
||||
<Loader />
|
||||
<Text $variation="600" $size="md">
|
||||
{activeToolInvocation?.toolName === 'summarize'
|
||||
? t('Summarizing...')
|
||||
: t('Search...')}
|
||||
{activeToolInvocationDisplayName}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -48,9 +48,13 @@ export const ToolInvocationItem: React.FC<ToolInvocationItemProps> = ({
|
||||
>
|
||||
<Loader />
|
||||
<Text $variation="600" $size="md">
|
||||
{t('Extracting documents: {{documents}} ...', {
|
||||
documents: documentIdentifiers.join(', '),
|
||||
})}
|
||||
{documentIdentifiers.length === 1
|
||||
? t('Extracting document: {{documents}} ...', {
|
||||
documents: documentIdentifiers[0],
|
||||
})
|
||||
: t('Extracting documents: {{documents}} ...', {
|
||||
documents: documentIdentifiers.join(', '),
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"Direct exchange with our team": "Échange direct avec notre équipe",
|
||||
"Enter your message or a question": "Entrez votre message ou une question",
|
||||
"Explore other LaSuite apps": "Explorer les autres applications de LaSuite",
|
||||
"Extracting document: {{documents}} ...": "Extraction du document : {{documents}}...",
|
||||
"Extracting documents: {{documents}} ...": "Extraction des documents : {{documents}}...",
|
||||
"Failed to activate account. Please try again.": "Échec de l'activation du compte. Veuillez réessayer.",
|
||||
"Failed to copy": "Échec de la copie",
|
||||
@@ -119,6 +120,7 @@
|
||||
"Start conversation": "Entamer la conversation",
|
||||
"Stop": "Stop",
|
||||
"Summarizing...": "Résumé en cours...",
|
||||
"Translation in progress...": "Traduction en cours...",
|
||||
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "L'Assistant est une IA souveraine destinée aux fonctionnaires. Il les aide dans leurs tâches quotidiennes (reformulation, résumé, traduction, recherche d'informations). Vos données restent en France, sur une infrastructure sécurisée et conforme aux normes nationales, et ne sont jamais utilisées à des fins commerciales.",
|
||||
"The Assistant is in Beta": "L'Assistant est en Bêta",
|
||||
"The conversation has been deleted.": "La conversation a été supprimée.",
|
||||
@@ -193,6 +195,7 @@
|
||||
"Direct exchange with our team": "Directe uitwisseling met ons team",
|
||||
"Enter your message or a question": "Voer uw bericht of een vraag in",
|
||||
"Explore other LaSuite apps": "Ontdek andere LaSuite-apps",
|
||||
"Extracting document: {{documents}} ...": "Document uitpakken: {{documents}}...",
|
||||
"Extracting documents: {{documents}} ...": "Documenten uitpakken: {{documents}}...",
|
||||
"Failed to activate account. Please try again.": "Account activeren mislukt. Probeer het opnieuw.",
|
||||
"Failed to copy": "Kopiëren mislukt",
|
||||
@@ -258,6 +261,7 @@
|
||||
"Start conversation": "Begin een gesprek",
|
||||
"Stop": "Stop",
|
||||
"Summarizing...": "Samenvatten...",
|
||||
"Translation in progress...": "Vertaling bezig...",
|
||||
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "De assistent is een soevereine AI voor overheidsambtenaren. Het helpt met dagelijkse taken (herformuleren, samenvatten, vertalen, informatiezoeken). Je gegevens blijven in Nederland op een veilige, door de staat compatibele infrastructuur en worden nooit voor commerciële doeleinden gebruikt.",
|
||||
"The Assistant is in Beta": "De Assistent is in bèta",
|
||||
"The conversation has been deleted.": "Het gesprek is verwijderd.",
|
||||
@@ -332,6 +336,7 @@
|
||||
"Direct exchange with our team": "Прямое общение с нашей командой",
|
||||
"Enter your message or a question": "Введите сообщение или вопрос",
|
||||
"Explore other LaSuite apps": "Посмотреть другие приложения LaSuite",
|
||||
"Extracting document: {{documents}} ...": "Извлечение документа: {{documents}}...",
|
||||
"Extracting documents: {{documents}} ...": "Извлечение документов: {{documents}}...",
|
||||
"Failed to activate account. Please try again.": "Не удалось активировать учётную запись. Пожалуйста, попробуйте снова.",
|
||||
"Failed to copy": "Не удалось скопировать",
|
||||
@@ -397,6 +402,7 @@
|
||||
"Start conversation": "Начать беседу",
|
||||
"Stop": "Остановить",
|
||||
"Summarizing...": "Обобщение...",
|
||||
"Translation in progress...": "Перевод выполняется...",
|
||||
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "Помощник — независимый ИИ для государственных служащих. Он помогает в выполнении повседневных задач (перефразирование, резюмирование, перевод, поиск информации). Ваши данные хранятся во Франции на безопасной инфраструктуре, соответствующей государственным требованиям, и никогда не используются в коммерческих целях.",
|
||||
"The Assistant is in Beta": "Помощник находится на этапе Бета-версии",
|
||||
"The conversation has been deleted.": "Беседа была удалена.",
|
||||
@@ -471,6 +477,7 @@
|
||||
"Direct exchange with our team": "Пряме спілкування з нашою командою",
|
||||
"Enter your message or a question": "Введіть ваше повідомлення або питання",
|
||||
"Explore other LaSuite apps": "Ознайомтесь з іншими застосунками LaSuite",
|
||||
"Extracting document: {{documents}} ...": "Видобування документа: {{documents}}...",
|
||||
"Extracting documents: {{documents}} ...": "Видобування документів: {{documents}}...",
|
||||
"Failed to activate account. Please try again.": "Не вдалося активувати обліковий запис. Спробуйте ще раз.",
|
||||
"Failed to copy": "Не вдалось скопіювати",
|
||||
@@ -536,6 +543,7 @@
|
||||
"Start conversation": "Почати розмову",
|
||||
"Stop": "Зупинити",
|
||||
"Summarizing...": "Узагальнення...",
|
||||
"Translation in progress...": "Переклад виконується...",
|
||||
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "Помічник — це незалежний штучний інтелект для державних службовців. Він допомагає у виконанні щоденних завдань (перефразування, узагальнення, переклад, пошук інформації). Ваші дані зберігаються у Франції на безпечній інфраструктурі, що відповідає державним вимогам, і ніколи не використовуються в комерційних цілях.",
|
||||
"The Assistant is in Beta": "Помічник у бета-версії",
|
||||
"The conversation has been deleted.": "Розмова була видалена.",
|
||||
|
||||
Reference in New Issue
Block a user