Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions apps/api/src/alicebot_api/mcp_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from typing import cast
from uuid import UUID, uuid4

from psycopg.errors import CheckViolation

from alicebot_api.continuity_capture import (
ContinuityCaptureValidationError,
capture_continuity_candidates,
Expand Down Expand Up @@ -140,6 +142,9 @@
from alicebot_api.vnext_event_log import append_event
from alicebot_api.vnext_memory_commit import (
VNextMemoryCommitService,
VNEXT_DOMAINS,
VNEXT_MEMORY_TYPES,
VNEXT_SENSITIVITY_LEVELS,
memory_commit_request_from_payload,
)
from alicebot_api.vnext_projects import ProjectAutomationRequest, VNextProjectService
Expand Down Expand Up @@ -3562,9 +3567,9 @@ def _vnext_agent_tool_schema(
"intent": {"type": "string"},
"title": {"type": "string"},
"canonical_text": {"type": "string"},
"memory_type": {"type": "string"},
"domain": {"type": "string"},
"sensitivity": {"type": "string"},
"memory_type": {"type": "string", "enum": list(VNEXT_MEMORY_TYPES)},
"domain": {"type": "string", "enum": list(VNEXT_DOMAINS)},
"sensitivity": {"type": "string", "enum": list(VNEXT_SENSITIVITY_LEVELS)},
"confidence": {"type": "number", "minimum": 0.0, "maximum": 1.0},
"source_type": {"type": "string"},
"source_refs": {"type": "array", "items": {"type": "string"}},
Expand Down Expand Up @@ -3805,6 +3810,11 @@ def call_mcp_tool(
TemporalStateValidationError,
) as exc:
raise MCPToolError(str(exc)) from exc
except CheckViolation as exc:
raise MCPToolError(
"vNext request violates a persisted schema constraint; use schema-backed enum values "
"for memory_type, domain, sensitivity, status, and action fields."
) from exc
except (TypeError, ValueError) as exc:
raise MCPToolError(str(exc)) from exc

Expand Down
130 changes: 127 additions & 3 deletions apps/api/src/alicebot_api/vnext_memory_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,93 @@

MEMORY_COMMIT_WRITE_MODES = ("commit", "confirm_inline", "propose_review", "reject")
MEMORY_COMMIT_STATUSES = ("committed", "confirmation_required", "review_required", "rejected")
VNEXT_DOMAINS = (
"professional",
"personal",
"family",
"health",
"spiritual",
"financial",
"legal",
"learning",
"relationship",
"project",
"agent_run",
"system",
"unknown",
)
VNEXT_SENSITIVITY_LEVELS = (
"public",
"internal",
"private",
"confidential",
"highly_sensitive",
"sacred",
"regulated",
"unknown",
)
VNEXT_MEMORY_TYPES = (
"preference",
"identity_fact",
"relationship_fact",
"project_fact",
"decision",
"commitment",
"routine",
"constraint",
"working_style",
"episode",
"semantic",
"project_state",
"belief",
"thesis",
"person",
"relationship",
"open_loop",
"value",
"pattern",
"contradiction",
"question",
"answer",
"artifact_summary",
"agent_run",
"system",
)
_MEMORY_TYPE_ALIASES = {
"fact": "semantic",
"note": "semantic",
"quote": "semantic",
"quote_collection": "semantic",
"quote_memory": "semantic",
"quote_note": "semantic",
"quotes": "semantic",
"quotation": "semantic",
"quotation_note": "semantic",
"quotations": "semantic",
"saved_quote": "semantic",
}
_DOMAIN_ALIASES = {
"work": "professional",
"career": "professional",
"finance": "financial",
"money": "financial",
"quote": "learning",
"quote_collection": "learning",
"quote_memory": "learning",
"quote_note": "learning",
"quotes": "learning",
"quotation": "learning",
"quotation_note": "learning",
"quotations": "learning",
"philosophy": "learning",
"saved_quote": "learning",
"wisdom": "learning",
"general": "unknown",
}
_SENSITIVITY_ALIASES = {
"sensitive": "confidential",
"secret": "highly_sensitive",
}
TRUSTED_COMMIT_PROFILES = {"trusted_local_agent", "admin_agent"}
PROJECT_COMMIT_PROFILES = {"project_scoped_agent"}
REVIEW_ONLY_PROFILES = {"memory_proposal_agent"}
Expand Down Expand Up @@ -127,6 +214,25 @@ def _normalized_text(value: object, *, field_name: str) -> str:
return normalized


def _enum_token(value: str) -> str:
return "_".join(value.casefold().replace("-", "_").split())


def _enum_value(
value: object,
*,
field_name: str,
allowed_values: tuple[str, ...],
aliases: Mapping[str, str] | None = None,
) -> str:
normalized = _normalized_text(value, field_name=field_name)
token = _enum_token(normalized)
canonical = aliases.get(token, token) if aliases is not None else token
if canonical not in allowed_values:
raise VNextMemoryCommitValidationError(f"{field_name} must be one of: {', '.join(allowed_values)}")
return canonical


def _optional_text(value: object) -> str | None:
if value is None:
return None
Expand Down Expand Up @@ -1063,9 +1169,24 @@ def memory_commit_request_from_payload(payload: Mapping[str, object], *, user_id
user_id=str(user_id),
title=_normalized_text(payload.get("title"), field_name="title"),
canonical_text=_normalized_text(payload.get("canonical_text"), field_name="canonical_text"),
memory_type=_normalized_text(payload.get("memory_type", "semantic"), field_name="memory_type"),
domain=_normalized_text(payload.get("domain", "unknown"), field_name="domain"),
sensitivity=_normalized_text(payload.get("sensitivity", "unknown"), field_name="sensitivity"),
memory_type=_enum_value(
payload.get("memory_type", "semantic"),
field_name="memory_type",
allowed_values=VNEXT_MEMORY_TYPES,
aliases=_MEMORY_TYPE_ALIASES,
),
domain=_enum_value(
payload.get("domain", "unknown"),
field_name="domain",
allowed_values=VNEXT_DOMAINS,
aliases=_DOMAIN_ALIASES,
),
sensitivity=_enum_value(
payload.get("sensitivity", "unknown"),
field_name="sensitivity",
allowed_values=VNEXT_SENSITIVITY_LEVELS,
aliases=_SENSITIVITY_ALIASES,
),
confidence=float(confidence),
intent=_normalized_text(payload.get("intent", "explicit_remember"), field_name="intent"),
source_type=_normalized_text(payload.get("source_type", "direct_user_instruction"), field_name="source_type"),
Expand All @@ -1086,6 +1207,9 @@ def memory_commit_request_from_payload(payload: Mapping[str, object], *, user_id
"MemoryCommitRequest",
"VNextMemoryCommitService",
"VNextMemoryCommitValidationError",
"VNEXT_DOMAINS",
"VNEXT_MEMORY_TYPES",
"VNEXT_SENSITIVITY_LEVELS",
"evaluate_memory_commit_policy",
"memory_commit_request_from_payload",
]
2 changes: 2 additions & 0 deletions docs/alpha/custom-agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Use `alice_vnext_context_pack` with the same identity fields, then submit output

For explicit "remember this" instructions, use `alice_vnext_commit_memory`:

Use canonical schema values for persisted labels. For quote saves, use `memory_type=semantic`; use `domain=learning` only when a quote collection needs an explicit domain. Avoid invented values like `memory_type=quote`, `domain=quotes`, or `sensitivity=sensitive`.

```json
{
"agent_id": "researcher",
Expand Down
3 changes: 3 additions & 0 deletions docs/alpha/hermes-skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ Recipes:
- Follow-up context: query open loops and recent decisions.
- Project briefing context: use project-scoped context before advising.
- Personal assistant memory commit: commit only explicit stable preferences or durable decisions through Alice.
- Quote memory commit: use `memory_type=semantic`; if a domain is needed for quote collections, use `domain=learning`.
- Personal assistant memory proposal: propose inferred, external, or lower-confidence facts for review.
- Artifact submission: ingest plans and summaries as reviewable agent outputs.

Use only schema-backed enum values for persisted fields. Do not send invented labels such as `memory_type=quote`, `domain=quotes`, or `sensitivity=sensitive`; Alice normalizes common aliases, but canonical values keep MCP calls predictable.

Good explicit commit:

```json
Expand Down
2 changes: 2 additions & 0 deletions docs/alpha/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ Trusted agents can write explicit "remember/save/add this to memory" instruction
- `review_required`: external, generated, low-confidence, or review-only-agent memory stays in `/vnext`.
- `rejected`: read-only, out-of-scope, unsafe, or policy-bypass attempts are blocked.

Use canonical schema values for persisted labels. For quote saves, use `memory_type=semantic`; use `domain=learning` only when a quote collection needs an explicit domain. Avoid invented values like `memory_type=quote`, `domain=quotes`, or `sensitivity=sensitive`.

```json
{
"agent_id": "hermes",
Expand Down
78 changes: 78 additions & 0 deletions tests/unit/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from uuid import UUID, uuid4

import pytest
from psycopg.errors import CheckViolation

import alicebot_api.mcp_server as mcp_server
import alicebot_api.mcp_tools as mcp_tools_module
Expand Down Expand Up @@ -116,6 +117,21 @@ def test_call_mcp_tool_requires_object_arguments() -> None:
call_mcp_tool(context, name="alice_recall", arguments=["not-a-json-object"])


def test_call_mcp_tool_converts_postgres_check_violation(monkeypatch) -> None:
context = MCPRuntimeContext(
database_url="postgresql://localhost/alicebot",
user_id=UUID("11111111-1111-4111-8111-111111111111"),
)

def raise_check_violation(_context, _arguments):
raise CheckViolation("memories_memory_type_check")

monkeypatch.setitem(mcp_tools_module._TOOL_HANDLERS, "alice_recall", raise_check_violation)

with pytest.raises(MCPToolError, match="persisted schema constraint"):
call_mcp_tool(context, name="alice_recall", arguments={})


class FakeVNextMCPStore:
def __init__(self) -> None:
self.events: list[dict[str, object]] = []
Expand Down Expand Up @@ -508,6 +524,68 @@ def fake_vnext_store_context(_context):
assert audit_payload["revisions"][0]["action"] == "agentic_memory_commit"


def test_alice_vnext_agentic_memory_commit_normalizes_quote_aliases(monkeypatch) -> None:
store = FakeVNextMCPStore()

@contextmanager
def fake_vnext_store_context(_context):
yield store

monkeypatch.setattr(mcp_tools_module, "_vnext_store_context", fake_vnext_store_context)
context = MCPRuntimeContext(
database_url="postgresql://localhost/alicebot",
user_id=UUID("11111111-1111-4111-8111-111111111111"),
)

payload = call_mcp_tool(
context,
name="alice_vnext_commit_memory",
arguments={
"agent_id": "hermes",
"agent_type": "personal_assistant",
"permission_profile": "trusted_local_agent",
"title": "Quote to remember",
"canonical_text": "Control your emotions or someone else will. - Unknown",
"memory_type": "quote",
"domain": "quotes",
"sensitivity": "private",
"confidence": 0.96,
},
)

assert payload["status"] == "committed"
assert payload["memory"]["memory_type"] == "semantic"
assert payload["memory"]["domain"] == "learning"
assert payload["memory"]["sensitivity"] == "private"


def test_alice_vnext_agentic_memory_commit_rejects_invalid_enum_before_store(monkeypatch) -> None:
def fail_if_store_opened(_context):
raise AssertionError("store should not be opened for invalid enum input")

monkeypatch.setattr(mcp_tools_module, "_vnext_store_context", fail_if_store_opened)
context = MCPRuntimeContext(
database_url="postgresql://localhost/alicebot",
user_id=UUID("11111111-1111-4111-8111-111111111111"),
)

with pytest.raises(MCPToolError, match="memory_type must be one of"):
call_mcp_tool(
context,
name="alice_vnext_commit_memory",
arguments={
"agent_id": "hermes",
"agent_type": "personal_assistant",
"permission_profile": "trusted_local_agent",
"title": "Invalid typed memory",
"canonical_text": "This should be rejected before Postgres.",
"memory_type": "totally_invalid_type",
"domain": "unknown",
"sensitivity": "unknown",
},
)


def test_alice_vnext_agentic_memory_confirm_mcp_tool(monkeypatch) -> None:
store = FakeVNextMCPStore()

Expand Down
Loading