Initial release: iai-mcp v0.1.0
Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: XNLLLLH <XNLLLLH@users.noreply.github.com>
This commit is contained in:
commit
f6b876fbe7
332 changed files with 97258 additions and 0 deletions
251
tests/test_curiosity.py
Normal file
251
tests/test_curiosity.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
"""Tests for LEARN-04 curiosity (D-23, D-24).
|
||||
|
||||
D-23 trigger: entropy > 0.7 bits, 3-turn cooldown.
|
||||
D-24 tiered style:
|
||||
- low entropy (0.4-0.7): silent log via events table (curiosity_silent_log)
|
||||
- mid entropy (0.7-0.9): inline hint in next response
|
||||
- high entropy (>0.9): direct clarifying question
|
||||
|
||||
compute_entropy operates in base-2 (bits) consistent with "0.7 bits".
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from datetime import datetime, timezone
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from iai_mcp.store import MemoryStore
|
||||
from iai_mcp.types import EMBED_DIM, MemoryRecord
|
||||
|
||||
|
||||
def _rec(vec=None, tags=None):
|
||||
vec = vec or [1.0] + [0.0] * (EMBED_DIM - 1)
|
||||
now = datetime.now(timezone.utc)
|
||||
return MemoryRecord(
|
||||
id=uuid4(),
|
||||
tier="episodic",
|
||||
literal_surface="r",
|
||||
aaak_index="",
|
||||
embedding=vec,
|
||||
community_id=None,
|
||||
centrality=0.0,
|
||||
detail_level=2,
|
||||
pinned=False,
|
||||
stability=0.0,
|
||||
difficulty=0.0,
|
||||
last_reviewed=None,
|
||||
never_decay=False,
|
||||
never_merge=False,
|
||||
provenance=[],
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
tags=list(tags or []),
|
||||
language="en",
|
||||
)
|
||||
|
||||
|
||||
class _Hit:
|
||||
def __init__(self, rid: UUID, score: float):
|
||||
self.record_id = rid
|
||||
self.score = score
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- constants
|
||||
|
||||
|
||||
def test_curiosity_thresholds():
|
||||
from iai_mcp import curiosity
|
||||
|
||||
assert curiosity.ENTROPY_LOW == 0.4
|
||||
assert curiosity.ENTROPY_MID == 0.7
|
||||
assert curiosity.ENTROPY_HIGH == 0.9
|
||||
assert curiosity.COOLDOWN_TURNS == 3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- compute_entropy
|
||||
|
||||
|
||||
def test_compute_entropy_uniform():
|
||||
"""Shannon entropy of [0.5, 0.5] = 1.0 bit."""
|
||||
from iai_mcp.curiosity import compute_entropy
|
||||
|
||||
e = compute_entropy([0.5, 0.5])
|
||||
assert abs(e - 1.0) < 1e-6
|
||||
|
||||
|
||||
def test_compute_entropy_skewed():
|
||||
from iai_mcp.curiosity import compute_entropy
|
||||
|
||||
e = compute_entropy([0.9, 0.1])
|
||||
# H([0.9,0.1]) = -(0.9*log2(0.9) + 0.1*log2(0.1)) ~ 0.469
|
||||
assert e < 0.5
|
||||
|
||||
|
||||
def test_compute_entropy_degenerate():
|
||||
from iai_mcp.curiosity import compute_entropy
|
||||
|
||||
assert compute_entropy([1.0]) == 0.0
|
||||
|
||||
|
||||
def test_compute_entropy_empty():
|
||||
from iai_mcp.curiosity import compute_entropy
|
||||
|
||||
assert compute_entropy([]) == 0.0
|
||||
|
||||
|
||||
def test_compute_entropy_zero_scores_handled():
|
||||
from iai_mcp.curiosity import compute_entropy
|
||||
|
||||
# Negative scores shouldn't crash (max(0, s) normalisation).
|
||||
e = compute_entropy([-1.0, 0.5, 0.5])
|
||||
assert e >= 0.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- fire_curiosity
|
||||
|
||||
|
||||
def test_fire_curiosity_below_threshold_silent(tmp_path):
|
||||
"""Low entropy (0.5) -> silent log, returns None."""
|
||||
from iai_mcp.curiosity import fire_curiosity
|
||||
from iai_mcp.events import query_events
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
r = _rec()
|
||||
store.insert(r)
|
||||
hits = [_Hit(r.id, 0.8)]
|
||||
q = fire_curiosity(
|
||||
store, hits, cue="ambiguous", entropy=0.5,
|
||||
session_id="s1", turn=1,
|
||||
)
|
||||
assert q is None
|
||||
silent = query_events(store, kind="curiosity_silent_log")
|
||||
assert len(silent) >= 1
|
||||
|
||||
|
||||
def test_fire_curiosity_below_ENTROPY_LOW_returns_none(tmp_path):
|
||||
"""Very low entropy (below ENTROPY_LOW=0.4) returns None without logging."""
|
||||
from iai_mcp.curiosity import fire_curiosity
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
q = fire_curiosity(
|
||||
store, [], cue="x", entropy=0.1,
|
||||
session_id="s-silent", turn=1,
|
||||
)
|
||||
assert q is None
|
||||
|
||||
|
||||
def test_fire_curiosity_mid_entropy_inline_hint(tmp_path):
|
||||
"""Entropy 0.8 -> CuriosityQuestion with tier='inline'."""
|
||||
from iai_mcp.curiosity import fire_curiosity
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
r = _rec()
|
||||
store.insert(r)
|
||||
hits = [_Hit(r.id, 0.6)]
|
||||
q = fire_curiosity(
|
||||
store, hits, cue="maybe", entropy=0.8,
|
||||
session_id="s2", turn=1,
|
||||
)
|
||||
assert q is not None
|
||||
assert q.tier == "inline"
|
||||
|
||||
|
||||
def test_fire_curiosity_high_entropy_direct_question(tmp_path):
|
||||
from iai_mcp.curiosity import fire_curiosity
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
r = _rec()
|
||||
store.insert(r)
|
||||
hits = [_Hit(r.id, 0.5)]
|
||||
q = fire_curiosity(
|
||||
store, hits, cue="unknown", entropy=0.95,
|
||||
session_id="s3", turn=1,
|
||||
)
|
||||
assert q is not None
|
||||
assert q.tier == "question"
|
||||
|
||||
|
||||
def test_fire_curiosity_cooldown_3_turns(tmp_path):
|
||||
"""Fire turn 1 -> fires. Turn 2 -> None (cooldown). Turn 3 -> None."""
|
||||
from iai_mcp.curiosity import fire_curiosity
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
r = _rec()
|
||||
store.insert(r)
|
||||
hits = [_Hit(r.id, 0.5)]
|
||||
q1 = fire_curiosity(store, hits, "x", 0.95, "s4", turn=1)
|
||||
assert q1 is not None
|
||||
q2 = fire_curiosity(store, hits, "x", 0.95, "s4", turn=2)
|
||||
assert q2 is None
|
||||
q3 = fire_curiosity(store, hits, "x", 0.95, "s4", turn=3)
|
||||
assert q3 is None
|
||||
|
||||
|
||||
def test_fire_curiosity_cooldown_releases(tmp_path):
|
||||
"""Turn 4 after turn 1 firing -> cooldown released."""
|
||||
from iai_mcp.curiosity import fire_curiosity
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
r = _rec()
|
||||
store.insert(r)
|
||||
hits = [_Hit(r.id, 0.5)]
|
||||
q1 = fire_curiosity(store, hits, "x", 0.95, "s5", turn=1)
|
||||
assert q1 is not None
|
||||
q4 = fire_curiosity(store, hits, "x", 0.95, "s5", turn=4)
|
||||
assert q4 is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- pending_questions
|
||||
|
||||
|
||||
def test_pending_questions_empty(tmp_path):
|
||||
from iai_mcp.curiosity import pending_questions
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
assert pending_questions(store) == []
|
||||
|
||||
|
||||
def test_pending_questions_filter_resolved(tmp_path):
|
||||
"""5 fired, 3 resolved -> pending_questions returns 2."""
|
||||
from iai_mcp.curiosity import fire_curiosity, pending_questions
|
||||
from iai_mcp.events import write_event
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
r = _rec()
|
||||
store.insert(r)
|
||||
hits = [_Hit(r.id, 0.5)]
|
||||
# Fire 5 questions across different sessions so cooldown doesn't block.
|
||||
q_ids: list = []
|
||||
for i in range(5):
|
||||
q = fire_curiosity(store, hits, f"cue{i}", 0.95, f"session-{i}", turn=1)
|
||||
assert q is not None
|
||||
q_ids.append(q.id)
|
||||
|
||||
# Resolve 3 via curiosity_resolved event
|
||||
for qid in q_ids[:3]:
|
||||
write_event(
|
||||
store, kind="curiosity_resolved",
|
||||
data={"question_id": str(qid)},
|
||||
severity="info",
|
||||
)
|
||||
|
||||
pending = pending_questions(store)
|
||||
assert len(pending) == 2
|
||||
|
||||
|
||||
def test_pending_questions_by_session(tmp_path):
|
||||
from iai_mcp.curiosity import fire_curiosity, pending_questions
|
||||
|
||||
store = MemoryStore(path=tmp_path)
|
||||
r = _rec()
|
||||
store.insert(r)
|
||||
hits = [_Hit(r.id, 0.5)]
|
||||
fire_curiosity(store, hits, "c", 0.95, "sA", turn=1)
|
||||
fire_curiosity(store, hits, "c", 0.95, "sB", turn=1)
|
||||
|
||||
onlyA = pending_questions(store, session_id="sA")
|
||||
onlyB = pending_questions(store, session_id="sB")
|
||||
assert len(onlyA) == 1
|
||||
assert len(onlyB) == 1
|
||||
Loading…
Add table
Add a link
Reference in a new issue