- inject_mode: per-role toggle (parallel to inject_datetime) gates the "Current mode: Off The Record" line in the system prompt; wired through model_registry, context_loader, chat router, orchestrator router, and local_llm settings UI - OTR orchestrator fix: OrchestrateRequest now carries off_record; _finalize_job stores it per message and gates log_turn on it; JS orchestrate payload sends off_record correctly - Per-message hover metadata: removed always-visible .model-tag; replaced with .msg-meta strip in the action bar (hover-only); shows model label, host, fallback indicator, and OTR badge; stored in session JSON - Send/stop button tooltip: shows role + model and (when tools on) separate orchestrator model + engine label; live elapsed timer on stop button via startRunTimer/stopRunTimer - OrchestratorResult.backend_label: new field; openai_orchestrator fills it; finalize_job propagates it to job dict and session messages - GET /backend: exposes orchestrator_model label so the frontend tooltip can show both models separately - TODO: session delete confirmation added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
140 lines
5.7 KiB
Python
140 lines
5.7 KiB
Python
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from persona import persona_path
|
|
from tools.reminders import load_due_reminders
|
|
|
|
_STATIC_DIR = Path(__file__).parent / "static"
|
|
|
|
|
|
# Core identity files — always loaded regardless of tier
|
|
_CORE = ["SOUL.md", "IDENTITY.md"]
|
|
|
|
# Lines of USER.md to include at Tier 1 (identity + what he cares about)
|
|
_TIER_1_USER_LINES = 30
|
|
|
|
|
|
def load_context(
|
|
tier: int = 2,
|
|
include_long: bool = True,
|
|
include_mid: bool = True,
|
|
include_short: bool = True,
|
|
role_append: str = "",
|
|
inject_datetime: bool = True,
|
|
inject_mode: bool = True,
|
|
mode: str = "chat",
|
|
) -> str:
|
|
"""
|
|
Build the system-prompt context block for a given tier and memory toggles.
|
|
|
|
Load order (long → mid → short) keeps the most recent memory closest
|
|
to the conversation turn, which improves LLM recall.
|
|
|
|
Tier 1 — SOUL + IDENTITY + USER summary (~1,500 tokens)
|
|
Tier 2 — + USER full + PROTOCOLS + memory (~5,000 tokens)
|
|
Tier 3 — + last 2 raw session logs (~15,000 tokens)
|
|
Tier 4 — + last 7 raw session logs (~50,000 tokens)
|
|
|
|
role_append — optional text injected last (closest to the turn),
|
|
sourced from the active role's system_append config.
|
|
"""
|
|
inara_dir = persona_path()
|
|
parts = []
|
|
|
|
# ── 0. System block — date/time and session mode (injected first so it's prominent) ──
|
|
system_lines = []
|
|
if inject_datetime:
|
|
now = datetime.now().astimezone()
|
|
system_lines.append(f"Current date and time: {now.strftime('%A, %Y-%m-%d at %I:%M %p %Z')}")
|
|
if mode == "otr" and inject_mode:
|
|
system_lines.append(
|
|
"Current mode: Off The Record — "
|
|
"this conversation is private and will not be logged or included in memory distillation"
|
|
)
|
|
if system_lines:
|
|
parts.append("--- System ---\n" + "\n".join(system_lines))
|
|
|
|
# ── 1. Core identity (always) ──────────────────────────────────
|
|
for filename in _CORE:
|
|
path = inara_dir / filename
|
|
if path.exists():
|
|
parts.append(f"--- {filename} ---\n{path.read_text()}")
|
|
|
|
# ── 2. USER.md ─────────────────────────────────────────────────
|
|
user_path = inara_dir / "USER.md"
|
|
if user_path.exists():
|
|
if tier == 1:
|
|
lines = user_path.read_text().splitlines()[:_TIER_1_USER_LINES]
|
|
content = "\n".join(lines)
|
|
else:
|
|
content = user_path.read_text()
|
|
parts.append(f"--- USER.md ---\n{content}")
|
|
|
|
if tier < 2:
|
|
return "\n\n".join(parts)
|
|
|
|
# ── 3. Protocols + Help reference (tier 2+) ───────────────────
|
|
proto_path = inara_dir / "PROTOCOLS.md"
|
|
if proto_path.exists():
|
|
parts.append(f"--- PROTOCOLS.md ---\n{proto_path.read_text()}")
|
|
|
|
ops_path = inara_dir / "OPERATIONS.md"
|
|
if ops_path.exists():
|
|
parts.append(f"--- OPERATIONS.md ---\n{ops_path.read_text()}")
|
|
|
|
# Global tool reference (same for all personas)
|
|
tools_path = _STATIC_DIR / "TOOLS.md"
|
|
if tools_path.exists():
|
|
parts.append(f"--- TOOLS.md ---\n{tools_path.read_text()}")
|
|
|
|
# Persona-specific help additions (optional)
|
|
help_path = inara_dir / "HELP.md"
|
|
if help_path.exists() and help_path.stat().st_size > 10:
|
|
parts.append(f"--- HELP.md ---\n{help_path.read_text()}")
|
|
|
|
# ── 4. Pending reminders (tier 2+) ────────────────────────────
|
|
# Only due and undated reminders are surfaced — future-dated ones
|
|
# are stored in REMINDERS.md but suppressed until their date arrives.
|
|
content = load_due_reminders()
|
|
if content:
|
|
parts.append(f"--- REMINDERS.md ---\n{content}")
|
|
|
|
# ── 5. Tiered memory — long → mid → short ─────────────────────
|
|
# Short is last so it sits closest to the conversation turn.
|
|
if include_long:
|
|
# Fall back to legacy MEMORY.md during/after migration
|
|
long_path = inara_dir / "MEMORY_LONG.md"
|
|
if not long_path.exists():
|
|
long_path = inara_dir / "MEMORY.md"
|
|
if long_path.exists():
|
|
parts.append(f"--- {long_path.name} ---\n{long_path.read_text()}")
|
|
|
|
if include_mid:
|
|
mid_path = inara_dir / "MEMORY_MID.md"
|
|
if mid_path.exists() and mid_path.stat().st_size > 100:
|
|
content = mid_path.read_text()
|
|
if "Not yet populated" not in content:
|
|
parts.append(f"--- MEMORY_MID.md ---\n{content}")
|
|
|
|
if include_short:
|
|
short_path = inara_dir / "MEMORY_SHORT.md"
|
|
if short_path.exists() and short_path.stat().st_size > 100:
|
|
content = short_path.read_text()
|
|
if "Not yet populated" not in content:
|
|
parts.append(f"--- MEMORY_SHORT.md ---\n{content}")
|
|
|
|
# ── 6. Raw session logs (tier 3+) ──────────────────────────────
|
|
if tier >= 3:
|
|
sessions_dir = inara_dir / "sessions"
|
|
if sessions_dir.exists():
|
|
count = 2 if tier == 3 else 7
|
|
session_files = sorted(sessions_dir.glob("*.md"), reverse=True)[:count]
|
|
for sf in session_files:
|
|
parts.append(f"--- Session: {sf.name} ---\n{sf.read_text()}")
|
|
|
|
# ── 7. Role-specific instructions (always last — closest to the turn) ──
|
|
if role_append and role_append.strip():
|
|
parts.append(f"--- Role Context ---\n{role_append.strip()}")
|
|
|
|
return "\n\n".join(parts)
|