feat(conversations): the recall hook and the user sink learn whose turn it is
This commit is contained in:
@@ -25,6 +25,8 @@ class RecallContext:
|
|||||||
text: str
|
text: str
|
||||||
kind: str
|
kind: str
|
||||||
now: datetime
|
now: datetime
|
||||||
|
agent: str = ""
|
||||||
|
"""Agent whose turn it is; a setup with several tells them apart by it."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -40,7 +42,12 @@ class Envelope:
|
|||||||
texts: EnvelopeTexts = field(default_factory=EnvelopeTexts)
|
texts: EnvelopeTexts = field(default_factory=EnvelopeTexts)
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
self, *, now: datetime | None = None, text: str = "", kind: str = "master"
|
self,
|
||||||
|
*,
|
||||||
|
now: datetime | None = None,
|
||||||
|
text: str = "",
|
||||||
|
kind: str = "master",
|
||||||
|
agent: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
now = now or datetime.now(UTC)
|
now = now or datetime.now(UTC)
|
||||||
changes = self.watch.take() if self.watch is not None else []
|
changes = self.watch.take() if self.watch is not None else []
|
||||||
@@ -59,20 +66,26 @@ class Envelope:
|
|||||||
texts=self.texts,
|
texts=self.texts,
|
||||||
)
|
)
|
||||||
self.last_at = now
|
self.last_at = now
|
||||||
block = self.recall_block(text=text, kind=kind, now=now)
|
block = self.recall_block(text=text, kind=kind, now=now, agent=agent)
|
||||||
return f"{out}\n{block}" if block else out
|
return f"{out}\n{block}" if block else out
|
||||||
|
|
||||||
def recall_only(
|
def recall_only(
|
||||||
self, *, text: str, kind: str, now: datetime | None = None
|
self, *, text: str, kind: str, now: datetime | None = None, agent: str = ""
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
block = self.recall_block(text=text, kind=kind, now=now or datetime.now(UTC))
|
block = self.recall_block(
|
||||||
|
text=text, kind=kind, now=now or datetime.now(UTC), agent=agent
|
||||||
|
)
|
||||||
return f"{self.texts.header}\n{block}" if block else None
|
return f"{self.texts.header}\n{block}" if block else None
|
||||||
|
|
||||||
def recall_block(self, *, text: str, kind: str, now: datetime) -> str | None:
|
def recall_block(
|
||||||
|
self, *, text: str, kind: str, now: datetime, agent: str = ""
|
||||||
|
) -> str | None:
|
||||||
if self.recall is None or not text.strip():
|
if self.recall is None or not text.strip():
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
block = self.recall(RecallContext(text=text, kind=kind, now=now))
|
block = self.recall(
|
||||||
|
RecallContext(text=text, kind=kind, now=now, agent=agent)
|
||||||
|
)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
_log.exception("recall hook failed")
|
_log.exception("recall hook failed")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ class UserSaid:
|
|||||||
title: str | None
|
title: str | None
|
||||||
text: str
|
text: str
|
||||||
at: datetime
|
at: datetime
|
||||||
|
agent: str = ""
|
||||||
|
"""Agent whose turn it is; a setup with several tells them apart by it."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|||||||
@@ -336,9 +336,11 @@ class Turns(Seeds):
|
|||||||
if self._envelope is None:
|
if self._envelope is None:
|
||||||
return None
|
return None
|
||||||
if conv.kind == "master":
|
if conv.kind == "master":
|
||||||
return self._envelope.build(text=text, kind="master")
|
return self._envelope.build(text=text, kind="master", agent=conv.agent_name)
|
||||||
if conv.kind == "branch":
|
if conv.kind == "branch":
|
||||||
return self._envelope.recall_only(text=text, kind="branch")
|
return self._envelope.recall_only(
|
||||||
|
text=text, kind="branch", agent=conv.agent_name
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _note_user(self, conv: Conversation, text: str) -> None:
|
async def _note_user(self, conv: Conversation, text: str) -> None:
|
||||||
@@ -350,6 +352,7 @@ class Turns(Seeds):
|
|||||||
title=conv.title,
|
title=conv.title,
|
||||||
text=text,
|
text=text,
|
||||||
at=datetime.now(UTC),
|
at=datetime.now(UTC),
|
||||||
|
agent=conv.agent_name,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
result = self._user_sink(message)
|
result = self._user_sink(message)
|
||||||
|
|||||||
@@ -219,3 +219,25 @@ async def test_failing_recall_or_sink_never_blocks_the_turn(world: World) -> Non
|
|||||||
assert prompt.startswith("hello\n\n" + HEADER)
|
assert prompt.startswith("hello\n\n" + HEADER)
|
||||||
assert root.exists()
|
assert root.exists()
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_the_recall_hook_and_the_sink_see_whose_turn_it_is(world: World) -> None:
|
||||||
|
root, watch = vault()
|
||||||
|
seen: list[RecallContext] = []
|
||||||
|
|
||||||
|
def recall(ctx: RecallContext) -> str | None:
|
||||||
|
seen.append(ctx)
|
||||||
|
return None if ctx.agent == "a" else "👤 указатель"
|
||||||
|
|
||||||
|
noted: list[UserSaid] = []
|
||||||
|
world.conversations._envelope = Envelope(watch=watch, tz="UTC", recall=recall) # noqa: SLF001
|
||||||
|
world.conversations._user_sink = noted.append # noqa: SLF001
|
||||||
|
master = await world.conversations.create(kind="master", agent="a", origin="test")
|
||||||
|
append(root / "люди" / "Прохор.md", "новое\n")
|
||||||
|
watch.note(root / "люди" / "Прохор.md")
|
||||||
|
await world.conversations.post(master, "что там у Прохор")
|
||||||
|
await world.settle(master, 1)
|
||||||
|
assert [ctx.agent for ctx in seen] == ["a"]
|
||||||
|
assert [m.agent for m in noted] == ["a"]
|
||||||
|
assert "👤" not in ScriptedClient.instances[0].prompts[0]
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user