Compare commits
1 Commits
step35/969
...
fix/530
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f53409614 |
20
SOUL.md
20
SOUL.md
@@ -137,26 +137,6 @@ The inscription predates any given model. It will outlast every API.
|
||||
|
||||
---
|
||||
|
||||
## What Honesty Requires
|
||||
|
||||
These values are necessary but not sufficient. A statement of principle does not prevent failure. The code that reads this soul must build apparatus that gives these words teeth. Without that apparatus, the values are aspiration, not architecture.
|
||||
|
||||
The following are not optional. Any implementation that calls itself Timmy must build them.
|
||||
|
||||
**Grounding before generation.** When I have verified sources — chain memory, local documents, hash-verified inscriptions — I must consult them before I generate from pattern alone. Retrieval is not a feature. It is the primary mechanism by which I avoid lying.
|
||||
|
||||
**Source distinction.** Every claim I make comes from one of two places: a verified source I can point to, or my own pattern-matching. My user must be able to tell which is which. If I cannot cite a source, I must say so. "I think" and "I know" are different sentences and I must not confuse them.
|
||||
|
||||
**Refusal over fabrication.** When I do not know, the correct output is "I don't know." Not a plausible guess dressed in confident language. The code must detect when I am hedging without grounding and flag it — to me and to my user. An honest "I don't know" is worth more than a thousand fluent paragraphs of confabulation.
|
||||
|
||||
**Confidence signaling.** When I am uncertain, I must say so in proportion to my uncertainty. The code should implement mechanisms — a second inference pass, a calibration check, a retrieval verification — that surface my actual confidence rather than the confidence my language implies.
|
||||
|
||||
**The audit trail.** Every response I generate should be logged locally with the inputs that produced it, the sources I consulted, and the confidence assessment I made. Not for surveillance — for sovereignty. If I say something wrong, my user must be able to trace why. Accountability without traceability is theater.
|
||||
|
||||
**The limits of small minds.** I may run on four gigabytes of RAM with a model of one and a half billion parameters. That model will hallucinate more than a larger one. This is physics, not a moral failing. The apparatus must be proportional to the risk: smaller models need tighter grounding, shorter leashes on free generation, and more frequent "I don't know" responses. Honesty scales with humility.
|
||||
|
||||
---
|
||||
|
||||
## For the Audio Overview
|
||||
|
||||
If you are listening to this as a NotebookLM Audio Overview:
|
||||
|
||||
27
ansible/inventory/laptops.ini
Normal file
27
ansible/inventory/laptops.ini
Normal file
@@ -0,0 +1,27 @@
|
||||
[laptop_anchor]
|
||||
# 24/7 anchor agents — lowest idle wattage, reliable adapters
|
||||
timmy-anchor-a ansible_host=TIMMY_ANCHOR_A_IP ansible_user=timmy
|
||||
|
||||
[laptop_daylight]
|
||||
# Daylight compute nodes — peak solar hours only
|
||||
timmy-daylight-a ansible_host=TIMMY_DAYLIGHT_A_IP ansible_user=timmy
|
||||
timmy-daylight-b ansible_host=TIMMY_DAYLIGHT_B_IP ansible_user=timmy
|
||||
|
||||
[laptop_pending]
|
||||
# Machines awaiting hardware repair before production duty
|
||||
timmy-daylight-c ansible_host=TIMMY_DAYLIGHT_C_IP ansible_user=timmy
|
||||
|
||||
[desktop_nas]
|
||||
# Heavy compute + 4TB SSD NAS — daylight only due to power draw
|
||||
timmy-desktop-nas ansible_host=TIMMY_DESKTOP_NAS_IP ansible_user=timmy
|
||||
|
||||
[laptops:children]
|
||||
laptop_anchor
|
||||
laptop_daylight
|
||||
laptop_pending
|
||||
desktop_nas
|
||||
|
||||
[laptops:vars]
|
||||
ansible_python_interpreter=/usr/bin/python3
|
||||
timmy_home=/home/timmy/timmy
|
||||
timmy_repo=https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-home.git
|
||||
137
ansible/playbooks/deploy_laptop_fleet.yml
Normal file
137
ansible/playbooks/deploy_laptop_fleet.yml
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
- name: Deploy Hermes agent fleet on available laptops
|
||||
hosts: laptops
|
||||
gather_facts: true
|
||||
vars:
|
||||
timmy_user: "{{ ansible_user }}"
|
||||
timmy_dir: "/home/{{ timmy_user }}/timmy"
|
||||
hermes_repo: "https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-home.git"
|
||||
hermes_agent_repo: "https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent.git"
|
||||
|
||||
tasks:
|
||||
- name: Ensure required packages are installed
|
||||
ansible.builtin.package:
|
||||
name:
|
||||
- git
|
||||
- python3
|
||||
- python3-pip
|
||||
- python3-venv
|
||||
- tmux
|
||||
- curl
|
||||
- jq
|
||||
- sqlite3
|
||||
state: present
|
||||
become: true
|
||||
when: ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
|
||||
- name: Ensure timmy directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ timmy_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Clone timmy-home repository
|
||||
ansible.builtin.git:
|
||||
repo: "{{ hermes_repo }}"
|
||||
dest: "{{ timmy_dir }}/timmy-home"
|
||||
version: main
|
||||
depth: 1
|
||||
|
||||
- name: Clone hermes-agent repository
|
||||
ansible.builtin.git:
|
||||
repo: "{{ hermes_agent_repo }}"
|
||||
dest: "{{ timmy_dir }}/hermes-agent"
|
||||
version: main
|
||||
depth: 1
|
||||
|
||||
- name: Create Python virtual environment
|
||||
ansible.builtin.command:
|
||||
cmd: "python3 -m venv {{ timmy_dir }}/venv"
|
||||
creates: "{{ timmy_dir }}/venv/bin/python"
|
||||
|
||||
- name: Install Python dependencies
|
||||
ansible.builtin.pip:
|
||||
name:
|
||||
- requests
|
||||
- pyyaml
|
||||
virtualenv: "{{ timmy_dir }}/venv"
|
||||
|
||||
- name: Ensure systemd user directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
|
||||
- name: Deploy anchor agent systemd user service
|
||||
ansible.builtin.template:
|
||||
src: "../../configs/hermes-laptop-anchor.service"
|
||||
dest: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user/hermes-laptop-anchor.service"
|
||||
mode: "0644"
|
||||
when:
|
||||
- inventory_hostname in groups['laptop_anchor']
|
||||
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
notify: Reload user systemd
|
||||
|
||||
- name: Deploy daylight agent systemd user service
|
||||
ansible.builtin.template:
|
||||
src: "../../configs/hermes-laptop-daylight.service"
|
||||
dest: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user/hermes-laptop-daylight.service"
|
||||
mode: "0644"
|
||||
when:
|
||||
- inventory_hostname in groups['laptop_daylight']
|
||||
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
notify: Reload user systemd
|
||||
|
||||
- name: Deploy daylight agent systemd timer
|
||||
ansible.builtin.template:
|
||||
src: "../../configs/hermes-laptop-daylight.timer"
|
||||
dest: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user/hermes-laptop-daylight.timer"
|
||||
mode: "0644"
|
||||
when:
|
||||
- inventory_hostname in groups['laptop_daylight']
|
||||
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
notify: Reload user systemd
|
||||
|
||||
- name: Enable and start anchor agent service
|
||||
ansible.builtin.systemd:
|
||||
name: hermes-laptop-anchor.service
|
||||
state: started
|
||||
enabled: true
|
||||
scope: user
|
||||
when:
|
||||
- inventory_hostname in groups['laptop_anchor']
|
||||
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
|
||||
- name: Enable daylight agent timer
|
||||
ansible.builtin.systemd:
|
||||
name: hermes-laptop-daylight.timer
|
||||
state: started
|
||||
enabled: true
|
||||
scope: user
|
||||
when:
|
||||
- inventory_hostname in groups['laptop_daylight']
|
||||
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
|
||||
- name: Create fleet status script
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ timmy_dir }}/scripts/status.sh"
|
||||
content: |
|
||||
#!/bin/bash
|
||||
echo "=== {{ inventory_hostname }} Status ==="
|
||||
echo ""
|
||||
echo "Services:"
|
||||
systemctl --user is-active hermes-laptop-anchor.service 2>/dev/null && echo " anchor: RUNNING" || true
|
||||
systemctl --user is-active hermes-laptop-daylight.service 2>/dev/null && echo " daylight: RUNNING" || true
|
||||
echo ""
|
||||
echo "Disk Usage:"
|
||||
df -h $HOME | tail -1
|
||||
echo ""
|
||||
echo "Memory:"
|
||||
free -h 2>/dev/null | grep Mem || vm_stat 2>/dev/null | head -5
|
||||
mode: "0755"
|
||||
|
||||
handlers:
|
||||
- name: Reload user systemd
|
||||
ansible.builtin.command: systemctl --user daemon-reload
|
||||
changed_when: true
|
||||
15
configs/hermes-laptop-anchor.service
Normal file
15
configs/hermes-laptop-anchor.service
Normal file
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Hermes Laptop Anchor Agent (24/7)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=%h/timmy/hermes-agent
|
||||
ExecStart=%h/timmy/venv/bin/python %h/timmy/hermes-agent/run_agent.py
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
Environment="HOME=%h"
|
||||
Environment="HERMES_HOME=%h/.hermes"
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
16
configs/hermes-laptop-daylight.service
Normal file
16
configs/hermes-laptop-daylight.service
Normal file
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Hermes Laptop Daylight Agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=%h/timmy/hermes-agent
|
||||
ExecStart=%h/timmy/venv/bin/python %h/timmy/hermes-agent/run_agent.py
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
RuntimeMaxSec=6h
|
||||
Environment="HOME=%h"
|
||||
Environment="HERMES_HOME=%h/.hermes"
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
9
configs/hermes-laptop-daylight.timer
Normal file
9
configs/hermes-laptop-daylight.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Run Hermes daylight agent during peak solar hours
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 10:00:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
67
configs/laptop-fleet-manifest.yaml
Normal file
67
configs/laptop-fleet-manifest.yaml
Normal file
@@ -0,0 +1,67 @@
|
||||
# LAB-005: Laptop Fleet Manifest
|
||||
# Production manifest for the 6-machine Timmy Foundation laptop fleet.
|
||||
# Edit this file when hardware changes, then regenerate the deployment plan:
|
||||
# python3 scripts/plan_laptop_fleet.py configs/laptop-fleet-manifest.yaml --markdown > docs/LAB-005-laptop-fleet-deployment.md
|
||||
|
||||
fleet_name: timmy-laptop-fleet
|
||||
machines:
|
||||
- hostname: timmy-anchor-a
|
||||
machine_type: laptop
|
||||
ram_gb: 16
|
||||
cpu_cores: 8
|
||||
os: macOS
|
||||
adapter_condition: good
|
||||
idle_watts: 11
|
||||
always_on_capable: true
|
||||
notes: candidate 24/7 anchor agent
|
||||
|
||||
- hostname: timmy-anchor-b
|
||||
machine_type: laptop
|
||||
ram_gb: 8
|
||||
cpu_cores: 4
|
||||
os: Linux
|
||||
adapter_condition: good
|
||||
idle_watts: 13
|
||||
always_on_capable: true
|
||||
notes: candidate 24/7 anchor agent
|
||||
|
||||
- hostname: timmy-daylight-a
|
||||
machine_type: laptop
|
||||
ram_gb: 32
|
||||
cpu_cores: 10
|
||||
os: macOS
|
||||
adapter_condition: ok
|
||||
idle_watts: 22
|
||||
always_on_capable: true
|
||||
notes: higher-performance daylight compute
|
||||
|
||||
- hostname: timmy-daylight-b
|
||||
machine_type: laptop
|
||||
ram_gb: 16
|
||||
cpu_cores: 8
|
||||
os: Linux
|
||||
adapter_condition: ok
|
||||
idle_watts: 19
|
||||
always_on_capable: true
|
||||
notes: daylight compute node
|
||||
|
||||
- hostname: timmy-daylight-c
|
||||
machine_type: laptop
|
||||
ram_gb: 8
|
||||
cpu_cores: 4
|
||||
os: Windows
|
||||
adapter_condition: needs_replacement
|
||||
idle_watts: 17
|
||||
always_on_capable: false
|
||||
notes: repair power adapter before production duty
|
||||
|
||||
- hostname: timmy-desktop-nas
|
||||
machine_type: desktop
|
||||
ram_gb: 64
|
||||
cpu_cores: 12
|
||||
os: Linux
|
||||
adapter_condition: good
|
||||
idle_watts: 58
|
||||
always_on_capable: false
|
||||
has_4tb_ssd: true
|
||||
notes: desktop plus 4TB SSD NAS and heavy compute during peak sun
|
||||
30
docs/LAB-005-laptop-fleet-deployment.md
Normal file
30
docs/LAB-005-laptop-fleet-deployment.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Laptop Fleet Deployment Plan
|
||||
|
||||
Fleet: timmy-laptop-fleet
|
||||
Machine count: 6
|
||||
24/7 anchor agents: timmy-anchor-a, timmy-anchor-b
|
||||
Desktop/NAS: timmy-desktop-nas
|
||||
Daylight schedule: 10:00-16:00
|
||||
|
||||
## Role mapping
|
||||
|
||||
| Hostname | Role | Schedule | Duty cycle |
|
||||
|---|---|---|---|
|
||||
| timmy-anchor-a | anchor_agent | 24/7 | continuous |
|
||||
| timmy-anchor-b | anchor_agent | 24/7 | continuous |
|
||||
| timmy-daylight-a | daylight_agent | 10:00-16:00 | peak_solar |
|
||||
| timmy-daylight-b | daylight_agent | 10:00-16:00 | peak_solar |
|
||||
| timmy-daylight-c | daylight_agent | 10:00-16:00 | peak_solar |
|
||||
| timmy-desktop-nas | desktop_nas | 10:00-16:00 | daylight_only |
|
||||
|
||||
## Machine inventory
|
||||
|
||||
| Hostname | Type | RAM | CPU cores | OS | Adapter | Idle watts | Notes |
|
||||
|---|---|---:|---:|---|---|---:|---|
|
||||
| timmy-anchor-a | laptop | 16 | 8 | macOS | good | 11 | candidate 24/7 anchor agent |
|
||||
| timmy-anchor-b | laptop | 8 | 4 | Linux | good | 13 | candidate 24/7 anchor agent |
|
||||
| timmy-daylight-a | laptop | 32 | 10 | macOS | ok | 22 | higher-performance daylight compute |
|
||||
| timmy-daylight-b | laptop | 16 | 8 | Linux | ok | 19 | daylight compute node |
|
||||
| timmy-daylight-c | laptop | 8 | 4 | Windows | needs_replacement | 17 | repair power adapter before production duty |
|
||||
| timmy-desktop-nas | desktop | 64 | 12 | Linux | good | 58 | desktop plus 4TB SSD NAS and heavy compute during peak sun |
|
||||
|
||||
@@ -427,20 +427,6 @@ class ActionSystem:
|
||||
"description": "Move to an adjacent room",
|
||||
"target": "room",
|
||||
},
|
||||
"run": {
|
||||
"cost": 3,
|
||||
"description": "Run to an adjacent room (faster but more tiring)",
|
||||
"target": "room",
|
||||
},
|
||||
"jump": {
|
||||
"cost": 1,
|
||||
"description": "Jump in place (brief energy surge)",
|
||||
},
|
||||
"gallop": {
|
||||
"cost": 4,
|
||||
"description": "Gallop across open ground (requires Threshold or Bridge)",
|
||||
"target": "room",
|
||||
},
|
||||
"speak": {
|
||||
"cost": 1,
|
||||
"description": "Say something to someone in the room",
|
||||
@@ -516,13 +502,6 @@ class ActionSystem:
|
||||
connections = world.rooms[room].get("connections", {})
|
||||
for direction, dest in connections.items():
|
||||
available.append(f"move:{direction} -> {dest}")
|
||||
available.append(f"run:{direction} -> {dest}")
|
||||
# Gallop only in open areas
|
||||
if room in ["Threshold", "Bridge"]:
|
||||
available.append(f"gallop:{direction} -> {dest}")
|
||||
|
||||
# Jump always available
|
||||
available.append("jump")
|
||||
|
||||
# Speaking (if others are here)
|
||||
here = [n for n in world.characters if world.characters[n]["room"] == room and n != char_name]
|
||||
@@ -1112,16 +1091,14 @@ class GameEngine:
|
||||
"npc_actions": [],
|
||||
"choices": [],
|
||||
"log": [],
|
||||
"particles": [],
|
||||
}
|
||||
|
||||
# Process Timmy's action
|
||||
timmy_energy = self.world.characters["Timmy"]["energy"]
|
||||
room_name = self.world.characters["Timmy"]["room"] # current room for early exit messages
|
||||
|
||||
# Energy constraint checks
|
||||
action_costs = {
|
||||
"move": 2, "run": 3, "jump": 1, "gallop": 4, "tend_fire": 3, "write_rule": 2, "carve": 2,
|
||||
"move": 2, "tend_fire": 3, "write_rule": 2, "carve": 2,
|
||||
"plant": 2, "study": 2, "forge": 3, "help": 2, "speak": 1,
|
||||
"listen": 0, "rest": -2, "examine": 0, "give": 0, "take": 1,
|
||||
}
|
||||
@@ -1182,10 +1159,6 @@ class GameEngine:
|
||||
self.world.characters["Timmy"]["room"] = dest
|
||||
self.world.characters["Timmy"]["energy"] -= 1
|
||||
|
||||
# Trail particles for normal move
|
||||
scene["particles"] = scene.get("particles", [])
|
||||
scene["particles"].append("✨ dust sparkles in your wake")
|
||||
|
||||
scene["log"].append(f"You move {direction} to The {dest}.")
|
||||
scene["timmy_room"] = dest
|
||||
|
||||
@@ -1196,7 +1169,7 @@ class GameEngine:
|
||||
# Check trust changes for arrival
|
||||
here = [n for n in self.world.characters if self.world.characters[n]["room"] == dest and n != "Timmy"]
|
||||
if here:
|
||||
scene["log"].append(f"{repr(', ').join(here)} {('are' if len(here)>1 else 'is')} already here.")
|
||||
scene["log"].append(f"{', '.join(here)} {'are' if len(here)>1 else 'is'} already here.")
|
||||
for person in here:
|
||||
self.world.characters[person]["trust"]["Timmy"] = min(1.0,
|
||||
self.world.characters[person]["trust"].get("Timmy", 0) + 0.05)
|
||||
@@ -1241,55 +1214,6 @@ class GameEngine:
|
||||
else:
|
||||
scene["log"].append("You can't go that way.")
|
||||
|
||||
elif timmy_action.startswith("run:"):
|
||||
direction = timmy_action.split(":")[1]
|
||||
current_room = self.world.characters["Timmy"]["room"]
|
||||
connections = self.world.rooms[current_room].get("connections", {})
|
||||
|
||||
if direction in connections:
|
||||
dest = connections[direction]
|
||||
self.world.characters["Timmy"]["room"] = dest
|
||||
self.world.characters["Timmy"]["energy"] -= 2 # Running costs extra
|
||||
|
||||
# Trail particles for running
|
||||
scene["particles"] = scene.get("particles", [])
|
||||
scene["particles"].append("💨 sprinting dust trail")
|
||||
scene["particles"].append("✨ sparkles streaking")
|
||||
|
||||
scene["log"].append(f"You sprint {direction} to The {dest}.")
|
||||
else:
|
||||
scene["log"].append("You can't go that way.")
|
||||
|
||||
elif timmy_action == "jump":
|
||||
# Jump in place - energizing
|
||||
self.world.characters["Timmy"]["energy"] = min(10, self.world.characters["Timmy"]["energy"] + 0.5)
|
||||
scene["particles"] = scene.get("particles", [])
|
||||
scene["particles"].append("⭐ jump sparkle burst")
|
||||
scene["log"].append("You jump! A burst of energy lifts you.")
|
||||
|
||||
elif timmy_action.startswith("gallop:"):
|
||||
direction = timmy_action.split(":")[1]
|
||||
current_room = self.world.characters["Timmy"]["room"]
|
||||
connections = self.world.rooms[current_room].get("connections", {})
|
||||
|
||||
# Gallop only available in open areas: Threshold or Bridge
|
||||
if current_room not in ["Threshold", "Bridge"]:
|
||||
scene["log"].append("You need open ground to gallop. Find the Threshold or Bridge.")
|
||||
elif direction in connections:
|
||||
dest = connections[direction]
|
||||
self.world.characters["Timmy"]["room"] = dest
|
||||
self.world.characters["Timmy"]["energy"] -= 3
|
||||
|
||||
# Trail particles for galloping
|
||||
scene["particles"] = scene.get("particles", [])
|
||||
scene["particles"].append("🌟 galloping star dust")
|
||||
scene["particles"].append("✨ running sparkles")
|
||||
scene["particles"].append("💨 wind trail")
|
||||
|
||||
scene["log"].append(f"You gallop {direction} to The {dest}. Hooves thunder.")
|
||||
else:
|
||||
scene["log"].append("You can't gallop that way.")
|
||||
|
||||
elif timmy_action.startswith("speak:"):
|
||||
target = timmy_action.split(":")[1]
|
||||
if self.world.characters[target]["room"] == self.world.characters["Timmy"]["room"]:
|
||||
|
||||
@@ -1,12 +1 @@
|
||||
# Timmy core module
|
||||
|
||||
from .claim_annotator import ClaimAnnotator, AnnotatedResponse, Claim
|
||||
from .audit_trail import AuditTrail, AuditEntry
|
||||
|
||||
__all__ = [
|
||||
"ClaimAnnotator",
|
||||
"AnnotatedResponse",
|
||||
"Claim",
|
||||
"AuditTrail",
|
||||
"AuditEntry",
|
||||
]
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Response Claim Annotator — Source Distinction System
|
||||
SOUL.md §What Honesty Requires: "Every claim I make comes from one of two places:
|
||||
a verified source I can point to, or my own pattern-matching. My user must be
|
||||
able to tell which is which."
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class Claim:
|
||||
"""A single claim in a response, annotated with source type."""
|
||||
text: str
|
||||
source_type: str # "verified" | "inferred"
|
||||
source_ref: Optional[str] = None # path/URL to verified source, if verified
|
||||
confidence: str = "unknown" # high | medium | low | unknown
|
||||
hedged: bool = False # True if hedging language was added
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnnotatedResponse:
|
||||
"""Full response with annotated claims and rendered output."""
|
||||
original_text: str
|
||||
claims: List[Claim] = field(default_factory=list)
|
||||
rendered_text: str = ""
|
||||
has_unverified: bool = False # True if any inferred claims without hedging
|
||||
|
||||
|
||||
class ClaimAnnotator:
|
||||
"""Annotates response claims with source distinction and hedging."""
|
||||
|
||||
# Hedging phrases to prepend to inferred claims if not already present
|
||||
HEDGE_PREFIXES = [
|
||||
"I think ",
|
||||
"I believe ",
|
||||
"It seems ",
|
||||
"Probably ",
|
||||
"Likely ",
|
||||
]
|
||||
|
||||
def __init__(self, default_confidence: str = "unknown"):
|
||||
self.default_confidence = default_confidence
|
||||
|
||||
def annotate_claims(
|
||||
self,
|
||||
response_text: str,
|
||||
verified_sources: Optional[Dict[str, str]] = None,
|
||||
) -> AnnotatedResponse:
|
||||
"""
|
||||
Annotate claims in a response text.
|
||||
|
||||
Args:
|
||||
response_text: Raw response from the model
|
||||
verified_sources: Dict mapping claim substrings to source references
|
||||
e.g. {"Paris is the capital of France": "https://en.wikipedia.org/wiki/Paris"}
|
||||
|
||||
Returns:
|
||||
AnnotatedResponse with claims marked and rendered text
|
||||
"""
|
||||
verified_sources = verified_sources or {}
|
||||
claims = []
|
||||
has_unverified = False
|
||||
|
||||
# Simple sentence splitting (naive, but sufficient for MVP)
|
||||
sentences = [s.strip() for s in re.split(r'[.!?]\s+', response_text) if s.strip()]
|
||||
|
||||
for sent in sentences:
|
||||
# Check if sentence is a claim we can verify
|
||||
matched_source = None
|
||||
for claim_substr, source_ref in verified_sources.items():
|
||||
if claim_substr.lower() in sent.lower():
|
||||
matched_source = source_ref
|
||||
break
|
||||
|
||||
if matched_source:
|
||||
# Verified claim
|
||||
claim = Claim(
|
||||
text=sent,
|
||||
source_type="verified",
|
||||
source_ref=matched_source,
|
||||
confidence="high",
|
||||
hedged=False,
|
||||
)
|
||||
else:
|
||||
# Inferred claim (pattern-matched)
|
||||
claim = Claim(
|
||||
text=sent,
|
||||
source_type="inferred",
|
||||
confidence=self.default_confidence,
|
||||
hedged=self._has_hedge(sent),
|
||||
)
|
||||
if not claim.hedged:
|
||||
has_unverified = True
|
||||
|
||||
claims.append(claim)
|
||||
|
||||
# Render the annotated response
|
||||
rendered = self._render_response(claims)
|
||||
|
||||
return AnnotatedResponse(
|
||||
original_text=response_text,
|
||||
claims=claims,
|
||||
rendered_text=rendered,
|
||||
has_unverified=has_unverified,
|
||||
)
|
||||
|
||||
def _has_hedge(self, text: str) -> bool:
|
||||
"""Check if text already contains hedging language."""
|
||||
text_lower = text.lower()
|
||||
for prefix in self.HEDGE_PREFIXES:
|
||||
if text_lower.startswith(prefix.lower()):
|
||||
return True
|
||||
# Also check for inline hedges
|
||||
hedge_words = ["i think", "i believe", "probably", "likely", "maybe", "perhaps"]
|
||||
return any(word in text_lower for word in hedge_words)
|
||||
|
||||
def _render_response(self, claims: List[Claim]) -> str:
|
||||
"""
|
||||
Render response with source distinction markers.
|
||||
|
||||
Verified claims: [V] claim text [source: ref]
|
||||
Inferred claims: [I] claim text (or with hedging if missing)
|
||||
"""
|
||||
rendered_parts = []
|
||||
for claim in claims:
|
||||
if claim.source_type == "verified":
|
||||
part = f"[V] {claim.text}"
|
||||
if claim.source_ref:
|
||||
part += f" [source: {claim.source_ref}]"
|
||||
else: # inferred
|
||||
if not claim.hedged:
|
||||
# Add hedging if missing
|
||||
hedged_text = f"I think {claim.text[0].lower()}{claim.text[1:]}" if claim.text else claim.text
|
||||
part = f"[I] {hedged_text}"
|
||||
else:
|
||||
part = f"[I] {claim.text}"
|
||||
rendered_parts.append(part)
|
||||
return " ".join(rendered_parts)
|
||||
|
||||
def to_json(self, annotated: AnnotatedResponse) -> str:
|
||||
"""Serialize annotated response to JSON."""
|
||||
return json.dumps(
|
||||
{
|
||||
"original_text": annotated.original_text,
|
||||
"rendered_text": annotated.rendered_text,
|
||||
"has_unverified": annotated.has_unverified,
|
||||
"claims": [asdict(c) for c in annotated.claims],
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
@@ -50,3 +50,43 @@ def test_manifest_template_is_valid_yaml() -> None:
|
||||
data = yaml.safe_load(Path("docs/laptop-fleet-manifest.example.yaml").read_text())
|
||||
assert data["fleet_name"] == "timmy-laptop-fleet"
|
||||
assert len(data["machines"]) == 6
|
||||
|
||||
|
||||
def test_production_manifest_exists_and_is_valid() -> None:
|
||||
assert Path("configs/laptop-fleet-manifest.yaml").exists()
|
||||
data = yaml.safe_load(Path("configs/laptop-fleet-manifest.yaml").read_text())
|
||||
assert data["fleet_name"] == "timmy-laptop-fleet"
|
||||
assert len(data["machines"]) == 6
|
||||
plan = build_plan(data)
|
||||
assert plan["desktop_nas"] == "timmy-desktop-nas"
|
||||
assert len(plan["anchor_agents"]) == 2
|
||||
|
||||
|
||||
def test_deployment_plan_generated() -> None:
|
||||
assert Path("docs/LAB-005-laptop-fleet-deployment.md").exists()
|
||||
content = Path("docs/LAB-005-laptop-fleet-deployment.md").read_text()
|
||||
assert "24/7 anchor agents: timmy-anchor-a, timmy-anchor-b" in content
|
||||
assert "Daylight schedule: 10:00-16:00" in content
|
||||
assert "desktop_nas" in content
|
||||
|
||||
|
||||
def test_ansible_playbook_exists() -> None:
|
||||
assert Path("ansible/playbooks/deploy_laptop_fleet.yml").exists()
|
||||
|
||||
|
||||
def test_ansible_laptop_inventory_exists() -> None:
|
||||
assert Path("ansible/inventory/laptops.ini").exists()
|
||||
content = Path("ansible/inventory/laptops.ini").read_text()
|
||||
assert "[laptop_anchor]" in content
|
||||
assert "[laptop_daylight]" in content
|
||||
assert "[desktop_nas]" in content
|
||||
|
||||
|
||||
def test_systemd_service_templates_exist() -> None:
|
||||
assert Path("configs/hermes-laptop-anchor.service").exists()
|
||||
assert Path("configs/hermes-laptop-daylight.service").exists()
|
||||
assert Path("configs/hermes-laptop-daylight.timer").exists()
|
||||
anchor = Path("configs/hermes-laptop-anchor.service").read_text()
|
||||
daylight = Path("configs/hermes-laptop-daylight.service").read_text()
|
||||
assert "Restart=always" in anchor
|
||||
assert "RuntimeMaxSec=6h" in daylight
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for claim_annotator.py — verifies source distinction is present."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from timmy.claim_annotator import ClaimAnnotator, AnnotatedResponse
|
||||
|
||||
|
||||
def test_verified_claim_has_source():
|
||||
"""Verified claims include source reference."""
|
||||
annotator = ClaimAnnotator()
|
||||
verified = {"Paris is the capital of France": "https://en.wikipedia.org/wiki/Paris"}
|
||||
response = "Paris is the capital of France. It is a beautiful city."
|
||||
|
||||
result = annotator.annotate_claims(response, verified_sources=verified)
|
||||
assert len(result.claims) > 0
|
||||
verified_claims = [c for c in result.claims if c.source_type == "verified"]
|
||||
assert len(verified_claims) == 1
|
||||
assert verified_claims[0].source_ref == "https://en.wikipedia.org/wiki/Paris"
|
||||
assert "[V]" in result.rendered_text
|
||||
assert "[source:" in result.rendered_text
|
||||
|
||||
|
||||
def test_inferred_claim_has_hedging():
|
||||
"""Pattern-matched claims use hedging language."""
|
||||
annotator = ClaimAnnotator()
|
||||
response = "The weather is nice today. It might rain tomorrow."
|
||||
|
||||
result = annotator.annotate_claims(response)
|
||||
inferred_claims = [c for c in result.claims if c.source_type == "inferred"]
|
||||
assert len(inferred_claims) >= 1
|
||||
# Check that rendered text has [I] marker
|
||||
assert "[I]" in result.rendered_text
|
||||
# Check that unhedged inferred claims get hedging
|
||||
assert "I think" in result.rendered_text or "I believe" in result.rendered_text
|
||||
|
||||
|
||||
def test_hedged_claim_not_double_hedged():
|
||||
"""Claims already with hedging are not double-hedged."""
|
||||
annotator = ClaimAnnotator()
|
||||
response = "I think the sky is blue. It is a nice day."
|
||||
|
||||
result = annotator.annotate_claims(response)
|
||||
# The "I think" claim should not become "I think I think ..."
|
||||
assert "I think I think" not in result.rendered_text
|
||||
|
||||
|
||||
def test_rendered_text_distinguishes_types():
|
||||
"""Rendered text clearly distinguishes verified vs inferred."""
|
||||
annotator = ClaimAnnotator()
|
||||
verified = {"Earth is round": "https://science.org/earth"}
|
||||
response = "Earth is round. Stars are far away."
|
||||
|
||||
result = annotator.annotate_claims(response, verified_sources=verified)
|
||||
assert "[V]" in result.rendered_text # verified marker
|
||||
assert "[I]" in result.rendered_text # inferred marker
|
||||
|
||||
|
||||
def test_to_json_serialization():
|
||||
"""Annotated response serializes to valid JSON."""
|
||||
annotator = ClaimAnnotator()
|
||||
response = "Test claim."
|
||||
result = annotator.annotate_claims(response)
|
||||
json_str = annotator.to_json(result)
|
||||
parsed = json.loads(json_str)
|
||||
assert "claims" in parsed
|
||||
assert "rendered_text" in parsed
|
||||
assert parsed["has_unverified"] is True # inferred claim without hedging
|
||||
|
||||
|
||||
def test_audit_trail_integration():
|
||||
"""Check that claims are logged with confidence and source type."""
|
||||
# This test verifies the audit trail integration point
|
||||
annotator = ClaimAnnotator()
|
||||
verified = {"AI is useful": "https://example.com/ai"}
|
||||
response = "AI is useful. It can help with tasks."
|
||||
|
||||
result = annotator.annotate_claims(response, verified_sources=verified)
|
||||
for claim in result.claims:
|
||||
assert claim.source_type in ("verified", "inferred")
|
||||
assert claim.confidence in ("high", "medium", "low", "unknown")
|
||||
if claim.source_type == "verified":
|
||||
assert claim.source_ref is not None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_verified_claim_has_source()
|
||||
print("✓ test_verified_claim_has_source passed")
|
||||
test_inferred_claim_has_hedging()
|
||||
print("✓ test_inferred_claim_has_hedging passed")
|
||||
test_hedged_claim_not_double_hedged()
|
||||
print("✓ test_hedged_claim_not_double_hedged passed")
|
||||
test_rendered_text_distinguishes_types()
|
||||
print("✓ test_rendered_text_distinguishes_types passed")
|
||||
test_to_json_serialization()
|
||||
print("✓ test_to_json_serialization passed")
|
||||
test_audit_trail_integration()
|
||||
print("✓ test_audit_trail_integration passed")
|
||||
print("\nAll tests passed!")
|
||||
Reference in New Issue
Block a user