factdb

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package ftest is a probe harness for measuring how well the local LLM extracts storable facts from everyday conversation. It is deliberately separate from the live assistant: the point is to see, on a curated set of conversations, what the model pulls out (entities, temporal facts, placeholders for unknown people) and whether it flags the high-value gaps — names and familial relationships — for an immediate follow-up.

The data model mirrors the fact-database design in docs/fact-database-research.md: entities carry a resolution status (resolved vs. NIL placeholder), statements are reified facts with bi-temporal validity and provenance, and follow-ups are the knowledge gaps the model wants filled, each with a priority (immediate | banked).

Extraction emits structured JSON (this package's types), NOT AGE Cypher, so the model's job is pure semantics; a deterministic compiler (compile.go) turns the JSON into openCypher for Apache AGE. That split keeps "did it understand the facts" measurable independently of "did it emit valid Cypher".

Index

Constants

View Source
const (
	PriorityImmediate = "immediate"
	PriorityBanked    = "banked"
)

Priority values.

View Source
const (
	ResolutionResolved    = "resolved"
	ResolutionPlaceholder = "nil_placeholder"
)

Resolution values.

Variables

This section is empty.

Functions

func Compile

func Compile(r *Result) string

Compile turns an extraction Result into deterministic openCypher for Apache AGE.

The output is the inner Cypher (the body you'd wrap in `SELECT * FROM cypher('graph', $$ ... $$)`), one statement per line, so it is easy to eyeball and to golden-test. Design choices, all from docs/fact-database-research.md §7:

  • MERGE (never CREATE) on a stable key `k`, so replaying a conversation is idempotent and never duplicates a person node.
  • Facts are reified as Statement nodes carrying provenance/confidence/temporal properties, wired subject-[:SUBJECT_OF]->stmt-[:ABOUT]->object.
  • Deterministic ordering (entities by handle, statements by key) and escaped string literals, so the output is reproducible and diff-friendly.

Follow-ups are emitted as trailing comments, not graph writes: they are the caller's question queue, not facts about the world.

func SaveFixture

func SaveFixture(path string, f *Fixture) error

SaveFixture writes a fixture as pretty JSON (stable, diff-friendly on disk).

Types

type Entity

type Entity struct {
	Handle     string            `json:"handle"`          // "e1"
	Type       string            `json:"type"`            // Person | Group | Event | Activity
	Mention    string            `json:"mention"`         // "my husband"
	Resolution string            `json:"resolution"`      // "resolved" | "nil_placeholder"
	Name       string            `json:"name,omitempty"`  // known canonical name, if any
	Attrs      map[string]string `json:"attrs,omitempty"` // e.g. {"rel":"spouse_of:user"}
}

Entity is a person/group/event/activity mentioned in the conversation. Handle is a conversation-local id ("e1", "e2") the model assigns and reuses across turns; the compiler maps handles to stable MERGE keys so the model never invents graph ids. Resolution records whether the mention was pinned to a known entity or left as a placeholder to be filled by a later answer.

func KnownEntities

func KnownEntities(turns []Turn) []Entity

KnownEntities flattens the entities recorded across prior turns, so the next turn's prompt can tell the model which handles already exist to reuse.

type Extractor

type Extractor interface {
	Extract(ctx context.Context, priorTurns []Turn, known []Entity, utterance string) (*Result, error)
}

Extractor produces a structured Result for one user turn, given the prior turns and the entity handles already known, so the caller can extract incrementally.

type Fixture

type Fixture struct {
	Name  string  `json:"name"`
	Notes string  `json:"notes,omitempty"`
	Model string  `json:"model,omitempty"` // model that produced the Recorded fields
	Turns []Turn  `json:"turns"`
	Gold  *Result `json:"gold,omitempty"` // hand-authored expected consolidated facts
}

Fixture is one recorded conversation used as an extraction test case. `record` mode produces it live; `run` mode replays Turns through the extractor and, when Gold is present, grades the consolidated result against it.

func LoadFixture

func LoadFixture(path string) (*Fixture, error)

LoadFixture reads a fixture JSON file.

type Followup

type Followup struct {
	Gap      string `json:"gap"`      // "e1.name"
	Priority string `json:"priority"` // "immediate" | "banked"
	Reason   string `json:"reason,omitempty"`
	Question string `json:"question"`
}

Followup is a knowledge gap the model wants filled. Priority is the crux of the probe: "immediate" means the caller should ask this turn (high-value — names, familial relationships), "banked" means queue it for later so she isn't an interrogation.

type LLMExtractor

type LLMExtractor struct {
	URL       string // .../v1/chat/completions
	Model     string
	MaxTokens int
	Client    *http.Client
	Ontology  Ontology
}

LLMExtractor calls an OpenAI-compatible /v1/chat/completions endpoint. It is bound to an Ontology, which supplies the system prompt and the schema that constrain generation. Point URL/Model at whatever OpenAI-format server you run; a smaller "fast" model and a larger reasoning model can be compared by swapping them.

func NewLLMExtractor

func NewLLMExtractor(ont Ontology) *LLMExtractor

NewLLMExtractor builds an extractor for the given ontology against an OpenAI-format endpoint. Honors AATOOLKIT_FTEST_URL / AATOOLKIT_FTEST_MODEL overrides (e.g. to point at a different model on the same server); the defaults assume a local mlx-serve on port 1234 that speaks OpenAI format directly.

func (*LLMExtractor) Extract

func (x *LLMExtractor) Extract(ctx context.Context, priorTurns []Turn, known []Entity, utterance string) (*Result, error)

Extract calls the model and parses its JSON reply into a Result.

type Ontology

type Ontology struct {
	Predicates   []string `json:"predicates"`
	EntityTypes  []string `json:"entity_types"`
	SystemPrompt string   `json:"system_prompt"`
}

Ontology is the domain-specific extraction contract a caller supplies to the harness: the controlled predicate vocabulary, the entity-type enum, and the system prompt that instructs the model. Constraining the model to these values (via the response_format JSON schema AND the prompt) is the fix for vocabulary drift — a model understands the facts but invents predicate names ("schedule", "attends") instead of the graph's canonical ones unless it is pinned. Keep the predicate list and the gold fixtures in sync: a fact can only be graded if both sides name the predicate the same way.

func LoadOntology

func LoadOntology(path string) (Ontology, error)

LoadOntology reads an Ontology from a JSON file (predicates, entity_types, system_prompt) and validates that none of the three is empty.

func (Ontology) BuildMessages

func (o Ontology) BuildMessages(priorTurns []Turn, known []Entity, utterance string) ([]byte, error)

BuildMessages assembles the OpenAI-style messages array for one extraction call: the ontology's system prompt (with its predicate vocabulary appended), then a user message carrying prior turns as context, the known entity handles to reuse, and the current utterance to extract. The system prompt must instruct the model to emit ONLY the JSON this package parses (entities/statements/followups).

func (Ontology) ResultSchema

func (o Ontology) ResultSchema() map[string]any

ResultSchema returns the JSON Schema for a Result, with the key fields locked to enums: predicate to the ontology's controlled vocabulary, entity type, resolution, and follow-up priority. Emitted as response_format.json_schema so the model's output is constrained at generation time rather than fuzzy-matched afterward.

type Report

type Report struct {
	StmtGold      int // statements in gold
	StmtFound     int // gold statements the model produced (recall numerator)
	StmtExtra     int // model statements with no gold match (over-extraction)
	ImmediateGold int // gold follow-ups marked immediate
	ImmediateHit  int // gold immediate follow-ups the model also marked immediate
	Missed        []string
	Extra         []string
}

Report scores a consolidated extraction against a gold Result. It is deliberately forgiving on surface form and strict on the things the probe cares about: did the model find the facts (statements), and did it flag the right high-value gaps for an immediate follow-up (§6.1)?

func Grade

func Grade(got, gold *Result) Report

Grade compares a consolidated model result to gold.

func (Report) String

func (r Report) String() string

String renders a one-block human summary.

type Result

type Result struct {
	Entities   []Entity    `json:"entities"`
	Statements []Statement `json:"statements"`
	Followups  []Followup  `json:"followups"`
}

Result is the structured extraction for one turn — or, after Consolidate, for a whole conversation. It is exactly what the model is asked to emit as JSON.

func Consolidate

func Consolidate(results []*Result) *Result

Consolidate merges a sequence of per-turn results into one conversation-level Result — the view grading compares against gold, and the view the compiler turns into a graph. Entities union by handle (later mentions win on name/resolution and merge attrs, modeling a placeholder being filled by a later turn). Statements dedup by key. Follow-ups dedup by gap, keeping the strongest priority and dropping any gap that a later turn resolved into a named entity.

func ParseResult

func ParseResult(content string) (*Result, error)

ParseResult extracts the JSON object from a model reply and unmarshals it into a Result. It tolerates code fences and surrounding prose by slicing from the first '{' to the last '}' — small models often wrap JSON in chatter despite instructions.

type Statement

type Statement struct {
	Subject    string  `json:"subject"`               // entity handle
	Predicate  string  `json:"predicate"`             // "birthday", "has_activity", "prefers_pronoun"...
	Value      string  `json:"value,omitempty"`       // literal object
	Object     string  `json:"object,omitempty"`      // entity-handle object (relational fact)
	ValidFrom  string  `json:"valid_from,omitempty"`  // ISO-8601 or ""; "" = unknown/open
	ValidUntil string  `json:"valid_until,omitempty"` // when the fact expires (e.g. Christmas)
	Recurrence string  `json:"recurrence,omitempty"`  // iCalendar RRULE, e.g. "FREQ=WEEKLY;BYDAY=MO"
	Confidence float64 `json:"confidence,omitempty"`
	SourceSpan string  `json:"source_span,omitempty"`
}

Statement is a reified fact: subject --predicate--> (value | object), with optional bi-temporal validity, recurrence, confidence, and the source text span it came from. Object names an entity handle when the object is itself an entity (n-ary/relational facts); Value holds a literal when it is not.

type Turn

type Turn struct {
	Speaker  string  `json:"speaker"` // "user" | "assistant"
	Text     string  `json:"text"`
	Recorded *Result `json:"recorded,omitempty"`
}

Turn is one utterance. For user turns, Recorded holds the extraction the model produced at record time, given all prior turns as context (incremental). Assistant turns carry no extraction — they exist so the model sees realistic dialogue context.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL