Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29e6f346ef | |||
| cb570a5adc | |||
| ce4f56adfa | |||
| bdc93b4b67 | |||
| 9c9d71eb31 | |||
| 4199038b8b | |||
| e3dfe18811 | |||
| d759b90aa1 |
@@ -22,6 +22,8 @@ from a fallback.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from chat.llm.classify import classify
|
from chat.llm.classify import classify
|
||||||
@@ -39,7 +41,7 @@ class AddresseeDecision(BaseModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
addressee_id: str
|
addressee_id: str
|
||||||
confidence: str = "medium" # "high" | "medium" | "low"
|
confidence: Literal["high", "medium", "low"] = "medium"
|
||||||
reason: str = ""
|
reason: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -379,8 +379,15 @@ def _witness_role_for(speaker_bot_id: str, host_bot_id: str | None) -> str:
|
|||||||
pinned the contract on ``search_memories``; this helper applies it
|
pinned the contract on ``search_memories``; this helper applies it
|
||||||
at the call site so a guest-as-speaker doesn't silently retrieve
|
at the call site so a guest-as-speaker doesn't silently retrieve
|
||||||
memories under the wrong POV mask.
|
memories under the wrong POV mask.
|
||||||
|
|
||||||
|
When ``host_bot_id`` is ``None`` (degenerate case from a half-seeded
|
||||||
|
chat or Phase-1 path), the speaker is treated as the host so the
|
||||||
|
query falls back to the host POV mask rather than silently masking
|
||||||
|
the speaker's own memories as a guest.
|
||||||
"""
|
"""
|
||||||
return "host" if speaker_bot_id == host_bot_id else "guest"
|
if host_bot_id is None or speaker_bot_id == host_bot_id:
|
||||||
|
return "host"
|
||||||
|
return "guest"
|
||||||
|
|
||||||
|
|
||||||
def _resolve_addressee(
|
def _resolve_addressee(
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ async def narrate_skip(
|
|||||||
model=narrative_model,
|
model=narrative_model,
|
||||||
max_tokens=200,
|
max_tokens=200,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
|
timeout_s=timeout_s,
|
||||||
)
|
)
|
||||||
text = (result or "").strip()
|
text = (result or "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
|
|||||||
@@ -125,6 +125,16 @@ def search_memories(
|
|||||||
so that stronger candidates yield smaller composite scores; the result is
|
so that stronger candidates yield smaller composite scores; the result is
|
||||||
sorted ascending and truncated to ``k``. The unmodified ``fts_rank`` and a
|
sorted ascending and truncated to ``k``. The unmodified ``fts_rank`` and a
|
||||||
debug-friendly ``composite_score`` are kept on each returned dict.
|
debug-friendly ``composite_score`` are kept on each returned dict.
|
||||||
|
|
||||||
|
The result ordering applies TWO independent significance boosts:
|
||||||
|
|
||||||
|
* **SQL-side** — ``ORDER BY (rank - significance * SIGNIFICANCE_RANK_BIAS)``
|
||||||
|
pushes higher-significance memories ahead in the FTS5 candidate set so
|
||||||
|
the over-fetch already prefers them for tied / near-tied BM25 ranks
|
||||||
|
(T57, §11.1).
|
||||||
|
* **Python-side** — a composite re-rank with ``_SIGNIFICANCE_WEIGHT``
|
||||||
|
reinforces the ordering after candidate retrieval, alongside the
|
||||||
|
recency boost above.
|
||||||
"""
|
"""
|
||||||
if witness_role not in _VALID_WITNESS_ROLES:
|
if witness_role not in _VALID_WITNESS_ROLES:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|||||||
@@ -97,3 +97,38 @@ async def test_classifier_failure_falls_back_to_host():
|
|||||||
assert result.addressee_id == "bot_a"
|
assert result.addressee_id == "bot_a"
|
||||||
assert result.reason == "fallback"
|
assert result.reason == "fallback"
|
||||||
assert result.confidence == "low"
|
assert result.confidence == "low"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_invalid_confidence_value_falls_back_to_default():
|
||||||
|
"""Pydantic rejects ``confidence`` values outside the literal set
|
||||||
|
(``high`` / ``medium`` / ``low``). After the retry budget is
|
||||||
|
exhausted, classify returns the configured fallback default —
|
||||||
|
here that's ``confidence="low"`` with ``reason="fallback"``.
|
||||||
|
"""
|
||||||
|
canned = [
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"addressee_id": "bot_a",
|
||||||
|
"confidence": "VERY_HIGH",
|
||||||
|
"reason": "out-of-range value",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"still_bad",
|
||||||
|
"still_bad",
|
||||||
|
]
|
||||||
|
client = MockLLMClient(canned=canned)
|
||||||
|
|
||||||
|
result = await detect_addressee(
|
||||||
|
client,
|
||||||
|
classifier_model="test-model",
|
||||||
|
user_prose="anything",
|
||||||
|
host_id="bot_a",
|
||||||
|
host_name="BotA",
|
||||||
|
guest_id="bot_b",
|
||||||
|
guest_name="BotB",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.addressee_id == "bot_a"
|
||||||
|
assert result.confidence == "low"
|
||||||
|
assert result.reason == "fallback"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import chat.state.world # noqa: F401
|
|||||||
import chat.state.events # noqa: F401
|
import chat.state.events # noqa: F401
|
||||||
import chat.state.threads # noqa: F401
|
import chat.state.threads # noqa: F401
|
||||||
from chat.llm.client import Message
|
from chat.llm.client import Message
|
||||||
from chat.services.prompt import assemble_narrative_prompt
|
from chat.services.prompt import _witness_role_for, assemble_narrative_prompt
|
||||||
|
|
||||||
|
|
||||||
def _seed_basic(conn) -> None:
|
def _seed_basic(conn) -> None:
|
||||||
@@ -852,3 +852,10 @@ def test_assemble_with_open_thread_renders_block(tmp_path):
|
|||||||
body = msgs[0].content
|
body = msgs[0].content
|
||||||
assert "Open threads:" in body
|
assert "Open threads:" in body
|
||||||
assert "Maya's job hunt" in body
|
assert "Maya's job hunt" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_witness_role_for_none_host_returns_host():
|
||||||
|
assert _witness_role_for("bot_a", None) == "host"
|
||||||
|
# Sanity check: existing semantics preserved.
|
||||||
|
assert _witness_role_for("bot_a", "bot_a") == "host"
|
||||||
|
assert _witness_role_for("bot_a", "bot_b") == "guest"
|
||||||
|
|||||||
@@ -98,6 +98,49 @@ class _RaisingMock:
|
|||||||
yield # pragma: no cover - make this a generator
|
yield # pragma: no cover - make this a generator
|
||||||
|
|
||||||
|
|
||||||
|
class _RecordingMock:
|
||||||
|
"""Mock LLMClient that records the kwargs passed to ``generate``.
|
||||||
|
|
||||||
|
Used to assert that callers plumb through optional parameters like
|
||||||
|
``timeout_s`` instead of swallowing them. Returns a fixed string so
|
||||||
|
the surrounding fallback path is not exercised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.captured_kwargs: dict | None = None
|
||||||
|
|
||||||
|
async def generate(
|
||||||
|
self, messages: Sequence[Message], *, model: str, **params
|
||||||
|
) -> str:
|
||||||
|
self.captured_kwargs = dict(params)
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
async def stream(
|
||||||
|
self, messages: Sequence[Message], *, model: str, **params
|
||||||
|
) -> AsyncIterator[str]:
|
||||||
|
raise RuntimeError("not used")
|
||||||
|
yield # pragma: no cover - make this a generator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_narrate_skip_passes_timeout_through():
|
||||||
|
mock = _RecordingMock()
|
||||||
|
await narrate_skip(
|
||||||
|
mock,
|
||||||
|
narrative_model="x",
|
||||||
|
skip_kind="jump",
|
||||||
|
speaker_bot=_SPEAKER,
|
||||||
|
you_name="Me",
|
||||||
|
current_time="late evening",
|
||||||
|
new_time="next morning",
|
||||||
|
current_activity="winding down for the night",
|
||||||
|
landing_state_hint="having coffee in the kitchen",
|
||||||
|
timeout_s=12.5,
|
||||||
|
)
|
||||||
|
assert mock.captured_kwargs is not None
|
||||||
|
assert mock.captured_kwargs.get("timeout_s") == 12.5
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_narrate_falls_back_on_generation_failure():
|
async def test_narrate_falls_back_on_generation_failure():
|
||||||
new_time = "next morning"
|
new_time = "next morning"
|
||||||
|
|||||||
Reference in New Issue
Block a user