"""Reaction instructions tool — read / update the per-user Synapse rules.
There are two standing documents, and both are the user's own words:
- `reaction` (the default) — which events to react to, what to do about them.
Read before acting on a Synapse event.
- `dispatcher` — routing: which event types belong to which profile, and what
counts as the same conversation. Read only by the dispatcher meta-pass.
Navi may refine either through this tool (self-improvement); every edit is
recorded with edited_by='navi' in that document's version history.
"""
import inspect
from navi.synapse.settings_store import INSTRUCTION_DOCS, SynapseSettingsStore
from navi.tools._internal.base import Tool, ToolContext, ToolResult, current_user_id
_DOC_OP_LABEL = {
"reaction": "reaction instructions",
"dispatcher": "dispatcher (routing) instructions",
}
class SynapseInstructionsTool(Tool):
name = "synapse_instructions"
description = (
"Read or update the user's Synapse instruction documents. `doc='reaction'` "
"(the default) holds the rules for which platform events Navi reacts to and "
"how — read them before acting on Synapse events. `doc='dispatcher'` holds the "
"routing rules for the dispatcher that picks the profile and the conversation. "
"Update either only when a change is clearly wanted and explained (the user "
"reviews the edit history)."
)
parameters = {
"type": "object",
"properties": {
"op": {
"type": "string",
"enum": ["read", "update"],
"description": "read: return the current document. update: replace it with `content`.",
},
"doc": {
"type": "string",
"enum": list(INSTRUCTION_DOCS),
"description": (
"reaction (default): the reaction rules. "
"dispatcher: the routing rules the dispatcher reads."
),
},
"content": {
"type": "string",
"description": "update only: the FULL new text of the document.",
},
"reason": {
"type": "string",
"description": "update only: one sentence explaining why you are changing the rules.",
},
},
"required": ["op"],
}
def __init__(self, pool_provider=None) -> None:
self._pool_provider = pool_provider
async def execute(self, params: dict, ctx: ToolContext | None = None) -> ToolResult:
user_id = ctx.user_id if ctx is not None else current_user_id.get(None)
if user_id is None:
return ToolResult(
success=False, output="",
error="No user context — reaction instructions are per-user.",
)
doc = params.get("doc") or "reaction"
if doc not in INSTRUCTION_DOCS:
return ToolResult(success=False, output="", error=f"Unknown doc '{doc}'.")
if not callable(self._pool_provider):
return ToolResult(
success=False, output="",
error="Reaction settings storage is not available (no pool).",
)
pool = self._pool_provider()
if inspect.isawaitable(pool): # PgSessionStore._get_pool is async
pool = await pool
store = SynapseSettingsStore(pool)
settings_row = await store.get(user_id)
op = params.get("op", "read")
if op == "read":
body = settings_row.document(doc).strip()
if not body:
return ToolResult(
success=True,
output=f"The {_DOC_OP_LABEL[doc]} document is empty — nothing is configured yet.",
)
return ToolResult(success=True, output=body)
if op != "update":
return ToolResult(success=False, output="", error=f"Unknown op '{op}'.")
content = (params.get("content") or "").strip()
if not content:
return ToolResult(
success=False, output="",
error="update requires non-empty `content` — use the full new text of the document.",
)
reason = (params.get("reason") or "").strip()
await store.save_instructions(
user_id, content, edited_by="navi", reason=reason or None, doc=doc,
)
return ToolResult(
success=True,
output=(
f"Updated the {_DOC_OP_LABEL[doc]} and recorded the edit"
+ (f" ({reason})" if reason else "")
+ ". The change applies to future Synapse reactions."
),
)