mirror of
https://github.com/pocketpaw/pocketpaw.git
synced 2026-05-21 01:04:57 +00:00
Replaces the file-backed pocket session store with a MongoDB-backed
MemoryStoreProtocol implementation that becomes the default memory
backend for ee deployments. Unifies chat persistence so user, agent,
pocket, group, and DM messages all land in the `messages` collection
keyed by either `session_key` (pocket / agent memory) or `group`
(group / DM chat).
Schema
- Session and Message gain a `context_type` discriminator
("pocket" | "group") with a model_validator that enforces field
presence rules. Existing constructors that omit context_type still
work via a small inference shim.
- `messages` and `memory_facts` collections back the MemoryStoreProtocol;
facts (LONG_TERM / DAILY) live separately from chat rows.
ee bootstrap
- `register_default_backend()` is called from `init_cloud_db` and swaps
the existing MemoryManager singleton's `._store` to MongoMemoryStore
in place, so cached references on `agent_loop.memory` and the chat
persistence bridge automatically pick up MongoDB instead of the file
store. POCKETPAW_MEMORY_BACKEND=file overrides the flip.
- `create_memory_store("mongodb")` lazy-imports MongoMemoryStore so OSS
installs don't pull in motor/beanie.
Chat persistence
- `chat_persistence.save_user_message` and `_on_outbound_message` now
write pocket-context messages keyed by the API session id (the WS
chat_id IS the Session.sessionId). The fake "PocketPaw Chat" group
auto-creation hack is gone.
- MongoMemoryStore normalises bus-style session keys ("websocket:X") to
UI-style ("websocket_X") on every read/write so chat_stream's agent
loop writes line up with `Session.sessionId` for history reads.
- `MongoMemoryStore.save` derives `sender_type` from `role` so assistant
messages don't get persisted as `sender_type="user"`.
Sessions API
- `SessionService.get_history` reads from the unified messages
collection (no file-memory fallback) for both pocket and group
contexts.
- `sessions/router.get_session_history` delegates to the service.
Agent dispatch
- `_should_agent_respond` precedence rewritten: silent always opts out;
if any agent is mentioned, only mentioned agents reply (regardless of
mode); otherwise fall back to per-agent mode. Fixes the case where two
`auto` agents both replied when the user mentioned one.
Observability
- Replaced silent `logger.debug` swallows in chat_persistence and
websocket_adapter with `logger.exception` / `logger.warning` so
persistence failures surface in dev.
Tests
- New tests/cloud/memory/ suite (~50 tests): protocol conformance,
discriminator validators, session/facts paths, backend selection,
OSS isolation.
- New tests/cloud/test_api_contracts.py snapshots the response shapes
for chat/groups and sessions endpoints so the schema rewrite is a
storage-only change for clients.
- New scripts/smoke_*.py and scripts/diag_*.py exercise each end-to-end
path against a real local MongoDB.
Spec
- SPEC.md captures the design decisions, the clean-slate assumption
(no migration), and the v1 scope (no workspace scoping on memory_facts,
no vector search).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
76 lines
2.7 KiB
Python
76 lines
2.7 KiB
Python
"""Smoke test — agent dispatch in group chat with mentions.
|
|
|
|
Verifies _should_agent_respond logic directly (avoids spinning up the agent
|
|
pool / LLM) for the matrix:
|
|
|
|
| mode | mention X | mention Y | none |
|
|
|--------------|-----------|-----------|------|
|
|
| silent | False | False | False|
|
|
| auto (X) | True | False | True |
|
|
| auto (Y) | False | True | True |
|
|
| mention_only | True (X) | True (Y) | False|
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import sys
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass
|
|
class FakeGroupAgent:
|
|
agent: str
|
|
respond_mode: str
|
|
|
|
|
|
def _mention(agent_id: str) -> dict:
|
|
return {"type": "agent", "id": agent_id, "display_name": f"@{agent_id}"}
|
|
|
|
|
|
async def main() -> int:
|
|
from ee.cloud.shared.agent_bridge import _should_agent_respond
|
|
|
|
AX = FakeGroupAgent("agent-x", "auto")
|
|
AY = FakeGroupAgent("agent-y", "auto")
|
|
MX = FakeGroupAgent("agent-x", "mention_only")
|
|
SY = FakeGroupAgent("agent-y", "silent")
|
|
|
|
cases: list[tuple[str, FakeGroupAgent, list[dict], bool]] = [
|
|
# --- the bug we fixed: two auto agents, only the mentioned one responds ---
|
|
("two-auto-mention-X / X", AX, [_mention("agent-x")], True),
|
|
("two-auto-mention-X / Y", AY, [_mention("agent-x")], False),
|
|
("two-auto-mention-Y / X", AX, [_mention("agent-y")], False),
|
|
("two-auto-mention-Y / Y", AY, [_mention("agent-y")], True),
|
|
# --- broadcast (no mentions): both auto agents respond ---
|
|
("two-auto-no-mention / X", AX, [], True),
|
|
("two-auto-no-mention / Y", AY, [], True),
|
|
# --- mention_only ---
|
|
("mention_only-mentioned", MX, [_mention("agent-x")], True),
|
|
("mention_only-not-mentioned", MX, [_mention("agent-y")], False),
|
|
("mention_only-no-mention", MX, [], False),
|
|
# --- silent always opts out, even when mentioned ---
|
|
("silent-mentioned", SY, [_mention("agent-y")], False),
|
|
("silent-no-mention", SY, [], False),
|
|
# --- non-agent mentions (user mentions) don't gate agent dispatch ---
|
|
("auto-only-user-mentions / X", AX, [{"type": "user", "id": "u1"}], True),
|
|
]
|
|
|
|
failures: list[str] = []
|
|
for name, ga, mentions, expected in cases:
|
|
actual = await _should_agent_respond(ga, "hello", mentions)
|
|
status = "OK" if actual == expected else "FAIL"
|
|
print(f" [{status}] {name}: expected={expected} actual={actual}")
|
|
if actual != expected:
|
|
failures.append(name)
|
|
|
|
if failures:
|
|
print(f"\n{len(failures)} FAIL(s): {failures}")
|
|
return 1
|
|
print("\nSMOKE OK")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(asyncio.run(main()))
|