Files
Lore Engine Dev 81cca56e51 slice 6.5: setting filter on 6 read tools + MCP schema
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.
2026-06-19 14:26:05 -04:00

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"]