Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bfb374664a | |||
| 5676ce68c0 | |||
| 50a395c546 | |||
| 69bf2cab5d |
+5
-10
@@ -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
|
||||
|
||||
@@ -153,9 +153,16 @@ class BaseAgent(Agent):
|
||||
This class initializes the agent with model from configuration.
|
||||
"""
|
||||
|
||||
def __init__(self, *, model_hrid, **kwargs):
|
||||
"""Initialize the agent with model configuration from settings."""
|
||||
_ignored_kwargs = {"model", "system_prompt", "tools", "toolsets"}
|
||||
def __init__(self, *, model_hrid, use_configuration_system_prompt: bool = True, **kwargs):
|
||||
"""Initialize the agent with model configuration from settings.
|
||||
|
||||
Args:
|
||||
model_hrid: HRID of the model to use, as defined in LLM_CONFIGURATIONS.
|
||||
use_configuration_system_prompt: If False, the system prompt defined in the
|
||||
LLM configuration will be ignored, allowing subclasses to define all
|
||||
instructions dynamically.
|
||||
"""
|
||||
_ignored_kwargs = {"model", "system_prompt", "tools", "toolsets", "instructions"}
|
||||
if set(kwargs).intersection(_ignored_kwargs):
|
||||
raise ValueError(f"{_ignored_kwargs} arguments must not be provided.")
|
||||
|
||||
@@ -174,7 +181,7 @@ class BaseAgent(Agent):
|
||||
# and pydantic_ai.models.infer_model()
|
||||
_model_instance = self.configuration.model_name
|
||||
|
||||
_system_prompt = self.configuration.system_prompt
|
||||
_system_prompt = self.configuration.system_prompt if use_configuration_system_prompt else None
|
||||
_base_toolset = (
|
||||
[
|
||||
FunctionToolset(
|
||||
@@ -190,6 +197,11 @@ 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
|
||||
)
|
||||
agent_kwargs = {
|
||||
"model": _model_instance,
|
||||
"tools": _tools,
|
||||
}
|
||||
if _system_prompt is not None:
|
||||
agent_kwargs["instructions"] = _system_prompt
|
||||
|
||||
super().__init__(**agent_kwargs, **kwargs)
|
||||
|
||||
@@ -11,12 +11,12 @@ from pydantic_ai import ModelMessage
|
||||
from pydantic_ai.models.function import AgentInfo, FunctionModel
|
||||
|
||||
from core.enums import get_language_name
|
||||
from chat.prompts import get_prompt
|
||||
|
||||
from .base import BaseAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
MOCKED_RESPONSE = """
|
||||
# **Ode to the AI Assistant** 🤖✨
|
||||
|
||||
@@ -96,16 +96,29 @@ class ConversationAgent(BaseAgent):
|
||||
|
||||
def __init__(self, *, language=None, **kwargs):
|
||||
"""Initialize the conversation agent."""
|
||||
super().__init__(**kwargs)
|
||||
# Ignore the static system_prompt from configuration; rely entirely on
|
||||
# dynamically registered instructions, including the external prompt.
|
||||
super().__init__(use_configuration_system_prompt=False, **kwargs)
|
||||
|
||||
# Do not call the real model on deployed instances if the setting is enabled
|
||||
if settings.WARNING_MOCK_CONVERSATION_AGENT:
|
||||
self._model = FunctionModel(stream_function=mocked_agent_model)
|
||||
|
||||
@self.system_prompt
|
||||
@self.instructions
|
||||
def assistant_system_prompt() -> str:
|
||||
"""
|
||||
Main system prompt for the assistant, loaded from the synced Prompt store.
|
||||
|
||||
This uses the `assistant_system` prompt from the external repository if
|
||||
available, with a fallback to settings.AI_AGENT_INSTRUCTIONS.
|
||||
"""
|
||||
|
||||
return get_prompt("assistant_system")
|
||||
|
||||
@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 +126,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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.2.9 on 2026-01-07 10:20
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('chat', '0004_chatconversationattachment_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Prompt',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('version', models.CharField(help_text='Git commit SHA from which this prompt was synced.', max_length=40)),
|
||||
('content', models.TextField(help_text='Jinja template content of the prompt.')),
|
||||
('metadata', models.JSONField(blank=True, help_text='Optional JSON metadata loaded from the sidecar .meta.json file.', null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -131,3 +131,27 @@ class ChatConversationAttachment(BaseModel):
|
||||
null=True,
|
||||
help_text="Original file key if the Markdown from another file",
|
||||
)
|
||||
|
||||
|
||||
class Prompt(BaseModel):
|
||||
"""
|
||||
Model storing LLM prompts synced from an external repository.
|
||||
|
||||
Each prompt is identified by a unique `name` (e.g. "assistant_system") and
|
||||
optionally linked to metadata coming from a sidecar JSON file.
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
version = models.CharField(
|
||||
max_length=40,
|
||||
help_text="Git commit SHA from which this prompt was synced.",
|
||||
)
|
||||
content = models.TextField(help_text="Jinja template content of the prompt.")
|
||||
metadata = models.JSONField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Optional JSON metadata loaded from the sidecar .meta.json file.",
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}@{self.version}"
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Synchronization helpers for external prompt repositories."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Iterable
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
from chat.models import Prompt
|
||||
|
||||
|
||||
def _raw_url(path: str, commit_sha: str) -> str:
|
||||
"""
|
||||
Build a raw.githubusercontent.com URL for a given file at a specific commit.
|
||||
"""
|
||||
|
||||
base_url = getattr(settings, "PROMPT_BASE_URL", "https://raw.githubusercontent.com")
|
||||
owner = settings.PROMPT_REPO_OWNER
|
||||
repo = settings.PROMPT_REPO_NAME
|
||||
|
||||
return f"{base_url}/{owner}/{repo}/{commit_sha}/{path}"
|
||||
|
||||
|
||||
def _fetch_text_file(path: str, commit_sha: str) -> str | None:
|
||||
"""
|
||||
Fetch a text file from the prompts repository at the given commit.
|
||||
"""
|
||||
|
||||
url = _raw_url(path, commit_sha)
|
||||
resp = requests.get(url, timeout=5)
|
||||
if resp.status_code == 200:
|
||||
return resp.text
|
||||
return None
|
||||
|
||||
|
||||
def handle_prompt_sync(payload: dict) -> None:
|
||||
"""
|
||||
Synchronize prompts from the external repository based on a GitHub push payload.
|
||||
|
||||
Strategy V1: full sync for a static list of prompt names.
|
||||
This is simple and robust, and prompt files are typically small.
|
||||
"""
|
||||
|
||||
commit_sha = payload.get("after")
|
||||
if not commit_sha:
|
||||
# Nothing to do if we don't know which commit to sync from
|
||||
return
|
||||
|
||||
# For now, we rely on a static list of prompt names from settings.
|
||||
# Example: PROMPT_NAMES = ["assistant_system", "summarizer"]
|
||||
prompt_names: Iterable[str] = getattr(
|
||||
settings,
|
||||
"PROMPT_NAMES",
|
||||
("assistant_system",),
|
||||
)
|
||||
|
||||
for name in prompt_names:
|
||||
# Try prompts/ subdirectory first, then root as fallback
|
||||
jinja_paths = [f"prompts/{name}.jinja", f"{name}.jinja"]
|
||||
meta_paths = [f"prompts/{name}.meta.json", f"{name}.meta.json"]
|
||||
|
||||
content = None
|
||||
for jinja_path in jinja_paths:
|
||||
content = _fetch_text_file(jinja_path, commit_sha)
|
||||
if content is not None:
|
||||
break
|
||||
|
||||
if content is None:
|
||||
# Prompt may have been removed or file missing; we simply skip it for now.
|
||||
continue
|
||||
|
||||
metadata = None
|
||||
for meta_path in meta_paths:
|
||||
metadata_text = _fetch_text_file(meta_path, commit_sha)
|
||||
if metadata_text:
|
||||
break
|
||||
else:
|
||||
metadata_text = None
|
||||
if metadata_text:
|
||||
try:
|
||||
metadata = json.loads(metadata_text)
|
||||
except json.JSONDecodeError:
|
||||
# Ignore invalid metadata for robustness
|
||||
metadata = None
|
||||
|
||||
Prompt.objects.update_or_create(
|
||||
name=name,
|
||||
defaults={
|
||||
"version": commit_sha,
|
||||
"content": content,
|
||||
"metadata": metadata,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Runtime access helpers for synced prompts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from chat.models import Prompt
|
||||
|
||||
|
||||
def get_prompt(name: str) -> str:
|
||||
"""
|
||||
Return the content of a prompt by name.
|
||||
|
||||
Fallback strategy:
|
||||
- if the prompt is not found in DB, return the legacy setting-based prompt
|
||||
for known names (e.g. assistant_system).
|
||||
"""
|
||||
|
||||
try:
|
||||
prompt = Prompt.objects.get(name=name)
|
||||
return prompt.content
|
||||
except ObjectDoesNotExist:
|
||||
# Fallbacks to keep backward compatibility and avoid hard failures.
|
||||
if name == "assistant_system":
|
||||
return settings.AI_AGENT_INSTRUCTIONS
|
||||
if name == "summarizer":
|
||||
return settings.SUMMARIZATION_SYSTEM_PROMPT
|
||||
|
||||
raise
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
+69
-109
@@ -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": [
|
||||
|
||||
+60
-154
@@ -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": [
|
||||
{
|
||||
|
||||
+24
-94
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -6,7 +6,8 @@ from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.http import Http404, StreamingHttpResponse
|
||||
from django.http import Http404, HttpResponse, StreamingHttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
import langfuse
|
||||
import magic
|
||||
@@ -26,6 +27,7 @@ from core.filters import remove_accents
|
||||
from activation_codes.permissions import IsActivatedUser
|
||||
from chat import models, serializers
|
||||
from chat.clients.pydantic_ai import AIAgentService
|
||||
from chat.prompt_sync import handle_prompt_sync
|
||||
from chat.serializers import ChatConversationRequestSerializer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -435,3 +437,40 @@ class ChatConversationAttachmentViewSet(
|
||||
)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def github_prompt_webhook(request):
|
||||
"""
|
||||
GitHub webhook endpoint to synchronize assistant prompts.
|
||||
|
||||
This endpoint expects a GitHub push payload and validates the request
|
||||
using the X-Hub-Signature-256 header and a shared secret.
|
||||
"""
|
||||
|
||||
signature = request.headers.get("X-Hub-Signature-256")
|
||||
secret = getattr(settings, "PROMPT_WEBHOOK_SECRET", None)
|
||||
|
||||
if not secret or not signature:
|
||||
return HttpResponse(status=403)
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json as _json
|
||||
|
||||
mac = hmac.new(
|
||||
secret.encode(),
|
||||
msg=request.body,
|
||||
digestmod=hashlib.sha256,
|
||||
)
|
||||
expected = "sha256=" + mac.hexdigest()
|
||||
if not hmac.compare_digest(expected, signature):
|
||||
return HttpResponse(status=403)
|
||||
|
||||
try:
|
||||
payload = _json.loads(request.body)
|
||||
except _json.JSONDecodeError:
|
||||
return HttpResponse(status=400)
|
||||
|
||||
handle_prompt_sync(payload)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@@ -663,6 +663,38 @@ class Base(BraveSettings, Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# External prompt repository (for agent system prompts, etc.)
|
||||
PROMPT_REPO_OWNER = values.Value(
|
||||
None,
|
||||
environ_name="PROMPT_REPO_OWNER",
|
||||
environ_prefix=None,
|
||||
)
|
||||
PROMPT_REPO_NAME = values.Value(
|
||||
None,
|
||||
environ_name="PROMPT_REPO_NAME",
|
||||
environ_prefix=None,
|
||||
)
|
||||
PROMPT_REPO_BRANCH = values.Value(
|
||||
"main",
|
||||
environ_name="PROMPT_REPO_BRANCH",
|
||||
environ_prefix=None,
|
||||
)
|
||||
PROMPT_WEBHOOK_SECRET = values.Value(
|
||||
None,
|
||||
environ_name="PROMPT_WEBHOOK_SECRET",
|
||||
environ_prefix=None,
|
||||
)
|
||||
PROMPT_BASE_URL = values.Value(
|
||||
"https://raw.githubusercontent.com",
|
||||
environ_name="PROMPT_BASE_URL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
PROMPT_NAMES = values.ListValue(
|
||||
default=["assistant_system"],
|
||||
environ_name="PROMPT_NAMES",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Tools
|
||||
AI_AGENT_TOOLS = values.ListValue(
|
||||
default=[],
|
||||
|
||||
@@ -9,7 +9,12 @@ from rest_framework.routers import DefaultRouter
|
||||
from core.api import viewsets
|
||||
|
||||
from activation_codes import viewsets as activation_viewsets
|
||||
from chat.views import ChatConversationAttachmentViewSet, ChatViewSet, LLMConfigurationView
|
||||
from chat.views import (
|
||||
ChatConversationAttachmentViewSet,
|
||||
ChatViewSet,
|
||||
LLMConfigurationView,
|
||||
github_prompt_webhook,
|
||||
)
|
||||
|
||||
# - Main endpoints
|
||||
router = DefaultRouter()
|
||||
@@ -40,4 +45,9 @@ urlpatterns = [
|
||||
),
|
||||
),
|
||||
path(f"api/{settings.API_VERSION}/config/", viewsets.ConfigView.as_view()),
|
||||
path(
|
||||
"webhooks/github/prompts",
|
||||
github_prompt_webhook,
|
||||
name="github-prompts-webhook",
|
||||
),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user