Merge 7d03594fec into 699c99368d
This commit is contained in:
commit
88852d340b
|
|
@ -14,6 +14,7 @@ PERFORMANCE_LOG_FORMAT=compact # compact|rich
|
|||
# API_WORKERS=1
|
||||
# SESSION_OBSERVERS_LIMIT=10
|
||||
# GET_CONTEXT_MAX_TOKENS=100000
|
||||
# REPRESENTATION_INJECTION_ORDER=explicit,deductive,inductive,contradiction
|
||||
# MAX_FILE_SIZE=5242880 # Bytes
|
||||
# MAX_MESSAGE_SIZE=25000 # Characters
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ LOG_LEVEL = "INFO"
|
|||
PERFORMANCE_LOG_FORMAT = "compact" # "compact" for single-line logs, "rich" for local panels
|
||||
SESSION_OBSERVERS_LIMIT = 10
|
||||
GET_CONTEXT_MAX_TOKENS = 100000
|
||||
REPRESENTATION_INJECTION_ORDER = ["explicit", "deductive", "inductive", "contradiction"]
|
||||
MAX_FILE_SIZE = 5242880 # 5MB
|
||||
MAX_MESSAGE_SIZE = 25000 # Characters
|
||||
EMBED_MESSAGES = true
|
||||
|
|
|
|||
|
|
@ -522,6 +522,7 @@ DREAM_SURPRISAL__INCLUDE_LEVELS=["explicit", "deductive"]
|
|||
LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
SESSION_OBSERVERS_LIMIT=10
|
||||
GET_CONTEXT_MAX_TOKENS=100000
|
||||
REPRESENTATION_INJECTION_ORDER=explicit,deductive,inductive,contradiction
|
||||
MAX_MESSAGE_SIZE=25000
|
||||
MAX_FILE_SIZE=5242880 # 5MB
|
||||
EMBED_MESSAGES=true
|
||||
|
|
@ -530,6 +531,15 @@ EMBEDDING_MAX_TOKENS_PER_REQUEST=300000
|
|||
NAMESPACE=honcho
|
||||
```
|
||||
|
||||
`REPRESENTATION_INJECTION_ORDER` controls the order of the explicit, deductive,
|
||||
inductive, and contradiction blocks injected into prompts and returned in rendered
|
||||
representations. It must contain each section exactly once. The default preserves
|
||||
the existing order; to put higher-level patterns first, use:
|
||||
|
||||
```bash
|
||||
REPRESENTATION_INJECTION_ORDER=inductive,explicit,deductive,contradiction
|
||||
```
|
||||
|
||||
**Optional Integrations:**
|
||||
```bash
|
||||
LANGFUSE_HOST=https://cloud.langfuse.com
|
||||
|
|
@ -649,6 +659,7 @@ A complete config.toml with all defaults. Copy and modify what you need:
|
|||
[app]
|
||||
LOG_LEVEL = "INFO"
|
||||
SESSION_OBSERVERS_LIMIT = 10
|
||||
REPRESENTATION_INJECTION_ORDER = ["explicit", "deductive", "inductive", "contradiction"]
|
||||
EMBED_MESSAGES = true
|
||||
NAMESPACE = "honcho"
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from pydantic_settings import (
|
|||
BaseSettings,
|
||||
DotEnvSettingsSource,
|
||||
EnvSettingsSource,
|
||||
NoDecode,
|
||||
PydanticBaseSettingsSource,
|
||||
SettingsConfigDict,
|
||||
)
|
||||
|
|
@ -29,6 +30,13 @@ EmbeddingTransport = Literal["openai", "gemini"]
|
|||
EmbeddingDimensionsMode = Literal["auto", "always", "never"]
|
||||
EmbeddingEncodingFormat = Literal["float", "base64"]
|
||||
EmbeddingEncodingFormatMode = Literal["auto", "float", "base64"]
|
||||
RepresentationSection = Literal["explicit", "deductive", "inductive", "contradiction"]
|
||||
REPRESENTATION_SECTIONS: tuple[RepresentationSection, ...] = (
|
||||
"explicit",
|
||||
"deductive",
|
||||
"inductive",
|
||||
"contradiction",
|
||||
)
|
||||
|
||||
# OpenAI-compatible models that reject the `dimensions=` request parameter.
|
||||
_EMBEDDING_KNOWN_REJECTING_MODELS: frozenset[str] = frozenset(
|
||||
|
|
@ -1497,6 +1505,9 @@ class AppSettings(HonchoSettings):
|
|||
GET_CONTEXT_MAX_TOKENS: Annotated[int, Field(default=100_000, gt=0, le=250_000)] = (
|
||||
100_000
|
||||
)
|
||||
REPRESENTATION_INJECTION_ORDER: Annotated[
|
||||
tuple[RepresentationSection, ...], NoDecode
|
||||
] = REPRESENTATION_SECTIONS
|
||||
|
||||
MAX_MESSAGE_SIZE: Annotated[int, Field(default=25_000, gt=0)] = 25_000
|
||||
EMBED_MESSAGES: bool = True
|
||||
|
|
@ -1571,6 +1582,41 @@ class AppSettings(HonchoSettings):
|
|||
raise ValueError(f"Invalid performance log format: {v}")
|
||||
return log_format
|
||||
|
||||
@field_validator("REPRESENTATION_INJECTION_ORDER", mode="before")
|
||||
@classmethod
|
||||
def validate_representation_injection_order(
|
||||
cls, value: Any
|
||||
) -> tuple[RepresentationSection, ...]:
|
||||
"""Normalize and validate the representation section order.
|
||||
|
||||
Args:
|
||||
value: Comma-separated string or sequence of supported section names.
|
||||
|
||||
Returns:
|
||||
The validated section order as a tuple.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value is not an exact permutation of all sections.
|
||||
"""
|
||||
sections: tuple[Any, ...]
|
||||
if isinstance(value, str):
|
||||
sections = tuple(section.strip() for section in value.split(","))
|
||||
elif isinstance(value, list | tuple):
|
||||
sections = tuple(cast(list[Any] | tuple[Any, ...], value))
|
||||
else:
|
||||
sections = ()
|
||||
|
||||
if len(sections) != len(REPRESENTATION_SECTIONS) or set(sections) != set(
|
||||
REPRESENTATION_SECTIONS
|
||||
):
|
||||
supported = ",".join(REPRESENTATION_SECTIONS)
|
||||
raise ValueError(
|
||||
"REPRESENTATION_INJECTION_ORDER must contain each supported "
|
||||
+ f"section exactly once: {supported}"
|
||||
)
|
||||
|
||||
return cast(tuple[RepresentationSection, ...], sections)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def propagate_namespace(self) -> "AppSettings":
|
||||
"""Propagate top-level NAMESPACE to nested settings if not explicitly set."""
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
from collections.abc import Sequence
|
||||
from collections.abc import Callable, Iterator, Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from src import models
|
||||
from src.config import RepresentationSection, settings
|
||||
from src.utils.formatting import parse_datetime_iso
|
||||
|
||||
# Conclusion levels whose `session_name` stamp is trustworthy enough to scope on.
|
||||
|
|
@ -306,6 +307,28 @@ class ContradictionObservation(ContradictionObservationBase, ObservationMetadata
|
|||
)
|
||||
|
||||
|
||||
RepresentationObservation = (
|
||||
ExplicitObservation
|
||||
| DeductiveObservation
|
||||
| InductiveObservation
|
||||
| ContradictionObservation
|
||||
)
|
||||
|
||||
|
||||
def _observation_without_timestamp(observation: RepresentationObservation) -> str:
|
||||
"""Format an observation without timestamp metadata.
|
||||
|
||||
Args:
|
||||
observation: Observation to format.
|
||||
|
||||
Returns:
|
||||
Raw content for an explicit observation, otherwise timestamp-free text.
|
||||
"""
|
||||
if isinstance(observation, ExplicitObservation):
|
||||
return observation.content
|
||||
return observation.str_no_timestamps()
|
||||
|
||||
|
||||
class Representation(BaseModel):
|
||||
"""
|
||||
A Representation is a traversable and diffable map of observations.
|
||||
|
|
@ -406,6 +429,42 @@ class Representation(BaseModel):
|
|||
self.inductive = self.inductive[-max_observations:]
|
||||
self.contradiction = self.contradiction[-max_observations:]
|
||||
|
||||
def _iter_sections(
|
||||
self,
|
||||
) -> Iterator[tuple[RepresentationSection, Sequence[RepresentationObservation]]]:
|
||||
"""Yield observation sections in the configured injection order.
|
||||
|
||||
Yields:
|
||||
Pairs containing a section name and its observation sequence.
|
||||
"""
|
||||
sections: dict[RepresentationSection, Sequence[RepresentationObservation]] = {
|
||||
"explicit": self.explicit,
|
||||
"deductive": self.deductive,
|
||||
"inductive": self.inductive,
|
||||
"contradiction": self.contradiction,
|
||||
}
|
||||
for section in settings.REPRESENTATION_INJECTION_ORDER:
|
||||
yield section, sections[section]
|
||||
|
||||
def _format_sections(
|
||||
self, format_observation: Callable[[RepresentationObservation], str]
|
||||
) -> str:
|
||||
"""Format every section in configured order, including empty headers.
|
||||
|
||||
Args:
|
||||
format_observation: Callable that renders one observation.
|
||||
|
||||
Returns:
|
||||
Newline-delimited sections with observations numbered per section.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
for section, observations in self._iter_sections():
|
||||
parts.append(f"{section.upper()}:\n")
|
||||
for index, observation in enumerate(observations, 1):
|
||||
parts.append(f"{index}. {format_observation(observation)}")
|
||||
parts.append("")
|
||||
return "\n".join(parts)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Format representation into a clean, readable string for LLM prompts.
|
||||
|
|
@ -424,30 +483,7 @@ class Representation(BaseModel):
|
|||
- The user's dog is 5 years old
|
||||
|
||||
"""
|
||||
|
||||
parts: list[str] = []
|
||||
|
||||
parts.append("EXPLICIT:\n")
|
||||
for i, observation in enumerate(self.explicit, 1):
|
||||
parts.append(f"{i}. {observation}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("DEDUCTIVE:\n")
|
||||
for i, observation in enumerate(self.deductive, 1):
|
||||
parts.append(f"{i}. {observation}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("INDUCTIVE:\n")
|
||||
for i, observation in enumerate(self.inductive, 1):
|
||||
parts.append(f"{i}. {observation}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("CONTRADICTION:\n")
|
||||
for i, observation in enumerate(self.contradiction, 1):
|
||||
parts.append(f"{i}. {observation}")
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
return self._format_sections(str)
|
||||
|
||||
def str_with_ids(self) -> str:
|
||||
"""
|
||||
|
|
@ -468,29 +504,7 @@ class Representation(BaseModel):
|
|||
- id:abc123
|
||||
- id:def456
|
||||
"""
|
||||
parts: list[str] = []
|
||||
|
||||
parts.append("EXPLICIT:\n")
|
||||
for i, observation in enumerate(self.explicit, 1):
|
||||
parts.append(f"{i}. {observation.str_with_id()}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("DEDUCTIVE:\n")
|
||||
for i, observation in enumerate(self.deductive, 1):
|
||||
parts.append(f"{i}. {observation.str_with_id()}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("INDUCTIVE:\n")
|
||||
for i, observation in enumerate(self.inductive, 1):
|
||||
parts.append(f"{i}. {observation.str_with_id()}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("CONTRADICTION:\n")
|
||||
for i, observation in enumerate(self.contradiction, 1):
|
||||
parts.append(f"{i}. {observation.str_with_id()}")
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
return self._format_sections(lambda observation: observation.str_with_id())
|
||||
|
||||
def str_no_timestamps(self) -> str:
|
||||
"""
|
||||
|
|
@ -513,29 +527,7 @@ class Representation(BaseModel):
|
|||
- id:def456
|
||||
|
||||
"""
|
||||
parts: list[str] = []
|
||||
|
||||
parts.append("EXPLICIT:\n")
|
||||
for i, observation in enumerate(self.explicit, 1):
|
||||
parts.append(f"{i}. {observation.content}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("DEDUCTIVE:\n")
|
||||
for i, observation in enumerate(self.deductive, 1):
|
||||
parts.append(f"{i}. {observation.str_no_timestamps()}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("INDUCTIVE:\n")
|
||||
for i, observation in enumerate(self.inductive, 1):
|
||||
parts.append(f"{i}. {observation.str_no_timestamps()}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("CONTRADICTION:\n")
|
||||
for i, observation in enumerate(self.contradiction, 1):
|
||||
parts.append(f"{i}. {observation.str_no_timestamps()}")
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
return self._format_sections(_observation_without_timestamp)
|
||||
|
||||
def format_as_markdown(self, include_ids: bool = False) -> str:
|
||||
"""
|
||||
|
|
@ -551,60 +543,59 @@ class Representation(BaseModel):
|
|||
|
||||
parts: list[str] = []
|
||||
|
||||
# Add explicit observations
|
||||
if self.explicit:
|
||||
parts.append("## Explicit Observations\n")
|
||||
for obs in self.explicit:
|
||||
# Don't need IDs for explicit as these are the lowest level of reasoning.
|
||||
# id_prefix = f"[id:{obs.id}] " if include_ids and obs.id else ""
|
||||
parts.append(f"{obs}")
|
||||
parts.append("")
|
||||
for section, observations in self._iter_sections():
|
||||
if not observations:
|
||||
continue
|
||||
|
||||
# Add deductive observations
|
||||
if self.deductive:
|
||||
parts.append("## Deductive Observations\n")
|
||||
for obs in self.deductive:
|
||||
id_prefix = f"[id:{obs.id}] " if include_ids and obs.id else ""
|
||||
timestamp = _strip_microseconds_and_timezone(obs.created_at)
|
||||
parts.append(f"{id_prefix}[{timestamp}] {obs.conclusion}")
|
||||
if obs.premises:
|
||||
parts.append(" Premises:")
|
||||
for premise in obs.premises:
|
||||
parts.append(f" - {premise}")
|
||||
if section == "explicit":
|
||||
parts.append("## Explicit Observations\n")
|
||||
for obs in self.explicit:
|
||||
# IDs are unnecessary for the lowest reasoning level.
|
||||
parts.append(f"{obs}")
|
||||
parts.append("")
|
||||
parts.append("")
|
||||
|
||||
# Add inductive observations
|
||||
if self.inductive:
|
||||
parts.append("## Inductive Observations\n")
|
||||
for obs in self.inductive:
|
||||
id_prefix = f"[id:{obs.id}] " if include_ids and obs.id else ""
|
||||
parts.append(
|
||||
f"{id_prefix} **Pattern** [{obs.confidence}]: {obs.conclusion}"
|
||||
)
|
||||
if obs.pattern_type:
|
||||
parts.append(f" **Type**: {obs.pattern_type}")
|
||||
if obs.sources:
|
||||
parts.append(" **Sources**:")
|
||||
for source in obs.sources[:5]:
|
||||
parts.append(f" - {source}")
|
||||
if len(obs.sources) > 5:
|
||||
parts.append(f" - ... and {len(obs.sources) - 5} more")
|
||||
elif section == "deductive":
|
||||
parts.append("## Deductive Observations\n")
|
||||
for obs in self.deductive:
|
||||
id_prefix = f"[id:{obs.id}] " if include_ids and obs.id else ""
|
||||
timestamp = _strip_microseconds_and_timezone(obs.created_at)
|
||||
parts.append(f"{id_prefix}[{timestamp}] {obs.conclusion}")
|
||||
if obs.premises:
|
||||
parts.append(" Premises:")
|
||||
for premise in obs.premises:
|
||||
parts.append(f" - {premise}")
|
||||
parts.append("")
|
||||
parts.append("")
|
||||
parts.append("")
|
||||
|
||||
# Add contradiction observations
|
||||
if self.contradiction:
|
||||
parts.append("## Contradictions\n")
|
||||
for obs in self.contradiction:
|
||||
id_prefix = f"[id:{obs.id}] " if include_ids and obs.id else ""
|
||||
parts.append(f"{id_prefix} **CONTRADICTION**: {obs.content}")
|
||||
if obs.sources:
|
||||
parts.append(" **Conflicting statements**:")
|
||||
for source in obs.sources:
|
||||
parts.append(f" - {source}")
|
||||
elif section == "inductive":
|
||||
parts.append("## Inductive Observations\n")
|
||||
for obs in self.inductive:
|
||||
id_prefix = f"[id:{obs.id}] " if include_ids and obs.id else ""
|
||||
parts.append(
|
||||
f"{id_prefix} **Pattern** [{obs.confidence}]: {obs.conclusion}"
|
||||
)
|
||||
if obs.pattern_type:
|
||||
parts.append(f" **Type**: {obs.pattern_type}")
|
||||
if obs.sources:
|
||||
parts.append(" **Sources**:")
|
||||
for source in obs.sources[:5]:
|
||||
parts.append(f" - {source}")
|
||||
if len(obs.sources) > 5:
|
||||
parts.append(f" - ... and {len(obs.sources) - 5} more")
|
||||
parts.append("")
|
||||
parts.append("")
|
||||
|
||||
else:
|
||||
parts.append("## Contradictions\n")
|
||||
for obs in self.contradiction:
|
||||
id_prefix = f"[id:{obs.id}] " if include_ids and obs.id else ""
|
||||
parts.append(f"{id_prefix} **CONTRADICTION**: {obs.content}")
|
||||
if obs.sources:
|
||||
parts.append(" **Conflicting statements**:")
|
||||
for source in obs.sources:
|
||||
parts.append(f" - {source}")
|
||||
parts.append("")
|
||||
parts.append("")
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,288 @@
|
|||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src import crud
|
||||
from src.config import AppSettings
|
||||
from src.embedding_client import embedding_client
|
||||
from src.utils import agent_tools
|
||||
from src.utils import representation as representation_module
|
||||
from src.utils.agent_tools import ToolContext
|
||||
from src.utils.representation import (
|
||||
ContradictionObservation,
|
||||
DeductiveObservation,
|
||||
ExplicitObservation,
|
||||
InductiveObservation,
|
||||
Representation,
|
||||
)
|
||||
from src.utils.types import ToolResult
|
||||
|
||||
DEFAULT_ORDER = ("explicit", "deductive", "inductive", "contradiction")
|
||||
PATTERNS_FIRST_ORDER = ("inductive", "explicit", "deductive", "contradiction")
|
||||
|
||||
|
||||
def _full_representation() -> Representation:
|
||||
created_at = datetime(2025, 1, 2, 3, 4, 5, tzinfo=timezone.utc)
|
||||
return Representation(
|
||||
explicit=[
|
||||
ExplicitObservation(
|
||||
id="exp-1",
|
||||
content="explicit fact",
|
||||
created_at=created_at,
|
||||
message_ids=[1],
|
||||
)
|
||||
],
|
||||
deductive=[
|
||||
DeductiveObservation(
|
||||
id="ded-1",
|
||||
conclusion="deductive fact",
|
||||
premises=["explicit fact"],
|
||||
created_at=created_at,
|
||||
message_ids=[1],
|
||||
)
|
||||
],
|
||||
inductive=[
|
||||
InductiveObservation(
|
||||
id="ind-1",
|
||||
conclusion="inductive pattern",
|
||||
confidence="high",
|
||||
pattern_type="preference",
|
||||
sources=["explicit fact"],
|
||||
created_at=created_at,
|
||||
message_ids=[1],
|
||||
)
|
||||
],
|
||||
contradiction=[
|
||||
ContradictionObservation(
|
||||
id="con-1",
|
||||
content="conflicting fact",
|
||||
sources=["a", "b"],
|
||||
created_at=created_at,
|
||||
message_ids=[1],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _assert_headers_in_order(rendered: str, headers: tuple[str, ...]) -> None:
|
||||
positions = [rendered.index(header) for header in headers]
|
||||
assert positions == sorted(positions)
|
||||
|
||||
|
||||
def _render_with_ids(representation: Representation) -> str:
|
||||
return representation.str_with_ids()
|
||||
|
||||
|
||||
def _render_without_timestamps(representation: Representation) -> str:
|
||||
return representation.str_no_timestamps()
|
||||
|
||||
|
||||
def _render_as_markdown(representation: Representation) -> str:
|
||||
return representation.format_as_markdown(include_ids=True)
|
||||
|
||||
|
||||
def test_representation_injection_order_defaults_to_current_order(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.delenv("REPRESENTATION_INJECTION_ORDER", raising=False)
|
||||
|
||||
assert AppSettings().REPRESENTATION_INJECTION_ORDER == DEFAULT_ORDER
|
||||
|
||||
|
||||
def test_representation_injection_order_accepts_comma_separated_env(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv(
|
||||
"REPRESENTATION_INJECTION_ORDER",
|
||||
"inductive, explicit, deductive, contradiction",
|
||||
)
|
||||
|
||||
assert AppSettings().REPRESENTATION_INJECTION_ORDER == PATTERNS_FIRST_ORDER
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"configured_order",
|
||||
[
|
||||
"explicit,deductive,inductive",
|
||||
"explicit,deductive,inductive,explicit",
|
||||
"explicit,deductive,inductive,unknown",
|
||||
],
|
||||
)
|
||||
def test_representation_injection_order_rejects_invalid_permutations(
|
||||
monkeypatch: pytest.MonkeyPatch, configured_order: str
|
||||
) -> None:
|
||||
monkeypatch.setenv("REPRESENTATION_INJECTION_ORDER", configured_order)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="REPRESENTATION_INJECTION_ORDER must contain each supported section exactly once",
|
||||
):
|
||||
AppSettings()
|
||||
|
||||
|
||||
def test_default_rendering_is_byte_for_byte_unchanged(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
representation_module,
|
||||
"settings",
|
||||
AppSettings(REPRESENTATION_INJECTION_ORDER=DEFAULT_ORDER),
|
||||
raising=False,
|
||||
)
|
||||
representation = _full_representation()
|
||||
|
||||
assert str(representation) == (
|
||||
"EXPLICIT:\n\n"
|
||||
"1. [2025-01-02 03:04:05] explicit fact\n\n"
|
||||
"DEDUCTIVE:\n\n"
|
||||
"1. [2025-01-02 03:04:05] deductive fact\n"
|
||||
" - explicit fact\n\n"
|
||||
"INDUCTIVE:\n\n"
|
||||
"1. [2025-01-02 03:04:05] [high] inductive pattern\n"
|
||||
" - explicit fact\n\n"
|
||||
"CONTRADICTION:\n\n"
|
||||
"1. [2025-01-02 03:04:05] CONTRADICTION: conflicting fact\n"
|
||||
" - a\n"
|
||||
" - b\n"
|
||||
)
|
||||
assert representation.str_with_ids() == (
|
||||
"EXPLICIT:\n\n"
|
||||
"1. [id:exp-1] [2025-01-02 03:04:05] explicit fact\n\n"
|
||||
"DEDUCTIVE:\n\n"
|
||||
"1. [id:ded-1] [2025-01-02 03:04:05] deductive fact\n"
|
||||
" - explicit fact\n\n"
|
||||
"INDUCTIVE:\n\n"
|
||||
"1. [id:ind-1] [2025-01-02 03:04:05] [high] inductive pattern\n"
|
||||
" - explicit fact\n\n"
|
||||
"CONTRADICTION:\n\n"
|
||||
"1. [id:con-1] [2025-01-02 03:04:05] CONTRADICTION: conflicting fact\n"
|
||||
" - a\n"
|
||||
" - b\n"
|
||||
)
|
||||
assert representation.str_no_timestamps() == (
|
||||
"EXPLICIT:\n\n"
|
||||
"1. explicit fact\n\n"
|
||||
"DEDUCTIVE:\n\n"
|
||||
"1. deductive fact\n"
|
||||
" - explicit fact\n\n"
|
||||
"INDUCTIVE:\n\n"
|
||||
"1. [high] inductive pattern\n"
|
||||
" - explicit fact\n\n"
|
||||
"CONTRADICTION:\n\n"
|
||||
"1. CONTRADICTION: conflicting fact\n"
|
||||
" - a\n"
|
||||
" - b\n"
|
||||
)
|
||||
assert representation.format_as_markdown(include_ids=True) == (
|
||||
"## Explicit Observations\n\n"
|
||||
"[2025-01-02 03:04:05] explicit fact\n\n"
|
||||
"## Deductive Observations\n\n"
|
||||
"[id:ded-1] [2025-01-02 03:04:05] deductive fact\n"
|
||||
" Premises:\n"
|
||||
" - explicit fact\n\n\n"
|
||||
"## Inductive Observations\n\n"
|
||||
"[id:ind-1] **Pattern** [high]: inductive pattern\n"
|
||||
" **Type**: preference\n"
|
||||
" **Sources**:\n"
|
||||
" - explicit fact\n\n\n"
|
||||
"## Contradictions\n\n"
|
||||
"[id:con-1] **CONTRADICTION**: conflicting fact\n"
|
||||
" **Conflicting statements**:\n"
|
||||
" - a\n"
|
||||
" - b\n\n"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("renderer", "ordered_headers"),
|
||||
[
|
||||
(
|
||||
str,
|
||||
("INDUCTIVE:", "EXPLICIT:", "DEDUCTIVE:", "CONTRADICTION:"),
|
||||
),
|
||||
(
|
||||
_render_with_ids,
|
||||
("INDUCTIVE:", "EXPLICIT:", "DEDUCTIVE:", "CONTRADICTION:"),
|
||||
),
|
||||
(
|
||||
_render_without_timestamps,
|
||||
("INDUCTIVE:", "EXPLICIT:", "DEDUCTIVE:", "CONTRADICTION:"),
|
||||
),
|
||||
(
|
||||
_render_as_markdown,
|
||||
(
|
||||
"## Inductive Observations",
|
||||
"## Explicit Observations",
|
||||
"## Deductive Observations",
|
||||
"## Contradictions",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_all_renderers_use_the_configured_section_order(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
renderer: Callable[[Representation], str],
|
||||
ordered_headers: tuple[str, ...],
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
representation_module,
|
||||
"settings",
|
||||
AppSettings(REPRESENTATION_INJECTION_ORDER=PATTERNS_FIRST_ORDER),
|
||||
raising=False,
|
||||
)
|
||||
|
||||
rendered = renderer(_full_representation())
|
||||
|
||||
_assert_headers_in_order(rendered, ordered_headers)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_memory_prompt_uses_the_configured_order(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
representation = _full_representation()
|
||||
monkeypatch.setattr(
|
||||
representation_module,
|
||||
"settings",
|
||||
AppSettings(REPRESENTATION_INJECTION_ORDER=PATTERNS_FIRST_ORDER),
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
embedding_client,
|
||||
"embed",
|
||||
AsyncMock(return_value=[0.1]),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
crud,
|
||||
"query_documents",
|
||||
AsyncMock(return_value=[object()]),
|
||||
)
|
||||
|
||||
def return_representation(_documents: object) -> Representation:
|
||||
return representation
|
||||
|
||||
monkeypatch.setattr(Representation, "from_documents", return_representation)
|
||||
context = ToolContext(
|
||||
workspace_name="workspace",
|
||||
observer="observer",
|
||||
observed="observed",
|
||||
session_name=None,
|
||||
current_messages=None,
|
||||
include_observation_ids=False,
|
||||
history_token_limit=8192,
|
||||
db_lock=asyncio.Lock(),
|
||||
)
|
||||
|
||||
result = await agent_tools._handle_search_memory( # pyright: ignore[reportPrivateUsage]
|
||||
context,
|
||||
{"query": "patterns", "top_k": 10},
|
||||
)
|
||||
|
||||
assert isinstance(result, ToolResult)
|
||||
_assert_headers_in_order(
|
||||
result.content,
|
||||
("INDUCTIVE:", "EXPLICIT:", "DEDUCTIVE:", "CONTRADICTION:"),
|
||||
)
|
||||
Loading…
Reference in New Issue