From 2bead91ecdf2691d2dd2695bc635497135fafe07 Mon Sep 17 00:00:00 2001 From: Lam Nguyen Date: Sat, 11 Jul 2026 11:17:31 +0700 Subject: [PATCH] feat(prompt): add public system prompt template override --- README.md | 17 ++- dana/common/protocols/war.py | 4 + dana/core/agent/star_agent.py | 12 ++ dana/core/prompt/prompt_api.py | 43 +++++- dana/core/prompt/prompt_builder.py | 7 +- dana/core/runtime/base.py | 10 ++ dana/core/runtime/codec/codec_base.py | 29 +++- docs/project-changelog.md | 1 + docs/project-roadmap.md | 1 + tests/unit/core/test_agent_runtime.py | 81 ++++++++++ tests/unit/core/test_done_flag_autonomy.py | 4 +- tests/unit/test_autonomy_in_template.py | 143 +++++++++++++++++- tests/unit/test_data_staragent_integration.py | 4 +- tests/unit/test_prompt_api_repository.py | 36 +++++ 14 files changed, 366 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 75ba81a..7259957 100644 --- a/README.md +++ b/README.md @@ -317,12 +317,20 @@ DANA_MOCK_LLM=true make test ```python agent = STARAgent( model: str, # e.g., "gpt-4.1" - system_prompt: Optional[str], # Custom system prompt tools: Optional[list[str]], # Enabled tool names max_tokens: int = 4096, # Context limit compression_threshold: float = 0.8 # Auto-compress at % ) +# Ephemeral replacement for this agent instance (no repository write) +agent.override_system_prompt_template("You are a domain specialist.") + +# Only codec runtimes can persist the replacement to their prompt repository +agent.override_system_prompt_template( + "You are a persistent domain specialist.", + persist=True, +) + # Process message response = await agent.process(message: str) -> str @@ -335,6 +343,13 @@ timeline = agent.state.timeline messages = await timeline.get_entries() ``` +`persist=False` is the default: the override is ephemeral, scoped to the agent +instance, and never written to the prompt repository. `persist=True` is supported +only by codec runtimes and writes to their configured prompt repository; base +runtimes raise `NotImplementedError`. The template fully replaces, rather than +extends, the default system prompt, so retain every required tool-usage and +output-format instruction in the replacement. + ### Custom Resources ```python diff --git a/dana/common/protocols/war.py b/dana/common/protocols/war.py index 71f8c34..45662fe 100644 --- a/dana/common/protocols/war.py +++ b/dana/common/protocols/war.py @@ -114,6 +114,10 @@ def available_resources(self) -> Sequence[ResourceProtocol]: class STARAgentProtocol(AgentProtocol): """Protocol for See-Think-Act-Reflect agents.""" + def override_system_prompt_template(self, template: str, *, persist: bool = False) -> None: + """Replace the complete system prompt template used by the agent.""" + ... + def _see(self, trace_inputs: DictParams) -> DictParams: """See the inputs and produce percepts. Args: diff --git a/dana/core/agent/star_agent.py b/dana/core/agent/star_agent.py index 467ea37..9b12185 100644 --- a/dana/core/agent/star_agent.py +++ b/dana/core/agent/star_agent.py @@ -143,6 +143,7 @@ def __init__( self._repository_factory = repository_factory self._codec = codec self._identity_override = identity_override + self._system_prompt_template_override: str | None = None if runtime is None: from dana.core.runtime import RuntimeRegistry @@ -528,6 +529,17 @@ def system_prompt(self) -> str: return self._runtime.system_prompt(self) return super().system_prompt + def override_system_prompt_template(self, template: str, *, persist: bool = False) -> None: + """Replace the complete system prompt template used for LLM requests. + + Args: + template: Full prompt template. Runtime-supported ``{{variables}}`` + continue to render normally. + persist: Save the template when the runtime has a prompt repository. + Defaults to an in-memory override scoped to this agent instance. + """ + self._runtime.override_system_prompt_template(self, template, persist=persist) + # ============================================================================ # PUBLIC API - STATE & CONTEXT MANAGEMENT # ============================================================================ diff --git a/dana/core/prompt/prompt_api.py b/dana/core/prompt/prompt_api.py index b473610..01a38f6 100644 --- a/dana/core/prompt/prompt_api.py +++ b/dana/core/prompt/prompt_api.py @@ -39,6 +39,9 @@ def system_prompt(self) -> str: ... @abstractmethod def available_tools_prompt(self) -> str: ... + @abstractmethod + def override_system_prompt_template(self, template: str, *, persist: bool = False) -> None: ... + @abstractmethod def reset(self) -> None: ... @@ -238,8 +241,11 @@ def __init__( self._resource_prompt_engineers = {} self._workflow_prompt_engineers = {} self._env = EnvironmentInfo(agent, self.relative_path) - self._system_prompt = None - self._template = None + self._system_prompt: str | None = None + self._template: str | None = None + self._system_prompt_template_override: str | None = None + self._persist_system_prompt_override = False + self._system_prompt_override_persisted = False def _instantiate_prompt_engineer( self, prompt_engineer_cls: type[BasePromptEngineer], component, relative_path: str, **kwargs @@ -281,10 +287,14 @@ def tool_instruction_prompt(self) -> str: @property def system_prompt(self) -> str: if self._system_prompt is None: - _template = self.load() - if _template is None or self._force_generate: + override = self._system_prompt_template_override + has_override = override is not None + _template = override if override is not None else self.load() + if has_override or _template is None or self._force_generate: # FILL STATIC VARIABLES BEFORE PERSIST - _template = self._template_system_prompt + if not has_override: + _template = self._template_system_prompt + assert _template is not None for variable in self.static_prompt_variables: if f"{{{{{variable}}}}}" in _template: attr = getattr(self, variable) @@ -294,10 +304,31 @@ def system_prompt(self) -> str: value = attr _template = _template.replace(f"{{{{{variable}}}}}", str(value)) self._template = _template - self.persist() + if not has_override or (self._persist_system_prompt_override and not self._system_prompt_override_persisted): + self.persist() + if has_override: + self._system_prompt_override_persisted = True + assert _template is not None self._system_prompt = self.render(_template) return self._system_prompt + def override_system_prompt_template(self, template: str, *, persist: bool = False) -> None: + """Replace the complete system prompt template for this prompt API. + + Args: + template: Full system prompt template. Supported ``{{variables}}`` are + rendered through the normal prompt API path. + persist: Save the override in the configured prompt repository. The + default keeps the override in memory for the current agent only. + """ + self._system_prompt_template_override = template + self._persist_system_prompt_override = persist + self._system_prompt_override_persisted = False + self._template = None + self._system_prompt = None + if persist: + _ = self.system_prompt + def render(self, template: str) -> str: variables = re.findall(r"\{\{(.*?)\}\}", template) for variable in variables: diff --git a/dana/core/prompt/prompt_builder.py b/dana/core/prompt/prompt_builder.py index a086b5d..fca386b 100644 --- a/dana/core/prompt/prompt_builder.py +++ b/dana/core/prompt/prompt_builder.py @@ -58,7 +58,7 @@ def __init__( identity_fn: Callable[[Any], str], template_fn: Callable[[bool], str], format_tool_fn: Callable[[Any], str], - system_prompt_fn: Callable[[], str] | None = None, + system_prompt_fn: Callable[[Any], str] | None = None, context_position: str = "prepend", skip_retrieved_context: bool = False, ) -> None: @@ -99,7 +99,7 @@ def build_prompt( # Build system prompt — use override fn if provided (codec path) if self._system_prompt_fn is not None: - system_prompt = self._system_prompt_fn() + system_prompt = self._system_prompt_fn(agent) else: system_prompt = self._build_system_prompt(agent, native_tools) @@ -146,7 +146,8 @@ def build_prompt( def _build_system_prompt(self, agent: Any, native_tools: Any) -> str: identity = self._identity_fn(agent) - template = self._template_fn(bool(native_tools)) + template_override = getattr(agent, "_system_prompt_template_override", None) + template = template_override if template_override is not None else self._template_fn(bool(native_tools)) values: dict[str, str] = {"identity": identity} values["resource_context"] = self._build_resource_context(agent) diff --git a/dana/core/runtime/base.py b/dana/core/runtime/base.py index d67f312..6098813 100644 --- a/dana/core/runtime/base.py +++ b/dana/core/runtime/base.py @@ -176,6 +176,16 @@ def get_system_prompt_template(self, native_tools: bool) -> str: return self.SYSTEM_PROMPT_TEMPLATE_NATIVE_TOOLS return self.SYSTEM_PROMPT_TEMPLATE_JSON + def override_system_prompt_template(self, agent: Any, template: str, *, persist: bool = False) -> None: + """Replace the complete system prompt template for this runtime instance. + + Repository persistence is available on codec runtimes through + ``LocalPromptAPI``. Base runtimes keep overrides in memory only. + """ + if persist: + raise NotImplementedError(f"{self.__class__.__name__} does not support persistent system prompt templates") + agent._system_prompt_template_override = template + def get_identity(self, agent) -> str: """Return the agent's identity description. diff --git a/dana/core/runtime/codec/codec_base.py b/dana/core/runtime/codec/codec_base.py index 4d5d83c..61267c3 100644 --- a/dana/core/runtime/codec/codec_base.py +++ b/dana/core/runtime/codec/codec_base.py @@ -43,7 +43,8 @@ def __init__( model=model, temperature=temperature, max_tokens=max_tokens, llm=llm, provider=provider, use_native_tools=use_native_tools ) self._codec = codec - self._prompt_api = None + self._prompt_api: LocalPromptAPI | None = None + self._prompt_apis: dict[int, LocalPromptAPI] = {} self._last_native_tools_state: bool | None = None # Track for cache invalidation # Codec runtimes don't use json_mode — reconfigure the shared LLMCaller. self._llm_caller._json_mode = False @@ -58,7 +59,7 @@ def __init__( identity_fn=self.get_identity, template_fn=self.get_system_prompt_template, format_tool_fn=self.format_tool_for_prompt, - system_prompt_fn=lambda: self._build_system_prompt(self._agent), + system_prompt_fn=self._build_system_prompt, context_position="append", skip_retrieved_context=True, ) @@ -90,17 +91,33 @@ def parse_response(self, response: LLMResponse) -> ParsedResponse: ... def _get_prompt_api(self, agent: STARAgent) -> LocalPromptAPI: - if self._prompt_api is None: - self._prompt_api = LocalPromptAPI(agent=agent, codec=self._codec, provider=self._provider) + agent_key = id(agent) + if agent_key not in self._prompt_apis: + self._prompt_apis[agent_key] = LocalPromptAPI( + agent=agent, + codec=self._codec, + provider=self._provider, + repository_factory=agent._repository_factory, + ) + self._prompt_api = self._prompt_apis[agent_key] return self._prompt_api def _build_system_prompt(self, agent: STARAgent) -> str: prompt_api = self._get_prompt_api(agent) return prompt_api.system_prompt + def system_prompt(self, agent: STARAgent) -> str: + """Return the same codec prompt used by ``build_prompt``.""" + return self._build_system_prompt(agent) + def invalidate_system_prompt_cache(self) -> None: - if self._prompt_api is not None: - self._prompt_api._system_prompt = None + for prompt_api in self._prompt_apis.values(): + prompt_api._system_prompt = None + + def override_system_prompt_template(self, agent: STARAgent, template: str, *, persist: bool = False) -> None: + """Replace the codec runtime's full system prompt template.""" + super().override_system_prompt_template(agent, template, persist=False) + self._get_prompt_api(agent).override_system_prompt_template(template, persist=persist) def call_llm( self, diff --git a/docs/project-changelog.md b/docs/project-changelog.md index fa258e8..fd43fa1 100644 --- a/docs/project-changelog.md +++ b/docs/project-changelog.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Added +- Public `STARAgent.override_system_prompt_template(template, persist=False)` API for replacing the complete system prompt template used by LLM requests. The default is an ephemeral, agent-instance override with no repository write; `persist=True` is codec-runtime-only and writes to the configured prompt repository. Because defaults are not merged, replacements must retain required tool-usage and output-format instructions. - LangSmith as an alternative tracing backend. `@observable` (`dana/common/observable.py`) dispatches to `langsmith.traceable` when `LANGSMITH_TRACING=true` or `DANA_LANGSMITH_ENABLED` truthy; exclusive with Langfuse (LangSmith takes precedence). No call-site changes — all 30+ `@observable` sites traced automatically. Add via `pip install dana[observability]`. LangSmith API key: `LANGSMITH_API_KEY`. - Single-knob env trigger `DANA_COMPACT_TRIGGER_TOKENS` (default 150000, clamp `[8k, 2M]`) for compression threshold (P3). - Optional `system_tokens_fn` / `tools_tokens_fn` callbacks on `CompressedTimeline` — fold system-prompt and tools-schema size into `needs_compression()` estimate without coupling to any provider. diff --git a/docs/project-roadmap.md b/docs/project-roadmap.md index 563a215..7d5e737 100644 --- a/docs/project-roadmap.md +++ b/docs/project-roadmap.md @@ -54,6 +54,7 @@ - [ ] Performance tuning & profiling - [ ] Additional resources (MCP, Skills) - [ ] Error recovery mechanisms +- [x] Public full-system-prompt template override API (ephemeral by default; codec-only repository persistence) **Planned Deliverables:** diff --git a/tests/unit/core/test_agent_runtime.py b/tests/unit/core/test_agent_runtime.py index dbaffa1..6eb4f30 100644 --- a/tests/unit/core/test_agent_runtime.py +++ b/tests/unit/core/test_agent_runtime.py @@ -1,3 +1,5 @@ +import pytest + from dana.common.llm.types import LLMMessage, LLMResponse from dana.core.agent.star_agent import STARAgent from dana.core.resource.base_resource import BaseResource @@ -62,6 +64,85 @@ class MockLLM: assert isinstance(messages[0], LLMMessage) +def test_default_runtime_system_prompt_override_reaches_built_messages(): + class MockLLM: + pass + + runtime = DefaultRuntime(llm=MockLLM()) + agent = STARAgent( + agent_type="runtime-test", + runtime=runtime, + auto_register=False, + enable_assistant=False, + enable_web_search=False, + enable_skills=False, + enable_code_execution=False, + ) + timeline = Timeline(agent=agent) + + agent.override_system_prompt_template("runtime override") + messages = runtime.build_prompt(agent, timeline) + + assert agent.system_prompt == "runtime override" + assert messages[0].content.endswith("runtime override") + + +def test_default_runtime_rejects_persistent_system_prompt_override(): + runtime = DefaultRuntime() + agent = STARAgent( + agent_type="runtime-test", + runtime=runtime, + auto_register=False, + enable_assistant=False, + enable_web_search=False, + enable_skills=False, + enable_code_execution=False, + ) + + with pytest.raises(NotImplementedError, match="does not support persistent"): + agent.override_system_prompt_template("persistent override", persist=True) + + +def test_system_prompt_override_wins_over_custom_runtime_template_hook(): + class CustomRuntime(DefaultRuntime): + def get_system_prompt_template(self, native_tools: bool) -> str: + return "custom runtime hook" + + runtime = CustomRuntime() + agent = STARAgent( + agent_type="runtime-test", + runtime=runtime, + auto_register=False, + enable_assistant=False, + enable_web_search=False, + enable_skills=False, + enable_code_execution=False, + ) + + agent.override_system_prompt_template("explicit override") + + assert agent.system_prompt == "explicit override" + + +def test_shared_default_runtime_keeps_overrides_agent_scoped(): + runtime = DefaultRuntime() + common = { + "runtime": runtime, + "auto_register": False, + "enable_assistant": False, + "enable_web_search": False, + "enable_skills": False, + "enable_code_execution": False, + } + first = STARAgent(agent_type="first", agent_id="first", **common) + second = STARAgent(agent_type="second", agent_id="second", **common) + + first.override_system_prompt_template("first override") + + assert first.system_prompt == "first override" + assert second.system_prompt != "first override" + + def test_default_runtime_parse_response_done_true(): runtime = DefaultRuntime() response = LLMResponse(content='{"done": true, "response": "Done", "tool_calls": []}', model="test") diff --git a/tests/unit/core/test_done_flag_autonomy.py b/tests/unit/core/test_done_flag_autonomy.py index 93bfff2..ef1923e 100644 --- a/tests/unit/core/test_done_flag_autonomy.py +++ b/tests/unit/core/test_done_flag_autonomy.py @@ -214,5 +214,5 @@ def test_prompt_contains_output_format(): agent = STARAgent(agent_type="prompt", auto_register=False, enable_web_search=False, enable_skills=False) system_prompt = agent.system_prompt - assert '"done"' in system_prompt - assert "JSON" in system_prompt + assert "" in system_prompt + assert "RESPONSE FORMAT" in system_prompt diff --git a/tests/unit/test_autonomy_in_template.py b/tests/unit/test_autonomy_in_template.py index 8e59ba9..2b31c29 100644 --- a/tests/unit/test_autonomy_in_template.py +++ b/tests/unit/test_autonomy_in_template.py @@ -1,5 +1,9 @@ """Test that autonomy instructions are properly placed in the system prompt template.""" +from concurrent.futures import ThreadPoolExecutor +from threading import Barrier +from unittest.mock import Mock + from dana.core.agent.star_agent import STARAgent from dana.core.knowledge.prompts.codecs import CSXMLCodec, KLXMLCodec from dana.core.prompt.prompt_api import TEMPLATE_SYSTEM_PROMPT @@ -77,9 +81,137 @@ def __init__(self): agent = TestAgent() system_prompt = agent.system_prompt - # Check autonomy section is present (DefaultRuntime format) - assert "## Output Format" in system_prompt or '"output_format"' in system_prompt - assert '"done"' in system_prompt + assert "" in system_prompt + assert "" in system_prompt + + def test_codec_runtime_full_prompt_override_refreshes_actual_llm_prompt(self): + agent = STARAgent( + agent_type="custom", + agent_id="custom-prompt-override", + auto_register=False, + enable_assistant=False, + enable_web_search=False, + enable_skills=False, + enable_code_execution=False, + ) + runtime = agent._runtime + prompt_api = runtime._get_prompt_api(agent) + prompt_api._store = Mock() + + assert prompt_api._repository_factory is agent._repository_factory + + agent.override_system_prompt_template("first prompt") + assert agent.system_prompt == "first prompt" + + agent.override_system_prompt_template("second prompt") + runtime._build_native_tools_if_supported = Mock() + runtime._get_runtime_context = Mock(return_value={}) + messages = runtime.build_prompt(agent, agent._timeline) + + assert agent.system_prompt == "second prompt" + assert messages[0].content == "second prompt" + prompt_api._store.get_active.assert_not_called() + prompt_api._store.create_snapshot.assert_not_called() + + def test_shared_codec_runtime_keeps_overrides_agent_scoped(self): + first = STARAgent( + agent_type="first", + agent_id="first-codec", + auto_register=False, + enable_assistant=False, + enable_web_search=False, + enable_skills=False, + enable_code_execution=False, + ) + runtime = first._runtime + second = STARAgent( + agent_type="second", + agent_id="second-codec", + runtime=runtime, + auto_register=False, + enable_assistant=False, + enable_web_search=False, + enable_skills=False, + enable_code_execution=False, + ) + first_store = Mock() + first_store.get_active.return_value = None + second_store = Mock() + second_store.get_active.return_value = None + runtime._get_prompt_api(first)._store = first_store + runtime._get_prompt_api(second)._store = second_store + + first.override_system_prompt_template("first codec override") + + assert first.system_prompt == "first codec override" + assert second.system_prompt != "first codec override" + + def test_shared_codec_runtime_builds_concurrent_prompts_for_current_agent(self): + first = STARAgent( + agent_type="first", + agent_id="first-concurrent-codec", + auto_register=False, + enable_assistant=False, + enable_web_search=False, + enable_skills=False, + enable_code_execution=False, + ) + runtime = first._runtime + second = STARAgent( + agent_type="second", + agent_id="second-concurrent-codec", + runtime=runtime, + auto_register=False, + enable_assistant=False, + enable_web_search=False, + enable_skills=False, + enable_code_execution=False, + ) + runtime._get_prompt_api(first)._store = Mock() + runtime._get_prompt_api(second)._store = Mock() + first.override_system_prompt_template("first concurrent override") + second.override_system_prompt_template("second concurrent override") + + barrier = Barrier(2) + + def wait_for_both_agents() -> dict: + barrier.wait() + return {} + + runtime._build_native_tools_if_supported = Mock() + runtime._build_tool_name_registry = Mock() + runtime._get_runtime_context = wait_for_both_agents + + with ThreadPoolExecutor(max_workers=2) as executor: + first_future = executor.submit(runtime.build_prompt, first, first._timeline) + second_future = executor.submit(runtime.build_prompt, second, second._timeline) + + assert first_future.result()[0].content == "first concurrent override" + assert second_future.result()[0].content == "second concurrent override" + + def test_codec_getter_matches_persisted_prompt_used_for_llm_messages(self): + agent = STARAgent( + agent_type="persisted", + agent_id="persisted-codec", + auto_register=False, + enable_assistant=False, + enable_web_search=False, + enable_skills=False, + enable_code_execution=False, + ) + runtime = agent._runtime + prompt_api = runtime._get_prompt_api(agent) + snapshot = Mock() + snapshot.content = "persisted codec prompt" + prompt_api._store = Mock() + prompt_api._store.get_active.return_value = snapshot + runtime._build_native_tools_if_supported = Mock() + runtime._get_runtime_context = Mock(return_value={}) + + messages = runtime.build_prompt(agent, agent._timeline) + + assert agent.system_prompt == "persisted codec prompt" + assert messages[0].content == "persisted codec prompt" def test_star_agent_subclass_inherits_autonomy(self): """Verify subclasses of STARAgent inherit autonomy instructions.""" @@ -100,6 +232,5 @@ def __init__(self): agent = CustomAgent() system_prompt = agent.system_prompt - # Subclass should also have autonomy (DefaultRuntime format) - assert "## Output Format" in system_prompt or '"output_format"' in system_prompt - assert '"done"' in system_prompt + assert "" in system_prompt + assert "" in system_prompt diff --git a/tests/unit/test_data_staragent_integration.py b/tests/unit/test_data_staragent_integration.py index 13c0176..0e9ce5e 100644 --- a/tests/unit/test_data_staragent_integration.py +++ b/tests/unit/test_data_staragent_integration.py @@ -5,8 +5,8 @@ """ from dana.common.llm.types import LLMMessage -from dana.core.timeline.timeline import TimelineEntry, TimelineEntryType from dana.core.prompt.prompt_builder import PromptBuilder +from dana.core.timeline.timeline import TimelineEntry, TimelineEntryType class DummyTimeline: @@ -58,7 +58,7 @@ def _build_prompt_messages(resource: DummyResource, task: str) -> list[LLMMessag identity_fn=lambda a: "IDENTITY", template_fn=lambda native: "{{identity}}", format_tool_fn=lambda sig: "", - system_prompt_fn=lambda: "SYSTEM", + system_prompt_fn=lambda _agent: "SYSTEM", skip_retrieved_context=False, # Enable context retrieval ) timeline = DummyTimeline(task) diff --git a/tests/unit/test_prompt_api_repository.py b/tests/unit/test_prompt_api_repository.py index 4cca992..909c3f3 100644 --- a/tests/unit/test_prompt_api_repository.py +++ b/tests/unit/test_prompt_api_repository.py @@ -229,3 +229,39 @@ def test_factory_passes_storage_config_correctly(self): assert api._store.storage_config == config finally: shutil.rmtree(temp_dir) + + +class TestLocalPromptAPITemplateOverride: + """Public full-template override behavior.""" + + @staticmethod + def _make_api() -> tuple[LocalPromptAPI, Mock]: + repository = Mock(spec=LocalPromptRepository) + factory = Mock(spec=RepositoryFactory) + factory.create.return_value = repository + api = LocalPromptAPI(agent=MockAgent(), codec=CSXMLCodec, repository_factory=factory) + return api, repository + + def test_ephemeral_override_bypasses_repository_and_survives_cache_invalidation(self): + api, repository = self._make_api() + + api.override_system_prompt_template("ephemeral prompt", persist=False) + + assert api.system_prompt == "ephemeral prompt" + repository.get_active.assert_not_called() + repository.create_snapshot.assert_not_called() + + api._system_prompt = None + assert api.system_prompt == "ephemeral prompt" + repository.get_active.assert_not_called() + repository.create_snapshot.assert_not_called() + + def test_persistent_override_creates_and_activates_snapshot(self): + api, repository = self._make_api() + repository.create_snapshot.return_value.version = "v1" + + api.override_system_prompt_template("persistent prompt", persist=True) + + repository.create_snapshot.assert_called_once() + repository.set_active.assert_called_once_with("v1") + assert api.system_prompt == "persistent prompt"