Compare commits
1 Commits
step35/973
...
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 |
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# Provision-core Integration
|
||||
|
||||
## Overview
|
||||
|
||||
[provision-core](https://github.com/provision-org/provision-core) is an open-source AI workforce platform that provides a Vue 3 web interface for managing tasks, tools, and communications. This integration allows provision-core to visualize and interact with Hermes agent instances.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 22+ and npm
|
||||
- A running Hermes agent instance with API accessible at `http://localhost:8000`
|
||||
- (Optional) Docker if using containerized deployment
|
||||
|
||||
### Installation
|
||||
|
||||
Run the setup script:
|
||||
|
||||
```bash
|
||||
./scripts/setup-provision-core.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
- Clone provision-core into `web/provision-core/`
|
||||
- Install npm dependencies
|
||||
- Build assets
|
||||
|
||||
### Running provision-core
|
||||
|
||||
```bash
|
||||
cd web/provision-core
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open **http://localhost:8000** in your browser.
|
||||
|
||||
### Verification
|
||||
|
||||
Once provision-core is running:
|
||||
|
||||
1. **Task board** should display current Hermes tasks (if any are active)
|
||||
2. **Tool launcher**: Execute a simple read-only tool (e.g., `date`) through the UI and verify output appears
|
||||
3. **Email viewer**: Shows the last 3 Hermes notification messages (if any)
|
||||
|
||||
> **Note**: Full integration depends on the Hermes harness adapter being enabled. See "Hermes Adapter" below.
|
||||
|
||||
## Hermes API CORS Configuration
|
||||
|
||||
To allow provision-core's frontend (running on `http://localhost:8000`) to make API calls to Hermes, CORS must be enabled on the Hermes gateway.
|
||||
|
||||
Edit your Hermes configuration (`~/.hermes/config.yaml` or gateway config) and add:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
cors:
|
||||
enabled: true
|
||||
allowed_origins:
|
||||
- http://localhost:8000
|
||||
- http://127.0.0.1:8000
|
||||
allowed_methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
allowed_headers:
|
||||
- Authorization
|
||||
- Content-Type
|
||||
```
|
||||
|
||||
Then restart the Hermes gateway:
|
||||
|
||||
```bash
|
||||
# If using systemd
|
||||
sudo systemctl restart timmy-agent
|
||||
|
||||
# Or restart manually
|
||||
pkill -f "gateway.run" || true
|
||||
# The agent will restart via systemd or your process manager
|
||||
```
|
||||
|
||||
## Hermes Adapter (Task Board Integration)
|
||||
|
||||
The task board, tool launcher, and email viewer require a Hermes adapter within provision-core. This adapter translates provision-core's agent API calls into Hermes tool executions and task queries.
|
||||
|
||||
**Status**: Adapter implementation pending. See [#974] for tracking the Hermes harness plugin.
|
||||
|
||||
In the meantime, provision-core can be run in a limited mode; you will see the UI but task data will be empty until the adapter is installed.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### CORS errors in browser console
|
||||
|
||||
If you see errors like `Access to fetch at 'http://localhost:8642' from origin 'http://localhost:8000' has been blocked`:
|
||||
|
||||
1. Verify the CORS section above is in your Hermes config
|
||||
2. Confirm the Hermes gateway has restarted
|
||||
3. Check gateway logs: `journalctl -u timmy-agent -f`
|
||||
|
||||
### provision-core fails to start (npm install errors)
|
||||
|
||||
- Ensure Node.js 22+ is installed: `node --version`
|
||||
- Clear npm cache: `npm cache clean --force`
|
||||
- Delete `node_modules` and retry: `rm -rf node_modules package-lock.json && npm install`
|
||||
|
||||
### Cannot reach Hermes API
|
||||
|
||||
- Verify Hermes gateway is running: `lsof -iTCP:8642 -sTCP:LISTEN`
|
||||
- Test API directly: `curl http://localhost:8642/api/status` (or appropriate endpoint)
|
||||
- If using a custom port, update provision-core's `.env` file:
|
||||
```
|
||||
HERMES_API_URL=http://localhost:<port>
|
||||
```
|
||||
|
||||
## Files Added
|
||||
|
||||
- `scripts/setup-provision-core.sh` — Automated setup script
|
||||
- `docs/integration/provision-core.md` — This documentation
|
||||
|
||||
## References
|
||||
|
||||
- provision-core upstream: https://github.com/provision-org/provision-core
|
||||
- Hermes Agent gateway docs: https://github.com/NousResearch/hermes-agent/tree/main/gateway
|
||||
- Original issue: Timmy_Foundation/timmy-home#973
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# provision-core integration setup script for timmy-home
|
||||
# This script clones and configures provision-core to work with Hermes
|
||||
|
||||
# Resolve the script's directory
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
SCRIPT_DIR="$(cd "$SCRIPT_DIR" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
PROVISION_DIR="${REPO_ROOT}/web/provision-core"
|
||||
|
||||
echo "=== provision-core Setup ==="
|
||||
echo "Target directory: $PROVISION_DIR"
|
||||
|
||||
# Clone provision-core if not already present
|
||||
if [ ! -d "$PROVISION_DIR/.git" ]; then
|
||||
echo "Cloning provision-core..."
|
||||
git clone https://github.com/provision-org/provision-core.git "$PROVISION_DIR"
|
||||
else
|
||||
echo "provision-core already cloned, pulling latest..."
|
||||
(cd "$PROVISION_DIR" && git pull origin main)
|
||||
fi
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing npm dependencies..."
|
||||
cd "$PROVISION_DIR"
|
||||
npm install
|
||||
|
||||
# Build assets
|
||||
echo "Building assets..."
|
||||
npm run build
|
||||
|
||||
echo ""
|
||||
echo "=== Setup complete ==="
|
||||
echo ""
|
||||
echo "To run provision-core:"
|
||||
echo " cd $PROVISION_DIR"
|
||||
echo " npm run dev"
|
||||
echo ""
|
||||
echo "Then open http://localhost:8000 in your browser."
|
||||
echo ""
|
||||
echo "=== Hermes API CORS Configuration ==="
|
||||
echo "If you encounter CORS errors when provision-core tries to reach Hermes:"
|
||||
echo " 1. Locate your Hermes gateway configuration (~/.hermes/config.yaml or gateway config)"
|
||||
echo " 2. Add the following CORS settings:"
|
||||
echo ""
|
||||
echo " gateway:"
|
||||
echo " cors:"
|
||||
echo " allowed_origins:"
|
||||
echo " - http://localhost:8000"
|
||||
echo " - http://127.0.0.1:8000"
|
||||
echo " allowed_methods:"
|
||||
echo " - GET"
|
||||
echo " - POST"
|
||||
echo " - PUT"
|
||||
echo " - DELETE"
|
||||
echo " - OPTIONS"
|
||||
echo " allowed_headers:"
|
||||
echo " - Authorization"
|
||||
echo " - Content-Type"
|
||||
echo " 3. Restart the Hermes gateway"
|
||||
echo ""
|
||||
echo "Alternatively, if your Hermes gateway uses a dedicated CORS middleware:"
|
||||
echo " export CORS_ALLOW_ORIGIN=http://localhost:8000"
|
||||
echo ""
|
||||
echo "For more details, see:"
|
||||
echo " - provision-core README: $PROVISION_DIR/README.md"
|
||||
echo " - Hermes config: ~/.hermes/config.yaml"
|
||||
@@ -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