904eda3388
First step of the cognition control plane (docs/COGNITION.md). The chat turn is now an explicit society of parts over a shared TurnContext blackboard: perceive (stub) -> route (session mode) -> compose (tiered prompt) -> deliberate. - lyra/mind.py (new): TurnContext + the pipeline + assemble(); moved build_messages and the deliberation helpers here (the assembly belongs in the control plane). - lyra/chat.py: slimmed to "speak + persist" — calls mind.assemble(), runs the tool/generation loop, persists. No behavior change (same prompt, same output). - tests: point test_time/test_chat at mind; add an assemble() structure test; make test_chat/test_tools hermetic (CHAT_DELIBERATE off so respond() doesn't make a real LLM call). Suite 86 green in ~5s, ruff clean, no import cycle. This is the frame; perceive/route/learn get filled in next phases — each opt-in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57 lines
2.0 KiB
Python
57 lines
2.0 KiB
Python
"""Lyra's tools: dispatch + the chat tool loop (call -> run -> feed back -> reply)."""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def lyra(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db"))
|
|
monkeypatch.setenv("CHAT_DELIBERATE", "false") # don't make a real LLM call in respond()
|
|
from lyra import llm
|
|
monkeypatch.setattr(llm, "embed", lambda texts: [[0.1, 0.2, 0.3] for _ in texts])
|
|
import lyra.memory as memory
|
|
importlib.reload(memory)
|
|
return memory
|
|
|
|
|
|
def test_journal_write_tool(lyra):
|
|
from lyra import tools
|
|
out = tools.dispatch("journal_write", '{"entry": "a private thought"}')
|
|
assert "journal" in out.lower()
|
|
entries = lyra.list_journal(kinds=("journal",))
|
|
assert any(e["content"] == "a private thought" and e["source"] == "chat" for e in entries)
|
|
|
|
|
|
def test_note_tool_with_tag(lyra):
|
|
from lyra import tools
|
|
tools.dispatch("note", {"content": "villain 3-bets light", "tag": "poker"})
|
|
notes = lyra.list_journal(kinds=("note",))
|
|
assert any("[poker] villain 3-bets light" == e["content"] for e in notes)
|
|
|
|
|
|
def test_unknown_tool_is_safe(lyra):
|
|
from lyra import tools
|
|
assert "unknown tool" in tools.dispatch("nope", {})
|
|
|
|
|
|
def test_chat_runs_tool_then_replies(lyra, monkeypatch):
|
|
from lyra import llm, chat
|
|
calls = {"n": 0}
|
|
|
|
def fake_chat_call(messages, backend="cloud", model=None, tools=None):
|
|
calls["n"] += 1
|
|
if calls["n"] == 1:
|
|
return ({"role": "assistant", "content": None, "tool_calls": []},
|
|
[{"id": "c1", "name": "journal_write", "arguments": '{"entry": "noted from chat"}'}])
|
|
return ({"role": "assistant", "content": "Done, Brian."}, None)
|
|
|
|
monkeypatch.setattr(llm, "chat_call", fake_chat_call)
|
|
reply = chat.respond("s1", "write that down for me", backend="cloud")
|
|
|
|
assert reply == "Done, Brian."
|
|
assert calls["n"] == 2 # one tool round, then the text reply
|
|
assert any("noted from chat" in e["content"] for e in lyra.list_journal())
|