Compare commits
2 Commits
fix/882
...
step35/969
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae2dd15c4e | ||
| 1fa6c3bad1 |
20
SOUL.md
20
SOUL.md
@@ -137,6 +137,26 @@ 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:
|
||||
|
||||
@@ -427,6 +427,20 @@ 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",
|
||||
@@ -502,6 +516,13 @@ 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]
|
||||
@@ -1091,14 +1112,16 @@ 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, "tend_fire": 3, "write_rule": 2, "carve": 2,
|
||||
"move": 2, "run": 3, "jump": 1, "gallop": 4, "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,
|
||||
}
|
||||
@@ -1159,6 +1182,10 @@ 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
|
||||
|
||||
@@ -1169,7 +1196,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"{', '.join(here)} {'are' if len(here)>1 else 'is'} already here.")
|
||||
scene["log"].append(f"{repr(', ').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)
|
||||
@@ -1214,6 +1241,55 @@ 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,65 +0,0 @@
|
||||
# MATH-006: Independent Math Review Gate
|
||||
*Prevents Timmy from publicly claiming mathematical novelty before human/formal verification.*
|
||||
|
||||
## Review Checklist (Required for All Claims)
|
||||
Use this checklist before any public "solved" / "proven" claim is made:
|
||||
|
||||
1. **Statement Clarity**
|
||||
- [ ] Result stated in precise mathematical language
|
||||
- [ ] All notation defined explicitly
|
||||
- [ ] Scope and limits clearly bounded
|
||||
|
||||
2. **Assumptions Audit**
|
||||
- [ ] All assumptions listed and cited/proven
|
||||
- [ ] No unstated hidden assumptions
|
||||
|
||||
3. **Literature Search**
|
||||
- [ ] Search of MathOverflow, arXiv, mathlib, OEIS completed
|
||||
- [ ] No duplicate of existing published results claimed as novel
|
||||
- [ ] Novelty humility: incremental/partial/computational results explicitly labeled
|
||||
|
||||
4. **Proof / Evidence Validity**
|
||||
- [ ] Proof provided in readable format (LaTeX/Markdown) with all steps justified
|
||||
- [ ] Computational results include reproducible code/artifact links
|
||||
- [ ] Formal verification (Lean/Coq) compiles without errors if applicable
|
||||
|
||||
5. **Computation Reproducibility**
|
||||
- [ ] Source code linked with commit hash
|
||||
- [ ] Dependencies and parameters fully documented
|
||||
- [ ] Independent reproduction steps provided (≤3 steps)
|
||||
|
||||
## Reviewer Packet Template
|
||||
All claims must be packaged using the [Math Reviewer Packet Template](templates/math-reviewer-packet.md) before submission to any review channel.
|
||||
|
||||
## Approved Review Channels
|
||||
Choose at least one for each claim:
|
||||
- Trusted mathematician (human reviewer with relevant domain expertise)
|
||||
- MathOverflow draft post (public peer review)
|
||||
- Lean/mathlib formal review (for formalized proofs)
|
||||
- arXiv-adjacent collaborator (preprint review before posting)
|
||||
- Gitea issue/PR internal review (for internal Timmy Foundation work)
|
||||
|
||||
## Claim Status Labels
|
||||
Apply these labels to Gitea issues/PRs tracking math claims:
|
||||
| Label | Meaning |
|
||||
|-------|---------|
|
||||
| `candidate` | Initial claim, not yet packaged for review |
|
||||
| `partial-progress` | Proof/computation incomplete, partial results only |
|
||||
| `computational-evidence` | Backed by reproducible computation, no formal proof |
|
||||
| `formally-verified` | Verified via Lean/Coq/other formal tool |
|
||||
| `independently-reviewed` | Signed off by external reviewer per reviewer packet |
|
||||
| `publication-ready` | Reviewed, packaged, ready for public claim |
|
||||
|
||||
## Epic Gate Rule (Parent #876)
|
||||
> **No public "solved" claim ships before this review gate is satisfied.**
|
||||
> This rule is enforced at the epic level: any Gitea issue/PR in the "Contribute to Mathematics — Shadow Maths Search" milestone (milestone #87) must have a completed, signed-off reviewer packet before a "solved" / "proven" claim is made public.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [x] Reviewer packet template exists at `specs/templates/math-reviewer-packet.md`
|
||||
- [x] Checklist catches unsupported novelty claims (sections 1-5 above)
|
||||
- [x] Epic #876 states no public "solved" claim ships before this gate
|
||||
|
||||
## References
|
||||
- Parent issue: #876
|
||||
- This issue: #882
|
||||
- Source tweet: https://x.com/rockachopa/status/2048170592759652597
|
||||
@@ -1,60 +0,0 @@
|
||||
# Math Reviewer Packet Template
|
||||
*Use this template to package any claimed mathematical result for independent review before public "solved" claims are made.*
|
||||
|
||||
## 1. Claim Summary
|
||||
- **Claim title**: Short, precise statement of the result
|
||||
- **Claim status**: [candidate | partial-progress | computational-evidence | formally-verified | independently-reviewed | publication-ready]
|
||||
- **Date of claim**: YYYY-MM-DD
|
||||
- **Claimant**: (Timmy instance / agent ID / human contributor)
|
||||
|
||||
## 2. Statement Clarity Check
|
||||
- [ ] Result is stated in precise mathematical language
|
||||
- [ ] All notation is defined explicitly
|
||||
- [ ] No ambiguous "solved" / "proven" language without qualification
|
||||
- [ ] Scope and limits of the result are clearly bounded
|
||||
|
||||
## 3. Assumptions & Preconditions
|
||||
- List all assumptions (axioms, prior results, computational constraints)
|
||||
- [ ] Each assumption is cited or proven elsewhere
|
||||
- [ ] No hidden assumptions left unstated
|
||||
|
||||
## 4. Literature Search
|
||||
- [ ] Prior work search conducted (MathOverflow, arXiv, mathlib, OEIS, relevant textbooks)
|
||||
- [ ] No duplicate of existing published results claimed as novel
|
||||
- [ ] Novelty humility: acknowledges if result is incremental, partial, or computational
|
||||
|
||||
## 5. Proof / Evidence Validity
|
||||
### For Proof-Based Results
|
||||
- [ ] Full proof provided in machine-readable format (LaTeX / Markdown)
|
||||
- [ ] Each step is logically justified
|
||||
- [ ] No gaps longer than 2 sentences without explicit citation or lemma
|
||||
|
||||
### For Computational Results
|
||||
- [ ] Code/artifact link provided (reproducible environment)
|
||||
- [ ] Random seeds / parameters fully documented
|
||||
- [ ] Output verified by independent script (if applicable)
|
||||
|
||||
### For Formal Verification
|
||||
- [ ] Lean / Coq / other formal proof assistant file linked
|
||||
- [ ] Compiles without errors on standard toolchain
|
||||
|
||||
## 6. Reproducibility Package
|
||||
- [ ] All source code used is linked (repo commit hash / Gitea issue/PR reference)
|
||||
- [ ] Dependencies listed with versions
|
||||
- [ ] Minimal reproduction steps provided (3 steps or fewer)
|
||||
|
||||
## 7. Review Channel & Sign-off
|
||||
- **Selected review channel**: (trusted mathematician / MathOverflow draft / Lean/mathlib review / arXiv-adjacent collaborator / other)
|
||||
- **Reviewer identity**: (handle / name / affiliation)
|
||||
- **Review date**: YYYY-MM-DD
|
||||
- **Review outcome**: [APPROVED | REVISION REQUIRED | REJECTED]
|
||||
- **Reviewer notes**: (free text)
|
||||
|
||||
## 8. Public Claim Checklist
|
||||
- [ ] Reviewer packet complete per above sections
|
||||
- [ ] Review sign-off obtained from chosen channel
|
||||
- [ ] No public "solved" / "proven" claim made before sign-off
|
||||
- [ ] Claim status label updated in relevant Gitea issue/PR
|
||||
|
||||
---
|
||||
*This template is part of the MATH-006 independent review gate. No public novelty claim ships without a completed, signed-off packet.*
|
||||
@@ -1 +1,12 @@
|
||||
# Timmy core module
|
||||
|
||||
from .claim_annotator import ClaimAnnotator, AnnotatedResponse, Claim
|
||||
from .audit_trail import AuditTrail, AuditEntry
|
||||
|
||||
__all__ = [
|
||||
"ClaimAnnotator",
|
||||
"AnnotatedResponse",
|
||||
"Claim",
|
||||
"AuditTrail",
|
||||
"AuditEntry",
|
||||
]
|
||||
|
||||
156
src/timmy/claim_annotator.py
Normal file
156
src/timmy/claim_annotator.py
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/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,
|
||||
)
|
||||
103
tests/timmy/test_claim_annotator.py
Normal file
103
tests/timmy/test_claim_annotator.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/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