From e0915bddd2d466023490cbaa5493047a9f8f54f4 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 25 Jun 2026 09:35:34 +0100 Subject: [PATCH] Resolve artifacts.artifact(id) for classic agents via the conversation parent The classic prompt renderer now passes artifact_parent={conversation_id} so a normal agent's prompt can resolve a prior-turn artifact with {{ artifacts.artifact(id) }}, scoped to its own conversation (parent-derived authz; a missing conversation_id safely yields an empty lookup). This makes the artifacts variable surfaced in the agent builder functional, and is the parent wiring an agent-as-tool / subagent feature would also rely on. --- application/agents/headless_runner.py | 1 + .../api/answer/services/stream_processor.py | 1 + .../answer/services/test_prompt_renderer.py | 38 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/application/agents/headless_runner.py b/application/agents/headless_runner.py index fca6faca..017f3df2 100644 --- a/application/agents/headless_runner.py +++ b/application/agents/headless_runner.py @@ -117,6 +117,7 @@ def run_agent_headless( user_id=owner, docs=retrieved_docs or None, docs_together=format_docs_for_prompt(retrieved_docs), + artifact_parent={"conversation_id": conversation_id}, ) except Exception as exc: logger.warning("Headless prompt rendering failed; using raw prompt: %s", exc) diff --git a/application/api/answer/services/stream_processor.py b/application/api/answer/services/stream_processor.py index ec0a2ab5..316688ee 100644 --- a/application/api/answer/services/stream_processor.py +++ b/application/api/answer/services/stream_processor.py @@ -1517,6 +1517,7 @@ class StreamProcessor: docs=docs, docs_together=docs_together, tools_data=tools_data, + artifact_parent={"conversation_id": self.conversation_id}, ) # Use the user_id that resolved the model so owner-scoped BYOM diff --git a/tests/api/answer/services/test_prompt_renderer.py b/tests/api/answer/services/test_prompt_renderer.py index c03e6471..f0902a94 100644 --- a/tests/api/answer/services/test_prompt_renderer.py +++ b/tests/api/answer/services/test_prompt_renderer.py @@ -379,6 +379,44 @@ class TestNamespaceManagerWithTools: assert "system" in context assert context["system"]["request_id"] == "req_123" + +class TestPromptRendererArtifactParent: + """render_prompt forwards artifact_parent so artifacts.artifact(id) resolves for classic agents.""" + + def test_render_resolves_artifact_by_id_with_conversation_parent(self): + from unittest.mock import patch + + from application.api.answer.services.prompt_renderer import PromptRenderer + + row = {"id": "art-1", "current_version": 1, "kind": "document", "title": "Report"} + version = {"mime_type": "text/html", "filename": "report.html", "size": 12} + with patch( + "application.storage.db.repositories.artifacts.ArtifactsRepository" + ) as repo_cls, patch("application.storage.db.session.db_readonly"): + repo = repo_cls.return_value + repo.get_artifact_in_parent.return_value = row + repo.get_version.return_value = version + + rendered = PromptRenderer().render_prompt( + "file={{ artifacts.artifact('art-1').filename }}", + artifact_parent={"conversation_id": "conv-1"}, + ) + + assert rendered == "file=report.html" + _, kwargs = repo.get_artifact_in_parent.call_args + assert kwargs.get("conversation_id") == "conv-1" + + def test_render_without_parent_yields_empty_lookup(self): + from application.api.answer.services.prompt_renderer import PromptRenderer + + # No artifact_parent (e.g. a first-turn conversation_id is None) must not error; + # the lookup returns an empty mapping and the attribute renders blank. + rendered = PromptRenderer().render_prompt( + "[{{ artifacts.artifact('art-1').filename }}]" + ) + + assert rendered == "[]" + def test_namespace_manager_get_builder(self): from application.templates.namespaces import NamespaceManager, SystemNamespace