Skip to content

Commit 6b45815

Browse files
fix(deriver): document explicit observation JSON shape
1 parent 73453f8 commit 6b45815

4 files changed

Lines changed: 76 additions & 1 deletion

File tree

src/deriver/prompts.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ def minimal_deriver_prompt(
7676
- EXPLICIT: "I took my dog for a walk in NYC" → "alice has a dog", "alice lives in NYC"
7777
- EXPLICIT: "alice attended college" + general knowledge → "alice completed high school or equivalent"
7878
79+
Respond with a json object in the following format:
80+
{{"explicit": [{{"content": "fact 1"}}, {{"content": "fact 2"}}]}}
81+
Each item in "explicit" must be an object with a "content" field, not a bare string.
82+
7983
{custom_instructions_section}
8084
8185
Target peer:

src/utils/representation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ class PromptRepresentation(BaseModel):
114114
"""
115115

116116
explicit: list[ExplicitObservationBase] = Field(
117-
description="Facts LITERALLY stated by the user - direct quotes or clear paraphrases only, no interpretation or inference. Example: ['The user is 25 years old', 'The user has a dog named Rover']",
117+
description='Facts LITERALLY stated by the user - direct quotes or clear paraphrases only, no interpretation or inference. Example: [{"content": "The user is 25 years old"}, {"content": "The user has a dog named Rover"}]',
118118
default_factory=list,
119119
)
120120

tests/deriver/test_prompts.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
1+
from collections.abc import AsyncGenerator, Generator
12
from unittest.mock import patch
23

34
import pytest
5+
from pydantic import ValidationError
46

57
from src.deriver.prompts import (
68
estimate_deriver_prompt_tokens,
79
estimate_minimal_deriver_prompt_tokens,
810
minimal_deriver_prompt,
911
)
12+
from src.utils.representation import PromptRepresentation
13+
14+
15+
@pytest.fixture(autouse=True)
16+
async def clean_queue_tables() -> AsyncGenerator[None, None]:
17+
"""Prompt-only tests do not need queue table cleanup."""
18+
yield
19+
20+
21+
@pytest.fixture(autouse=True)
22+
def mock_tracked_db() -> Generator[None, None, None]:
23+
"""Prompt-only tests do not need tracked_db patching."""
24+
yield
1025

1126

1227
def test_minimal_deriver_prompt_includes_custom_instructions_when_present() -> None:
@@ -30,6 +45,30 @@ def test_minimal_deriver_prompt_omits_custom_instructions_when_absent() -> None:
3045
assert "CUSTOM INSTRUCTIONS:" not in prompt
3146

3247

48+
def test_minimal_deriver_prompt_describes_explicit_json_object_shape() -> None:
49+
prompt = minimal_deriver_prompt(
50+
peer_id="alice",
51+
messages="alice: I just had my 25th birthday",
52+
custom_instructions=None,
53+
)
54+
55+
assert '"explicit": [{"content": "fact 1"}' in prompt
56+
assert 'Each item in "explicit" must be an object with a "content" field' in prompt
57+
assert "not a bare string" in prompt
58+
with pytest.raises(ValidationError):
59+
PromptRepresentation.model_validate_json(
60+
'{"explicit": ["alice is 25 years old"]}'
61+
)
62+
assert PromptRepresentation.model_validate_json(
63+
'{"explicit": [{"content": "alice is 25 years old"}]}'
64+
).explicit[0].content == "alice is 25 years old"
65+
explicit_schema_description = PromptRepresentation.model_json_schema()["properties"][
66+
"explicit"
67+
]["description"]
68+
assert '{"content": "The user is 25 years old"}' in explicit_schema_description
69+
assert "['The user is 25 years old'" not in explicit_schema_description
70+
71+
3372
def test_estimate_deriver_prompt_tokens_increases_with_custom_instructions() -> None:
3473
base_tokens = estimate_minimal_deriver_prompt_tokens()
3574
custom_tokens = estimate_deriver_prompt_tokens(

tests/llm/test_backends/test_openai.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,38 @@ async def test_structured_output_json_object_mode_request_shape() -> None:
813813
assert result.content.answer == "ok"
814814

815815

816+
@pytest.mark.asyncio
817+
async def test_structured_output_json_object_mode_prompt_representation_schema_shape() -> None:
818+
"""PromptRepresentation schema hints explicit observations as content objects."""
819+
client = Mock()
820+
client.chat.completions.parse = AsyncMock()
821+
client.chat.completions.create = AsyncMock(
822+
return_value=_structured_create_return(
823+
'{"explicit": [{"content": "alice likes coffee"}]}'
824+
)
825+
)
826+
827+
backend = OpenAIBackend(client)
828+
result = await backend.complete(
829+
model="glm-4.6",
830+
messages=[{"role": "user", "content": "Hello"}],
831+
max_tokens=100,
832+
response_format=PromptRepresentation,
833+
extra_params={"structured_output_mode": "json_object"},
834+
)
835+
836+
call = _await_kwargs(client.chat.completions.create)
837+
system_messages = [m for m in call["messages"] if m["role"] == "system"]
838+
assert system_messages, "expected a system message carrying the schema"
839+
system_content = system_messages[0]["content"]
840+
schema = json.loads(system_content.split("JSON schema:\n", 1)[1])
841+
explicit_description = schema["properties"]["explicit"]["description"]
842+
assert '{"content": "The user is 25 years old"}' in explicit_description
843+
assert "['The user is 25 years old'" not in explicit_description
844+
assert isinstance(result.content, PromptRepresentation)
845+
assert result.content.explicit[0].content == "alice likes coffee"
846+
847+
816848
@pytest.mark.asyncio
817849
async def test_structured_output_json_object_mode_repairs_markdown() -> None:
818850
"""A provider that ignores json_object and returns prose must not crash —

0 commit comments

Comments
 (0)