Implement deriver custom instructions
This commit is contained in:
parent
8a42a9b5b9
commit
6a746f1de4
|
|
@ -23,7 +23,7 @@ from src.utils.representation import PromptRepresentation, Representation
|
|||
from src.utils.tokens import track_deriver_input_tokens
|
||||
|
||||
from .prompts import (
|
||||
estimate_minimal_deriver_prompt_tokens,
|
||||
estimate_deriver_prompt_tokens,
|
||||
minimal_deriver_system_prompt,
|
||||
minimal_deriver_user_prompt,
|
||||
)
|
||||
|
|
@ -98,7 +98,8 @@ async def process_representation_tasks_batch(
|
|||
)
|
||||
|
||||
# Track token usage - count only tokens from messages being processed
|
||||
prompt_tokens = estimate_minimal_deriver_prompt_tokens()
|
||||
custom_instructions = message_level_configuration.reasoning.custom_instructions
|
||||
prompt_tokens = estimate_deriver_prompt_tokens(custom_instructions)
|
||||
queue_item_message_ids_set = set(queue_item_message_ids)
|
||||
messages_tokens = sum(
|
||||
msg.token_count for msg in messages if msg.id in queue_item_message_ids_set
|
||||
|
|
@ -143,7 +144,11 @@ async def process_representation_tasks_batch(
|
|||
{"role": "system", "content": minimal_deriver_system_prompt()},
|
||||
{
|
||||
"role": "user",
|
||||
"content": minimal_deriver_user_prompt(observed, formatted_messages),
|
||||
"content": minimal_deriver_user_prompt(
|
||||
observed,
|
||||
formatted_messages,
|
||||
custom_instructions=custom_instructions,
|
||||
),
|
||||
},
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ from inspect import cleandoc as c
|
|||
from src.utils.tokens import estimate_tokens
|
||||
|
||||
|
||||
def _custom_instructions_section(custom_instructions: str | None) -> str:
|
||||
"""Render the optional custom instructions block for the deriver prompt."""
|
||||
if not custom_instructions or not custom_instructions.strip():
|
||||
return ""
|
||||
|
||||
return c(
|
||||
f"""
|
||||
CUSTOM INSTRUCTIONS:
|
||||
{custom_instructions.strip()}
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def minimal_deriver_system_prompt() -> str:
|
||||
"""Generate the cacheable instructions for observation extraction."""
|
||||
return c(
|
||||
|
|
@ -38,12 +51,20 @@ EXAMPLES:
|
|||
)
|
||||
|
||||
|
||||
def minimal_deriver_user_prompt(peer_id: str, messages: str) -> str:
|
||||
def minimal_deriver_user_prompt(
|
||||
peer_id: str,
|
||||
messages: str,
|
||||
*,
|
||||
custom_instructions: str | None = None,
|
||||
) -> str:
|
||||
"""Generate the per-request message payload for observation extraction."""
|
||||
instructions_section = _custom_instructions_section(custom_instructions)
|
||||
return c(
|
||||
f"""
|
||||
Peer identifier: {peer_id}
|
||||
|
||||
{instructions_section}
|
||||
|
||||
Messages to analyze:
|
||||
<messages>
|
||||
{messages}
|
||||
|
|
@ -55,6 +76,8 @@ Messages to analyze:
|
|||
def minimal_deriver_prompt(
|
||||
peer_id: str,
|
||||
messages: str,
|
||||
*,
|
||||
custom_instructions: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Generate the combined prompt for fast observation extraction.
|
||||
|
|
@ -66,7 +89,7 @@ def minimal_deriver_prompt(
|
|||
f"""
|
||||
{minimal_deriver_system_prompt()}
|
||||
|
||||
{minimal_deriver_user_prompt(peer_id, messages)}
|
||||
{minimal_deriver_user_prompt(peer_id, messages, custom_instructions=custom_instructions)}
|
||||
"""
|
||||
)
|
||||
|
||||
|
|
@ -84,3 +107,22 @@ def estimate_minimal_deriver_prompt_tokens() -> int:
|
|||
return estimate_tokens(prompt)
|
||||
except ValueError:
|
||||
return 300
|
||||
|
||||
|
||||
def estimate_deriver_prompt_tokens(custom_instructions: str | None = None) -> int:
|
||||
"""Estimate deriver prompt tokens, including optional custom instructions."""
|
||||
if not custom_instructions or not custom_instructions.strip():
|
||||
return estimate_minimal_deriver_prompt_tokens()
|
||||
|
||||
return estimate_tokens(
|
||||
"\n\n".join(
|
||||
[
|
||||
minimal_deriver_system_prompt(),
|
||||
minimal_deriver_user_prompt(
|
||||
peer_id="",
|
||||
messages="",
|
||||
custom_instructions=custom_instructions,
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ class MessageConfiguration(BaseModel):
|
|||
|
||||
class ResolvedReasoningConfiguration(BaseModel):
|
||||
enabled: bool
|
||||
custom_instructions: str | None = None
|
||||
|
||||
|
||||
class ResolvedPeerCardConfiguration(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,9 +1,22 @@
|
|||
import signal
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src import models
|
||||
from src.deriver.deriver import process_representation_tasks_batch
|
||||
from src.schemas import (
|
||||
ResolvedConfiguration,
|
||||
ResolvedDreamConfiguration,
|
||||
ResolvedPeerCardConfiguration,
|
||||
ResolvedReasoningConfiguration,
|
||||
ResolvedSummaryConfiguration,
|
||||
)
|
||||
from src.utils.clients import HonchoLLMCallResponse
|
||||
from src.utils.representation import ExplicitObservationBase, PromptRepresentation
|
||||
from src.utils.representation import Representation
|
||||
from src.utils.work_unit import construct_work_unit_key, parse_work_unit_key
|
||||
|
||||
|
|
@ -183,6 +196,79 @@ class TestBackwardsCompatibility:
|
|||
|
||||
assert observers == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestCustomInstructions:
|
||||
async def test_deriver_passes_custom_instructions_into_prompt(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def fake_prompt(
|
||||
peer_id: str,
|
||||
messages: str,
|
||||
*,
|
||||
custom_instructions: str | None = None,
|
||||
) -> str:
|
||||
captured["peer_id"] = peer_id
|
||||
captured["messages"] = messages
|
||||
captured["custom_instructions"] = custom_instructions
|
||||
return "prompt"
|
||||
|
||||
mock_response = HonchoLLMCallResponse(
|
||||
content=PromptRepresentation(
|
||||
explicit=[ExplicitObservationBase(content="Alice likes tea")]
|
||||
),
|
||||
output_tokens=5,
|
||||
finish_reasons=["stop"],
|
||||
)
|
||||
|
||||
monkeypatch.setattr("src.deriver.deriver.minimal_deriver_prompt", fake_prompt)
|
||||
monkeypatch.setattr(
|
||||
"src.deriver.deriver.honcho_llm_call",
|
||||
AsyncMock(return_value=mock_response),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.crud.representation.RepresentationManager.save_representation",
|
||||
AsyncMock(),
|
||||
)
|
||||
|
||||
message = SimpleNamespace(
|
||||
id=1,
|
||||
content="I like tea.",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
peer_name="alice",
|
||||
token_count=4,
|
||||
session_name="session-1",
|
||||
workspace_name="workspace-1",
|
||||
)
|
||||
configuration = ResolvedConfiguration(
|
||||
reasoning=ResolvedReasoningConfiguration(
|
||||
enabled=True,
|
||||
custom_instructions="Focus on durable preferences only.",
|
||||
),
|
||||
peer_card=ResolvedPeerCardConfiguration(use=True, create=True),
|
||||
summary=ResolvedSummaryConfiguration(
|
||||
enabled=True,
|
||||
messages_per_short_summary=10,
|
||||
messages_per_long_summary=20,
|
||||
),
|
||||
dream=ResolvedDreamConfiguration(enabled=True),
|
||||
)
|
||||
|
||||
await process_representation_tasks_batch(
|
||||
messages=[message],
|
||||
message_level_configuration=configuration,
|
||||
observers=["alice"],
|
||||
observed="alice",
|
||||
queue_item_message_ids=[1],
|
||||
)
|
||||
|
||||
assert captured["peer_id"] == "alice"
|
||||
assert captured["custom_instructions"] == "Focus on durable preferences only."
|
||||
assert "I like tea." in captured["messages"]
|
||||
|
||||
# async def test_representation_batch_uses_earliest_cutoff(
|
||||
# self,
|
||||
# db_session: AsyncSession,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
from src.deriver.prompts import (
|
||||
estimate_deriver_prompt_tokens,
|
||||
estimate_minimal_deriver_prompt_tokens,
|
||||
minimal_deriver_prompt,
|
||||
)
|
||||
|
||||
|
||||
class TestMinimalDeriverPrompt:
|
||||
def test_includes_custom_instructions_section_when_present(self) -> None:
|
||||
prompt = minimal_deriver_prompt(
|
||||
peer_id="alice",
|
||||
messages="alice: hello",
|
||||
custom_instructions="Focus on durable preferences only.",
|
||||
)
|
||||
|
||||
assert "CUSTOM INSTRUCTIONS:" in prompt
|
||||
assert "Focus on durable preferences only." in prompt
|
||||
|
||||
def test_omits_custom_instructions_section_when_absent(self) -> None:
|
||||
prompt = minimal_deriver_prompt(peer_id="alice", messages="alice: hello")
|
||||
|
||||
assert "CUSTOM INSTRUCTIONS:" not in prompt
|
||||
|
||||
def test_custom_instructions_increase_prompt_token_estimate(self) -> None:
|
||||
base_tokens = estimate_minimal_deriver_prompt_tokens()
|
||||
custom_tokens = estimate_deriver_prompt_tokens(
|
||||
"Focus on durable preferences only."
|
||||
)
|
||||
|
||||
assert custom_tokens > base_tokens
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
from types import SimpleNamespace
|
||||
|
||||
from src.schemas import MessageConfiguration, ReasoningConfiguration
|
||||
from src.utils.config_helpers import get_configuration
|
||||
|
||||
|
||||
class TestGetConfiguration:
|
||||
def test_preserves_workspace_custom_instructions(self) -> None:
|
||||
workspace = SimpleNamespace(
|
||||
configuration={
|
||||
"reasoning": {
|
||||
"enabled": True,
|
||||
"custom_instructions": "Focus on durable preferences.",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
config = get_configuration(None, None, workspace)
|
||||
|
||||
assert config.reasoning.enabled is True
|
||||
assert config.reasoning.custom_instructions == "Focus on durable preferences."
|
||||
|
||||
def test_message_custom_instructions_override_session_and_workspace(self) -> None:
|
||||
workspace = SimpleNamespace(
|
||||
configuration={"reasoning": {"custom_instructions": "workspace scope"}}
|
||||
)
|
||||
session = SimpleNamespace(
|
||||
configuration={"reasoning": {"custom_instructions": "session scope"}}
|
||||
)
|
||||
message = MessageConfiguration(
|
||||
reasoning=ReasoningConfiguration(custom_instructions="message scope")
|
||||
)
|
||||
|
||||
config = get_configuration(message, session, workspace)
|
||||
|
||||
assert config.reasoning.custom_instructions == "message scope"
|
||||
Loading…
Reference in New Issue