From 29f76fe04016b8324f44ef18e2ece61f72b8d7ff Mon Sep 17 00:00:00 2001 From: Laurent Paoletti Date: Thu, 11 Dec 2025 09:24:13 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9B(back)=20fix=20system=20prompt?= =?UTF-8?q?=20compatibility=20with=20self-hosted=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 15 +- src/backend/chat/agents/base.py | 4 +- src/backend/chat/agents/conversation.py | 9 +- src/backend/chat/clients/pydantic_ai.py | 12 +- .../agents/test_build_conversation_agent.py | 38 +++- .../chat/conversations/test_conversation.py | 212 ++++++----------- .../test_conversation_with_document_upload.py | 178 ++++++--------- .../test_conversation_with_document_url.py | 214 +++++------------- .../test_conversation_with_history.py | 10 +- .../test_conversation_with_image_url.py | 118 ++-------- src/backend/chat/tools/document_search_rag.py | 2 +- 11 files changed, 272 insertions(+), 540 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e88288f..1d53a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ and this project adheres to ### Fixed -- 🐛(e2e) fix test-e2e-chronium +- 🐛(e2e) fix test-e2e-chromium +- 🐛(back) fix system prompt compatibility with self-hosted models #200 ## [0.0.10] - 2025-12-15 @@ -34,6 +35,7 @@ and this project adheres to ## [0.0.9] - 2025-11-17 ### Added + - ✨(front) add code copy button - ✨(RAG) add generic collection RAG tools #159 @@ -41,7 +43,6 @@ and this project adheres to - 🔊(langfuse) enable tracing with redacted content #162 - ## [0.0.8] - 2025-11-10 ### Fixed @@ -56,28 +57,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 @@ -94,14 +91,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 @@ -111,6 +106,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 @@ -118,7 +114,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 @@ -141,7 +136,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 diff --git a/src/backend/chat/agents/base.py b/src/backend/chat/agents/base.py index 2087a7b..be49443 100644 --- a/src/backend/chat/agents/base.py +++ b/src/backend/chat/agents/base.py @@ -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) diff --git a/src/backend/chat/agents/conversation.py b/src/backend/chat/agents/conversation.py index 87ef32c..227259e 100644 --- a/src/backend/chat/agents/conversation.py +++ b/src/backend/chat/agents/conversation.py @@ -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: diff --git a/src/backend/chat/clients/pydantic_ai.py b/src/backend/chat/clients/pydantic_ai.py index 0d4163f..f39453d 100644 --- a/src/backend/chat/clients/pydantic_ai.py +++ b/src/backend/chat/clients/pydantic_ai.py @@ -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, diff --git a/src/backend/chat/tests/agents/test_build_conversation_agent.py b/src/backend/chat/tests/agents/test_build_conversation_agent.py index 36613d6..ccc16a5 100644 --- a/src/backend/chat/tests/agents/test_build_conversation_agent.py +++ b/src/backend/chat/tests/agents/test_build_conversation_agent.py @@ -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): diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation.py b/src/backend/chat/tests/views/chat/conversations/test_conversation.py index 39112e2..a5172f2 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation.py @@ -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", diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py index 04cb3a3..eea77fa 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py @@ -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":""}\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":""}\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": [ diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_url.py b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_url.py index 264dcf4..dd5c38b 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_url.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_url.py @@ -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?", diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_history.py b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_history.py index da1663e..8c530bf 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_history.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_history.py @@ -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": [ { diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_image_url.py b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_image_url.py index 087e83c..2ea172f 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_image_url.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_image_url.py @@ -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": [ { diff --git a/src/backend/chat/tools/document_search_rag.py b/src/backend/chat/tools/document_search_rag.py index f59c119..46827fa 100644 --- a/src/backend/chat/tools/document_search_rag.py +++ b/src/backend/chat/tools/document_search_rag.py @@ -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 ( From daf90cf110200bb5f69860a344657ecc2678cc9e Mon Sep 17 00:00:00 2001 From: Laurent Paoletti Date: Tue, 23 Dec 2025 16:40:51 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9A=B0=EF=B8=8F(back)=20remove=20dead=20?= =?UTF-8?q?code=20and=20unused=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Laurent Paoletti --- CHANGELOG.md | 1 + package.json | 6 -- ...top_steaming.py => test_stop_streaming.py} | 2 +- src/backend/chat/tools/exceptions.py | 11 ---- src/backend/chat/views.py | 2 +- src/backend/core/api/__init__.py | 14 ----- src/backend/core/api/serializers.py | 20 ------- src/backend/core/authentication/__init__.py | 52 ----------------- src/backend/core/fields.py | 25 -------- src/backend/core/filters.py | 22 ------- .../templates/core/generate_document.html | 14 ----- src/backend/core/templatetags/__init__.py | 0 src/backend/core/templatetags/extra_tags.py | 58 ------------------- 13 files changed, 3 insertions(+), 224 deletions(-) delete mode 100644 package.json rename src/backend/chat/tests/views/chat/conversations/{test_stop_steaming.py => test_stop_streaming.py} (97%) delete mode 100644 src/backend/core/fields.py delete mode 100644 src/backend/core/templates/core/generate_document.html delete mode 100644 src/backend/core/templatetags/__init__.py delete mode 100644 src/backend/core/templatetags/extra_tags.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d53a61..766c900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to - 🐛(e2e) fix test-e2e-chromium - 🐛(back) fix system prompt compatibility with self-hosted models #200 +- ⚰️(back) remove dead code and unused files ## [0.0.10] - 2025-12-15 diff --git a/package.json b/package.json deleted file mode 100644 index 24afa17..0000000 --- a/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "@ai-sdk/react": "^1.2.12", - "@ai-sdk/ui-utils": "^1.2.11" - } -} diff --git a/src/backend/chat/tests/views/chat/conversations/test_stop_steaming.py b/src/backend/chat/tests/views/chat/conversations/test_stop_streaming.py similarity index 97% rename from src/backend/chat/tests/views/chat/conversations/test_stop_steaming.py rename to src/backend/chat/tests/views/chat/conversations/test_stop_streaming.py index d11ee60..8e984cb 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_stop_steaming.py +++ b/src/backend/chat/tests/views/chat/conversations/test_stop_streaming.py @@ -1,4 +1,4 @@ -"""Test the post_stop_steaming view.""" +"""Test the post_stop_streaming view.""" from unittest.mock import patch diff --git a/src/backend/chat/tools/exceptions.py b/src/backend/chat/tools/exceptions.py index fe0dd62..5625b81 100644 --- a/src/backend/chat/tools/exceptions.py +++ b/src/backend/chat/tools/exceptions.py @@ -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. diff --git a/src/backend/chat/views.py b/src/backend/chat/views.py index 0437237..dbff818 100644 --- a/src/backend/chat/views.py +++ b/src/backend/chat/views.py @@ -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. diff --git a/src/backend/core/api/__init__.py b/src/backend/core/api/__init__.py index c88af71..ea14585 100644 --- a/src/backend/core/api/__init__.py +++ b/src/backend/core/api/__init__.py @@ -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) diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index ba61de4..e05fe24 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -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"] diff --git a/src/backend/core/authentication/__init__.py b/src/backend/core/authentication/__init__.py index 977382d..e69de29 100644 --- a/src/backend/core/authentication/__init__.py +++ b/src/backend/core/authentication/__init__.py @@ -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'" diff --git a/src/backend/core/fields.py b/src/backend/core/fields.py deleted file mode 100644 index 1125622..0000000 --- a/src/backend/core/fields.py +++ /dev/null @@ -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) diff --git a/src/backend/core/filters.py b/src/backend/core/filters.py index bf7ac87..c8ee0a1 100644 --- a/src/backend/core/filters.py +++ b/src/backend/core/filters.py @@ -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) diff --git a/src/backend/core/templates/core/generate_document.html b/src/backend/core/templates/core/generate_document.html deleted file mode 100644 index ac9f291..0000000 --- a/src/backend/core/templates/core/generate_document.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Generate Document - - -

Generate Document

-
- {% csrf_token %} - {{ form.as_p }} - -
- - diff --git a/src/backend/core/templatetags/__init__.py b/src/backend/core/templatetags/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backend/core/templatetags/extra_tags.py b/src/backend/core/templatetags/extra_tags.py deleted file mode 100644 index 109bd7b..0000000 --- a/src/backend/core/templatetags/extra_tags.py +++ /dev/null @@ -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 "" From a0b31e1e618763c48f5a4c7fe6e85078e69ecaa3 Mon Sep 17 00:00:00 2001 From: Berry den Hartog <38954346+berrydenhartog@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:20:45 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=90=9B(front)=20fix=20link=20color=20?= =?UTF-8?q?in=20LeftPanelConversationItem=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix link color component for default theme --- .../left-panel/components/LeftPanelConversationItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelConversationItem.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelConversationItem.tsx index 3521318..299a9f4 100644 --- a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelConversationItem.tsx +++ b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelConversationItem.tsx @@ -58,7 +58,7 @@ export const LeftPanelConversationItem = ({ > From 13c6499c665c81aeb8b6000fb7520e25d2e7e407 Mon Sep 17 00:00:00 2001 From: Quentin BEY Date: Thu, 15 Jan 2026 23:06:45 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=94=A5(chat)=20remove=20thinking=20pa?= =?UTF-8?q?rt=20from=20frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 4 +++ .../src/features/chat/components/Chat.tsx | 35 ++++--------------- .../__tests__/app-conversations/chat.spec.ts | 2 +- 3 files changed, 11 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 766c900..e1ef55f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ and this project adheres to - 🐛(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 diff --git a/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx b/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx index 0efdbcb..a620c48 100644 --- a/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx +++ b/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx @@ -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'; @@ -816,30 +811,12 @@ export const Chat = ({ )} {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' ? ( - - {part.reasoning} - - ) : part.type === 'tool-invocation' && - isCurrentlyStreaming && - isLastAssistantMessageInConversation ? ( + (part: ToolInvocationUIPart, partIndex: number) => + part.type === 'tool-invocation' && + isCurrentlyStreaming && + isLastAssistantMessageInConversation ? ( { const messageContent = page.getByTestId('assistant-message-content'); await expect(messageContent).toBeVisible(); - await expect(messageContent).not.toBeEmpty(); + await expect(messageContent).not.toBeEmpty(); // Check history const chatHistoryLink = page