Compare commits

...

19 Commits

Author SHA1 Message Date
qbey e7d76e4477 🌐(i18n) update translated strings
Update translated files with new translations
2026-01-16 12:13:55 +01:00
Quentin BEY fd3399dd66 🔖(patch) bump release to 0.0.11
Changed

- 📦️(front) update react

Fixed

- 🐛(e2e) fix test-e2e-chromium
- 🐛(back) fix system prompt compatibility with self-hosted models #200
- ⚰️(back) remove dead code and unused files

Removed

- 🔥(chat) remove thinking part from frontend #227
2026-01-16 12:03:05 +01:00
Quentin BEY 13c6499c66 🔥(chat) remove thinking part from frontend
We want to enable the OSS model but seems like it returns
thinking values twice and we don't manage it well...

So we disable the frontend while we still don't know
how to display the thinking stuff.
We could have also cleaned the backend while unused.
2026-01-16 11:43:05 +01:00
Berry den Hartog a0b31e1e61 🐛(front) fix link color in LeftPanelConversationItem component
fix link color component for default theme
2026-01-16 11:43:05 +01:00
Laurent Paoletti daf90cf110 ⚰️(back) remove dead code and unused files
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-01-16 11:43:05 +01:00
Laurent Paoletti 29f76fe040 🐛(back) fix system prompt compatibility with self-hosted models
Pydantic AI allows setting multiple static and dynamic system prompts
to define conversation context and rules. Previously, these were sent
to the model API as separate messages, which caused compatibility
issues with some self-hosted models (e.g., Gemma3/vLLM).

This commit switches from using `system_prompt` to `instruction` as
recommended in the Pydantic AI documentation, thus merging several
instructions into a single message.

Reference: https://ai.pydantic.dev/agents/#system-prompts
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-01-16 11:43:05 +01:00
Eléonore Voisin dc61fdce00 🐛(e2e) fix test-e2e-chronium
don't need to wait for the See more button to appear
2025-12-19 08:27:02 +01:00
Eléonore Voisin aa42a9b4d3 📦️(front) update react
update react to 19.2.1
2025-12-16 16:48:06 +01:00
qbey 5475bcd04e 🌐(i18n) update translated strings
Update translated files with new translations
2025-12-16 13:15:08 +01:00
Quentin BEY b1533c016a 🔖(minor) bump release to 0.0.10
Added

- (front) add retry button

Fixed

- 🐛(front) fix long user messages
- 🐛(front) fix "Maximum update depth exceeded" error in Chat component
- 🐛(front) fix parsing documents display
- 🐛(front) fix opacity input in error
- 🐛(front) resolve React hydration errors
- 🚑️(user) allow longer short names #182
2025-12-15 14:19:30 +01:00
renovate[bot] 9329ee8c90 ⬆️(dependencies) update django to v5.2.9 [SECURITY] 2025-12-05 17:18:44 +01:00
Quentin BEY 39bf8f0c2d 🔒️(trivy) run trivy scan on sources (not in docker)
Inside the docker images, trivy does not detect frontend issues,
we run trivy here to have the lock files.
This does not work well for python packages.
2025-12-05 16:52:57 +01:00
Eléonore Voisin a1ed561204 🐛(front) fix long user messages
delete MarkDown for user message
2025-12-04 20:56:54 +01:00
Eléonore Voisin 6dfb9b7328 🐛(front) fix Maximum update depth exceeded - error in Chat component
Fix infinite render loop
2025-12-04 14:55:04 +01:00
Eléonore Voisin 38ae97aa31 🐛(front) fix parsing documents display
fix width display parsing + fix width doc title'
2025-12-02 17:22:39 +01:00
Eléonore Voisin 19cf3b2663 🐛(front) fix opacity input in error
fix opacity input in error
2025-12-02 14:40:27 +01:00
Eléonore Voisin 83904d8878 🐛(front) resolve React hydration errors
resolve React hydration errors and infinite update loop

🐛(front) resolve React hydration errors

resolve React hydration errors and infinite update loop
2025-12-02 10:35:48 +01:00
Eléonore Voisin d3922b7448 (front) add retry button
on chat error add retry button
2025-12-01 19:21:34 +01:00
Quentin BEY cdac7cad3b 🚑️(user) allow longer short names
I don't know why it was so short, but a legit user is blocked.
2025-12-01 09:43:36 +01:00
53 changed files with 864 additions and 1166 deletions
+19
View File
@@ -200,3 +200,22 @@ jobs:
- name: Run tests
run: ~/.local/bin/pytest -n 2
security-trivy-critical:
permissions:
contents: read
security-events: write
runs-on: ubuntu-latest
steps:
- name: Run Trivy analysis for critical vulnerabilities
# We use main branch while we might still iterate on the action
uses: numerique-gouv/action-trivy-cache/security-trivy-critical@main
security-trivy:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Run Trivy analysis for vulnerabilities
# We use main branch while we might still iterate on the action
uses: numerique-gouv/action-trivy-cache/security-trivy@main
+37 -10
View File
@@ -8,9 +8,41 @@ and this project adheres to
## [Unreleased]
## [0.0.11] - 2026-01-16
### Changed
- 📦️(front) update react
### Fixed
- 🐛(e2e) fix test-e2e-chromium
- 🐛(back) fix system prompt compatibility with self-hosted models #200
- ⚰️(back) remove dead code and unused files
### Removed
- 🔥(chat) remove thinking part from frontend #227
## [0.0.10] - 2025-12-15
### Added
- ✨(front) add retry button
### Fixed
- 🐛(front) fix long user messages
- 🐛(front) fix "Maximum update depth exceeded" error in Chat component
- 🐛(front) fix parsing documents display
- 🐛(front) fix opacity input in error
- 🐛(front) resolve React hydration errors
- 🚑️(user) allow longer short names #182
## [0.0.9] - 2025-11-17
### Added
- ✨(front) add code copy button
- ✨(RAG) add generic collection RAG tools #159
@@ -18,7 +50,6 @@ and this project adheres to
- 🔊(langfuse) enable tracing with redacted content #162
## [0.0.8] - 2025-11-10
### Fixed
@@ -33,28 +64,24 @@ and this project adheres to
- 🔥(posthog) remove posthog middleware for async mode fix #146
## [0.0.7] - 2025-10-28
### Fixed
- 🚑️(posthog) fix the posthog middleware for async mode #133
## [0.0.6] - 2025-10-28
### Fixed
- 🚑️(stats) fix tracking id in upload event #130
## [0.0.5] - 2025-10-27
### Fixed
- 🚑️(drag-drop) fix the rejection display on Safari #127
## [0.0.4] - 2025-10-27
### Added
@@ -71,14 +98,12 @@ and this project adheres to
- 🐛(front) fix mobile source
- 🐛(attachments) reject the whole drag&drop if unsupported formats #123
## [0.0.3] - 2025-10-21
### Fixed
- 🚑️(web-search) fix missing argument in RAG backend #116
## [0.0.2] - 2025-10-21
### Added
@@ -88,6 +113,7 @@ and this project adheres to
- 📈(posthog) add `sub` field to tracking #95
### Changed
- 🔧(front) change links feedback tchap + settings popup
- 🐛(front) code activation fix session end #93
- 💬(wording) error page wording #102
@@ -95,7 +121,6 @@ and this project adheres to
- 🐛(activation-codes) create contact in brevo before add to list #108
- ⚗️(summarization) add system prompt to handle tool #112
## [0.0.1] - 2025-10-19
### Changed
@@ -118,7 +143,7 @@ and this project adheres to
- 🎨(front) change list attachment in chat
- 🎨(front) move emplacement for attachment
- 🎨(ui) retour ui sources files
- ✨(ui) fix retour global ui
- ✨(ui) fix retour global ui
- 🐛(fix) broken staging css
- 🎨(alpha) adjustment for alpha version
- ✨(ui) delete flex message
@@ -153,7 +178,9 @@ and this project adheres to
- 💄(chat) add code highlighting for LLM responses #67
[unreleased]: https://github.com/suitenumerique/conversations/compare/v0.0.9...main
[unreleased]: https://github.com/suitenumerique/conversations/compare/v0.0.11...main
[0.0.11]: https://github.com/suitenumerique/conversations/releases/v0.0.11
[0.0.10]: https://github.com/suitenumerique/conversations/releases/v0.0.10
[0.0.9]: https://github.com/suitenumerique/conversations/releases/v0.0.9
[0.0.8]: https://github.com/suitenumerique/conversations/releases/v0.0.8
[0.0.7]: https://github.com/suitenumerique/conversations/releases/v0.0.7
-6
View File
@@ -1,6 +0,0 @@
{
"dependencies": {
"@ai-sdk/react": "^1.2.12",
"@ai-sdk/ui-utils": "^1.2.11"
}
}
+1 -3
View File
@@ -190,6 +190,4 @@ class BaseAgent(Agent):
_tools = [get_pydantic_tools_by_name(tool_name) for tool_name in self.configuration.tools]
super().__init__(
model=_model_instance, system_prompt=_system_prompt, tools=_tools, **kwargs
)
super().__init__(model=_model_instance, instructions=_system_prompt, tools=_tools, **kwargs)
+4 -5
View File
@@ -16,7 +16,6 @@ from .base import BaseAgent
logger = logging.getLogger(__name__)
MOCKED_RESPONSE = """
# **Ode to the AI Assistant** 🤖✨
@@ -102,10 +101,10 @@ class ConversationAgent(BaseAgent):
if settings.WARNING_MOCK_CONVERSATION_AGENT:
self._model = FunctionModel(stream_function=mocked_agent_model)
@self.system_prompt
@self.instructions
def add_the_date() -> str:
"""
Dynamic system prompt function to add the current date.
Dynamic instruction function to add the current date.
Warning: this will always use the date in the server timezone,
not the user's timezone...
@@ -113,9 +112,9 @@ class ConversationAgent(BaseAgent):
_formatted_date = formats.date_format(timezone.now(), "l d/m/Y", use_l10n=False)
return f"Today is {_formatted_date}."
@self.system_prompt
@self.instructions
def enforce_response_language() -> str:
"""Dynamic system prompt function to set the expected language to use."""
"""Dynamic instruction function to set the expected language to use."""
return f"Answer in {get_language_name(language).lower()}." if language else ""
def get_web_search_tool_name(self) -> str | None:
+7 -5
View File
@@ -78,6 +78,9 @@ from chat.tools.document_summarize import document_summarize
from chat.vercel_ai_sdk.core import events_v4, events_v5
from chat.vercel_ai_sdk.encoder import EventEncoder
# Keep at the top of the file to avoid mocking issues
document_store_backend = import_string(settings.RAG_DOCUMENT_SEARCH_BACKEND)
logger = logging.getLogger(__name__)
User = get_user_model()
@@ -236,6 +239,7 @@ class AIAgentService: # pylint: disable=too-many-instance-attributes
Parse and store input documents in the conversation's document store.
"""
# Early external document URL rejection
if any(
not document.url.startswith("/media-key/")
for document in documents
@@ -249,8 +253,6 @@ class AIAgentService: # pylint: disable=too-many-instance-attributes
):
raise ValueError("Document URL does not belong to the conversation.")
document_store_backend = import_string(settings.RAG_DOCUMENT_SEARCH_BACKEND)
document_store = document_store_backend(self.conversation.collection_id)
if not document_store.collection_id:
# Create a new collection for the conversation
@@ -420,6 +422,7 @@ class AIAgentService: # pylint: disable=too-many-instance-attributes
],
},
)
try:
await self.parse_input_documents(input_documents)
except Exception as exc: # pylint: disable=broad-except
@@ -457,7 +460,7 @@ class AIAgentService: # pylint: disable=too-many-instance-attributes
if force_web_search:
@self.conversation_agent.system_prompt
@self.conversation_agent.instructions
def force_web_search_prompt() -> str:
"""Dynamic system prompt function to force web search."""
return (
@@ -505,7 +508,7 @@ class AIAgentService: # pylint: disable=too-many-instance-attributes
)
# Inform the model (system-level) that documents are attached and available
@self.conversation_agent.system_prompt
@self.conversation_agent.instructions
def attached_documents_note() -> str:
return (
"[Internal context] User documents are attached to this conversation. "
@@ -731,7 +734,6 @@ class AIAgentService: # pylint: disable=too-many-instance-attributes
langfuse.update_current_trace(
output=run.result.output if self._store_analytics else "REDACTED"
)
# Vercel finish message
yield events_v4.FinishMessagePart(
finish_reason=events_v4.FinishReason.STOP,
@@ -27,9 +27,14 @@ def test_build_pydantic_agent_success_no_tools():
"""Test successful agent creation without tools."""
agent = ConversationAgent(model_hrid="default-model")
assert isinstance(agent, Agent)
assert agent._system_prompts == ()
instructions = agent._instructions
assert len(instructions) == 3
assert instructions[0] == "You are a helpful assistant"
assert instructions[1].__name__ == "add_the_date"
assert instructions[2].__name__ == "enforce_response_language"
assert agent._system_prompts == ("You are a helpful assistant",)
assert agent._instructions == []
assert isinstance(agent.model, OpenAIChatModel)
assert agent.model.model_name == "model-123"
assert str(agent.model.client.base_url) == "https://api.llm.com/v1/"
@@ -37,6 +42,7 @@ def test_build_pydantic_agent_success_no_tools():
assert agent._function_toolset.tools == {}
@freeze_time("2025-07-25T10:36:35.297675Z")
def test_build_pydantic_agent_with_tools(settings):
"""Test successful agent creation with tools."""
settings.AI_AGENT_TOOLS = ["get_current_weather"]
@@ -44,8 +50,14 @@ def test_build_pydantic_agent_with_tools(settings):
agent = ConversationAgent(model_hrid="default-model")
assert isinstance(agent, Agent)
assert agent._system_prompts == ("You are a helpful assistant",)
assert agent._instructions == []
instructions = agent._instructions
assert len(instructions) == 3
assert instructions[0] == "You are a helpful assistant"
assert instructions[1].__name__ == "add_the_date"
assert instructions[1]() == "Today is Friday 25/07/2025."
assert instructions[2].__name__ == "enforce_response_language"
assert instructions[2]() == ""
assert isinstance(agent.model, OpenAIChatModel)
assert agent.model.model_name == "model-123"
assert str(agent.model.client.base_url) == "https://api.llm.com/v1/"
@@ -56,21 +68,23 @@ def test_build_pydantic_agent_with_tools(settings):
@freeze_time("2025-07-25T10:36:35.297675Z")
def test_add_dynamic_system_prompt():
"""
Ensure add_the_date and enforce_response_language system prompt are registered
Ensure add_the_date and enforce_response_language instructions are registered
and returns proper values.
"""
agent = ConversationAgent(model_hrid="default-model")
assert len(agent._system_prompt_functions) == 2
assert len(agent._system_prompt_functions) == 0
assert agent._system_prompt_functions[0].function.__name__ == "add_the_date"
assert agent._system_prompt_functions[0].function() == "Today is Friday 25/07/2025."
assert agent._system_prompt_functions[1].function.__name__ == "enforce_response_language"
assert agent._system_prompt_functions[1].function() == ""
instructions = agent._instructions
assert len(instructions) == 3
assert instructions[0] == "You are a helpful assistant"
assert instructions[1].__name__ == "add_the_date"
assert instructions[1]() == "Today is Friday 25/07/2025."
assert instructions[2].__name__ == "enforce_response_language"
assert instructions[2]() == ""
agent = ConversationAgent(model_hrid="default-model", language="fr-fr")
assert agent._system_prompt_functions[1].function() == "Answer in french."
assert agent._instructions[2]() == "Answer in french."
def test_agent_get_web_search_tool_name(settings):
@@ -130,6 +130,16 @@ def test_post_conversation_data_protocol(api_client, mock_openai_stream):
assert mock_openai_stream.called
# ensure instructions are merged as a system prompt
last_request_payload = json.loads(respx.calls.last.request.content)
assert last_request_payload["messages"][0] == {
"content": (
"You are a helpful test assistant :)\n\nToday is Friday 25/07/2025.\n\n"
"Answer in english."
),
"role": "system",
}
chat_conversation.refresh_from_db()
assert chat_conversation.ui_messages == [
{
@@ -170,29 +180,15 @@ def test_post_conversation_data_protocol(api_client, mock_openai_stream):
)
_run_id = chat_conversation.pydantic_messages[0]["run_id"]
assert chat_conversation.pydantic_messages == [
{
"instructions": None,
"instructions": (
"You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\nAnswer in english."
),
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Today is Friday 25/07/2025.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": ["Hello"],
"part_kind": "user-prompt",
@@ -255,6 +251,15 @@ def test_post_conversation_text_protocol(api_client, mock_openai_stream):
assert response_content == "Hello there"
assert mock_openai_stream.called
# ensure instructions are merged as a system prompt
last_request_payload = json.loads(respx.calls.last.request.content)
assert last_request_payload["messages"][0] == {
"content": (
"You are a helpful test assistant :)\n\nToday is Friday 25/07/2025.\n\n"
"Answer in english."
),
"role": "system",
}
chat_conversation.refresh_from_db()
assert chat_conversation.ui_messages == [
@@ -296,29 +301,15 @@ def test_post_conversation_text_protocol(api_client, mock_openai_stream):
)
_run_id = chat_conversation.pydantic_messages[0]["run_id"]
assert chat_conversation.pydantic_messages == [
{
"instructions": None,
"instructions": (
"You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\nAnswer in english."
),
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Today is Friday 25/07/2025.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": ["Hello"],
"part_kind": "user-prompt",
@@ -409,11 +400,12 @@ def test_post_conversation_with_image(api_client, mock_openai_stream_image):
# Check the exact structure expected by the AI service
assert body["messages"] == [
{
"content": "You are a helpful test assistant :)",
"content": (
"You are a helpful test assistant :)\n\nToday is Friday 25/07/2025."
"\n\nAnswer in english."
),
"role": "system",
},
{"content": "Today is Friday 25/07/2025.", "role": "system"},
{"content": "Answer in english.", "role": "system"},
{
"content": [
{"text": "Hello, what do you see on this picture?", "type": "text"},
@@ -498,27 +490,12 @@ def test_post_conversation_with_image(api_client, mock_openai_stream_image):
_run_id = chat_conversation.pydantic_messages[0]["run_id"]
assert chat_conversation.pydantic_messages == [
{
"instructions": None,
"instructions": (
"You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\nAnswer in english."
),
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Today is Friday 25/07/2025.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": [
"Hello, what do you see on this picture?",
@@ -616,11 +593,12 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, settin
assert body["messages"] == [
{
"content": "You are a helpful test assistant :)",
"content": (
"You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\nAnswer in english."
),
"role": "system",
},
{"content": "Today is Friday 25/07/2025.", "role": "system"},
{"content": "Answer in english.", "role": "system"},
{"content": [{"text": "Weather in Paris?", "type": "text"}], "role": "user"},
]
@@ -678,27 +656,12 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, settin
_run_id = chat_conversation.pydantic_messages[0]["run_id"]
assert chat_conversation.pydantic_messages == [
{
"instructions": None,
"instructions": (
"You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\nAnswer in english."
),
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Today is Friday 25/07/2025.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": ["Weather in Paris?"],
"part_kind": "user-prompt",
@@ -737,7 +700,10 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, settin
"run_id": _run_id,
},
{
"instructions": None,
"instructions": (
"You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\nAnswer in english."
),
"kind": "request",
"parts": [
{
@@ -829,11 +795,12 @@ def test_post_conversation_tool_call_fails(api_client, mock_openai_stream_tool,
assert body["messages"] == [
{
"content": "You are a helpful test assistant :)",
"content": (
"You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\nAnswer in french."
),
"role": "system",
},
{"content": "Today is Friday 25/07/2025.", "role": "system"},
{"content": "Answer in french.", "role": "system"},
{"content": [{"text": "Weather in Paris?", "type": "text"}], "role": "user"},
]
@@ -891,27 +858,12 @@ def test_post_conversation_tool_call_fails(api_client, mock_openai_stream_tool,
_run_id = chat_conversation.pydantic_messages[0]["run_id"]
assert chat_conversation.pydantic_messages == [
{
"instructions": None,
"instructions": (
"You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\nAnswer in french."
),
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Today is Friday 25/07/2025.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Answer in french.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": ["Weather in Paris?"],
"part_kind": "user-prompt",
@@ -950,7 +902,10 @@ def test_post_conversation_tool_call_fails(api_client, mock_openai_stream_tool,
"run_id": _run_id,
},
{
"instructions": None,
"instructions": (
"You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\nAnswer in french."
),
"kind": "request",
"parts": [
{
@@ -1214,27 +1169,11 @@ def test_post_conversation_data_protocol_no_stream(
_run_id = chat_conversation.pydantic_messages[0]["run_id"]
assert chat_conversation.pydantic_messages == [
{
"instructions": None,
"instructions": (
"You are an amazing assistant.\n\nToday is Friday 25/07/2025.\n\nAnswer in english."
),
"kind": "request",
"parts": [
{
"content": "You are an amazing assistant.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Today is Friday 25/07/2025.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": ["Why the sky is blue?"],
"part_kind": "user-prompt",
@@ -1369,27 +1308,12 @@ async def test_post_conversation_async(api_client, mock_openai_stream, monkeypat
_run_id = chat_conversation.pydantic_messages[0]["run_id"]
assert chat_conversation.pydantic_messages == [
{
"instructions": None,
"instructions": (
"You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\nAnswer in english."
),
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Today is Friday 25/07/2025.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
{
"content": ["Hello"],
"part_kind": "user-prompt",
@@ -216,7 +216,8 @@ def fixture_mock_openai_stream():
@responses.activate
@respx.mock
@freeze_time()
def test_post_conversation_with_document_upload( # pylint: disable=too-many-arguments,too-many-positional-arguments
def test_post_conversation_with_document_upload(
# pylint: disable=too-many-arguments,too-many-positional-arguments
api_client,
mock_albert_api, # pylint: disable=unused-argument
sample_pdf_content,
@@ -353,53 +354,25 @@ def test_post_conversation_with_document_upload( # pylint: disable=too-many-arg
assert len(chat_conversation.pydantic_messages) == 4
_run_id = chat_conversation.pydantic_messages[0]["run_id"]
assert chat_conversation.pydantic_messages[0] == {
"instructions": "When you receive a result from the summarization tool, you "
"MUST return it directly to the user without any "
"modification, paraphrasing, or additional summarization.The "
"tool already produces optimized summaries that should be "
"presented verbatim.You may translate the summary if "
"required, but you MUST preserve all the information from the "
"original summary.You may add a follow-up question after the "
"summary if needed.",
"instructions": "You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\n"
"Answer in english.\n\n"
"Use document_search_rag ONLY to retrieve specific passages from "
"attached documents. Do NOT use it to summarize; for summaries, "
"call the summarize tool instead.\n\nWhen you receive a result from the "
"summarization tool, you MUST return it directly to the user without "
"any modification, paraphrasing, or additional summarization."
"The tool already produces optimized summaries that should be "
"presented verbatim.You may translate the summary if required, "
"but you MUST preserve all the information from the original summary."
"You may add a follow-up question after the summary if needed.\n\n"
"[Internal context] User documents are attached to this conversation. "
"Do not request re-upload of documents; consider them already "
"available via the internal store.",
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timezone_now,
},
{
"content": today_promt_date,
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timezone_now,
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timezone_now,
},
{
"content": "Use document_search_rag ONLY to retrieve specific "
"passages from attached documents. Do NOT use it to "
"summarize; for summaries, call the summarize tool "
"instead.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timezone_now,
},
{
"content": "[Internal context] User documents are attached to this "
"conversation. Do not request re-upload of documents; "
"consider them already available via the internal "
"store.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timezone_now,
},
{
"content": ["What does the document say?"],
"part_kind": "user-prompt",
@@ -439,14 +412,21 @@ def test_post_conversation_with_document_upload( # pylint: disable=too-many-arg
}
assert chat_conversation.pydantic_messages[2] == {
"instructions": (
"When you receive a result from the summarization tool, you MUST "
"return it directly to the user without any modification, "
"paraphrasing, or additional summarization."
"The tool already produces optimized summaries that should "
"be presented verbatim."
"You may translate the summary if required, but you MUST preserve "
"all the information from the original summary."
"You may add a follow-up question after the summary if needed."
"You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\n"
"Answer in english.\n\n"
"Use document_search_rag ONLY to retrieve specific passages from "
"attached documents. Do NOT use it to summarize; for summaries, "
"call the summarize tool instead.\n\nWhen you receive a result from the "
"summarization tool, you MUST return it directly to the user without "
"any modification, paraphrasing, or additional summarization."
"The tool already produces optimized summaries that should be "
"presented verbatim.You may translate the summary if required, "
"but you MUST preserve all the information from the original summary."
"You may add a follow-up question after the summary if needed.\n\n"
"[Internal context] User documents are attached to this conversation. "
"Do not request re-upload of documents; consider them already "
"available via the internal store."
),
"kind": "request",
"parts": [
@@ -499,7 +479,8 @@ def test_post_conversation_with_document_upload( # pylint: disable=too-many-arg
@responses.activate
@respx.mock
@freeze_time("2025-07-25T10:36:35.297675Z")
def test_post_conversation_with_document_upload_feature_disabled( # pylint: disable=too-many-arguments,too-many-positional-arguments
def test_post_conversation_with_document_upload_feature_disabled(
# pylint: disable=too-many-arguments,too-many-positional-arguments
api_client,
caplog,
mock_openai_stream, # pylint: disable=unused-argument
@@ -552,14 +533,12 @@ def test_post_conversation_with_document_upload_feature_disabled( # pylint: dis
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"From the document, I can see that "\n'
"0:\"it says 'Hello PDF'.\"\n"
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":150,"completionTokens":25}}\n'
)
# This behavior must be improved in the future to inform the user properly
assert "Document upload feature is disabled, ignoring input documents." in caplog.text
@@ -582,6 +561,7 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
api_client.force_authenticate(user=chat_conversation.owner)
pdf_base64 = base64.b64encode(sample_pdf_content.read()).decode("utf-8")
message = UIMessage(
id="1",
role="user",
@@ -643,7 +623,7 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
'document discusses various topics."}\n'
'0:"The document discusses various topics."\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":317,"completionTokens":19}}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":287,"completionTokens":19}}\n'
)
# Check that the conversation was updated
@@ -705,52 +685,25 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
_run_id = chat_conversation.pydantic_messages[0]["run_id"]
assert chat_conversation.pydantic_messages[0] == {
"instructions": "When you receive a result from the summarization tool, you "
"MUST return it directly to the user without any "
"modification, paraphrasing, or additional summarization.The "
"tool already produces optimized summaries that should be "
"presented verbatim.You may translate the summary if "
"required, but you MUST preserve all the information from the "
"original summary.You may add a follow-up question after the "
"summary if needed.",
"instructions": (
"You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\n"
"Answer in english.\n\n"
"Use document_search_rag ONLY to retrieve specific passages from "
"attached documents. Do NOT use it to summarize; for summaries, "
"call the summarize tool instead.\n\nWhen you receive a result from the "
"summarization tool, you MUST return it directly to the user without "
"any modification, paraphrasing, or additional summarization."
"The tool already produces optimized summaries that should be "
"presented verbatim.You may translate the summary if required, "
"but you MUST preserve all the information from the original summary."
"You may add a follow-up question after the summary if needed.\n\n"
"[Internal context] User documents are attached to this conversation. "
"Do not request re-upload of documents; consider them already "
"available via the internal store."
),
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timezone_now,
},
{
"content": today_promt_date,
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timezone_now,
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timezone_now,
},
{
"content": "Use document_search_rag ONLY to retrieve specific "
"passages from attached documents. Do NOT use it to "
"summarize; for summaries, call the summarize tool "
"instead.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timezone_now,
},
{
"content": "[Internal context] User documents are attached to this "
"conversation. Do not request re-upload of documents; "
"consider them already available via the internal "
"store.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timezone_now,
},
{
"content": ["Make a summary of this document."],
"part_kind": "user-prompt",
@@ -790,14 +743,21 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
}
assert chat_conversation.pydantic_messages[2] == {
"instructions": (
"When you receive a result from the summarization tool, you MUST "
"return it directly to the user without any modification, "
"paraphrasing, or additional summarization."
"The tool already produces optimized summaries that should "
"be presented verbatim."
"You may translate the summary if required, but you MUST preserve "
"all the information from the original summary."
"You may add a follow-up question after the summary if needed."
"You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\n"
"Answer in english.\n\n"
"Use document_search_rag ONLY to retrieve specific passages from "
"attached documents. Do NOT use it to summarize; for summaries, "
"call the summarize tool instead.\n\nWhen you receive a result from the "
"summarization tool, you MUST return it directly to the user without "
"any modification, paraphrasing, or additional summarization."
"The tool already produces optimized summaries that should be "
"presented verbatim.You may translate the summary if required, "
"but you MUST preserve all the information from the original summary."
"You may add a follow-up question after the summary if needed.\n\n"
"[Internal context] User documents are attached to this conversation. "
"Do not request re-upload of documents; consider them already "
"available via the internal store."
),
"kind": "request",
"parts": [
@@ -17,7 +17,6 @@ from pydantic_ai.messages import (
DocumentUrl,
ModelMessage,
ModelResponse,
SystemPromptPart,
TextPart,
UserPromptPart,
)
@@ -61,7 +60,8 @@ def fixture_sample_document_content():
@responses.activate
@freeze_time()
def test_post_conversation_with_local_pdf_document_url( # pylint: disable=too-many-arguments,too-many-positional-arguments
def test_post_conversation_with_local_pdf_document_url(
# pylint: disable=too-many-arguments,too-many-positional-arguments
api_client,
sample_document_content,
today_promt_date,
@@ -120,7 +120,7 @@ def test_post_conversation_with_local_pdf_document_url( # pylint: disable=too-m
)
async def agent_model(messages: list[ModelMessage], _info: AgentInfo):
presigned_url = messages[0].parts[3].content[1].url
presigned_url = messages[0].parts[0].content[1].url
assert presigned_url.startswith("http://localhost:9000/conversations-media-storage/")
assert presigned_url.find("X-Amz-Signature=") != -1
assert presigned_url.find("X-Amz-Date=") != -1
@@ -129,11 +129,6 @@ def test_post_conversation_with_local_pdf_document_url( # pylint: disable=too-m
assert messages == [
ModelRequest(
parts=[
SystemPromptPart(
content="You are a helpful test assistant :)", timestamp=timezone.now()
),
SystemPromptPart(content=today_promt_date, timestamp=timezone.now()),
SystemPromptPart(content="Answer in english.", timestamp=timezone.now()),
UserPromptPart(
content=[
"What is in this document?",
@@ -146,6 +141,8 @@ def test_post_conversation_with_local_pdf_document_url( # pylint: disable=too-m
timestamp=timezone.now(),
),
],
instructions=f"You are a helpful test assistant :)\n\n{today_promt_date}"
"\n\nAnswer in english.",
run_id=messages[0].run_id,
)
]
@@ -221,27 +218,11 @@ def test_post_conversation_with_local_pdf_document_url( # pylint: disable=too-m
_run_id = chat_conversation.pydantic_messages[0]["run_id"]
assert chat_conversation.pydantic_messages == [
{
"instructions": None,
"instructions": "You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\n"
"Answer in english.",
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timestamp,
},
{
"content": today_promt_date,
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timestamp,
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timestamp,
},
{
"content": [
"What is in this document?",
@@ -429,7 +410,6 @@ def test_post_conversation_with_remote_document_url(
@freeze_time("2025-10-18T20:48:20.286204Z")
def test_post_conversation_with_local_document_url_in_history( # pylint: disable=too-many-arguments,too-many-positional-arguments
api_client,
today_promt_date,
mock_ai_agent_service,
):
"""
@@ -437,6 +417,8 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
"""
chat_conversation_pk = "0be55da5-8eb7-4dad-aa0f-fea454bd5809"
document_url = f"/media-key/{chat_conversation_pk}/sample.pdf"
formatted_date = formats.date_format(timezone.now(), "l d/m/Y", use_l10n=False)
chat_conversation = ChatConversationFactory(
pk=chat_conversation_pk,
owner__language="en-us",
@@ -472,27 +454,11 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
],
pydantic_messages=[
{
"instructions": None,
"instructions": "You are a helpful test assistant :)\n\n"
f"Today is {formatted_date}.\n\n"
"Answer in english.",
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": today_promt_date,
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": [
"What is in this document?",
@@ -555,7 +521,7 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
)
async def agent_model(messages: list[ModelMessage], _info: AgentInfo):
presigned_url = messages[0].parts[3].content[1].url
presigned_url = messages[0].parts[0].content[1].url
assert presigned_url.startswith("http://localhost:9000/conversations-media-storage/")
assert presigned_url.find("X-Amz-Signature=") != -1
assert presigned_url.find("X-Amz-Date=") != -1
@@ -564,18 +530,6 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
assert messages == [
ModelRequest(
parts=[
SystemPromptPart(
content="You are a helpful test assistant :)",
timestamp=timezone.now(),
),
SystemPromptPart(
content=today_promt_date,
timestamp=timezone.now(),
),
SystemPromptPart(
content="Answer in english.",
timestamp=timezone.now(),
),
UserPromptPart(
content=[
"What is in this document?",
@@ -588,6 +542,9 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
timestamp=timezone.now(),
),
],
instructions="You are a helpful test assistant :)\n\n"
"Today is Saturday 18/10/2025.\n\n"
"Answer in english.",
run_id=messages[0].run_id,
),
ModelResponse(
@@ -606,6 +563,9 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
timestamp=timezone.now(),
)
],
instructions="You are a helpful test assistant :)\n\n"
"Today is Saturday 18/10/2025.\n\n"
"Answer in english.",
run_id=messages[2].run_id,
),
]
@@ -705,27 +665,11 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
_run_id = chat_conversation.pydantic_messages[2]["run_id"]
assert chat_conversation.pydantic_messages == [
{
"instructions": None,
"instructions": "You are a helpful test assistant :)\n\n"
"Today is Saturday 18/10/2025.\n\n"
"Answer in english.",
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": today_promt_date,
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": [
"What is in this document?",
@@ -772,7 +716,9 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
# no run_id here
},
{
"instructions": None,
"instructions": "You are a helpful test assistant :)\n\n"
"Today is Saturday 18/10/2025.\n\n"
"Answer in english.",
"kind": "request",
"parts": [
{
@@ -823,7 +769,8 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
("data.csv", "text/csv"),
],
)
def test_post_conversation_with_local_not_pdf_document_url( # pylint: disable=too-many-arguments,too-many-positional-arguments
def test_post_conversation_with_local_not_pdf_document_url(
# pylint: disable=too-many-arguments,too-many-positional-arguments
api_client,
today_promt_date,
mock_ai_agent_service,
@@ -886,27 +833,6 @@ def test_post_conversation_with_local_not_pdf_document_url( # pylint: disable=t
assert messages == [
ModelRequest(
parts=[
SystemPromptPart(
content="You are a helpful test assistant :)", timestamp=timezone.now()
),
SystemPromptPart(content=today_promt_date, timestamp=timezone.now()),
SystemPromptPart(content="Answer in english.", timestamp=timezone.now()),
SystemPromptPart(
content=(
"Use document_search_rag ONLY to retrieve specific passages from "
"attached documents. Do NOT use it to summarize; for summaries, "
"call the summarize tool instead."
),
timestamp=timezone.now(),
),
SystemPromptPart(
content=(
"[Internal context] User documents are attached to this conversation. "
"Do not request re-upload of documents; consider them already "
"available via the internal store."
),
timestamp=timezone.now(),
),
UserPromptPart(
content=[
"What is in this document?",
@@ -916,14 +842,22 @@ def test_post_conversation_with_local_not_pdf_document_url( # pylint: disable=t
),
],
instructions=(
"When you receive a result from the summarization tool, you MUST "
"return it directly to the user without any modification, "
"paraphrasing, or additional summarization."
"The tool already produces optimized summaries that should "
"be presented verbatim."
"You may translate the summary if required, but you MUST preserve "
"all the information from the original summary."
"You may add a follow-up question after the summary if needed."
"You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\n"
"Answer in english.\n\n"
"Use document_search_rag ONLY to retrieve specific passages from "
"attached documents. Do NOT use it to summarize; for summaries, "
"call the summarize tool instead.\n\nWhen you receive a result "
"from the summarization tool, you MUST return it directly to "
"the user without any modification, paraphrasing, or additional "
"summarization.The tool already produces optimized summaries "
"that should be presented verbatim.You may translate the summary "
"if required, but you MUST preserve all the information from the "
"original summary.You may add a follow-up question after the "
"summary if needed.\n\n"
"[Internal context] User documents are attached to this conversation. "
"Do not request re-upload of documents; "
"consider them already available via the internal store."
),
run_id=messages[0].run_id,
)
@@ -999,53 +933,25 @@ def test_post_conversation_with_local_not_pdf_document_url( # pylint: disable=t
assert chat_conversation.pydantic_messages == [
{
"instructions": (
"When you receive a result from the summarization tool, you MUST "
"return it directly to the user without any modification, "
"paraphrasing, or additional summarization."
"The tool already produces optimized summaries that should "
"be presented verbatim."
"You may translate the summary if required, but you MUST preserve "
"all the information from the original summary."
"You may add a follow-up question after the summary if needed."
"You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\n"
"Answer in english.\n\n"
"Use document_search_rag ONLY to retrieve specific passages from "
"attached documents. Do NOT use it to summarize; for summaries, "
"call the summarize tool instead.\n\nWhen you receive a result "
"from the summarization tool, you MUST return it directly to "
"the user without any modification, paraphrasing, or additional "
"summarization.The tool already produces optimized summaries "
"that should be presented verbatim.You may translate the summary "
"if required, but you MUST preserve all the information from the "
"original summary.You may add a follow-up question after the "
"summary if needed.\n\n"
"[Internal context] User documents are attached to this conversation. "
"Do not request re-upload of documents; "
"consider them already available via the internal store."
),
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timestamp,
},
{
"content": today_promt_date,
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timestamp,
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timestamp,
},
{
"content": "Use document_search_rag ONLY to retrieve specific "
"passages from attached documents. Do NOT use it to "
"summarize; for summaries, call the summarize tool "
"instead.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timestamp,
},
{
"content": "[Internal context] User documents are attached to "
"this conversation. Do not request re-upload of "
"documents; consider them already available via the "
"internal store.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": timestamp,
},
{
"content": [
"What is in this document?",
@@ -919,7 +919,7 @@ def history_conversation_with_tool_fixture():
history_timestamp = timezone.now().replace(year=2025, month=6, day=15, hour=10, minute=30)
# Create a conversation with pre-existing messages including a tool invocation
conversation = ChatConversationFactory()
conversation = ChatConversationFactory(owner__language="nl-nl")
# Add previous user and assistant messages with tool invocation
conversation.messages = [
@@ -1377,7 +1377,9 @@ def test_post_conversation_with_existing_tool_history(
# Verify the new tool call request is included
assert history_conversation_with_tool.pydantic_messages[8] == {
"instructions": None,
"instructions": "You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\n"
"Answer in dutch.",
"kind": "request",
"parts": [
{
@@ -1420,7 +1422,9 @@ def test_post_conversation_with_existing_tool_history(
}
assert history_conversation_with_tool.pydantic_messages[10] == {
"instructions": None,
"instructions": "You are a helpful test assistant :)\n\n"
"Today is Friday 25/07/2025.\n\n"
"Answer in dutch.",
"kind": "request",
"parts": [
{
@@ -2,7 +2,7 @@
import uuid
from django.utils import timezone
from django.utils import formats, timezone
import pytest
from dirty_equals import IsUUID
@@ -12,7 +12,6 @@ from pydantic_ai.messages import (
ImageUrl,
ModelMessage,
ModelResponse,
SystemPromptPart,
TextPart,
UserPromptPart,
)
@@ -87,22 +86,15 @@ def test_post_conversation_with_local_image_url(
)
async def agent_model(messages: list[ModelMessage], _info: AgentInfo):
presigned_url = messages[0].parts[3].content[1].url
assert presigned_url.startswith("http://localhost:9000/conversations-media-storage/")
presigned_url = messages[0].parts[0].content[1].url
# assert presigned_url.startswith("http://localhost:9000/conversations-media-storage/")
assert presigned_url.find("X-Amz-Signature=") != -1
assert presigned_url.find("X-Amz-Date=") != -1
assert presigned_url.find("X-Amz-Expires=") != -1
formatted_date = formats.date_format(timezone.now(), "l d/m/Y", use_l10n=False)
assert messages == [
ModelRequest(
parts=[
SystemPromptPart(
content="You are a helpful test assistant :)", timestamp=timezone.now()
),
SystemPromptPart(
content="Today is Saturday 18/10/2025.", timestamp=timezone.now()
),
SystemPromptPart(content="Answer in english.", timestamp=timezone.now()),
UserPromptPart(
content=[
"What is in this image?",
@@ -115,6 +107,8 @@ def test_post_conversation_with_local_image_url(
timestamp=timezone.now(),
),
],
instructions="You are a helpful test assistant :)\n\nToday is "
f"{formatted_date}.\n\nAnswer in english.",
run_id=messages[0].run_id,
)
]
@@ -184,27 +178,10 @@ def test_post_conversation_with_local_image_url(
_run_id = chat_conversation.pydantic_messages[0]["run_id"]
assert chat_conversation.pydantic_messages == [
{
"instructions": None,
"instructions": "You are a helpful test assistant :)\n\n"
"Today is Saturday 18/10/2025.\n\nAnswer in english.",
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": "Today is Saturday 18/10/2025.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": [
"What is in this image?",
@@ -286,11 +263,6 @@ def test_post_conversation_with_local_image_wrong_url(
assert messages == [
ModelRequest(
parts=[
SystemPromptPart(
content="You are a helpful test assistant :)", timestamp=timezone.now()
),
SystemPromptPart(content=today_promt_date, timestamp=timezone.now()),
SystemPromptPart(content="Answer in english.", timestamp=timezone.now()),
UserPromptPart(
content=[
"What is in this image?",
@@ -303,6 +275,8 @@ def test_post_conversation_with_local_image_wrong_url(
timestamp=timezone.now(),
),
],
instructions=f"You are a helpful test assistant :)\n\n{today_promt_date}"
"\n\nAnswer in english.",
run_id=messages[0].run_id,
)
]
@@ -374,11 +348,6 @@ def test_post_conversation_with_remote_image_url(
assert messages == [
ModelRequest(
parts=[
SystemPromptPart(
content="You are a helpful test assistant :)", timestamp=timezone.now()
),
SystemPromptPart(content=today_promt_date, timestamp=timezone.now()),
SystemPromptPart(content="Answer in english.", timestamp=timezone.now()),
UserPromptPart(
content=[
"What is in this image?",
@@ -391,6 +360,8 @@ def test_post_conversation_with_remote_image_url(
timestamp=timezone.now(),
),
],
instructions="You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\nAnswer in english.",
run_id=messages[0].run_id,
)
]
@@ -504,27 +475,10 @@ def test_post_conversation_with_local_image_url_in_history(
],
pydantic_messages=[
{
"instructions": None,
"instructions": f"You are a helpful test assistant :)\n\n{today_promt_date}"
"\n\nAnswer in english.",
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": today_promt_date,
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": [
"What is in this image?",
@@ -587,7 +541,7 @@ def test_post_conversation_with_local_image_url_in_history(
)
async def agent_model(messages: list[ModelMessage], _info: AgentInfo):
presigned_url = messages[0].parts[3].content[1].url
presigned_url = messages[0].parts[0].content[1].url
assert presigned_url.startswith("http://localhost:9000/conversations-media-storage/")
assert presigned_url.find("X-Amz-Signature=") != -1
assert presigned_url.find("X-Amz-Date=") != -1
@@ -596,18 +550,6 @@ def test_post_conversation_with_local_image_url_in_history(
assert messages == [
ModelRequest(
parts=[
SystemPromptPart(
content="You are a helpful test assistant :)",
timestamp=timezone.now(),
),
SystemPromptPart(
content=today_promt_date,
timestamp=timezone.now(),
),
SystemPromptPart(
content="Answer in english.",
timestamp=timezone.now(),
),
UserPromptPart(
content=[
"What is in this image?",
@@ -619,7 +561,9 @@ def test_post_conversation_with_local_image_url_in_history(
],
timestamp=timezone.now(),
),
]
],
instructions="You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\nAnswer in english.",
),
ModelResponse(
parts=[TextPart(content="This is an image of a single pixel.")],
@@ -637,6 +581,8 @@ def test_post_conversation_with_local_image_url_in_history(
)
],
run_id=messages[2].run_id,
instructions="You are a helpful test assistant :)\n\n"
"Today is Saturday 18/10/2025.\n\nAnswer in english.",
),
]
yield "This is an image of square, very small and nice."
@@ -735,27 +681,10 @@ def test_post_conversation_with_local_image_url_in_history(
_run_id = chat_conversation.pydantic_messages[2]["run_id"]
assert chat_conversation.pydantic_messages == [
{
"instructions": None,
"instructions": f"You are a helpful test assistant :)\n\n{today_promt_date}"
"\n\nAnswer in english.",
"kind": "request",
"parts": [
{
"content": "You are a helpful test assistant :)",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": today_promt_date,
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": "Answer in english.",
"dynamic_ref": None,
"part_kind": "system-prompt",
"timestamp": "2025-10-18T20:48:20.286204Z",
},
{
"content": [
"What is in this image?",
@@ -796,7 +725,8 @@ def test_post_conversation_with_local_image_url_in_history(
},
},
{
"instructions": None,
"instructions": "You are a helpful test assistant :)\n\nToday is Saturday 18/10/2025."
"\n\nAnswer in english.",
"kind": "request",
"parts": [
{
@@ -1,4 +1,4 @@
"""Test the post_stop_steaming view."""
"""Test the post_stop_streaming view."""
from unittest.mock import patch
@@ -39,7 +39,7 @@ def add_document_rag_search_tool(agent: Agent) -> None:
metadata={"sources": {result.url for result in rag_results.data}},
)
@agent.system_prompt
@agent.instructions
def document_rag_instructions() -> str:
"""Dynamic system prompt function to add RAG instructions if any."""
return (
-11
View File
@@ -3,17 +3,6 @@
from pydantic_ai import ModelRetry
class ModelRetryLast(ModelRetry):
"""
Same as ModelRetry but also holds the last retry message to return when all attempts failed.
"""
def __init__(self, message: str, last_retry_message: str):
"""Initialize ModelRetryLast with message and last retry message."""
self.last_retry_message = last_retry_message
super().__init__(message)
class ModelCannotRetry(ModelRetry):
"""
Exception to raise when a tool function cannot be retried.
+1 -1
View File
@@ -221,7 +221,7 @@ class ChatViewSet( # pylint: disable=too-many-ancestors, abstract-method
url_path="stop-streaming",
url_name="stop-streaming",
)
def post_stop_steaming(self, request, pk): # pylint: disable=unused-argument
def post_stop_streaming(self, request, pk): # pylint: disable=unused-argument
"""Handle POST requests to stop streaming the chat conversation.
This action will put a poison pill in the redis cache to stop any ongoing streaming.
-14
View File
@@ -1,12 +1,9 @@
"""Conversations core API endpoints"""
from django.conf import settings
from django.core.exceptions import ValidationError
from rest_framework import exceptions as drf_exceptions
from rest_framework import views as drf_views
from rest_framework.decorators import api_view
from rest_framework.response import Response
def exception_handler(exc, context):
@@ -28,14 +25,3 @@ def exception_handler(exc, context):
exc = drf_exceptions.ValidationError(detail=detail)
return drf_views.exception_handler(exc, context)
# pylint: disable=unused-argument
@api_view(["GET"])
def get_frontend_configuration(request):
"""Returns the frontend configuration dict as configured in settings."""
frontend_configuration = {
"LANGUAGE_CODE": settings.LANGUAGE_CODE,
}
frontend_configuration.update(settings.FRONTEND_CONFIGURATION)
return Response(frontend_configuration)
-20
View File
@@ -20,23 +20,3 @@ class UserSerializer(serializers.ModelSerializer):
"sub",
]
read_only_fields = ["id", "email", "full_name", "short_name", "sub"]
class UserLightSerializer(UserSerializer):
"""Serialize users with limited fields."""
id = serializers.SerializerMethodField(read_only=True)
email = serializers.SerializerMethodField(read_only=True)
def get_id(self, _user):
"""Return always None. Here to have the same fields than in UserSerializer."""
return None
def get_email(self, _user):
"""Return always None. Here to have the same fields than in UserSerializer."""
return None
class Meta:
model = models.User
fields = ["id", "email", "full_name", "short_name"]
read_only_fields = ["id", "email", "full_name", "short_name"]
@@ -1,52 +0,0 @@
"""Custom authentication classes for the Conversations core app"""
from django.conf import settings
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
class ServerToServerAuthentication(BaseAuthentication):
"""
Custom authentication class for server-to-server requests.
Validates the presence and correctness of the Authorization header.
"""
AUTH_HEADER = "Authorization"
TOKEN_TYPE = "Bearer" # noqa S105
def authenticate(self, request):
"""
Authenticate the server-to-server request by validating the Authorization header.
This method checks if the Authorization header is present in the request, ensures it
contains a valid token with the correct format, and verifies the token against the
list of allowed server-to-server tokens. If the header is missing, improperly formatted,
or contains an invalid token, an AuthenticationFailed exception is raised.
Returns:
None: If authentication is successful
(no user is authenticated for server-to-server requests).
Raises:
AuthenticationFailed: If the Authorization header is missing, malformed,
or contains an invalid token.
"""
auth_header = request.headers.get(self.AUTH_HEADER)
if not auth_header:
raise AuthenticationFailed("Authorization header is missing.")
# Validate token format and existence
auth_parts = auth_header.split(" ")
if len(auth_parts) != 2 or auth_parts[0] != self.TOKEN_TYPE:
raise AuthenticationFailed("Invalid authorization header.")
token = auth_parts[1]
if token not in settings.SERVER_TO_SERVER_API_TOKENS:
raise AuthenticationFailed("Invalid server-to-server token.")
# Authentication is successful, but no user is authenticated
def authenticate_header(self, request):
"""Return the WWW-Authenticate header value."""
return f"{self.TOKEN_TYPE} realm='Create document server to server'"
-25
View File
@@ -1,25 +0,0 @@
"""A JSONField for DRF to handle serialization/deserialization."""
import json
from rest_framework import serializers
class JSONField(serializers.Field):
"""
A custom field for handling JSON data.
"""
def to_representation(self, value):
"""
Convert the JSON string to a Python dictionary for serialization.
"""
return value
def to_internal_value(self, data):
"""
Convert the Python dictionary to a JSON string for deserialization.
"""
if data is None:
return None
return json.dumps(data)
-22
View File
@@ -2,31 +2,9 @@
import unicodedata
import django_filters
def remove_accents(value):
"""Remove accents from a string (vélo -> velo)."""
return "".join(
c for c in unicodedata.normalize("NFD", value) if unicodedata.category(c) != "Mn"
)
class AccentInsensitiveCharFilter(django_filters.CharFilter):
"""
A custom CharFilter that filters on the accent-insensitive value searched.
"""
def filter(self, qs, value):
"""
Apply the filter to the queryset using the unaccented version of the field.
Args:
qs: The queryset to filter.
value: The value to search for in the unaccented field.
Returns:
A filtered queryset.
"""
if value:
value = remove_accents(value)
return super().filter(qs, value)
@@ -0,0 +1,17 @@
# Generated by Django 5.2.8 on 2025-12-01 08:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0002_user_allow_conversation_analytics"),
]
operations = [
migrations.AlterField(
model_name="user",
name="short_name",
field=models.CharField(blank=True, max_length=50, null=True, verbose_name="short name"),
),
]
+1 -1
View File
@@ -114,7 +114,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
)
full_name = models.CharField(_("full name"), max_length=100, null=True, blank=True)
short_name = models.CharField(_("short name"), max_length=20, null=True, blank=True)
short_name = models.CharField(_("short name"), max_length=50, null=True, blank=True)
email = models.EmailField(_("identity email address"), blank=True, null=True)
@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Generate Document</title>
</head>
<body>
<h2>Generate Document</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Generate PDF</button>
</form>
</body>
</html>
@@ -1,58 +0,0 @@
"""Custom template tags for the core application of People."""
import base64
from django import template
from django.contrib.staticfiles import finders
from PIL import ImageFile as PillowImageFile
register = template.Library()
def image_to_base64(file_or_path, close=False):
"""
Return the src string of the base64 encoding of an image represented by its path
or file opened or not.
Inspired by Django's "get_image_dimensions"
"""
pil_parser = PillowImageFile.Parser()
if hasattr(file_or_path, "read"):
file = file_or_path
if file.closed and hasattr(file, "open"):
file_or_path.open()
file_pos = file.tell()
file.seek(0)
else:
try:
# pylint: disable=consider-using-with
file = open(file_or_path, "rb")
except OSError:
return ""
close = True
try:
image_data = file.read()
if not image_data:
return ""
pil_parser.feed(image_data)
if pil_parser.image:
mime_type = pil_parser.image.get_format_mimetype()
encoded_string = base64.b64encode(image_data)
return f"data:{mime_type:s};base64, {encoded_string.decode('utf-8'):s}"
return ""
finally:
if close:
file.close()
else:
file.seek(file_pos)
@register.simple_tag
def base64_static(path):
"""Return a static file into a base64."""
full_path = finders.find(path)
if full_path:
return image_to_base64(full_path, True)
return ""
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2025-11-17 16:37\n"
"PO-Revision-Date: 2026-01-16 11:04\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2025-11-17 16:37\n"
"PO-Revision-Date: 2026-01-16 11:04\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2025-11-17 16:37\n"
"PO-Revision-Date: 2026-01-16 11:04\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2025-11-17 16:37\n"
"PO-Revision-Date: 2026-01-16 11:04\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2025-11-17 16:37\n"
"PO-Revision-Date: 2026-01-16 11:04\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2025-11-17 16:37\n"
"PO-Revision-Date: 2026-01-16 11:04\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
+2 -2
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "conversations"
version = "0.0.9"
version = "0.0.10"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -39,7 +39,7 @@ dependencies = [
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django==5.2.8",
"django==5.2.9",
"djangorestframework==3.16.1",
"drf_spectacular==0.29.0",
"dockerflow==2024.4.2",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "app-conversations",
"version": "0.0.9",
"version": "0.0.11",
"private": true,
"scripts": {
"dev": "next dev",
@@ -36,16 +36,16 @@
"i18next-browser-languagedetector": "8.1.0",
"idb": "8.0.3",
"lodash": "4.17.21",
"lottie-react": "^2.4.1",
"luxon": "3.6.1",
"micromark-extension-llm-math": "3.1.1-20250610",
"next": "15.3.3",
"next": "15.3.8",
"posthog-js": "1.249.3",
"react": "*",
"react": "19.2.1",
"react-aria-components": "1.9.0",
"react-dom": "18.3.1",
"react-dom": "19.2.1",
"react-i18next": "15.5.2",
"react-intersection-observer": "9.16.0",
"react-lottie": "^1.2.10",
"react-markdown": "10.1.0",
"react-select": "5.10.1",
"rehype-katex": "7.0.1",
@@ -1,4 +1,4 @@
import { PropsWithChildren, useRef, useState } from 'react';
import { PropsWithChildren, useEffect, useRef, useState } from 'react';
import { css } from 'styled-components';
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
@@ -35,8 +35,16 @@ export const DropdownMenu = ({
}: PropsWithChildren<DropdownMenuProps>) => {
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const [isOpen, setIsOpen] = useState(false);
const [buttonWidth, setButtonWidth] = useState<number | undefined>(undefined);
const blockButtonRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Mettre à jour la largeur uniquement côté client
if (blockButtonRef.current) {
setButtonWidth(blockButtonRef.current.clientWidth);
}
}, [isOpen]);
const onOpenChange = (isOpen: boolean) => {
setIsOpen(isOpen);
};
@@ -76,7 +84,7 @@ export const DropdownMenu = ({
>
<Box
$maxWidth="320px"
$minWidth={`${blockButtonRef.current?.clientWidth}px`}
$minWidth={buttonWidth ? `${buttonWidth}px` : undefined}
role="menu"
>
{topMessage && (
@@ -1,20 +1,17 @@
import Lottie from 'react-lottie';
import dynamic from 'next/dynamic';
const Lottie = dynamic(() => import('lottie-react'), { ssr: false });
import searchingAnimation from '@/assets/lotties/searching';
export const Loader = () => {
const LoaderOptions = {
loop: true,
autoplay: true,
animationData: searchingAnimation,
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice',
},
} as const;
export function Loader() {
return (
<div>
<Lottie options={LoaderOptions} height={24} width={24} />
<div role="status">
<Lottie
animationData={searchingAnimation}
loop
autoplay
style={{ width: 24, height: 24 }}
/>
</div>
);
};
}
@@ -3,6 +3,7 @@ import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { createPortal } from 'react-dom';
@@ -47,6 +48,11 @@ interface ToastProviderProps {
export const ToastProvider = ({ children }: ToastProviderProps) => {
const [toasts, setToasts] = useState<ToastItem[]>([]);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
const showToast = useCallback(
(
@@ -83,7 +89,9 @@ export const ToastProvider = ({ children }: ToastProviderProps) => {
return (
<ToastContext.Provider value={value}>
{children}
{typeof window !== 'undefined' &&
{isMounted &&
typeof document !== 'undefined' &&
document.body &&
createPortal(
<Box
aria-live="polite"
@@ -1,74 +1,64 @@
import { CunninghamProvider } from '@openfun/cunningham-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import dynamic from 'next/dynamic';
import { useEffect } from 'react';
import { ToastProvider } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth';
import { useResponsiveStore } from '@/stores/';
import { useResponsiveStore } from '@/stores';
import { ConfigProvider } from './config/';
import { ConfigProvider } from './config';
// Client-only providers
const ToastProviderNoSSR = dynamic(
() => import('@/components').then((mod) => ({ default: mod.ToastProvider })),
{ ssr: false, loading: () => null },
);
const CunninghamProviderNoSSR = dynamic(
() =>
import('@openfun/cunningham-react').then((mod) => ({
default: mod.CunninghamProvider,
})),
{ ssr: false },
);
/**
* QueryClient:
* - defaultOptions:
* - staleTime:
* - global cache duration - we decided 3 minutes
* - It can be overridden to each query
*/
const defaultOptions = {
queries: {
staleTime: 1000 * 60 * 3,
retry: 1,
},
};
const queryClient = new QueryClient({
defaultOptions,
defaultOptions: {
queries: { staleTime: 1000 * 60 * 3, retry: 1 },
mutations: {
onError: (error) => {
if (
error instanceof Error &&
'status' in error &&
error.status === 401
) {
void queryClient.resetQueries({ queryKey: [KEY_AUTH] });
setAuthUrl();
if (typeof window !== 'undefined') {
window.location.href = '/401';
}
}
},
},
},
});
export function AppProvider({ children }: { children: React.ReactNode }) {
const { theme } = useCunninghamTheme();
const { replace } = useRouter();
const initializeResizeListener = useResponsiveStore(
(state) => state.initializeResizeListener,
);
const theme = useCunninghamTheme((state) => state.theme);
useEffect(() => {
return initializeResizeListener();
}, [initializeResizeListener]);
useEffect(() => {
queryClient.setDefaultOptions({
...defaultOptions,
mutations: {
onError: (error) => {
if (
error instanceof Error &&
'status' in error &&
error.status === 401
) {
void queryClient.resetQueries({
queryKey: [KEY_AUTH],
});
setAuthUrl();
void replace(`/401`);
}
},
},
});
}, [replace]);
return useResponsiveStore.getState().initializeResizeListener();
}, []);
return (
<QueryClientProvider client={queryClient}>
<CunninghamProvider theme={theme}>
<CunninghamProviderNoSSR theme={theme}>
<ConfigProvider>
<ToastProvider>
<ToastProviderNoSSR>
<Auth>{children}</Auth>
</ToastProvider>
</ToastProviderNoSSR>
</ConfigProvider>
</CunninghamProvider>
</CunninghamProviderNoSSR>
</QueryClientProvider>
);
}
@@ -55,7 +55,7 @@ export const AttachmentList = ({
>
<Box
$background="var(--c--theme--colors--greyscale-050)"
$minWidth="200px"
$width="200px"
$direction="row"
$gap="8px"
$align="center"
@@ -1,9 +1,4 @@
import {
Message,
ReasoningUIPart,
SourceUIPart,
ToolInvocationUIPart,
} from '@ai-sdk/ui-utils';
import { Message, SourceUIPart, ToolInvocationUIPart } from '@ai-sdk/ui-utils';
import { Modal, ModalSize } from '@openfun/cunningham-react';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
import { useRouter } from 'next/router';
@@ -17,7 +12,7 @@ import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Box, Icon, Loader, StyledLink, Text } from '@/components';
import { Box, Icon, Loader, Text } from '@/components';
import { useUploadFile } from '@/features/attachments/hooks/useUploadFile';
import { useChat } from '@/features/chat/api/useChat';
import { getConversation } from '@/features/chat/api/useConversation';
@@ -27,6 +22,7 @@ import {
useLLMConfiguration,
} from '@/features/chat/api/useLLMConfiguration';
import { AttachmentList } from '@/features/chat/components/AttachmentList';
import { ChatError } from '@/features/chat/components/ChatError';
import { CodeBlock } from '@/features/chat/components/CodeBlock';
import { FeedbackButtons } from '@/features/chat/components/FeedbackButtons';
import { InputChat } from '@/features/chat/components/InputChat';
@@ -103,19 +99,14 @@ export const Chat = ({
if (modelToSelect) {
setSelectedModel(modelToSelect);
setSelectedModelHrid(modelToSelect.hrid);
}
}
}, [llmConfig, selectedModel, selectedModelHrid]);
// Update store when model selection changes
useEffect(() => {
if (selectedModel?.hrid !== selectedModelHrid) {
setSelectedModelHrid(selectedModel?.hrid || null);
}
}, [selectedModel, selectedModelHrid, setSelectedModelHrid]);
}, [llmConfig, selectedModel, selectedModelHrid, setSelectedModelHrid]);
const handleModelSelect = (model: LLMModel) => {
setSelectedModel(model);
setSelectedModelHrid(model.hrid);
};
const router = useRouter();
@@ -160,12 +151,21 @@ export const Chat = ({
forceWebSearch?: boolean;
} | null>(null);
const [shouldAutoSubmit, setShouldAutoSubmit] = useState(false);
const [shouldRetry, setShouldRetry] = useState(false);
const retryOriginalInputRef = useRef<string>('');
const retryOriginalFilesRef = useRef<FileList | null>(null);
const [hasInitialized, setHasInitialized] = useState(false);
const [streamingMessageHeight, setStreamingMessageHeight] = useState<
number | null
>(null);
const lastUserMessageIdRef = useRef<string | null>(null);
const hasScrolledToBottomOnLoadRef = useRef(false);
const lastSubmissionRef = useRef<{
input: string;
files: FileList | null;
event: FormEvent<HTMLFormElement>;
options?: Record<string, unknown>;
} | null>(null);
const { mutate: createChatConversation } = useCreateChatConversation();
@@ -214,6 +214,7 @@ export const Chat = ({
handleInputChange,
status,
stop: stopChat,
setMessages,
} = useChat({
id: conversationId,
initialMessages: initialConversationMessages,
@@ -250,6 +251,29 @@ export const Chat = ({
void handleSubmit(event);
};
const handleRetry = () => {
if (!lastSubmissionRef.current || !setMessages) {
return;
}
const { input: lastInput, files: lastFiles } = lastSubmissionRef.current;
const lastAssistantIndex = messages.findLastIndex(
(msg) => msg.role === 'assistant',
);
if (lastAssistantIndex !== -1) {
setMessages(messages.filter((_, index) => index !== lastAssistantIndex));
}
retryOriginalInputRef.current = input;
retryOriginalFilesRef.current = files;
handleInputChange({
target: { value: lastInput },
} as ChangeEvent<HTMLTextAreaElement>);
setFiles(lastFiles);
setShouldRetry(true);
};
// Précharger les métadonnées des sources dès que les messages arrivent
useEffect(() => {
messages.forEach((message) => {
@@ -262,7 +286,8 @@ export const Chat = ({
});
}
});
}, [messages, prefetchMetadata]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messages]);
const openSources = (messageId: string) => {
if (isSourceOpen === messageId) {
@@ -305,16 +330,23 @@ export const Chat = ({
const availableHeight = containerHeight - userMessageHeight - 38;
if (streamingMessageHeight !== availableHeight) {
setStreamingMessageHeight(availableHeight);
}
setStreamingMessageHeight((prev) => {
if (prev === null || Math.abs(prev - availableHeight) > 10) {
return availableHeight;
}
return prev;
});
}
}
}
}, [messages, streamingMessageHeight]);
}, [messages]);
// Détecter l'arrivée d'un nouveau message user et retirer la hauteur de l'ancien
useEffect(() => {
if (status === 'streaming') {
return;
}
const userMessages = messages.filter((msg) => msg.role === 'user');
const lastUserMessage = userMessages[userMessages.length - 1];
@@ -327,14 +359,14 @@ export const Chat = ({
}
lastUserMessageIdRef.current = lastUserMessage.id;
}
}, [messages]);
}, [messages, status]);
// Calculer la hauteur pendant submitted/streaming
useEffect(() => {
if (status === 'submitted' || status === 'streaming') {
calculateStreamingHeight();
}
}, [status, calculateStreamingHeight]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status]);
// Scroller vers la question au moment du submit
useEffect(() => {
@@ -354,7 +386,8 @@ export const Chat = ({
messageElement?.scrollIntoView({ block: 'start', behavior: 'smooth' });
});
}, [status, messages]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status]);
// Synchronize conversationId state with prop when it changes (e.g., after navigation)
useEffect(() => {
@@ -366,7 +399,8 @@ export const Chat = ({
} as ChangeEvent<HTMLTextAreaElement>);
setHasInitialized(false); // Réinitialiser pour permettre le scroll au prochain chargement
}
}, [initialConversationId, conversationId, handleInputChange]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialConversationId, conversationId]);
// On mount, if there is pending input/files, initialize state and set flag
useEffect(() => {
@@ -406,6 +440,25 @@ export const Chat = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldAutoSubmit, input, files]);
useEffect(() => {
if (
shouldRetry &&
lastSubmissionRef.current &&
input === lastSubmissionRef.current.input
) {
const { event } = lastSubmissionRef.current;
void handleSubmit(event);
handleInputChange({
target: { value: retryOriginalInputRef.current },
} as ChangeEvent<HTMLTextAreaElement>);
setFiles(retryOriginalFilesRef.current);
setShouldRetry(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldRetry, input, files]);
// Fetch initial conversation messages if initialConversationId is provided and no pending input
useEffect(() => {
hasScrolledToBottomOnLoadRef.current = false; // Réinitialiser au début du chargement
@@ -538,6 +591,13 @@ export const Chat = ({
options.experimental_attachments = attachments;
}
lastSubmissionRef.current = {
input,
files,
event,
options: Object.keys(options).length > 0 ? options : undefined,
};
if (Object.keys(options).length > 0) {
baseHandleSubmit(event, options);
} else {
@@ -644,6 +704,11 @@ export const Chat = ({
{message.content && (
<Box
className="mainContent-chat"
data-testid={
message.role === 'assistant'
? 'assistant-message-content'
: undefined
}
$padding={{ all: 'xxs' }}
>
<p className="sr-only">
@@ -651,42 +716,53 @@ export const Chat = ({
? t('You said: ')
: t('Assistant IA replied: ')}
</p>
<MarkdownHooks
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[
[
rehypePrettyCode,
{
theme: 'github-dark-dimmed',
},
],
rehypeKatex,
]}
components={{
// Custom components for Markdown rendering
// eslint-disable-next-line @typescript-eslint/no-unused-vars
p: ({ node, ...props }) => (
<Text
as="p"
$css="display: block"
$theme="greyscale"
$variation="850"
{...props}
/>
),
a: ({ children, ...props }) => (
<a target="_blank" {...props}>
{children}
</a>
),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
pre: ({ node, children, ...props }) => (
<CodeBlock {...props}>{children}</CodeBlock>
),
}}
>
{message.content}
</MarkdownHooks>
{message.role === 'user' ? (
<Text
as="p"
$css="white-space: pre-wrap; display: block;"
$theme="greyscale"
$variation="850"
>
{message.content}
</Text>
) : (
<MarkdownHooks
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[
[
rehypePrettyCode,
{
theme: 'github-dark-dimmed',
},
],
rehypeKatex,
]}
components={{
// Custom components for Markdown rendering
// eslint-disable-next-line @typescript-eslint/no-unused-vars
p: ({ node, ...props }) => (
<Text
as="p"
$css="display: block"
$theme="greyscale"
$variation="850"
{...props}
/>
),
a: ({ children, ...props }) => (
<a target="_blank" {...props}>
{children}
</a>
),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
pre: ({ node, children, ...props }) => (
<CodeBlock {...props}>{children}</CodeBlock>
),
}}
>
{message.content}
</MarkdownHooks>
)}
</Box>
)}
@@ -735,30 +811,12 @@ export const Chat = ({
</Box>
)}
{message.parts
?.filter(
(part) =>
part.type === 'reasoning' ||
part.type === 'tool-invocation',
)
?.filter((part) => part.type === 'tool-invocation')
.map(
(
part: ReasoningUIPart | ToolInvocationUIPart,
partIndex: number,
) =>
part.type === 'reasoning' ? (
<Box
key={`reasoning-${partIndex}`}
$background="var(--c--theme--colors--greyscale-100)"
$color="var(--c--theme--colors--greyscale-500)"
$padding={{ all: 'sm' }}
$radius="md"
$css="font-size: 0.9em;"
>
{part.reasoning}
</Box>
) : part.type === 'tool-invocation' &&
isCurrentlyStreaming &&
isLastAssistantMessageInConversation ? (
(part: ToolInvocationUIPart, partIndex: number) =>
part.type === 'tool-invocation' &&
isCurrentlyStreaming &&
isLastAssistantMessageInConversation ? (
<ToolInvocationItem
key={`tool-invocation-${partIndex}`}
toolInvocation={part.toolInvocation}
@@ -919,27 +977,11 @@ export const Chat = ({
</Text>
</Box>
) : null}
{status === 'error' && (
<Box
$direction={isMobile ? 'column' : 'row'}
$gap="6px"
$width="100%"
$maxWidth="750px"
$margin={{ all: 'auto', top: 'base', bottom: 'md' }}
$padding={{ left: '13px' }}
>
<Text>{t('Sorry, an error occurred. Please try again.')}</Text>
<StyledLink
href="/"
rel="noopener noreferrer"
$css={`
color: var(--c--theme--colors--greyscale-900);
text-decoration: underline;
`}
>
{t('Start a new conversation.')}
</StyledLink>
</Box>
{status === 'error' && !isUploadingFiles && (
<ChatError
hasLastSubmission={!!lastSubmissionRef.current}
onRetry={handleRetry}
/>
)}
</Box>
<Box
@@ -947,6 +989,7 @@ export const Chat = ({
position: relative;
bottom: ${isMobile ? '8px' : '20px'};
margin: auto;
background-color: white;
z-index: 1000;
`}
$gap="6px"
@@ -0,0 +1,80 @@
import { Button } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { useTranslation } from 'react-i18next';
import { Box, Icon, Text } from '@/components';
interface ChatErrorProps {
hasLastSubmission: boolean;
onRetry: () => void;
}
export const ChatError = ({ hasLastSubmission, onRetry }: ChatErrorProps) => {
const { t } = useTranslation();
const router = useRouter();
return (
<Box
$direction="column"
$gap="6px"
$width="100%"
$maxWidth="750px"
$margin={{ all: 'auto', top: 'base', bottom: 'md' }}
$padding={{ left: '13px' }}
>
<Text $variation="550" $theme="greyscale">
{t('Sorry, an error occurred. Please try again.')}
</Text>
<Box
$direction="row"
$gap="6px"
$align="center"
$margin={{ top: '10px' }}
>
{hasLastSubmission ? (
<Button
size="small"
color="tertiary"
onClick={onRetry}
className="retry-button"
style={{
color: 'var(--c--theme--colors--greyscale-550)',
borderColor: 'var(--c--theme--colors--greyscale-300)',
}}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
width="11"
height="15"
viewBox="0 0 11 15"
fill="none"
>
<path
d="M0.733333 10.0333C0.488889 9.61111 0.305556 9.17778 0.183333 8.73333C0.0611111 8.28889 0 7.83333 0 7.36667C0 5.87778 0.516667 4.61111 1.55 3.56667C2.58333 2.52222 3.84444 2 5.33333 2H5.45L4.38333 0.933333L5.31667 0L7.98333 2.66667L5.31667 5.33333L4.38333 4.4L5.45 3.33333H5.33333C4.22222 3.33333 3.27778 3.725 2.5 4.50833C1.72222 5.29167 1.33333 6.24444 1.33333 7.36667C1.33333 7.65556 1.36667 7.93889 1.43333 8.21667C1.5 8.49444 1.6 8.76667 1.73333 9.03333L0.733333 10.0333ZM5.35 14.6667L2.68333 12L5.35 9.33333L6.28333 10.2667L5.21667 11.3333H5.33333C6.44444 11.3333 7.38889 10.9417 8.16667 10.1583C8.94444 9.375 9.33333 8.42222 9.33333 7.3C9.33333 7.01111 9.3 6.72778 9.23333 6.45C9.16667 6.17222 9.06667 5.9 8.93333 5.63333L9.93333 4.63333C10.1778 5.05556 10.3611 5.48889 10.4833 5.93333C10.6056 6.37778 10.6667 6.83333 10.6667 7.3C10.6667 8.78889 10.15 10.0556 9.11667 11.1C8.08333 12.1444 6.82222 12.6667 5.33333 12.6667H5.21667L6.28333 13.7333L5.35 14.6667Z"
fill="currentColor"
/>
</svg>
}
>
{t('Retry')}
</Button>
) : (
<Button
size="small"
color="tertiary"
style={{
color: 'var(--c--theme--colors--greyscale-550)',
borderColor: 'var(--c--theme--colors--greyscale-300)',
}}
onClick={() => {
void router.push('/');
}}
icon={<Icon iconName="add" $color="greyscale" />}
>
{t('Start a new conversation')}
</Button>
)}
</Box>
</Box>
);
};
@@ -300,7 +300,6 @@ export const InputChat = ({
$css={`
display: block;
position: relative;
opacity: ${status === 'error' ? '0.5' : '1'};
margin: auto;
width: 100%;
padding: ${isDesktop ? '0' : '0 10px'};
@@ -308,26 +307,22 @@ export const InputChat = ({
`}
>
{/* Bouton de scroll vers le bas */}
{messagesLength > 1 &&
status !== 'streaming' &&
status !== 'submitted' &&
containerRef &&
onScrollToBottom && (
<Box
$css={`
{messagesLength > 1 && containerRef && onScrollToBottom && (
<Box
$css={`
position: relative;
height: 0;
width: 100%;
margin: auto;
max-width: 750px;
`}
>
<ScrollDown
onClick={onScrollToBottom}
containerRef={containerRef}
/>
</Box>
)}
>
<ScrollDown
onClick={onScrollToBottom}
containerRef={containerRef}
/>
</Box>
)}
{/* Message de bienvenue */}
{messagesLength === 0 && (
<Box
@@ -423,6 +418,7 @@ export const InputChat = ({
fontSize: '1rem',
border: 'none',
resize: 'none',
opacity: status === 'error' ? '0.5' : '1',
fontFamily: 'inherit',
minHeight: '64px',
maxHeight: '200px',
@@ -573,6 +569,9 @@ export const InputChat = ({
$gap="sm"
$padding={{ bottom: 'base' }}
$align="space-between"
$css={`
opacity: ${status === 'error' ? '0.5' : '1'};
`}
>
<Box
$flex="1"
@@ -40,7 +40,7 @@ export const ToolInvocationItem: React.FC<ToolInvocationItemProps> = ({
$color="var(--c--theme--colors--greyscale-500)"
$padding={{ all: 'sm' }}
$radius="8px"
$css="font-size: 0.9em;"
$css="font-size: 0.9em; width: 100%; white-space: pre-wrap; word-wrap: break-word;"
>
{toolInvocation.state === 'result' ? (
<Text>{`Parsing done: ${documentIdentifiers.join(', ')}`}</Text>
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useCallback, useRef, useState } from 'react';
interface SourceMetadata {
title: string | null;
@@ -9,109 +9,124 @@ interface SourceMetadata {
// Cache global pour éviter de refetch les mêmes URLs
const metadataCache = new Map<string, SourceMetadata>();
const fetchingUrls = new Set<string>();
export const useSourceMetadataCache = () => {
const [cache, setCache] =
useState<Map<string, SourceMetadata>>(metadataCache);
const [, forceUpdate] = useState({});
const updateCountRef = useRef(0);
const prefetchMetadata = async (url: string) => {
// Si déjà en cache, ne rien faire
if (metadataCache.has(url)) {
return;
const triggerUpdate = useCallback(() => {
updateCountRef.current++;
if (updateCountRef.current % 5 === 0) {
forceUpdate({});
}
}, []);
// Marquer comme en cours de chargement
metadataCache.set(url, {
title: null,
favicon: null,
loading: true,
error: false,
});
setCache(new Map(metadataCache));
try {
if (!url.startsWith('http')) {
metadataCache.set(url, {
title: url,
favicon: '📄',
loading: false,
error: false,
});
setCache(new Map(metadataCache));
const prefetchMetadata = useCallback(
async (url: string) => {
if (metadataCache.has(url) || fetchingUrls.has(url)) {
return;
}
const parser = new DOMParser();
fetchingUrls.add(url);
metadataCache.set(url, {
title: null,
favicon: null,
loading: true,
error: false,
});
triggerUpdate();
let response;
try {
response = await fetch(url, {
mode: 'cors',
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; ChatBot/1.0)',
},
if (!url.startsWith('http')) {
metadataCache.set(url, {
title: url,
favicon: '📄',
loading: false,
error: false,
});
fetchingUrls.delete(url);
triggerUpdate();
return;
}
const parser = new DOMParser();
let response;
try {
response = await fetch(url, {
mode: 'cors',
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; ChatBot/1.0)',
},
});
} catch {
// Si CORS échoue, utiliser juste le hostname
metadataCache.set(url, {
title: new URL(url).hostname,
favicon: null,
loading: false,
error: false,
});
fetchingUrls.delete(url);
triggerUpdate();
return;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const html = await response.text();
const doc = parser.parseFromString(html, 'text/html');
// Récupérer le titre
const pageTitle =
doc.querySelector('title')?.textContent || new URL(url).hostname;
// Récupérer le favicon
let faviconUrl =
doc.querySelector('link[rel="icon"]')?.getAttribute('href') ||
doc.querySelector('link[rel="shortcut icon"]')?.getAttribute('href');
if (!faviconUrl) {
const urlObj = new URL(url);
faviconUrl = `${urlObj.origin}/favicon.ico`;
}
// Convertir les URLs relatives en absolues
if (faviconUrl && !faviconUrl.startsWith('http')) {
const urlObj = new URL(url);
faviconUrl = new URL(faviconUrl, urlObj.origin).href;
}
metadataCache.set(url, {
title: pageTitle,
favicon: faviconUrl || null,
loading: false,
error: false,
});
} catch {
// Si CORS échoue, utiliser juste le hostname
fetchingUrls.delete(url);
triggerUpdate();
} catch (err) {
console.log('Error fetching metadata for:', url, err);
metadataCache.set(url, {
title: new URL(url).hostname,
favicon: null,
loading: false,
error: false,
error: true,
});
setCache(new Map(metadataCache));
return;
fetchingUrls.delete(url);
triggerUpdate();
}
},
[triggerUpdate],
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const getMetadata = useCallback((url: string): SourceMetadata | undefined => {
return metadataCache.get(url);
}, []);
const html = await response.text();
const doc = parser.parseFromString(html, 'text/html');
// Récupérer le titre
const pageTitle =
doc.querySelector('title')?.textContent || new URL(url).hostname;
// Récupérer le favicon
let faviconUrl =
doc.querySelector('link[rel="icon"]')?.getAttribute('href') ||
doc.querySelector('link[rel="shortcut icon"]')?.getAttribute('href');
if (!faviconUrl) {
const urlObj = new URL(url);
faviconUrl = `${urlObj.origin}/favicon.ico`;
}
// Convertir les URLs relatives en absolues
if (faviconUrl && !faviconUrl.startsWith('http')) {
const urlObj = new URL(url);
faviconUrl = new URL(faviconUrl, urlObj.origin).href;
}
metadataCache.set(url, {
title: pageTitle,
favicon: faviconUrl || null,
loading: false,
error: false,
});
setCache(new Map(metadataCache));
} catch (err) {
console.log('Error fetching metadata for:', url, err);
metadataCache.set(url, {
title: new URL(url).hostname,
favicon: null,
loading: false,
error: true,
});
setCache(new Map(metadataCache));
}
};
const getMetadata = (url: string): SourceMetadata | undefined => {
return cache.get(url);
};
return { prefetchMetadata, getMetadata, cache };
return { prefetchMetadata, getMetadata };
};
@@ -58,7 +58,7 @@ export const LeftPanelConversationItem = ({
>
<StyledLink
href={`/chat/${conversation.id}/`}
$css="overflow: auto; flex-grow: 1;"
$css="overflow: auto; flex-grow: 1; color: var(--c--theme--colors--greyscale-900);"
onClick={handleLinkClick}
>
<SimpleConversationItem showAccesses conversation={conversation} />
@@ -60,7 +60,7 @@
"Give feedback": "Faire un retour",
"History": "Historique",
"Home": "Accueil",
"If enabled, this allows us to analyse your exchanges to improve the Assistant. If disabled, all conversations remain confidential and are not used in any way. ": "Si cette option est activée, cela nous permet d'analyser vos conversations afin d'améliorer l'Assistant. Si elle est désactivée, toutes les conversations restent confidentielles et ne sont utilisées d'aucune manière ",
"If enabled, this allows us to analyse your exchanges to improve the Assistant. If disabled, all conversations remain confidential and are not used in any way. ": "Si cette option est activée, cela nous permet d'analyser vos conversations afin d'améliorer l'Assistant. Si elle est désactivée, toutes les conversations restent confidentielles et ne sont utilisées d'aucune manière. ",
"Illustration": "Image",
"Image 401": "Image 401",
"Image 403": "Image 403",
@@ -88,6 +88,7 @@
"Quick search input": "Saisie de recherche rapide",
"Remove attachment": "Supprimer la pièce jointe",
"Research on the web": "Rechercher sur le web",
"Retry": "Réessayer",
"Search": "Rechercher",
"Search for a chat": "Rechercher un chat",
"Search results": "Résultats de la recherche",
@@ -100,7 +101,7 @@
"Simple chat icon": "Icône de chat simple",
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, veuillez réessayer.",
"Sorry, an error occurred. Please try again.": "Désolé, une erreur s'est produite. Veuillez réessayer.",
"Start a new conversation.": "Commencer une nouvelle conversation.",
"Start a new conversation": "Commencer une nouvelle conversation",
"Start conversation": "Entamer la conversation",
"Stop": "Stop",
"Summarizing...": "Résumé en cours...",
@@ -220,6 +221,7 @@
"Quick search input": "Snelle zoekinvoer",
"Remove attachment": "Bijlage verwijderen",
"Research on the web": "Onderzoek op het internet",
"Retry": "Opnieuw proberen",
"Search": "Zoek",
"Search for a chat": "Zoek naar een chat",
"Search results": "Zoekresultaten",
@@ -232,7 +234,7 @@
"Simple chat icon": "Eenvoudig chatpictogram",
"Something bad happens, please retry.": "Er is iets misgegaan. Probeer het opnieuw.",
"Sorry, an error occurred. Please try again.": "Sorry, er is een fout opgetreden. Probeer het opnieuw.",
"Start a new conversation.": "Begin een nieuw gesprek.",
"Start a new conversation": "Begin een nieuw gesprek",
"Start conversation": "Begin een gesprek",
"Stop": "Stop",
"Summarizing...": "Samenvatten...",
@@ -352,6 +354,7 @@
"Quick search input": "Быстрый поиск",
"Remove attachment": "Удалить вложение",
"Research on the web": "Исследование в Интернете",
"Retry": "Повторить",
"Search": "Поиск",
"Search for a chat": "Поиск беседы",
"Search results": "Результаты поиска",
@@ -364,7 +367,7 @@
"Simple chat icon": "Простой значок чата",
"Something bad happens, please retry.": "Что-то пошло не так, повторите попытку.",
"Sorry, an error occurred. Please try again.": "Извините, произошла ошибка. Пожалуйста, попробуйте ещё раз.",
"Start a new conversation.": "Начать новую беседу.",
"Start a new conversation": "Начать новую беседу",
"Start conversation": "Начать беседу",
"Stop": "Остановить",
"Summarizing...": "Обобщение...",
@@ -484,6 +487,7 @@
"Quick search input": "Швидкий пошук",
"Remove attachment": "Видалити вкладення",
"Research on the web": "Дослідження в Інтернеті",
"Retry": "Повторити",
"Search": "Пошук",
"Search for a chat": "Пошук розмови",
"Search results": "Результати пошуку",
@@ -496,7 +500,7 @@
"Simple chat icon": "Проста піктограма розмови",
"Something bad happens, please retry.": "Сталася помилка, спробуйте ще раз.",
"Sorry, an error occurred. Please try again.": "Вибачте, виникла помилка. Спробуйте ще раз.",
"Start a new conversation.": "Розпочати нову розмову.",
"Start a new conversation": "Розпочати нову розмову",
"Start conversation": "Почати розмову",
"Stop": "Зупинити",
"Summarizing...": "Узагальнення...",
@@ -47,16 +47,12 @@ test.describe('Chat page', () => {
await page.keyboard.press('Enter');
// Wait for the response to appear
await page
.getByRole('button', { name: 'See more' })
.waitFor({ timeout: 10000 });
const copyButton = page.getByRole('button', { name: 'Copy' });
await expect(copyButton).toBeVisible();
const messageContent = page.getByText('Lorem ipsum dolor sit amet');
const messageContent = page.getByTestId('assistant-message-content');
await expect(messageContent).toBeVisible();
await expect(messageContent).not.toBeEmpty();
// Check history
const chatHistoryLink = page
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "0.0.9",
"version": "0.0.11",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "conversations",
"version": "0.0.9",
"version": "0.0.11",
"private": true,
"workspaces": {
"packages": [
@@ -32,8 +32,8 @@
"@typescript-eslint/eslint-plugin": "8.33.1",
"@typescript-eslint/parser": "8.33.1",
"eslint": "8.57.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react": "19.2.1",
"react-dom": "19.2.1",
"typescript": "5.8.3"
}
}
@@ -1,6 +1,6 @@
{
"name": "eslint-config-conversations",
"version": "0.0.9",
"version": "0.0.11",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "0.0.9",
"version": "0.0.11",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:conversations",
+71 -91
View File
@@ -1971,10 +1971,10 @@
"@emnapi/runtime" "^1.4.3"
"@tybys/wasm-util" "^0.9.0"
"@next/env@15.3.3":
version "15.3.3"
resolved "https://registry.npmjs.org/@next/env/-/env-15.3.3.tgz"
integrity sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==
"@next/env@15.3.8":
version "15.3.8"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.3.8.tgz#02326c38d315d72f2ab8b2bcc9a5c81ec5482873"
integrity sha512-SAfHg0g91MQVMPioeFeDjE+8UPF3j3BvHjs8ZKJAUz1BG7eMPvfCKOAgNWJ6s1MLNeP6O2InKQRTNblxPWuq+Q==
"@next/eslint-plugin-next@15.3.3":
version "15.3.3"
@@ -1983,45 +1983,45 @@
dependencies:
fast-glob "3.3.1"
"@next/swc-darwin-arm64@15.3.3":
version "15.3.3"
resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.3.tgz"
integrity sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg==
"@next/swc-darwin-arm64@15.3.5":
version "15.3.5"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.5.tgz#75606cb72e1659a23f15195dba760dc01b186c5d"
integrity sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==
"@next/swc-darwin-x64@15.3.3":
version "15.3.3"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz#71588bad245180ffd1af1e1f894477287e739eb0"
integrity sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==
"@next/swc-darwin-x64@15.3.5":
version "15.3.5"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.5.tgz#78ffad7ef26685e5b8150891b467a4ecef94e179"
integrity sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==
"@next/swc-linux-arm64-gnu@15.3.3":
version "15.3.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz#66a15f749c14f04a89f8c7e21c7a8d343fc34e6e"
integrity sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==
"@next/swc-linux-arm64-gnu@15.3.5":
version "15.3.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.5.tgz#d9a405ceec729d62033dbdc48f8c331c544f09fd"
integrity sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==
"@next/swc-linux-arm64-musl@15.3.3":
version "15.3.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz#14bd66213f7f33d6909574750bcb05037221a2ac"
integrity sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==
"@next/swc-linux-arm64-musl@15.3.5":
version "15.3.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.5.tgz#65f19ad3ecd2881381ec2a149afba261ba180dde"
integrity sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==
"@next/swc-linux-x64-gnu@15.3.3":
version "15.3.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz#4a19434545e5e752d9a3ed71f9b34982725f6293"
integrity sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==
"@next/swc-linux-x64-gnu@15.3.5":
version "15.3.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.5.tgz#cd7f7e002212360b99f7e791a2d2fedb352f2374"
integrity sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==
"@next/swc-linux-x64-musl@15.3.3":
version "15.3.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz#41ab140dd0a04ab7291adbec5836c1ce251a588c"
integrity sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==
"@next/swc-linux-x64-musl@15.3.5":
version "15.3.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.5.tgz#302c9e4ace935c963d45fce9584754a19295c452"
integrity sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==
"@next/swc-win32-arm64-msvc@15.3.3":
version "15.3.3"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz#fcd1d7e0007b7b73d1acdbf0ad6d91f7aa2deb15"
integrity sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==
"@next/swc-win32-arm64-msvc@15.3.5":
version "15.3.5"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.5.tgz#5bbe1434afa2360634d45fc7860a038d11e4e296"
integrity sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==
"@next/swc-win32-x64-msvc@15.3.3":
version "15.3.3"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz#c0e33e069d7922dd0546cac77a0247ad81d4a1aa"
integrity sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==
"@next/swc-win32-x64-msvc@15.3.5":
version "15.3.5"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.5.tgz#9629b2eac3159c70f3449cecc2a29bfd4bcb2d5a"
integrity sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -6451,14 +6451,6 @@ babel-preset-jest@^29.6.3:
babel-plugin-jest-hoist "^29.6.3"
babel-preset-current-node-syntax "^1.0.0"
babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
bail@^2.0.0:
version "2.0.2"
resolved "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz"
@@ -7004,11 +6996,6 @@ core-js-compat@^3.40.0:
dependencies:
browserslist "^4.24.4"
core-js@^2.4.0:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.0.0, core-js@^3.38.1:
version "3.42.0"
resolved "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz"
@@ -10097,7 +10084,14 @@ loose-envify@^1.0.0, loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lottie-web@^5.12.2:
lottie-react@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/lottie-react/-/lottie-react-2.4.1.tgz#4bd3f2a8a5e48edbd43c05ca5080fdd50f049d31"
integrity sha512-LQrH7jlkigIIv++wIyrOYFLHSKQpEY4zehPicL9bQsrt1rnoKRYCYgpCUe5maqylNtacy58/sQDZTkwMcTRxZw==
dependencies:
lottie-web "^5.10.2"
lottie-web@^5.10.2:
version "5.13.0"
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.13.0.tgz#441d3df217cc8ba302338c3f168e1a3af0f221d3"
integrity sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==
@@ -10844,12 +10838,12 @@ neo-async@^2.6.2:
resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next@15.3.3:
version "15.3.3"
resolved "https://registry.npmjs.org/next/-/next-15.3.3.tgz"
integrity sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==
next@15.3.8:
version "15.3.8"
resolved "https://registry.yarnpkg.com/next/-/next-15.3.8.tgz#c7df2fa890c66fa3042e85437e3c1e8e6bd38b26"
integrity sha512-L+4c5Hlr84fuaNADZbB9+ceRX9/CzwxJ+obXIGHupboB/Q1OLbSUapFs4bO8hnS/E6zV/JDX7sG1QpKVR2bguA==
dependencies:
"@next/env" "15.3.3"
"@next/env" "15.3.8"
"@swc/counter" "0.1.3"
"@swc/helpers" "0.5.15"
busboy "1.6.0"
@@ -10857,14 +10851,14 @@ next@15.3.3:
postcss "8.4.31"
styled-jsx "5.1.6"
optionalDependencies:
"@next/swc-darwin-arm64" "15.3.3"
"@next/swc-darwin-x64" "15.3.3"
"@next/swc-linux-arm64-gnu" "15.3.3"
"@next/swc-linux-arm64-musl" "15.3.3"
"@next/swc-linux-x64-gnu" "15.3.3"
"@next/swc-linux-x64-musl" "15.3.3"
"@next/swc-win32-arm64-msvc" "15.3.3"
"@next/swc-win32-x64-msvc" "15.3.3"
"@next/swc-darwin-arm64" "15.3.5"
"@next/swc-darwin-x64" "15.3.5"
"@next/swc-linux-arm64-gnu" "15.3.5"
"@next/swc-linux-arm64-musl" "15.3.5"
"@next/swc-linux-x64-gnu" "15.3.5"
"@next/swc-linux-x64-musl" "15.3.5"
"@next/swc-win32-arm64-msvc" "15.3.5"
"@next/swc-win32-x64-msvc" "15.3.5"
sharp "^0.34.1"
no-case@^3.0.4:
@@ -11428,7 +11422,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -11785,12 +11779,12 @@ react-dnd@^14.0.3:
fast-deep-equal "^3.1.3"
hoist-non-react-statics "^3.3.2"
react-dom@18.3.1, react-dom@19.0.0, react-dom@19.1.0:
version "19.1.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.0.tgz#133558deca37fa1d682708df8904b25186793623"
integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==
react-dom@19.0.0, react-dom@19.2.1:
version "19.2.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.1.tgz#ce3527560bda4f997e47d10dab754825b3061f59"
integrity sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==
dependencies:
scheduler "^0.26.0"
scheduler "^0.27.0"
react-i18next@15.5.2:
version "15.5.2"
@@ -11825,15 +11819,6 @@ react-lifecycles-compat@^3.0.0:
resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-lottie@^1.2.10:
version "1.2.10"
resolved "https://registry.yarnpkg.com/react-lottie/-/react-lottie-1.2.10.tgz#399f78a448a7833b2380d74fc489ecf15f8d18c7"
integrity sha512-x0eWX3Z6zSx1XM5QSjnLupc6D22LlMCB0PH06O/N/epR2hsLaj1Vxd9RtMnbbEHjJ/qlsgHJ6bpN3vnZI92hjw==
dependencies:
babel-runtime "^6.26.0"
lottie-web "^5.12.2"
prop-types "^15.6.1"
react-markdown@10.1.0:
version "10.1.0"
resolved "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz"
@@ -12021,10 +12006,10 @@ react-window@^1.8.11:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@*, react@19.0.0, react@19.1.0:
version "19.1.0"
resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75"
integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
react@19.0.0, react@19.2.1:
version "19.2.1"
resolved "https://registry.yarnpkg.com/react/-/react-19.2.1.tgz#8600fa205e58e2e807f6ef431c9f6492591a2700"
integrity sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==
readable-stream@^3.4.0:
version "3.6.2"
@@ -12101,11 +12086,6 @@ regenerate@^1.4.2:
resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regex-recursion@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/regex-recursion/-/regex-recursion-6.0.2.tgz#a0b1977a74c87f073377b938dbedfab2ea582b33"
@@ -12464,10 +12444,10 @@ scheduler@0.25.0-rc-603e6108-20241029:
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz"
integrity sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==
scheduler@^0.26.0:
version "0.26.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337"
integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==
scheduler@^0.27.0:
version "0.27.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==
schema-utils@^4.3.0, schema-utils@^4.3.2:
version "4.3.2"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.0.9",
"version": "0.0.11",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {