Skip to content

Commit ee36a6f

Browse files
authored
Add files via upload
1 parent 1ef5552 commit ee36a6f

5 files changed

Lines changed: 82 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@ All notable changes to Mnemolis are documented here, from v3.45.0 onward. For ev
44

55
---
66

7+
## [3.56.1]
8+
9+
### Fixed — fusion no longer carries `history`'s honest abstentions as noise
10+
11+
Found while CLI-verifying v3.56.0 for release: *"what are the latest AI trends"* and *"tell me about the history of ancient rome"* correctly avoided the keyword triggers (those regression pins held), but the **LLM fusion picker included the `history` source anyway** — its own NAME attracts it for those phrasings — and its plain-prose *"I couldn't identify which sensor or metric you mean…"* abstention was fused into otherwise-good answers as a trailing noise block.
12+
13+
Fixed with the project's established two-part pattern (the discourse-framing precedent explicitly rejects description-nudging as the sole lever):
14+
15+
- The source **description is corrected where it was genuinely inaccurate** — it now states it covers THIS house's own sensors and services only, NOT world history, general knowledge, news, or public/industry trends.
16+
- The **verifiable lever**: `history`'s five honest-abstention messages ("couldn't identify which sensor", "couldn't identify which kind of event", the ambiguity ask, "requires temporal pattern detection", "no recorded metrics yet") joined `_looks_empty()`'s canonical phrase list, so fusion drops them exactly as it drops any other member with nothing to contribute. The `**` structural guard means no real history answer (headers are always `**{name} — …**`) can ever match; the plain-prose event-count answers ("1 opening today, …") were verified to survive the new phrases, with pins.
17+
18+
Field-verified before tagging: both genre queries re-run against a live deployment came back with no trailing `[HISTORY]` block, on both fresh phrasings and the original cached-then-expired ones.
19+
20+
### Changed
21+
- **Dependency: `mcp[cli]` 1.27.2 → 1.28.1** (Dependabot). Full test suite — including the MCP transport tests — run against 1.28.1 before packaging, not just trusted from the PR.
22+
- Version bumped to 3.56.1. Test suite: **1697 passing** (3 new pins for the abstention/`_looks_empty` contract), ruff clean. One stale comment fixed (`cache_ttl_history_seconds` referenced the removed sampling-interval setting).
23+
24+
---
25+
726
## [3.56.0]
827

928
### Added — the `history` source: time-series memory for the house (opt-in, off by default)

app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ async def lifespan(app: FastAPI):
388388
app = FastAPI(
389389
title="Mnemolis",
390390
description="Unified local knowledge search API with multi-source fusion. Routes queries to Kiwix, Open-Meteo, FreshRSS, SearXNG, Uptime Kuma, or multiple sources concurrently.",
391-
version="3.56.0",
391+
version="3.56.1",
392392
lifespan=lifespan,
393393
)
394394

app/router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ def _search_changes(query: str) -> str:
427427
"uptime": "Uptime Kuma monitor status. Use when asked about service status, what is down, or network health.",
428428
"ha": "Home Assistant entity states. Use for house status summaries, which lights are on, door and lock status, battery levels, indoor sensors, motion events, or power consumption.",
429429
"changes": "Snapshot diff engine. Use when asked what changed, any new outages, weather changes, or new news since a given time.",
430-
"history": "Recorded history of the house's own sensors and services — temperature, CO2, humidity, power, service uptime, and door/motion events over time. Use for questions about past values, highs/lows, averages, counts, or trends.",
430+
"history": "Recorded readings from THIS house's own sensors and services only — temperature, CO2, humidity, power, service uptime, and door/motion events over time. Use for questions about this home's past sensor values, highs/lows, averages, counts, or sensor trends. NOT for world history, general knowledge, news, or public/industry trends.",
431431
"fusion": "Multi-source fusion — queries multiple sources concurrently and merges results. Use for complex queries that benefit from combining offline knowledge, live web, and recent news.",
432432
}
433433

app/sources/fusion.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,19 @@ def _looks_empty(result: str) -> bool:
216216
"unable to retrieve", "no valid sources", "no results returned",
217217
"no entity states returned", "no matching entities found",
218218
"no significant changes",
219+
# history's four honest-abstention messages (v3.56.0 field finding:
220+
# LLM fusion routing includes `history` for phrasings like "latest AI
221+
# trends" and "history of ancient rome" — the source's own name pulls
222+
# it in — and its plain-prose "couldn't identify" abstention was fused
223+
# into otherwise-good answers as a trailing noise block). All four are
224+
# plain prose with no `**`, so the structural guard above keeps every
225+
# real history answer — whose headers are always `**{name} — …**` —
226+
# untouchable by this list. Same verified-against-the-actual-return-
227+
# statements discipline as every phrase above.
228+
"couldn't identify which sensor", "couldn't identify which kind of event",
229+
"sensors match that — which did you mean",
230+
"requires temporal pattern detection",
231+
"no recorded metrics yet",
219232
)
220233

221234

tests/test_history_source.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,51 @@ def test_outdoor_sensor_resolves_via_area_class(self, enabled, temp_history_db):
382382
with patch("app.sources.home_assistant._detect_area", return_value="outside"):
383383
out = HS.search("how cold did it get outside today")
384384
assert "99.9" in out and "which did you mean" not in out
385+
386+
def test_abstention_messages_are_looks_empty_for_fusion(self, enabled, temp_history_db):
387+
# Field finding: LLM fusion routing pulled `history` in for
388+
# "latest AI trends" / "history of ancient rome" (the source's
389+
# own name attracts it) and fused the plain-prose abstention in
390+
# as a trailing noise block. The canonical _looks_empty filter
391+
# must recognize all five abstentions — and must NOT catch a
392+
# real answer, whose header's `**` is the structural guard.
393+
from app.sources.fusion import _looks_empty
394+
# Empty catalog -> "No recorded metrics yet" abstention.
395+
with patch("app.sources.home_assistant._detect_area", return_value=None):
396+
assert _looks_empty(HS.search("what are the latest AI trends today")) is True
397+
398+
_seed(temp_history_db, [
399+
("sensor.lr", 74.0, "°F", "Living Room Temperature", "living_room", "temperature"),
400+
])
401+
with patch("app.sources.home_assistant._detect_area", return_value=None):
402+
# Populated catalog, unresolvable query -> "couldn't identify".
403+
assert _looks_empty(HS.search("what are the latest AI trends today")) is True
404+
# A real answer must survive.
405+
assert _looks_empty(HS.search("living room temperature today")) is False
406+
407+
def test_ambiguity_ask_is_looks_empty_for_fusion(self, enabled, temp_history_db):
408+
from app.sources.fusion import _looks_empty
409+
_seed(temp_history_db, [
410+
("sensor.a", 74.0, "°F", "Living Room Temperature", "living_room", "temperature"),
411+
("sensor.b", 99.9, "°F", "Temperature", "outside", "temperature"),
412+
])
413+
with patch("app.sources.home_assistant._detect_area", return_value=None):
414+
ask = HS.search("average temperature today")
415+
assert "which did you mean" in ask
416+
assert _looks_empty(ask) is True
417+
418+
def test_event_count_answer_is_not_looks_empty(self, enabled, temp_temporal_db):
419+
# "1 opening today, most recently …" is plain prose with no `**`;
420+
# it must survive the new phrases. Same for the honest zero.
421+
from app.sources.fusion import _looks_empty
422+
now = datetime.now(timezone.utc)
423+
con = sqlite3.connect(temp_temporal_db)
424+
con.execute("INSERT INTO temporal_events (source, event_type, timestamp, raw_detail) "
425+
"VALUES (?,?,?,?)",
426+
("ha", "binary_sensor.front_door:opened", _iso(now - timedelta(hours=1)), ""))
427+
con.commit()
428+
con.close()
429+
with patch.object(settings, "temporal_pattern_detection_enabled", True):
430+
out = HS.search("how many times did the front door open today")
431+
assert out.startswith("1 opening")
432+
assert _looks_empty(out) is False

0 commit comments

Comments
 (0)