Per AC 6.5, 6.6 — adds a keyword-only 'setting' parameter to: - lookup (filter on matched name's setting membership) - entity_context (entity-level filter; empty shape if excluded) - was_true_at (both subject and object must be in setting) - true_during (subject must be in setting) - entities_present (located entity must be in setting) - events_during (event subject must be in setting) The filter resolves via graph.setting_entities(setting_id) — O(1) reverse lookup from slice 6.2's Protocol methods. An unknown setting returns empty results (defensive). Omitting 'setting' preserves slice 4 / 9 behaviour (back-compat fence). MCP tool schemas updated for all 6 entries to expose 'setting' as an optional [string, null] parameter; opt_string_params toggled so 'null' is coerced to None by the dispatcher. The cross-setting fact test (Roland ENCOUNTERED The Wanderer) is the canonical LLM-target: with setting='mardonari', Roland's home setting, the answer is was_true=False because The Wanderer is in the_wild_dream. +8 tests (739 → 747). All green. No regressions.
1182 lines
40 KiB
Python
1182 lines
40 KiB
Python
"""Lore Engine POC — MCP tool registry (slice 2.6.1).
|
|
|
|
This module exposes the 24 tool functions that the MCP server
|
|
serves to LLM clients. The registry is a flat list of
|
|
:class:`ToolEntry`; each entry pairs a hand-written JSON Schema
|
|
with a Python callable.
|
|
|
|
Design notes
|
|
------------
|
|
|
|
**Uniform ``(graph, **kwargs)`` signature.** The MCPServer
|
|
dispatcher calls ``tool.fn(graph, **arguments)``. Some of the
|
|
underlying functions (e.g. ``latest_run``, ``list_ontology_rules``)
|
|
don't take a graph at all; others take optional filters with
|
|
defaults. Rather than overload the dispatcher, we wrap each
|
|
function in a tiny adapter that:
|
|
|
|
1. Accepts the graph (ignored if the underlying fn doesn't use it).
|
|
2. Forwards the rest of the kwargs verbatim.
|
|
3. Coerces ``Optional`` string-typed args (``"null"``, ``"None"``,
|
|
``""``) to ``None`` — useful for clients that send all string
|
|
fields and can't express JSON null cleanly.
|
|
|
|
**Hand-written JSON Schemas.** 24 schemas is small. Auto-
|
|
generating them from Python type hints would add a dependency
|
|
(jsonschema or pydantic) for ~150 lines of code; not worth it.
|
|
|
|
**No new dependencies.** This module imports only from the
|
|
Lore Engine POC.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Callable, Optional
|
|
|
|
from . import consistency_tools
|
|
from .mcp_server import ToolEntry
|
|
from .read_tools import (
|
|
ancestors_of,
|
|
descendants_of,
|
|
entities_present,
|
|
entity_context,
|
|
event_chain,
|
|
events_during,
|
|
list_lineage,
|
|
list_offspring,
|
|
location_hierarchy,
|
|
lookup,
|
|
lore_about,
|
|
timeline,
|
|
true_during,
|
|
)
|
|
from .tools import Graph, was_true_at
|
|
from .write_tools import (
|
|
add_entity,
|
|
add_lore_source,
|
|
add_relation,
|
|
define_calendar,
|
|
define_date,
|
|
define_era,
|
|
delete_entity,
|
|
mark_verified,
|
|
merge_entities,
|
|
retcon,
|
|
set_alias,
|
|
update_entity,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Adapter helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
_NULL_TOKENS = frozenset({"null", "none", "nil", ""})
|
|
|
|
|
|
def _coerce_optional(value: Any) -> Any:
|
|
"""Map ``"null"`` / ``"none"`` / ``""`` to ``None``."""
|
|
if isinstance(value, str) and value.strip().lower() in _NULL_TOKENS:
|
|
return None
|
|
return value
|
|
|
|
|
|
def _make_adapter(
|
|
fn: Callable,
|
|
graph_is_used: bool,
|
|
opt_string_params: tuple[str, ...] = (),
|
|
) -> Callable:
|
|
"""Wrap ``fn`` so the dispatcher can call it as ``fn(graph, **kwargs)``.
|
|
|
|
``graph_is_used=False`` means the underlying function doesn't
|
|
accept a graph; we drop the first arg.
|
|
|
|
``opt_string_params`` lists parameter names that should be
|
|
coerced via :func:`_coerce_optional` before forwarding.
|
|
|
|
The returned closure has ``__wrapped__ = fn`` so callers can
|
|
recover the original function (the registry uses this on
|
|
ToolEntry so tests can assert binding).
|
|
"""
|
|
|
|
def adapter(graph: Graph, **kwargs):
|
|
cleaned = {k: v for k, v in kwargs.items()}
|
|
for name in opt_string_params:
|
|
if name in cleaned:
|
|
cleaned[name] = _coerce_optional(cleaned[name])
|
|
if graph_is_used:
|
|
return fn(graph, **cleaned)
|
|
return fn(**cleaned)
|
|
|
|
adapter.__wrapped__ = fn # type: ignore[attr-defined]
|
|
return adapter
|
|
|
|
|
|
def _entry(
|
|
name: str,
|
|
description: str,
|
|
input_schema: dict,
|
|
fn: Callable,
|
|
graph_is_used: bool,
|
|
opt_string_params: tuple[str, ...] = (),
|
|
) -> ToolEntry:
|
|
"""Convenience: build a ToolEntry with an adapter and stash
|
|
the underlying fn on ``underlying_fn``."""
|
|
adapter = _make_adapter(
|
|
fn, graph_is_used=graph_is_used, opt_string_params=opt_string_params
|
|
)
|
|
return ToolEntry(
|
|
name=name,
|
|
description=description,
|
|
input_schema=input_schema,
|
|
fn=adapter,
|
|
underlying_fn=fn,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tool entries
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
TOOL_REGISTRY: list[ToolEntry] = [
|
|
# --- Slice 0 ---
|
|
_entry(
|
|
name="was_true_at",
|
|
description=(
|
|
"Time-bounded edge query. Returns whether "
|
|
"(subject, relation, object) was true at the given "
|
|
"at_time. The canonical first-call for an LLM asking "
|
|
"'did X happen by Y?'"
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"relation": {
|
|
"type": "string",
|
|
"description": "Edge relation (e.g. MEMBER_OF).",
|
|
},
|
|
"subject": {
|
|
"type": "string",
|
|
"description": "Subject name (e.g. 'Roland Raventhorne').",
|
|
},
|
|
"object": {
|
|
"type": "string",
|
|
"description": "Object name (e.g. 'House Raventhorne').",
|
|
},
|
|
"at_time": {
|
|
"type": "string",
|
|
"description": (
|
|
"Time atom (e.g. '3rd_age.year_345') to test."
|
|
),
|
|
},
|
|
"current_time": {
|
|
"type": ["string", "null"],
|
|
"description": (
|
|
"Optional 'now' anchor; defaults to at_time."
|
|
),
|
|
},
|
|
"setting": {
|
|
"type": ["string", "null"],
|
|
"description": (
|
|
"Slice 6.5 — restrict to entities that "
|
|
"EXISTS_IN this setting (e.g. 'mardonari'). "
|
|
"Both subject and object must be members; "
|
|
"the answer is was_true=False otherwise."
|
|
),
|
|
},
|
|
},
|
|
"required": ["relation", "subject", "object", "at_time"],
|
|
},
|
|
fn=was_true_at,
|
|
graph_is_used=True,
|
|
opt_string_params=("current_time", "setting"),
|
|
),
|
|
# --- Slice 2 — 10 consistency tools ---
|
|
_entry(
|
|
name="run_consistency_check",
|
|
description=(
|
|
"Force-run the consistency engine over the graph. "
|
|
"Stashes the violation list on the singleton runner; "
|
|
"all subsequent get_* tools return from that list."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {},
|
|
},
|
|
fn=consistency_tools.run_consistency_check,
|
|
graph_is_used=True,
|
|
),
|
|
_entry(
|
|
name="latest_run",
|
|
description=(
|
|
"Most recent consistency run summary, or null if no "
|
|
"run has happened yet."
|
|
),
|
|
input_schema={"type": "object", "properties": {}},
|
|
fn=consistency_tools.latest_run,
|
|
graph_is_used=False,
|
|
),
|
|
_entry(
|
|
name="get_contradictions",
|
|
description=(
|
|
"List contradictions from the most recent consistency run. "
|
|
"Filters: subject (exact match), severity ('warn'|'error'), "
|
|
"limit (cap on result size)."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"subject": {"type": ["string", "null"]},
|
|
"severity": {"type": ["string", "null"]},
|
|
"limit": {"type": ["integer", "null"]},
|
|
},
|
|
},
|
|
fn=consistency_tools.get_contradictions,
|
|
graph_is_used=False,
|
|
opt_string_params=("subject", "severity"),
|
|
),
|
|
_entry(
|
|
name="get_anachronisms",
|
|
description=(
|
|
"List anachronisms from the most recent run. Filters: "
|
|
"entity (matches entity_name), limit, include_flagged."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"entity": {"type": ["string", "null"]},
|
|
"limit": {"type": ["integer", "null"]},
|
|
"include_flagged": {"type": "boolean"},
|
|
},
|
|
},
|
|
fn=consistency_tools.get_anachronisms,
|
|
graph_is_used=False,
|
|
opt_string_params=("entity",),
|
|
),
|
|
_entry(
|
|
name="get_orphans",
|
|
description=(
|
|
"List orphans (entities with no meaningful relations) "
|
|
"from the most recent run. Filters: reason, limit."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"reason": {"type": ["string", "null"]},
|
|
"limit": {"type": ["integer", "null"]},
|
|
},
|
|
},
|
|
fn=consistency_tools.get_orphans,
|
|
graph_is_used=False,
|
|
opt_string_params=("reason",),
|
|
),
|
|
_entry(
|
|
name="get_ontology_violations",
|
|
description=(
|
|
"List ontology violations from the most recent run. "
|
|
"Filters: rule_id, severity, limit."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"rule_id": {"type": ["string", "null"]},
|
|
"severity": {"type": ["string", "null"]},
|
|
"limit": {"type": ["integer", "null"]},
|
|
},
|
|
},
|
|
fn=consistency_tools.get_ontology_violations,
|
|
graph_is_used=False,
|
|
opt_string_params=("rule_id", "severity"),
|
|
),
|
|
_entry(
|
|
name="flag_for_review",
|
|
description=(
|
|
"Acknowledge a violation by id. Suppresses future "
|
|
"auto-flagging. Returns {flagged, node_id, reason}."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"node_id": {"type": "string"},
|
|
"reason": {"type": "string"},
|
|
},
|
|
"required": ["node_id", "reason"],
|
|
},
|
|
fn=consistency_tools.flag_for_review,
|
|
graph_is_used=False,
|
|
),
|
|
_entry(
|
|
name="explain_violation",
|
|
description=(
|
|
"Return the rule that produced a violation plus the "
|
|
"offending edges / claim pair."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"node_id": {"type": "string"},
|
|
},
|
|
"required": ["node_id"],
|
|
},
|
|
fn=consistency_tools.explain_violation,
|
|
graph_is_used=False,
|
|
),
|
|
_entry(
|
|
name="add_ontology_rule",
|
|
description=(
|
|
"Register a new ontology rule by id, cypher, description, "
|
|
"severity. The rule is added to the in-process registry."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {"type": "string"},
|
|
"cypher": {"type": "string"},
|
|
"description": {"type": "string"},
|
|
"severity": {"type": "string"},
|
|
},
|
|
"required": ["id", "cypher", "description", "severity"],
|
|
},
|
|
fn=consistency_tools.add_ontology_rule,
|
|
graph_is_used=False,
|
|
),
|
|
_entry(
|
|
name="list_ontology_rules",
|
|
description=(
|
|
"Return every registered ontology rule (the 10 starter "
|
|
"rules plus any user-added ones)."
|
|
),
|
|
input_schema={"type": "object", "properties": {}},
|
|
fn=consistency_tools.list_ontology_rules,
|
|
graph_is_used=False,
|
|
),
|
|
# --- Slice 4 — 1 read tool ---
|
|
_entry(
|
|
name="lookup",
|
|
description=(
|
|
"Find entities whose name matches the query (case-"
|
|
"insensitive substring). Returns [{name, type, "
|
|
"match_confidence}]. The LLM's most-used entry point."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {"type": "string"},
|
|
"type_": {"type": ["string", "null"]},
|
|
"setting": {
|
|
"type": ["string", "null"],
|
|
"description": (
|
|
"Slice 6.5 — restrict to entities that "
|
|
"EXISTS_IN this setting."
|
|
),
|
|
},
|
|
},
|
|
"required": ["query"],
|
|
},
|
|
fn=lookup,
|
|
graph_is_used=True,
|
|
opt_string_params=("type_", "setting"),
|
|
),
|
|
# --- Slice 9 — MCP surface expansion: the 12 read_tools that
|
|
# were already implemented in lore_engine_poc.read_tools and
|
|
# unit-tested in tests/test_tools/, but had not been exposed
|
|
# over the MCP wire. Adding them here is purely a registration
|
|
# step (hand-written JSON Schema + adapter); the underlying
|
|
# functions and their tests are unchanged.
|
|
_entry(
|
|
name="entity_context",
|
|
description=(
|
|
"One-hop summary for an entity. Returns the entity's "
|
|
"factions, locations, possessions, and inferred "
|
|
"lifespan — optionally time-bucketed to a specific "
|
|
"at_time. Use this when the LLM wants 'everything we "
|
|
"know about X' in one call."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Entity name (e.g. 'Aldric Raventhorne').",
|
|
},
|
|
"at_time": {
|
|
"type": ["string", "null"],
|
|
"description": (
|
|
"Optional time atom (e.g. '3rd_age.year_345'); "
|
|
"omit for the full-timeline view."
|
|
),
|
|
},
|
|
"setting": {
|
|
"type": ["string", "null"],
|
|
"description": (
|
|
"Slice 6.5 — restrict to entities that "
|
|
"EXISTS_IN this setting."
|
|
),
|
|
},
|
|
},
|
|
"required": ["name"],
|
|
},
|
|
fn=entity_context,
|
|
graph_is_used=True,
|
|
opt_string_params=("at_time", "setting"),
|
|
),
|
|
_entry(
|
|
name="true_during",
|
|
description=(
|
|
"Edges of a relation from a subject that were active "
|
|
"somewhere inside an era (e.g. '3rd_age'). Optional "
|
|
"object filter. Use this for 'was X true during Y?' "
|
|
"questions with a range rather than a point."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"relation": {
|
|
"type": "string",
|
|
"description": "Edge relation (e.g. MEMBER_OF).",
|
|
},
|
|
"subject": {
|
|
"type": "string",
|
|
"description": "Subject name.",
|
|
},
|
|
"era": {
|
|
"type": "string",
|
|
"description": (
|
|
"Era or time range (e.g. '3rd_age' or "
|
|
"'3rd_age.year_300..year_400')."
|
|
),
|
|
},
|
|
"object_": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional object filter.",
|
|
},
|
|
"setting": {
|
|
"type": ["string", "null"],
|
|
"description": (
|
|
"Slice 6.5 — restrict to subjects that "
|
|
"EXISTS_IN this setting."
|
|
),
|
|
},
|
|
},
|
|
"required": ["relation", "subject", "era"],
|
|
},
|
|
fn=true_during,
|
|
graph_is_used=True,
|
|
opt_string_params=("object_", "setting"),
|
|
),
|
|
_entry(
|
|
name="entities_present",
|
|
description=(
|
|
"Entities in a location at a given at_time, time-"
|
|
"filtered. Optionally restricted by entity type. Uses "
|
|
"LOCATED_IN (Person/Creature/Item) and CONTROLS "
|
|
"(Faction) edges whose window contains at_time."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"location": {
|
|
"type": "string",
|
|
"description": "Location name (e.g. 'Greyhaven').",
|
|
},
|
|
"at_time": {
|
|
"type": "string",
|
|
"description": "Time atom (e.g. '3rd_age.year_345').",
|
|
},
|
|
"type_": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional entity-type filter.",
|
|
},
|
|
"setting": {
|
|
"type": ["string", "null"],
|
|
"description": (
|
|
"Slice 6.5 — restrict to entities that "
|
|
"EXISTS_IN this setting."
|
|
),
|
|
},
|
|
},
|
|
"required": ["location", "at_time"],
|
|
},
|
|
fn=entities_present,
|
|
graph_is_used=True,
|
|
opt_string_params=("type_", "setting"),
|
|
),
|
|
_entry(
|
|
name="timeline",
|
|
description=(
|
|
"All edges touching an entity (as subject or object), "
|
|
"optionally filtered by relation type and a time "
|
|
"window, sorted chronologically by valid_from. The "
|
|
"narrative 'what happened to X?' view."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"entity": {
|
|
"type": "string",
|
|
"description": "Entity name.",
|
|
},
|
|
"relation_type": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional relation filter (e.g. 'MEMBER_OF').",
|
|
},
|
|
"start_time": {
|
|
"type": ["string", "null"],
|
|
"description": "Inclusive lower bound on valid_from.",
|
|
},
|
|
"end_time": {
|
|
"type": ["string", "null"],
|
|
"description": "Inclusive upper bound on valid_from.",
|
|
},
|
|
},
|
|
"required": ["entity"],
|
|
},
|
|
fn=timeline,
|
|
graph_is_used=True,
|
|
opt_string_params=("relation_type", "start_time", "end_time"),
|
|
),
|
|
_entry(
|
|
name="list_lineage",
|
|
description=(
|
|
"The lineage a person belongs to, plus members within "
|
|
"depth hops. Returns {lineage, members, cadet_branches, "
|
|
"depth_covered}. Use to answer 'what house / clan is X "
|
|
"part of?'."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"person": {
|
|
"type": "string",
|
|
"description": "Person name.",
|
|
},
|
|
"depth": {
|
|
"type": "integer",
|
|
"description": "Member-traversal depth (default 2).",
|
|
},
|
|
},
|
|
"required": ["person"],
|
|
},
|
|
fn=list_lineage,
|
|
graph_is_used=True,
|
|
),
|
|
_entry(
|
|
name="list_offspring",
|
|
description=(
|
|
"Direct children of a person via PARENT_OF. Returns a "
|
|
"list of canonical names. For multi-hop descendants, "
|
|
"use descendants_of instead."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"person": {
|
|
"type": "string",
|
|
"description": "Person name.",
|
|
},
|
|
},
|
|
"required": ["person"],
|
|
},
|
|
fn=list_offspring,
|
|
graph_is_used=True,
|
|
),
|
|
_entry(
|
|
name="ancestors_of",
|
|
description=(
|
|
"Ancestors of a person via reverse PARENT_OF walks, "
|
|
"bounded by generations (default 3). Returns ancestors "
|
|
"in BFS order (closest first). Cycle-safe."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"person": {
|
|
"type": "string",
|
|
"description": "Person name.",
|
|
},
|
|
"generations": {
|
|
"type": "integer",
|
|
"description": "Maximum generations to walk (default 3).",
|
|
},
|
|
},
|
|
"required": ["person"],
|
|
},
|
|
fn=ancestors_of,
|
|
graph_is_used=True,
|
|
),
|
|
_entry(
|
|
name="descendants_of",
|
|
description=(
|
|
"Descendants of a person via forward PARENT_OF walks, "
|
|
"bounded by generations (default 3). Returns descendants "
|
|
"in BFS order. Cycle-safe."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"person": {
|
|
"type": "string",
|
|
"description": "Person name.",
|
|
},
|
|
"generations": {
|
|
"type": "integer",
|
|
"description": "Maximum generations to walk (default 3).",
|
|
},
|
|
},
|
|
"required": ["person"],
|
|
},
|
|
fn=descendants_of,
|
|
graph_is_used=True,
|
|
),
|
|
_entry(
|
|
name="location_hierarchy",
|
|
description=(
|
|
"The PART_OF chain above or below a location. "
|
|
"direction='up' walks to parents (location → region → "
|
|
"world); direction='down' walks to children. Use for "
|
|
"'what contains X?' or 'what's inside X?' queries."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"location": {
|
|
"type": "string",
|
|
"description": "Location name.",
|
|
},
|
|
"direction": {
|
|
"type": "string",
|
|
"description": "'up' (default) or 'down'.",
|
|
},
|
|
},
|
|
"required": ["location"],
|
|
},
|
|
fn=location_hierarchy,
|
|
graph_is_used=True,
|
|
),
|
|
_entry(
|
|
name="event_chain",
|
|
description=(
|
|
"Bounded multi-hop walk of CAUSED / PRECEDED / "
|
|
"CONCURRENT_WITH edges from an event. Returns causes, "
|
|
"effects, and concurrent events within depth hops. "
|
|
"Use to answer 'what led to / followed X?'."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"event": {
|
|
"type": "string",
|
|
"description": "Event name.",
|
|
},
|
|
"depth": {
|
|
"type": "integer",
|
|
"description": "Maximum hops in each direction (default 2).",
|
|
},
|
|
},
|
|
"required": ["event"],
|
|
},
|
|
fn=event_chain,
|
|
graph_is_used=True,
|
|
),
|
|
_entry(
|
|
name="events_during",
|
|
description=(
|
|
"Events whose OCCURRED_DURING window intersects an "
|
|
"era, optionally filtered by location and event type. "
|
|
"The 'what happened in era X (and where)?' view."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"era": {
|
|
"type": "string",
|
|
"description": "Era or time range (e.g. '3rd_age').",
|
|
},
|
|
"location": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional location filter.",
|
|
},
|
|
"type_": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional event-type filter.",
|
|
},
|
|
"start_time": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional inclusive start of overlap window.",
|
|
},
|
|
"end_time": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional inclusive end of overlap window.",
|
|
},
|
|
"setting": {
|
|
"type": ["string", "null"],
|
|
"description": (
|
|
"Slice 6.5 — restrict to events whose "
|
|
"subject EXISTS_IN this setting."
|
|
),
|
|
},
|
|
},
|
|
"required": ["era"],
|
|
},
|
|
fn=events_during,
|
|
graph_is_used=True,
|
|
opt_string_params=("location", "type_", "start_time", "end_time", "setting"),
|
|
),
|
|
_entry(
|
|
name="lore_about",
|
|
description=(
|
|
"LoreSource documents that mention an entity. Returns "
|
|
"a list of {path, name, reliability, snippet, sources}. "
|
|
"Optionally filtered by source type and capped at "
|
|
"limit (default 10). The 'where in the codex is X "
|
|
"mentioned?' tool."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"entity": {
|
|
"type": "string",
|
|
"description": "Entity name.",
|
|
},
|
|
"type_": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional source-type filter.",
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Maximum number of sources to return (default 10).",
|
|
},
|
|
},
|
|
"required": ["entity"],
|
|
},
|
|
fn=lore_about,
|
|
graph_is_used=True,
|
|
opt_string_params=("type_",),
|
|
),
|
|
# --- Slice 10 — 12 write tools exposed over MCP. The
|
|
# slice-4.7 trio (add_entity / add_relation / add_lore_source)
|
|
# was already implemented in write_tools.py but had not been
|
|
# exposed over the wire (the slice 9 doc noted they were
|
|
# callable from scripts/02_demo.py only). Slice 10.4 wires
|
|
# all 12 write tools into the MCP registry; the underlying
|
|
# functions and their unit tests are unchanged.
|
|
_entry(
|
|
name="add_entity",
|
|
description=(
|
|
"Create a new entity (node) in the graph. Returns "
|
|
"{id, name, label}. The label must be in the "
|
|
"ALLOWED_LABELS set (~36 labels from the ontology). "
|
|
"Use this when the LLM wants to register a new "
|
|
"Person, Faction, Location, etc. before adding "
|
|
"relations that point at it."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"label": {
|
|
"type": "string",
|
|
"description": "Entity label (e.g. 'Person').",
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Canonical entity name.",
|
|
},
|
|
"properties": {
|
|
"type": ["object", "null"],
|
|
"description": (
|
|
"Optional metadata payload. Slice 4.7 "
|
|
"stores only name+label; the payload is "
|
|
"forwarded for slice-5 TypeTemplate use."
|
|
),
|
|
},
|
|
},
|
|
"required": ["label", "name"],
|
|
},
|
|
fn=add_entity,
|
|
graph_is_used=True,
|
|
),
|
|
_entry(
|
|
name="add_relation",
|
|
description=(
|
|
"Append a time-bounded edge to the graph. Returns "
|
|
"{edge_id, subject, relation, object, valid_from, "
|
|
"valid_until}. The relation is free-form (no "
|
|
"edge-type allowlist in slice 4.7). Use valid_from / "
|
|
"valid_until to time-bound the relation; omit both "
|
|
"for an open-ended assertion."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"from_name": {
|
|
"type": "string",
|
|
"description": "Subject (source) entity name.",
|
|
},
|
|
"relation": {
|
|
"type": "string",
|
|
"description": "Relation type (e.g. 'MEMBER_OF').",
|
|
},
|
|
"to_name": {
|
|
"type": "string",
|
|
"description": "Object (target) entity name.",
|
|
},
|
|
"valid_from": {
|
|
"type": ["string", "null"],
|
|
"description": "Inclusive lower time bound.",
|
|
},
|
|
"valid_until": {
|
|
"type": ["string", "null"],
|
|
"description": "Inclusive upper time bound.",
|
|
},
|
|
},
|
|
"required": ["from_name", "relation", "to_name"],
|
|
},
|
|
fn=add_relation,
|
|
graph_is_used=True,
|
|
opt_string_params=("valid_from", "valid_until"),
|
|
),
|
|
_entry(
|
|
name="add_lore_source",
|
|
description=(
|
|
"Register a LoreSource (markdown / YAML) in the graph. "
|
|
"Does NOT chunk or embed the content; that's the LLM "
|
|
"extraction path (slice 3). Returns {id, title, "
|
|
"source_type, author, reliability}."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Source title (canonical display name).",
|
|
},
|
|
"source_type": {
|
|
"type": "string",
|
|
"description": (
|
|
"One of 'prose', 'timeline', 'family_tree', "
|
|
"'gazetteer', 'bestiary', 'culture', "
|
|
"'magic_system', 'dialogue'."
|
|
),
|
|
},
|
|
"content": {
|
|
"type": "string",
|
|
"description": "Raw source content (not chunked).",
|
|
},
|
|
"author": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional author name.",
|
|
},
|
|
},
|
|
"required": ["title", "source_type"],
|
|
},
|
|
fn=add_lore_source,
|
|
graph_is_used=True,
|
|
opt_string_params=("author",),
|
|
),
|
|
_entry(
|
|
name="set_alias",
|
|
description=(
|
|
"Register an alternative name (alias) for an entity. "
|
|
"Returns {name, alias}. After this, lookups by the "
|
|
"alias resolve to the canonical name."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Canonical entity name.",
|
|
},
|
|
"alias": {
|
|
"type": "string",
|
|
"description": "The alias to register.",
|
|
},
|
|
},
|
|
"required": ["name", "alias"],
|
|
},
|
|
fn=set_alias,
|
|
graph_is_used=True,
|
|
),
|
|
_entry(
|
|
name="update_entity",
|
|
description=(
|
|
"Update an entity's type label and/or rename it. "
|
|
"Rename cascades to every edge pointing at the old "
|
|
"name. Returns {name, label, edges_repointed}. The "
|
|
"old name is preserved as an alias of the new one."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Current canonical name.",
|
|
},
|
|
"label": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional new type label.",
|
|
},
|
|
"name_": {
|
|
"type": ["string", "null"],
|
|
"description": (
|
|
"Optional new canonical name. The trailing "
|
|
"underscore avoids colliding with the "
|
|
"positional 'name' argument."
|
|
),
|
|
},
|
|
},
|
|
"required": ["name"],
|
|
},
|
|
fn=update_entity,
|
|
graph_is_used=True,
|
|
opt_string_params=("label",),
|
|
),
|
|
_entry(
|
|
name="delete_entity",
|
|
description=(
|
|
"Remove an entity and every edge that touches it. "
|
|
"Returns {name, edges_removed} so the caller can audit "
|
|
"the blast radius. Aliases are dropped too."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Canonical entity name to remove.",
|
|
},
|
|
},
|
|
"required": ["name"],
|
|
},
|
|
fn=delete_entity,
|
|
graph_is_used=True,
|
|
),
|
|
_entry(
|
|
name="retcon",
|
|
description=(
|
|
"Amend an existing edge's time bounds, relation, or "
|
|
"object. Targets the edge by its stable edge_id "
|
|
"(returned by add_relation). Stamps retcon_at / "
|
|
"retcon_note on the edge for the audit log. Returns "
|
|
"{edge_id, subject, relation, object, valid_from, "
|
|
"valid_until, retcon_at, retcon_note}."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"edge_id": {
|
|
"type": "string",
|
|
"description": "Stable id of the edge to amend.",
|
|
},
|
|
"valid_from": {
|
|
"type": ["string", "null"],
|
|
"description": "New lower time bound (or null/omit to keep).",
|
|
},
|
|
"valid_until": {
|
|
"type": ["string", "null"],
|
|
"description": "New upper time bound (or null/omit to keep).",
|
|
},
|
|
"relation": {
|
|
"type": ["string", "null"],
|
|
"description": "New relation type (or null/omit to keep).",
|
|
},
|
|
"object_": {
|
|
"type": ["string", "null"],
|
|
"description": (
|
|
"New object name (or null/omit to keep). "
|
|
"The trailing underscore avoids the Python "
|
|
"builtin."
|
|
),
|
|
},
|
|
"note": {
|
|
"type": ["string", "null"],
|
|
"description": "Free-text reason for the retcon.",
|
|
},
|
|
},
|
|
"required": ["edge_id"],
|
|
},
|
|
fn=retcon,
|
|
graph_is_used=True,
|
|
opt_string_params=("valid_from", "valid_until", "relation", "object_", "note"),
|
|
),
|
|
_entry(
|
|
name="mark_verified",
|
|
description=(
|
|
"Record a human verification of an edge. Appends a "
|
|
"(1.0, 1.0, 'human_verified') source tuple so the "
|
|
"aggregate confidence floors to 1.0. Doesn't touch "
|
|
"time bounds. Returns {edge_id, verified_by, "
|
|
"verified_at, verified_note}."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"edge_id": {
|
|
"type": "string",
|
|
"description": "Stable id of the edge to verify.",
|
|
},
|
|
"verifier": {
|
|
"type": "string",
|
|
"description": (
|
|
"Verifier's name (email or handle). "
|
|
"Required, non-empty."
|
|
),
|
|
},
|
|
"note": {
|
|
"type": ["string", "null"],
|
|
"description": "Free-text verification note.",
|
|
},
|
|
},
|
|
"required": ["edge_id", "verifier"],
|
|
},
|
|
fn=mark_verified,
|
|
graph_is_used=True,
|
|
opt_string_params=("note",),
|
|
),
|
|
_entry(
|
|
name="merge_entities",
|
|
description=(
|
|
"Fold one canonical name into another. All edges "
|
|
"pointing at 'from_name' are re-pointed to 'to_name'; "
|
|
"the from_name is preserved as an alias. Refuses if "
|
|
"the two have different labels. Returns {merged, "
|
|
"into, edges_repointed}."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"from_name": {
|
|
"type": "string",
|
|
"description": "Source canonical name (the one being folded).",
|
|
},
|
|
"to_name": {
|
|
"type": "string",
|
|
"description": "Target canonical name (the survivor).",
|
|
},
|
|
},
|
|
"required": ["from_name", "to_name"],
|
|
},
|
|
fn=merge_entities,
|
|
graph_is_used=True,
|
|
),
|
|
_entry(
|
|
name="define_calendar",
|
|
description=(
|
|
"Create a Calendar node. Returns {name, days_per_year, "
|
|
"months, description}. Use as the prerequisite for "
|
|
"define_era and define_date."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Calendar name (e.g. 'Imperial Calendar').",
|
|
},
|
|
"days_per_year": {
|
|
"type": ["integer", "null"],
|
|
"description": "Optional number of days per year.",
|
|
},
|
|
"months": {
|
|
"type": ["integer", "null"],
|
|
"description": "Optional number of months.",
|
|
},
|
|
"description": {
|
|
"type": ["string", "null"],
|
|
"description": "Free-text description.",
|
|
},
|
|
},
|
|
"required": ["name"],
|
|
},
|
|
fn=define_calendar,
|
|
graph_is_used=True,
|
|
opt_string_params=("description",),
|
|
),
|
|
_entry(
|
|
name="define_era",
|
|
description=(
|
|
"Create an Era node, link it to a calendar via "
|
|
"PART_OF, and (when applicable) link to the most "
|
|
"recent prior era in the same calendar via PRECEDED. "
|
|
"Returns {name, calendar, start, end, description}."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Era name (also the time-atom prefix).",
|
|
},
|
|
"calendar": {
|
|
"type": "string",
|
|
"description": "Calendar this era belongs to (must exist).",
|
|
},
|
|
"start": {
|
|
"type": "string",
|
|
"description": "Start time atom (e.g. '3rd_age.year_0').",
|
|
},
|
|
"end": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional end time atom.",
|
|
},
|
|
"description": {
|
|
"type": ["string", "null"],
|
|
"description": "Free-text description.",
|
|
},
|
|
},
|
|
"required": ["name", "calendar", "start"],
|
|
},
|
|
fn=define_era,
|
|
graph_is_used=True,
|
|
opt_string_params=("end", "description"),
|
|
),
|
|
_entry(
|
|
name="define_date",
|
|
description=(
|
|
"Create a Date node for a point in a calendar. The "
|
|
"canonical time atom is '{era}.year_{Y}[.month_{M}"
|
|
"[.day_{D}]]'. Stamps INSTANCE_OF Calendar and, if "
|
|
"an era is given, DURING Era. Idempotent: calling "
|
|
"twice with the same args is a no-op. Returns "
|
|
"{canonical, calendar, year, month, day, era, label}."
|
|
),
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"calendar": {
|
|
"type": "string",
|
|
"description": "Calendar the date belongs to (must exist).",
|
|
},
|
|
"year": {
|
|
"type": "integer",
|
|
"description": "Year (>= 1 when an era is given).",
|
|
},
|
|
"month": {
|
|
"type": ["integer", "null"],
|
|
"description": "Optional month (1..12).",
|
|
},
|
|
"day": {
|
|
"type": ["integer", "null"],
|
|
"description": "Optional day (1..31).",
|
|
},
|
|
"era": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional era (must exist; prefixes the canonical atom).",
|
|
},
|
|
"label": {
|
|
"type": ["string", "null"],
|
|
"description": "Optional display label for the date.",
|
|
},
|
|
},
|
|
"required": ["calendar", "year"],
|
|
},
|
|
fn=define_date,
|
|
graph_is_used=True,
|
|
opt_string_params=("era", "label"),
|
|
),
|
|
]
|
|
|
|
|
|
__all__ = ["TOOL_REGISTRY", "ToolEntry"] |