This commit was merged in pull request #1467.
This commit is contained in:
3
data/narration.json
Normal file
3
data/narration.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"discovery": "You discovered a hidden cave in the {location}."
|
||||||
|
}
|
||||||
@@ -22,16 +22,59 @@ Refs: #953 (The Sovereignty Loop), #955, #956, #961
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, TypeVar
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
|
from timmy.sovereignty.auto_crystallizer import (
|
||||||
|
crystallize_reasoning,
|
||||||
|
get_rule_store,
|
||||||
|
)
|
||||||
from timmy.sovereignty.metrics import emit_sovereignty_event, get_metrics_store
|
from timmy.sovereignty.metrics import emit_sovereignty_event, get_metrics_store
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
# ── Module-level narration cache ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
_narration_cache: dict[str, str] | None = None
|
||||||
|
_narration_cache_mtime: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _load_narration_store() -> dict[str, str]:
|
||||||
|
"""Load narration templates from disk, with mtime-based caching."""
|
||||||
|
global _narration_cache, _narration_cache_mtime
|
||||||
|
|
||||||
|
from config import settings
|
||||||
|
|
||||||
|
narration_path = Path(settings.repo_root) / "data" / "narration.json"
|
||||||
|
if not narration_path.exists():
|
||||||
|
_narration_cache = {}
|
||||||
|
return _narration_cache
|
||||||
|
|
||||||
|
try:
|
||||||
|
mtime = narration_path.stat().st_mtime
|
||||||
|
except OSError:
|
||||||
|
if _narration_cache is not None:
|
||||||
|
return _narration_cache
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if _narration_cache is not None and mtime == _narration_cache_mtime:
|
||||||
|
return _narration_cache
|
||||||
|
|
||||||
|
try:
|
||||||
|
with narration_path.open() as f:
|
||||||
|
_narration_cache = json.load(f)
|
||||||
|
_narration_cache_mtime = mtime
|
||||||
|
except Exception:
|
||||||
|
if _narration_cache is None:
|
||||||
|
_narration_cache = {}
|
||||||
|
|
||||||
|
return _narration_cache
|
||||||
|
|
||||||
|
|
||||||
# ── Perception Layer ──────────────────────────────────────────────────────────
|
# ── Perception Layer ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -81,10 +124,7 @@ async def sovereign_perceive(
|
|||||||
raw = await vlm.analyze(screenshot)
|
raw = await vlm.analyze(screenshot)
|
||||||
|
|
||||||
# Step 3: parse
|
# Step 3: parse
|
||||||
if parse_fn is not None:
|
state = parse_fn(raw) if parse_fn is not None else raw
|
||||||
state = parse_fn(raw)
|
|
||||||
else:
|
|
||||||
state = raw
|
|
||||||
|
|
||||||
# Step 4: crystallize
|
# Step 4: crystallize
|
||||||
if crystallize_fn is not None:
|
if crystallize_fn is not None:
|
||||||
@@ -140,11 +180,6 @@ async def sovereign_decide(
|
|||||||
dict[str, Any]
|
dict[str, Any]
|
||||||
The decision result, with at least an ``"action"`` key.
|
The decision result, with at least an ``"action"`` key.
|
||||||
"""
|
"""
|
||||||
from timmy.sovereignty.auto_crystallizer import (
|
|
||||||
crystallize_reasoning,
|
|
||||||
get_rule_store,
|
|
||||||
)
|
|
||||||
|
|
||||||
store = rule_store if rule_store is not None else get_rule_store()
|
store = rule_store if rule_store is not None else get_rule_store()
|
||||||
|
|
||||||
# Step 1: check rules
|
# Step 1: check rules
|
||||||
@@ -207,29 +242,16 @@ async def sovereign_narrate(
|
|||||||
template_store:
|
template_store:
|
||||||
Optional narration template store (dict-like mapping event types
|
Optional narration template store (dict-like mapping event types
|
||||||
to template strings with ``{variable}`` slots). If ``None``,
|
to template strings with ``{variable}`` slots). If ``None``,
|
||||||
tries to load from ``data/narration.json``.
|
uses mtime-cached templates from ``data/narration.json``.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
str
|
str
|
||||||
The narration text.
|
The narration text.
|
||||||
"""
|
"""
|
||||||
import json
|
# Load templates from cache instead of disk every time
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from config import settings
|
|
||||||
|
|
||||||
# Load template store
|
|
||||||
if template_store is None:
|
if template_store is None:
|
||||||
narration_path = Path(settings.repo_root) / "data" / "narration.json"
|
template_store = _load_narration_store()
|
||||||
if narration_path.exists():
|
|
||||||
try:
|
|
||||||
with narration_path.open() as f:
|
|
||||||
template_store = json.load(f)
|
|
||||||
except Exception:
|
|
||||||
template_store = {}
|
|
||||||
else:
|
|
||||||
template_store = {}
|
|
||||||
|
|
||||||
event_type = event.get("type", "unknown")
|
event_type = event.get("type", "unknown")
|
||||||
|
|
||||||
@@ -270,8 +292,7 @@ def _crystallize_narration_template(
|
|||||||
Replaces concrete values in the narration with format placeholders
|
Replaces concrete values in the narration with format placeholders
|
||||||
based on event keys, then saves to ``data/narration.json``.
|
based on event keys, then saves to ``data/narration.json``.
|
||||||
"""
|
"""
|
||||||
import json
|
global _narration_cache, _narration_cache_mtime
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
|
|
||||||
@@ -289,6 +310,9 @@ def _crystallize_narration_template(
|
|||||||
narration_path.parent.mkdir(parents=True, exist_ok=True)
|
narration_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with narration_path.open("w") as f:
|
with narration_path.open("w") as f:
|
||||||
json.dump(template_store, f, indent=2)
|
json.dump(template_store, f, indent=2)
|
||||||
|
# Update cache so next read skips disk
|
||||||
|
_narration_cache = template_store
|
||||||
|
_narration_cache_mtime = narration_path.stat().st_mtime
|
||||||
logger.info("Crystallized narration template for event type '%s'", event_type)
|
logger.info("Crystallized narration template for event type '%s'", event_type)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to persist narration template: %s", exc)
|
logger.warning("Failed to persist narration template: %s", exc)
|
||||||
@@ -347,17 +371,18 @@ def sovereignty_enforced(
|
|||||||
def decorator(fn: Callable) -> Callable:
|
def decorator(fn: Callable) -> Callable:
|
||||||
@functools.wraps(fn)
|
@functools.wraps(fn)
|
||||||
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
session_id = kwargs.get("session_id", "")
|
||||||
|
store = get_metrics_store()
|
||||||
|
|
||||||
# Check cache
|
# Check cache
|
||||||
if cache_check is not None:
|
if cache_check is not None:
|
||||||
cached = cache_check(args, kwargs)
|
cached = cache_check(args, kwargs)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
store = get_metrics_store()
|
store.record(sovereign_event, session_id=session_id)
|
||||||
store.record(sovereign_event, session_id=kwargs.get("session_id", ""))
|
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
# Cache miss — run the model
|
# Cache miss — run the model
|
||||||
store = get_metrics_store()
|
store.record(miss_event, session_id=session_id)
|
||||||
store.record(miss_event, session_id=kwargs.get("session_id", ""))
|
|
||||||
result = await fn(*args, **kwargs)
|
result = await fn(*args, **kwargs)
|
||||||
|
|
||||||
# Crystallize
|
# Crystallize
|
||||||
@@ -367,7 +392,7 @@ def sovereignty_enforced(
|
|||||||
store.record(
|
store.record(
|
||||||
"skill_crystallized",
|
"skill_crystallized",
|
||||||
metadata={"layer": layer},
|
metadata={"layer": layer},
|
||||||
session_id=kwargs.get("session_id", ""),
|
session_id=session_id,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Crystallization failed for %s: %s", layer, exc)
|
logger.warning("Crystallization failed for %s: %s", layer, exc)
|
||||||
|
|||||||
Reference in New Issue
Block a user