An intention's supporting beliefs came from a text search on the target, and ties on rank and confidence had no order, so a verification miss could revise an unrelated belief about the same service. Beliefs that mention the proposed action now come first, and belief search breaks ties by id. The incident scenario asserts the first support is the belief about restarting, and waits for the blocked intention and the procedural memory instead of reading them once.
1059 lines
41 KiB
Go
1059 lines
41 KiB
Go
package beliefs
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/andydixon/noema/internal/storage"
|
|
"github.com/andydixon/noema/pkg/concepts"
|
|
"github.com/andydixon/noema/pkg/events"
|
|
"github.com/andydixon/noema/pkg/ids"
|
|
)
|
|
|
|
// Belief is a proposition with derived confidence and status.
|
|
type Belief struct {
|
|
ID ids.ID `json:"id"`
|
|
MindID ids.ID `json:"mind_id"`
|
|
Proposition string `json:"proposition"`
|
|
Subject string `json:"subject"`
|
|
Epistemic concepts.Epistemic `json:"epistemic"`
|
|
Status Status `json:"status"`
|
|
Confidence float64 `json:"confidence"`
|
|
Prior float64 `json:"prior"`
|
|
Version int `json:"version"`
|
|
Source string `json:"source"`
|
|
Detail map[string]any `json:"detail"`
|
|
OriginEvent ids.ID `json:"origin_event,omitempty"`
|
|
SupersededBy ids.ID `json:"superseded_by,omitempty"`
|
|
PinnedStatus *Status `json:"pinned_status,omitempty"`
|
|
FirstCreated time.Time `json:"first_created"`
|
|
LastEvaluated time.Time `json:"last_evaluated"`
|
|
Evaluations int `json:"evaluations"`
|
|
// Temporal scope and validity (see temporal.go). ValidUntil is set when a
|
|
// later observation superseded a transient state; the belief then says
|
|
// what was true, and when.
|
|
TemporalScope string `json:"temporal_scope"`
|
|
ObservedAt *time.Time `json:"observed_at,omitempty"`
|
|
ValidFrom *time.Time `json:"valid_from,omitempty"`
|
|
ValidUntil *time.Time `json:"valid_until,omitempty"`
|
|
RedactedAt *time.Time `json:"redacted_at,omitempty"`
|
|
}
|
|
|
|
// EvidenceRow is stored evidence.
|
|
type EvidenceRow struct {
|
|
ID ids.ID `json:"id"`
|
|
Evidence `json:",inline"`
|
|
Note string `json:"note"`
|
|
EventID ids.ID `json:"event_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// Alternative is a competing explanation kept alongside a belief.
|
|
type Alternative struct {
|
|
ID ids.ID `json:"id"`
|
|
Proposition string `json:"proposition"`
|
|
Confidence float64 `json:"confidence"`
|
|
Note string `json:"note"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// Version is one entry in the append-only history.
|
|
type Version struct {
|
|
Version int `json:"version"`
|
|
Status Status `json:"status"`
|
|
Confidence float64 `json:"confidence"`
|
|
Proposition string `json:"proposition"`
|
|
Reason string `json:"reason"`
|
|
Breakdown Breakdown `json:"breakdown"`
|
|
EventID ids.ID `json:"event_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// Link relates two beliefs.
|
|
type Link struct {
|
|
ID ids.ID `json:"id"`
|
|
FromID ids.ID `json:"from_id"`
|
|
ToID ids.ID `json:"to_id"`
|
|
Relation string `json:"relation"`
|
|
Confidence float64 `json:"confidence"`
|
|
Other *Belief `json:"other,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// Correction is an operator statement about the mind's content.
|
|
type Correction struct {
|
|
ID ids.ID `json:"id"`
|
|
Kind string `json:"kind"`
|
|
TargetKind string `json:"target_kind"`
|
|
TargetID string `json:"target_id"`
|
|
Statement string `json:"statement"`
|
|
Reason string `json:"reason"`
|
|
CreatedBy string `json:"created_by"`
|
|
EventID ids.ID `json:"event_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// ErrInvalid marks bad input; ErrBlocked means a do-not-infer correction applies.
|
|
var (
|
|
ErrInvalid = errors.New("invalid belief")
|
|
ErrBlocked = errors.New("blocked by operator correction")
|
|
)
|
|
|
|
// Emitter appends events inside the caller's transaction and returns them.
|
|
// The brain cycle and the API each provide one so provenance events are
|
|
// written atomically with belief rows.
|
|
type Emitter func(ctx context.Context, q storage.Querier, n events.New) (events.Event, error)
|
|
|
|
// Store persists beliefs.
|
|
type Store struct {
|
|
db *pgxpool.Pool
|
|
}
|
|
|
|
// NewStore creates a store.
|
|
func NewStore(db *pgxpool.Pool) *Store { return &Store{db: db} }
|
|
|
|
// New describes a belief to create.
|
|
type New struct {
|
|
MindID ids.ID
|
|
Proposition string
|
|
Subject string
|
|
Epistemic concepts.Epistemic
|
|
Source string
|
|
Evidence []EvidenceInput
|
|
Alternatives []string
|
|
Detail map[string]any
|
|
OriginEvent ids.ID
|
|
Correlation ids.ID
|
|
Causation ids.ID
|
|
// ObservedAt is when the state described was observed (defaults to now);
|
|
// TemporalScope overrides the deterministic classifier when the producer knows.
|
|
ObservedAt *time.Time
|
|
TemporalScope string
|
|
}
|
|
|
|
// EvidenceInput is evidence supplied by callers.
|
|
type EvidenceInput struct {
|
|
Direction string
|
|
Kind string
|
|
RefID string
|
|
Source string
|
|
Weight float64
|
|
Credibility float64
|
|
Note string
|
|
// Provenance (see Evidence). Family defaults to Source; Derivation
|
|
// defaults to direct. Producers that know their roots must say so.
|
|
Derivation string
|
|
Family string
|
|
RootIDs []string
|
|
}
|
|
|
|
// Result is a belief with its computed breakdown and the events written.
|
|
type Result struct {
|
|
Belief Belief
|
|
Breakdown Breakdown
|
|
Events []events.Event
|
|
}
|
|
|
|
// Create stores a belief, its evidence, alternatives and first version, emits
|
|
// belief_created/hypothesis_created, and runs contradiction detection.
|
|
func (s *Store) Create(ctx context.Context, q storage.Querier, emit Emitter, n New) (Result, error) {
|
|
n.Proposition = strings.TrimSpace(n.Proposition)
|
|
if n.MindID.IsZero() || n.Proposition == "" || len(n.Proposition) > 2000 {
|
|
return Result{}, fmt.Errorf("%w: proposition required (max 2000 chars)", ErrInvalid)
|
|
}
|
|
if !n.Epistemic.Valid() {
|
|
n.Epistemic = concepts.Hypothesis
|
|
}
|
|
if blocked, c := s.blocked(ctx, q, n.MindID, n.Proposition); blocked {
|
|
return Result{}, fmt.Errorf("%w: %s", ErrBlocked, c.Statement)
|
|
}
|
|
var ev []Evidence
|
|
var notes []string
|
|
for _, e := range n.Evidence {
|
|
ne := normalise(e)
|
|
ne, note := s.applyReliability(ctx, q, n.MindID, strings.TrimSpace(n.Subject), ne, e.Note)
|
|
ev = append(ev, ne)
|
|
notes = append(notes, note)
|
|
}
|
|
prior := PriorFor(n.Epistemic)
|
|
bd := Compute(prior, ev)
|
|
st := Derive(bd, n.Epistemic)
|
|
observed := time.Now()
|
|
if n.ObservedAt != nil {
|
|
observed = *n.ObservedAt
|
|
}
|
|
scope := n.TemporalScope
|
|
if scope == "" {
|
|
scope = ClassifyScope(n.Proposition)
|
|
}
|
|
b := Belief{ID: ids.New(), MindID: n.MindID, Proposition: n.Proposition, Subject: strings.TrimSpace(n.Subject), Epistemic: n.Epistemic, Status: st, Confidence: bd.Confidence,
|
|
Prior: prior, Version: 1, Source: n.Source, Detail: n.Detail, OriginEvent: n.OriginEvent, FirstCreated: time.Now(), LastEvaluated: time.Now(), Evaluations: 1,
|
|
TemporalScope: scope, ObservedAt: &observed}
|
|
if scope == ScopeTransient {
|
|
b.ValidFrom = &observed
|
|
}
|
|
if b.Detail == nil {
|
|
b.Detail = map[string]any{}
|
|
}
|
|
detail, _ := json.Marshal(b.Detail)
|
|
if _, err := q.Exec(ctx, `INSERT INTO beliefs (id, mind_id, proposition, subject, epistemic, status, confidence, prior, version, source, detail, origin_event, temporal_scope, observed_at, valid_from)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,1,$9,$10,$11,$12,$13,$14)`, b.ID, b.MindID, b.Proposition, b.Subject, string(b.Epistemic), string(b.Status), b.Confidence, b.Prior, b.Source, detail, nullID(b.OriginEvent), b.TemporalScope, b.ObservedAt, b.ValidFrom); err != nil {
|
|
return Result{}, err
|
|
}
|
|
typ := events.BeliefCreated
|
|
if n.Epistemic == concepts.Hypothesis {
|
|
typ = events.HypothesisCreated
|
|
}
|
|
refs := []events.EvidenceRef{{Kind: "belief", ID: string(b.ID)}}
|
|
for _, e := range n.Evidence {
|
|
if e.RefID != "" {
|
|
refs = append(refs, events.EvidenceRef{Kind: e.Kind, ID: e.RefID, Note: e.Direction + ": " + e.Note})
|
|
}
|
|
}
|
|
created, err := emit(ctx, q, events.New{MindID: b.MindID, Type: typ, Source: n.Source, Summary: fmt.Sprintf("%s (%.2f, %s): %s", typ, b.Confidence, b.Status, b.Proposition),
|
|
Salience: 0.5 + 0.3*b.Confidence, Confidence: b.Confidence, CausationID: n.Causation, CorrelationID: n.Correlation, Evidence: refs,
|
|
Metadata: map[string]any{"belief_id": string(b.ID), "status": b.Status, "subject": b.Subject}, Payload: map[string]any{"belief_id": b.ID, "proposition": b.Proposition, "status": b.Status, "confidence": b.Confidence, "breakdown": bd, "alternatives": n.Alternatives}})
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
res := Result{Belief: b, Breakdown: bd, Events: []events.Event{created}}
|
|
for i, e := range ev {
|
|
if err := s.insertEvidence(ctx, q, b, e, notes[i], created.ID); err != nil {
|
|
return Result{}, err
|
|
}
|
|
}
|
|
for _, alt := range n.Alternatives {
|
|
if alt = strings.TrimSpace(alt); alt != "" {
|
|
if _, err := q.Exec(ctx, `INSERT INTO belief_alternatives (id, belief_id, mind_id, proposition, confidence) VALUES ($1,$2,$3,$4,$5)`, ids.New(), b.ID, b.MindID, alt, 0.2); err != nil {
|
|
return Result{}, err
|
|
}
|
|
}
|
|
}
|
|
if err := s.insertVersion(ctx, q, b, bd, "created", created.ID); err != nil {
|
|
return Result{}, err
|
|
}
|
|
contra, err := s.detectContradictions(ctx, q, emit, b, created.ID)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
res.Events = append(res.Events, contra...)
|
|
return res, nil
|
|
}
|
|
|
|
func normalise(e EvidenceInput) Evidence {
|
|
if e.Direction != "against" {
|
|
e.Direction = "for"
|
|
}
|
|
if e.Weight <= 0 || e.Weight > 1 {
|
|
e.Weight = 0.5
|
|
}
|
|
if e.Credibility <= 0 || e.Credibility > 1 {
|
|
e.Credibility = 0.5
|
|
}
|
|
if e.Kind == "" {
|
|
e.Kind = "note"
|
|
}
|
|
if e.Derivation == "" {
|
|
e.Derivation = DerivationDirect
|
|
}
|
|
if e.Family == "" {
|
|
e.Family = e.Source
|
|
}
|
|
return Evidence{Direction: e.Direction, Kind: e.Kind, RefID: e.RefID, Source: e.Source, Weight: e.Weight, Credibility: e.Credibility,
|
|
Derivation: e.Derivation, Family: e.Family, RootIDs: e.RootIDs, ProvenanceStatus: "recorded"}
|
|
}
|
|
|
|
func (s *Store) insertEvidence(ctx context.Context, q storage.Querier, b Belief, e Evidence, note string, eventID ids.ID) error {
|
|
if e.Derivation == "" {
|
|
e.Derivation = DerivationDirect
|
|
}
|
|
if e.Family == "" {
|
|
e.Family = FamilyOf(e)
|
|
}
|
|
roots := e.RootIDs
|
|
if roots == nil {
|
|
roots = []string{}
|
|
}
|
|
_, err := q.Exec(ctx, `INSERT INTO belief_evidence (id, belief_id, mind_id, direction, kind, ref_id, source, weight, credibility, note, event_id, derivation, family, root_ids, provenance_status) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::uuid[],'recorded')`,
|
|
ids.New(), b.ID, b.MindID, e.Direction, e.Kind, e.RefID, e.Source, e.Weight, e.Credibility, note, nullID(eventID), e.Derivation, e.Family, roots)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) insertVersion(ctx context.Context, q storage.Querier, b Belief, bd Breakdown, reason string, eventID ids.ID) error {
|
|
raw, _ := json.Marshal(bd)
|
|
_, err := q.Exec(ctx, `INSERT INTO belief_versions (id, belief_id, mind_id, version, status, confidence, proposition, reason, breakdown, event_id) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
|
|
ids.New(), b.ID, b.MindID, b.Version, string(b.Status), b.Confidence, b.Proposition, reason, raw, nullID(eventID))
|
|
return err
|
|
}
|
|
|
|
func nullID(id ids.ID) *string {
|
|
if id.IsZero() {
|
|
return nil
|
|
}
|
|
s := string(id)
|
|
return &s
|
|
}
|
|
|
|
const cols = `b.id, b.mind_id, b.proposition, b.subject, b.epistemic, b.status, b.confidence, b.prior, b.version, b.source, b.detail, b.origin_event, b.superseded_by, b.pinned_status, b.first_created, b.last_evaluated, b.evaluations, b.temporal_scope, b.observed_at, b.valid_from, b.valid_until, b.redacted_at`
|
|
|
|
func scan(row pgx.Row) (Belief, error) {
|
|
var b Belief
|
|
var epi, status string
|
|
var detail []byte
|
|
var origin, superseded, pinned *string
|
|
if err := row.Scan(&b.ID, &b.MindID, &b.Proposition, &b.Subject, &epi, &status, &b.Confidence, &b.Prior, &b.Version, &b.Source, &detail, &origin, &superseded, &pinned, &b.FirstCreated, &b.LastEvaluated, &b.Evaluations, &b.TemporalScope, &b.ObservedAt, &b.ValidFrom, &b.ValidUntil, &b.RedactedAt); err != nil {
|
|
return b, storage.Translate(err)
|
|
}
|
|
b.Epistemic, b.Status = concepts.Epistemic(epi), Status(status)
|
|
json.Unmarshal(detail, &b.Detail) //nolint:errcheck
|
|
if b.Detail == nil {
|
|
b.Detail = map[string]any{}
|
|
}
|
|
if origin != nil {
|
|
b.OriginEvent = ids.ID(*origin)
|
|
}
|
|
if superseded != nil {
|
|
b.SupersededBy = ids.ID(*superseded)
|
|
}
|
|
if pinned != nil {
|
|
st := Status(*pinned)
|
|
b.PinnedStatus = &st
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// Get fetches a belief scoped to a mind. A nil q reads from the pool.
|
|
func (s *Store) Get(ctx context.Context, q storage.Querier, mind, id ids.ID) (Belief, error) {
|
|
if q == nil {
|
|
q = s.db
|
|
}
|
|
return scan(q.QueryRow(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.id=$1 AND b.mind_id=$2`, id, mind))
|
|
}
|
|
|
|
// Filter selects beliefs.
|
|
type Filter struct {
|
|
Statuses []Status
|
|
MinConfidence float64
|
|
MaxConfidence float64
|
|
Subject string
|
|
Search string
|
|
Limit int
|
|
}
|
|
|
|
// List returns beliefs by confidence descending.
|
|
func (s *Store) List(ctx context.Context, mind ids.ID, f Filter) ([]Belief, error) {
|
|
where := []string{"b.mind_id = $1"}
|
|
args := []any{mind}
|
|
arg := func(v any) string { args = append(args, v); return fmt.Sprintf("$%d", len(args)) }
|
|
if len(f.Statuses) > 0 {
|
|
ss := make([]string, len(f.Statuses))
|
|
for i, st := range f.Statuses {
|
|
ss[i] = string(st)
|
|
}
|
|
where = append(where, "b.status = ANY("+arg(ss)+")")
|
|
} else {
|
|
where = append(where, "b.status <> 'superseded'")
|
|
}
|
|
if f.MinConfidence > 0 {
|
|
where = append(where, "b.confidence >= "+arg(f.MinConfidence))
|
|
}
|
|
if f.MaxConfidence > 0 {
|
|
where = append(where, "b.confidence <= "+arg(f.MaxConfidence))
|
|
}
|
|
if f.Subject != "" {
|
|
where = append(where, "b.subject = "+arg(f.Subject))
|
|
}
|
|
if q := strings.TrimSpace(f.Search); q != "" {
|
|
where = append(where, "b.search @@ websearch_to_tsquery('english', "+arg(q)+")")
|
|
}
|
|
limit := f.Limit
|
|
if limit <= 0 || limit > 500 {
|
|
limit = 100
|
|
}
|
|
rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE `+strings.Join(where, " AND ")+` ORDER BY b.confidence DESC, b.last_evaluated DESC LIMIT `+arg(limit), args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Belief
|
|
for rows.Next() {
|
|
b, err := scan(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, b)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// EvidenceFor returns all evidence rows of a belief. A nil q reads from the pool.
|
|
func (s *Store) EvidenceFor(ctx context.Context, q storage.Querier, mind, id ids.ID) ([]EvidenceRow, error) {
|
|
if q == nil {
|
|
q = s.db
|
|
}
|
|
rows, err := q.Query(ctx, `SELECT id, direction, kind, ref_id, source, weight, credibility, retracted, note, event_id, created_at, derivation, family, root_ids::text[], provenance_status FROM belief_evidence WHERE belief_id=$1 AND mind_id=$2 ORDER BY created_at`, id, mind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []EvidenceRow
|
|
for rows.Next() {
|
|
var r EvidenceRow
|
|
var evID *string
|
|
if err := rows.Scan(&r.ID, &r.Direction, &r.Kind, &r.RefID, &r.Source, &r.Weight, &r.Credibility, &r.Retracted, &r.Note, &evID, &r.CreatedAt, &r.Derivation, &r.Family, &r.RootIDs, &r.ProvenanceStatus); err != nil {
|
|
return nil, err
|
|
}
|
|
if evID != nil {
|
|
r.EventID = ids.ID(*evID)
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// AddEvidence attaches evidence, recomputes confidence and status, appends a
|
|
// version when anything changed and emits the matching event.
|
|
func (s *Store) AddEvidence(ctx context.Context, q storage.Querier, emit Emitter, mind, id ids.ID, in EvidenceInput, reason string, causation, correlation ids.ID) (Result, error) {
|
|
b, err := s.Get(ctx, q, mind, id)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
if b.Status == StatusSuperseded {
|
|
return Result{}, fmt.Errorf("%w: belief is superseded", ErrInvalid)
|
|
}
|
|
e := normalise(in)
|
|
e, note := s.applyReliability(ctx, q, mind, b.Subject, e, in.Note)
|
|
if err := s.insertEvidence(ctx, q, b, e, note, ids.Nil); err != nil {
|
|
return Result{}, err
|
|
}
|
|
return s.reevaluate(ctx, q, emit, b, reason, causation, correlation, &e, in)
|
|
}
|
|
|
|
// Reevaluate recomputes a belief from its stored evidence (used after
|
|
// retractions and corrections).
|
|
func (s *Store) Reevaluate(ctx context.Context, q storage.Querier, emit Emitter, mind, id ids.ID, reason string) (Result, error) {
|
|
b, err := s.Get(ctx, q, mind, id)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
return s.reevaluate(ctx, q, emit, b, reason, ids.Nil, ids.Nil, nil, EvidenceInput{})
|
|
}
|
|
|
|
func (s *Store) reevaluate(ctx context.Context, q storage.Querier, emit Emitter, b Belief, reason string, causation, correlation ids.ID, added *Evidence, in EvidenceInput) (Result, error) {
|
|
rows, err := s.EvidenceFor(ctx, q, b.MindID, b.ID)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
ev := make([]Evidence, 0, len(rows))
|
|
for _, r := range rows {
|
|
ev = append(ev, r.Evidence)
|
|
}
|
|
bd := Compute(b.Prior, ev)
|
|
st := Derive(bd, b.Epistemic)
|
|
if b.PinnedStatus != nil {
|
|
st = *b.PinnedStatus
|
|
}
|
|
old := b
|
|
b.Confidence, b.Status, b.LastEvaluated, b.Evaluations = bd.Confidence, st, time.Now(), b.Evaluations+1
|
|
changed := old.Confidence != b.Confidence || old.Status != b.Status
|
|
res := Result{Belief: b, Breakdown: bd}
|
|
typ := events.BeliefUpdated
|
|
summary := fmt.Sprintf("belief re-evaluated (%.2f → %.2f, %s): %s", old.Confidence, b.Confidence, b.Status, b.Proposition)
|
|
if added != nil {
|
|
if added.Direction == "for" {
|
|
typ = events.HypothesisSupported
|
|
if b.Epistemic != concepts.Hypothesis {
|
|
typ = events.BeliefUpdated
|
|
}
|
|
summary = fmt.Sprintf("support added (%.2f → %.2f): %s", old.Confidence, b.Confidence, b.Proposition)
|
|
} else {
|
|
typ = events.HypothesisWeakened
|
|
if b.Epistemic != concepts.Hypothesis {
|
|
typ = events.BeliefChallenged
|
|
}
|
|
summary = fmt.Sprintf("counter-evidence added (%.2f → %.2f): %s", old.Confidence, b.Confidence, b.Proposition)
|
|
}
|
|
}
|
|
refs := []events.EvidenceRef{{Kind: "belief", ID: string(b.ID)}}
|
|
if added != nil && added.RefID != "" {
|
|
refs = append(refs, events.EvidenceRef{Kind: added.Kind, ID: added.RefID, Note: added.Direction + ": " + in.Note})
|
|
}
|
|
ev1, err := emit(ctx, q, events.New{MindID: b.MindID, Type: typ, Source: firstNonEmpty(in.Source, "beliefs"), Summary: summary, Salience: 0.3 + 0.4*absf(b.Confidence-old.Confidence)*4, Confidence: b.Confidence,
|
|
CausationID: causation, CorrelationID: correlation, Evidence: refs,
|
|
Metadata: map[string]any{"belief_id": string(b.ID), "status": b.Status, "from": old.Confidence, "to": b.Confidence},
|
|
Payload: map[string]any{"belief_id": b.ID, "proposition": b.Proposition, "from": map[string]any{"confidence": old.Confidence, "status": old.Status}, "to": map[string]any{"confidence": b.Confidence, "status": b.Status}, "breakdown": bd, "reason": reason}})
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
res.Events = append(res.Events, ev1)
|
|
if changed {
|
|
b.Version++
|
|
res.Belief = b
|
|
if err := s.insertVersion(ctx, q, b, bd, reason, ev1.ID); err != nil {
|
|
return Result{}, err
|
|
}
|
|
}
|
|
if _, err := q.Exec(ctx, `UPDATE beliefs SET confidence=$3, status=$4, version=$5, last_evaluated=now(), evaluations=evaluations+1 WHERE id=$1 AND mind_id=$2`, b.ID, b.MindID, b.Confidence, string(b.Status), b.Version); err != nil {
|
|
return Result{}, err
|
|
}
|
|
if changed && added != nil && added.Direction == "for" {
|
|
contra, err := s.detectContradictions(ctx, q, emit, b, ev1.ID)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
res.Events = append(res.Events, contra...)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func firstNonEmpty(a, b string) string {
|
|
if a != "" {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func absf(v float64) float64 {
|
|
if v < 0 {
|
|
return -v
|
|
}
|
|
return v
|
|
}
|
|
|
|
// detectContradictions compares b against the mind's active beliefs. A
|
|
// contradiction links both, emits an event and adds mutual counter-evidence
|
|
// so both confidences reflect the conflict until it is resolved.
|
|
func (s *Store) detectContradictions(ctx context.Context, q storage.Querier, emit Emitter, b Belief, causation ids.ID) ([]events.Event, error) {
|
|
rows, err := q.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.mind_id=$1 AND b.id<>$2 AND b.status NOT IN ('rejected','superseded') AND ($3 = '' OR b.subject = $3 OR b.subject = '') LIMIT 500`, b.MindID, b.ID, b.Subject)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var others []Belief
|
|
for rows.Next() {
|
|
o, err := scan(rows)
|
|
if err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
others = append(others, o)
|
|
}
|
|
rows.Close()
|
|
var out []events.Event
|
|
for _, o := range others {
|
|
ok, sim := Contradicts(b.Proposition, o.Proposition)
|
|
if !ok {
|
|
continue
|
|
}
|
|
var exists bool
|
|
q.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM belief_links WHERE relation='contradicts' AND ((from_id=$1 AND to_id=$2) OR (from_id=$2 AND to_id=$1)))`, b.ID, o.ID).Scan(&exists) //nolint:errcheck
|
|
if exists {
|
|
continue
|
|
}
|
|
if StateChange(o, b) {
|
|
// A later observation of a transient state: the older belief was
|
|
// true of its time and is closed, not contested and not weakened.
|
|
ev, err := s.closeState(ctx, q, emit, o, b, sim, causation)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, ev)
|
|
continue
|
|
}
|
|
ev, err := emit(ctx, q, events.New{MindID: b.MindID, Type: events.ContradictionDetected, Source: "beliefs.contradiction", Summary: fmt.Sprintf("contradiction: %q vs %q", trunc(b.Proposition, 60), trunc(o.Proposition, 60)),
|
|
Salience: 0.8, Confidence: sim, CausationID: causation,
|
|
Evidence: []events.EvidenceRef{{Kind: "belief", ID: string(b.ID)}, {Kind: "belief", ID: string(o.ID)}},
|
|
Metadata: map[string]any{"belief_id": string(b.ID), "other_id": string(o.ID), "similarity": sim},
|
|
Payload: map[string]any{"a": map[string]any{"id": b.ID, "proposition": b.Proposition, "confidence": b.Confidence}, "b": map[string]any{"id": o.ID, "proposition": o.Proposition, "confidence": o.Confidence}, "similarity": sim, "method": "negation+jaccard"}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, ev)
|
|
if _, err := q.Exec(ctx, `INSERT INTO belief_links (id, mind_id, from_id, to_id, relation, confidence, event_id) VALUES ($1,$2,$3,$4,'contradicts',$5,$6) ON CONFLICT DO NOTHING`, ids.New(), b.MindID, b.ID, o.ID, sim, ev.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
// Each belief becomes counter-evidence for the other, weighted by the other's confidence.
|
|
if err := s.insertEvidence(ctx, q, b, Evidence{Direction: "against", Kind: "belief", RefID: string(o.ID), Source: "contradiction", Weight: 0.5, Credibility: o.Confidence, Derivation: DerivationInference, Family: "belief:" + string(o.ID)}, "contradicts: "+trunc(o.Proposition, 120), ev.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.insertEvidence(ctx, q, o, Evidence{Direction: "against", Kind: "belief", RefID: string(b.ID), Source: "contradiction", Weight: 0.5, Credibility: b.Confidence, Derivation: DerivationInference, Family: "belief:" + string(b.ID)}, "contradicts: "+trunc(b.Proposition, 120), ev.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, x := range []Belief{b, o} {
|
|
rows, err := s.EvidenceFor(ctx, q, x.MindID, x.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var evs []Evidence
|
|
for _, r := range rows {
|
|
evs = append(evs, r.Evidence)
|
|
}
|
|
bd := Compute(x.Prior, evs)
|
|
st := Derive(bd, x.Epistemic)
|
|
if x.PinnedStatus != nil {
|
|
st = *x.PinnedStatus
|
|
}
|
|
if bd.Confidence != x.Confidence || st != x.Status {
|
|
x.Confidence, x.Status, x.Version = bd.Confidence, st, x.Version+1
|
|
if err := s.insertVersion(ctx, q, x, bd, "contradiction detected", ev.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := q.Exec(ctx, `UPDATE beliefs SET confidence=$3, status=$4, version=$5, last_evaluated=now() WHERE id=$1 AND mind_id=$2`, x.ID, x.MindID, x.Confidence, string(x.Status), x.Version); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Retract is the operator saying "this belief is wrong". Status is pinned to
|
|
// rejected, a correction is recorded, and optionally a do-not-infer rule.
|
|
func (s *Store) Retract(ctx context.Context, emit Emitter, mind, id ids.ID, reason, by string, doNotInfer bool) (Result, error) {
|
|
var res Result
|
|
err := storage.Tx(ctx, s.db, func(tx pgx.Tx) error {
|
|
b, err := s.Get(ctx, tx, mind, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// An operator saying "wrong" is an outcome for the sources that backed
|
|
// it, in this belief's domain only: nobody's standing elsewhere moves.
|
|
if err := s.RecordBeliefOutcome(ctx, tx, mind, id, false); err != nil {
|
|
return err
|
|
}
|
|
ev, err := emit(ctx, tx, events.New{MindID: mind, Type: events.BeliefRetracted, Source: "operator", Summary: fmt.Sprintf("operator %s rejected belief: %s (%s)", by, b.Proposition, reason),
|
|
Salience: 0.7, Confidence: 1, Evidence: []events.EvidenceRef{{Kind: "belief", ID: string(b.ID)}},
|
|
Metadata: map[string]any{"belief_id": string(b.ID), "actor": by, "do_not_infer": doNotInfer}, Payload: map[string]any{"belief_id": b.ID, "proposition": b.Proposition, "reason": reason, "previous": map[string]any{"confidence": b.Confidence, "status": b.Status}}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.insertEvidence(ctx, tx, b, Evidence{Direction: "against", Kind: "operator", RefID: string(ev.ID), Source: "operator", Weight: 1, Credibility: 0.95}, reason, ev.ID); err != nil {
|
|
return err
|
|
}
|
|
rej := StatusRejected
|
|
b.PinnedStatus, b.Status, b.Version = &rej, rej, b.Version+1
|
|
rows, _ := s.EvidenceFor(ctx, tx, mind, id)
|
|
var evs []Evidence
|
|
for _, r := range rows {
|
|
evs = append(evs, r.Evidence)
|
|
}
|
|
bd := Compute(b.Prior, evs)
|
|
b.Confidence = bd.Confidence
|
|
if err := s.insertVersion(ctx, tx, b, bd, "operator correction: "+reason, ev.ID); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(ctx, `UPDATE beliefs SET status='rejected', pinned_status='rejected', confidence=$3, version=$4, last_evaluated=now() WHERE id=$1 AND mind_id=$2`, id, mind, b.Confidence, b.Version); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(ctx, `INSERT INTO corrections (id, mind_id, kind, target_kind, target_id, statement, reason, created_by, event_id) VALUES ($1,$2,'belief_wrong','belief',$3,$4,$5,$6,$7)`, ids.New(), mind, id, b.Proposition, reason, by, ev.ID); err != nil {
|
|
return err
|
|
}
|
|
if doNotInfer {
|
|
if _, err := tx.Exec(ctx, `INSERT INTO corrections (id, mind_id, kind, target_kind, target_id, statement, reason, created_by, event_id) VALUES ($1,$2,'do_not_infer','belief',$3,$4,$5,$6,$7)`, ids.New(), mind, id, b.Proposition, reason, by, ev.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
res = Result{Belief: b, Breakdown: bd, Events: []events.Event{ev}}
|
|
return nil
|
|
})
|
|
return res, err
|
|
}
|
|
|
|
// AddCorrection records a free-standing correction such as "do not infer X".
|
|
func (s *Store) AddCorrection(ctx context.Context, q storage.Querier, mind ids.ID, c Correction) (Correction, error) {
|
|
c.ID, c.CreatedAt = ids.New(), time.Now()
|
|
_, err := q.Exec(ctx, `INSERT INTO corrections (id, mind_id, kind, target_kind, target_id, statement, reason, created_by, event_id) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
|
|
c.ID, mind, c.Kind, c.TargetKind, c.TargetID, c.Statement, c.Reason, c.CreatedBy, nullID(c.EventID))
|
|
return c, err
|
|
}
|
|
|
|
// Corrections lists corrections for a mind, newest first.
|
|
func (s *Store) Corrections(ctx context.Context, mind ids.ID, kind string) ([]Correction, error) {
|
|
rows, err := s.db.Query(ctx, `SELECT id, kind, target_kind, target_id, statement, reason, created_by, event_id, created_at FROM corrections WHERE mind_id=$1 AND ($2='' OR kind=$2) ORDER BY created_at DESC LIMIT 500`, mind, kind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Correction
|
|
for rows.Next() {
|
|
var c Correction
|
|
var ev *string
|
|
if err := rows.Scan(&c.ID, &c.Kind, &c.TargetKind, &c.TargetID, &c.Statement, &c.Reason, &c.CreatedBy, &ev, &c.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if ev != nil {
|
|
c.EventID = ids.ID(*ev)
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// blocked checks do-not-infer corrections against a proposition.
|
|
func (s *Store) blocked(ctx context.Context, q storage.Querier, mind ids.ID, proposition string) (bool, Correction) {
|
|
rows, err := q.Query(ctx, `SELECT id, statement, reason FROM corrections WHERE mind_id=$1 AND kind='do_not_infer'`, mind)
|
|
if err != nil {
|
|
return false, Correction{}
|
|
}
|
|
defer rows.Close()
|
|
pt, _ := Polarity(proposition)
|
|
for rows.Next() {
|
|
var c Correction
|
|
if err := rows.Scan(&c.ID, &c.Statement, &c.Reason); err != nil {
|
|
return false, Correction{}
|
|
}
|
|
ct, _ := Polarity(c.Statement)
|
|
if Jaccard(pt, ct) >= 0.7 {
|
|
return true, c
|
|
}
|
|
}
|
|
return false, Correction{}
|
|
}
|
|
|
|
// Versions returns the append-only history, newest first.
|
|
func (s *Store) Versions(ctx context.Context, mind, id ids.ID) ([]Version, error) {
|
|
rows, err := s.db.Query(ctx, `SELECT version, status, confidence, proposition, reason, breakdown, event_id, created_at FROM belief_versions WHERE belief_id=$1 AND mind_id=$2 ORDER BY version DESC`, id, mind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Version
|
|
for rows.Next() {
|
|
var v Version
|
|
var st string
|
|
var bd []byte
|
|
var ev *string
|
|
if err := rows.Scan(&v.Version, &st, &v.Confidence, &v.Proposition, &v.Reason, &bd, &ev, &v.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
v.Status = Status(st)
|
|
json.Unmarshal(bd, &v.Breakdown) //nolint:errcheck
|
|
if ev != nil {
|
|
v.EventID = ids.ID(*ev)
|
|
}
|
|
out = append(out, v)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// Alternatives lists competing explanations.
|
|
func (s *Store) Alternatives(ctx context.Context, mind, id ids.ID) ([]Alternative, error) {
|
|
rows, err := s.db.Query(ctx, `SELECT id, proposition, confidence, note, status, created_at FROM belief_alternatives WHERE belief_id=$1 AND mind_id=$2 ORDER BY created_at`, id, mind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Alternative
|
|
for rows.Next() {
|
|
var a Alternative
|
|
if err := rows.Scan(&a.ID, &a.Proposition, &a.Confidence, &a.Note, &a.Status, &a.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, a)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// AddAlternative records another candidate explanation.
|
|
func (s *Store) AddAlternative(ctx context.Context, mind, id ids.ID, proposition, note string) (Alternative, error) {
|
|
proposition = strings.TrimSpace(proposition)
|
|
if proposition == "" {
|
|
return Alternative{}, fmt.Errorf("%w: proposition required", ErrInvalid)
|
|
}
|
|
a := Alternative{ID: ids.New(), Proposition: proposition, Confidence: 0.2, Note: note, Status: "open", CreatedAt: time.Now()}
|
|
tag, err := s.db.Exec(ctx, `INSERT INTO belief_alternatives (id, belief_id, mind_id, proposition, confidence, note) SELECT $1,$2,$3,$4,$5,$6 WHERE EXISTS (SELECT 1 FROM beliefs WHERE id=$2 AND mind_id=$3)`, a.ID, id, mind, a.Proposition, a.Confidence, a.Note)
|
|
if err != nil {
|
|
return Alternative{}, err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return Alternative{}, storage.ErrNotFound
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// Links returns contradiction and support links with the other belief loaded.
|
|
func (s *Store) Links(ctx context.Context, mind, id ids.ID) ([]Link, error) {
|
|
rows, err := s.db.Query(ctx, `SELECT l.id, l.from_id, l.to_id, l.relation, l.confidence, l.created_at, `+cols+` FROM belief_links l
|
|
JOIN beliefs b ON b.id = CASE WHEN l.from_id=$1 THEN l.to_id ELSE l.from_id END WHERE l.mind_id=$2 AND (l.from_id=$1 OR l.to_id=$1) ORDER BY l.created_at`, id, mind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Link
|
|
for rows.Next() {
|
|
var l Link
|
|
var other Belief
|
|
var epi, status string
|
|
var detail []byte
|
|
var origin, superseded, pinned *string
|
|
if err := rows.Scan(&l.ID, &l.FromID, &l.ToID, &l.Relation, &l.Confidence, &l.CreatedAt,
|
|
&other.ID, &other.MindID, &other.Proposition, &other.Subject, &epi, &status, &other.Confidence, &other.Prior, &other.Version, &other.Source, &detail, &origin, &superseded, &pinned, &other.FirstCreated, &other.LastEvaluated, &other.Evaluations, &other.TemporalScope, &other.ObservedAt, &other.ValidFrom, &other.ValidUntil, &other.RedactedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
other.Epistemic, other.Status = concepts.Epistemic(epi), Status(status)
|
|
l.Other = &other
|
|
out = append(out, l)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// RetractEvidence marks a piece of evidence as not counting (memory retracted etc.).
|
|
func (s *Store) RetractEvidence(ctx context.Context, q storage.Querier, mind ids.ID, kind, refID string) ([]ids.ID, error) {
|
|
rows, err := q.Query(ctx, `UPDATE belief_evidence SET retracted=true WHERE mind_id=$1 AND kind=$2 AND ref_id=$3 AND NOT retracted RETURNING belief_id`, mind, kind, refID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []ids.ID
|
|
for rows.Next() {
|
|
var id ids.ID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, id)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// Stats summarises beliefs for the overview.
|
|
type Stats struct {
|
|
Total int64 `json:"total"`
|
|
ByStatus map[string]int64 `json:"by_status"`
|
|
}
|
|
|
|
// Stats counts beliefs by status.
|
|
func (s *Store) Stats(ctx context.Context, mind ids.ID) (Stats, error) {
|
|
st := Stats{ByStatus: map[string]int64{}}
|
|
rows, err := s.db.Query(ctx, `SELECT status, count(*) FROM beliefs WHERE mind_id=$1 GROUP BY status`, mind)
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var k string
|
|
var n int64
|
|
if err := rows.Scan(&k, &n); err != nil {
|
|
return st, err
|
|
}
|
|
st.ByStatus[k] = n
|
|
st.Total += n
|
|
}
|
|
return st, rows.Err()
|
|
}
|
|
|
|
// Contested returns beliefs currently in conflict, for attention and reflection.
|
|
func (s *Store) Contested(ctx context.Context, mind ids.ID, limit int) ([]Belief, error) {
|
|
return s.List(ctx, mind, Filter{Statuses: []Status{StatusContested}, Limit: limit})
|
|
}
|
|
|
|
func trunc(s string, n int) string {
|
|
if len(s) > n {
|
|
return s[:n-1] + "…"
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Search returns live beliefs matching any of the words in text, best match
|
|
// first. Used to answer "what do you believe about X".
|
|
func (s *Store) Search(ctx context.Context, mind ids.ID, text string, limit int) ([]Belief, error) {
|
|
toks, _ := Polarity(text)
|
|
if len(toks) == 0 {
|
|
return nil, nil
|
|
}
|
|
if limit <= 0 || limit > 50 {
|
|
limit = 10
|
|
}
|
|
q := strings.Join(toks, " | ")
|
|
rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.mind_id=$1 AND b.status NOT IN ('superseded') AND b.search @@ to_tsquery('english', $2)
|
|
ORDER BY ts_rank_cd(b.search, to_tsquery('english', $2)) DESC, b.confidence DESC, b.id LIMIT $3`, mind, q, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Belief
|
|
for rows.Next() {
|
|
b, err := scan(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, b)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// Change describes a belief whose status or confidence moved.
|
|
type Change struct {
|
|
Belief Belief `json:"belief"`
|
|
FromStatus Status `json:"from_status"`
|
|
FromConf float64 `json:"from_confidence"`
|
|
Reason string `json:"reason"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// RecentChanges lists beliefs whose latest version differs in status from an
|
|
// earlier version within the window ("what have you changed your mind about").
|
|
func (s *Store) RecentChanges(ctx context.Context, mind ids.ID, since time.Time, limit int) ([]Change, error) {
|
|
if limit <= 0 || limit > 50 {
|
|
limit = 10
|
|
}
|
|
rows, err := s.db.Query(ctx, `WITH latest AS (
|
|
SELECT DISTINCT ON (belief_id) belief_id, status, confidence, reason, created_at FROM belief_versions WHERE mind_id=$1 AND created_at >= $2 ORDER BY belief_id, version DESC),
|
|
first AS (
|
|
SELECT DISTINCT ON (belief_id) belief_id, status, confidence FROM belief_versions WHERE mind_id=$1 ORDER BY belief_id, version ASC)
|
|
SELECT `+cols+`, f.status, f.confidence, l.reason, l.created_at FROM latest l JOIN first f ON f.belief_id=l.belief_id JOIN beliefs b ON b.id=l.belief_id
|
|
WHERE l.status <> f.status ORDER BY l.created_at DESC LIMIT $3`, mind, since, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Change
|
|
for rows.Next() {
|
|
var c Change
|
|
var epi, status, fromStatus string
|
|
var detail []byte
|
|
var origin, superseded, pinned *string
|
|
b := &c.Belief
|
|
if err := rows.Scan(&b.ID, &b.MindID, &b.Proposition, &b.Subject, &epi, &status, &b.Confidence, &b.Prior, &b.Version, &b.Source, &detail, &origin, &superseded, &pinned, &b.FirstCreated, &b.LastEvaluated, &b.Evaluations, &b.TemporalScope, &b.ObservedAt, &b.ValidFrom, &b.ValidUntil, &b.RedactedAt, &fromStatus, &c.FromConf, &c.Reason, &c.At); err != nil {
|
|
return nil, err
|
|
}
|
|
b.Epistemic, b.Status, c.FromStatus = concepts.Epistemic(epi), Status(status), Status(fromStatus)
|
|
out = append(out, c)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// Uncertain returns beliefs in the middle band or contested, least certain first.
|
|
func (s *Store) Uncertain(ctx context.Context, mind ids.ID, limit int) ([]Belief, error) {
|
|
if limit <= 0 || limit > 50 {
|
|
limit = 10
|
|
}
|
|
rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.mind_id=$1 AND b.status NOT IN ('rejected','superseded') AND (b.status='contested' OR (b.confidence BETWEEN 0.3 AND 0.7))
|
|
ORDER BY abs(b.confidence-0.5) ASC, b.last_evaluated DESC LIMIT $2`, mind, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Belief
|
|
for rows.Next() {
|
|
b, err := scan(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, b)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// closeState ends the validity of an older transient belief when a newer
|
|
// observation shows the state changed. Its score is untouched; its status
|
|
// becomes superseded with valid_until set, and a version and an event
|
|
// record why. "The door was open at 10:00" remains true.
|
|
func (s *Store) closeState(ctx context.Context, q storage.Querier, emit Emitter, older, newer Belief, sim float64, causation ids.ID) (events.Event, error) {
|
|
until := *newer.ObservedAt
|
|
ev, err := emit(ctx, q, events.New{MindID: older.MindID, Type: events.BeliefUpdated, Source: "beliefs.temporal", Summary: fmt.Sprintf("state changed: %q held until %s; now %q", trunc(older.Proposition, 60), until.Format("15:04:05"), trunc(newer.Proposition, 60)),
|
|
Salience: 0.5, Confidence: sim, CausationID: causation,
|
|
Evidence: []events.EvidenceRef{{Kind: "belief", ID: string(older.ID), Note: "closed"}, {Kind: "belief", ID: string(newer.ID), Note: "current"}},
|
|
Metadata: map[string]any{"belief_id": string(older.ID), "superseded_by": string(newer.ID), "valid_until": until, "reason": "state changed"},
|
|
Payload: map[string]any{"older": map[string]any{"id": older.ID, "proposition": older.Proposition, "valid_from": older.ValidFrom, "valid_until": until}, "newer": map[string]any{"id": newer.ID, "proposition": newer.Proposition, "observed_at": newer.ObservedAt}, "method": "temporal supersession"}})
|
|
if err != nil {
|
|
return events.Event{}, err
|
|
}
|
|
if _, err := q.Exec(ctx, `UPDATE beliefs SET status='superseded', superseded_by=$3, valid_until=$4, version=version+1, last_evaluated=now(), evaluations=evaluations+1 WHERE id=$1 AND mind_id=$2`, older.ID, older.MindID, newer.ID, until); err != nil {
|
|
return events.Event{}, err
|
|
}
|
|
older.Version++
|
|
older.Status = StatusSuperseded
|
|
if err := s.insertVersion(ctx, q, older, Breakdown{Prior: older.Prior, Confidence: older.Confidence, Meaning: ScoreMeaning, Notes: []string{"state changed; score unchanged, validity closed"}}, "state changed: superseded by a later observation", ev.ID); err != nil {
|
|
return events.Event{}, err
|
|
}
|
|
if _, err := q.Exec(ctx, `INSERT INTO belief_links (id, mind_id, from_id, to_id, relation, confidence, event_id) VALUES ($1,$2,$3,$4,'supersedes',$5,$6) ON CONFLICT DO NOTHING`, ids.New(), older.MindID, newer.ID, older.ID, sim, ev.ID); err != nil {
|
|
return events.Event{}, err
|
|
}
|
|
return ev, nil
|
|
}
|
|
|
|
// History lists superseded beliefs about a topic with their validity, so a
|
|
// mind can say what was true and when.
|
|
func (s *Store) History(ctx context.Context, mind ids.ID, text string, limit int) ([]Belief, error) {
|
|
if limit <= 0 || limit > 50 {
|
|
limit = 10
|
|
}
|
|
toks, _ := Polarity(text)
|
|
if len(toks) == 0 {
|
|
return nil, nil
|
|
}
|
|
q := strings.Join(toks, " | ")
|
|
rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.mind_id=$1 AND b.status='superseded' AND b.valid_until IS NOT NULL AND b.search @@ to_tsquery('english', $2) ORDER BY b.valid_until DESC LIMIT $3`, mind, q, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Belief
|
|
for rows.Next() {
|
|
b, err := scan(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, b)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// VersionAt is a belief as it stood at a moment, from the append-only
|
|
// version history.
|
|
type VersionAt struct {
|
|
BeliefID ids.ID `json:"belief_id"`
|
|
Proposition string `json:"proposition"`
|
|
Status string `json:"status"`
|
|
Confidence float64 `json:"confidence"`
|
|
Version int `json:"version"`
|
|
RecordedAt time.Time `json:"recorded_at"`
|
|
}
|
|
|
|
// AsOf returns what the mind held at a moment: for each belief matching the
|
|
// text (all beliefs when text has no usable terms), the latest version
|
|
// recorded at or before at. Beliefs created later are absent by definition.
|
|
func (s *Store) AsOf(ctx context.Context, mind ids.ID, text string, at time.Time, limit int) ([]VersionAt, error) {
|
|
if limit <= 0 || limit > 50 {
|
|
limit = 10
|
|
}
|
|
toks, _ := Polarity(text)
|
|
q := strings.Join(toks, " | ")
|
|
rows, err := s.db.Query(ctx, `SELECT DISTINCT ON (v.belief_id) v.belief_id, v.proposition, v.status, v.confidence, v.version, v.created_at
|
|
FROM belief_versions v JOIN beliefs b ON b.id=v.belief_id
|
|
WHERE v.mind_id=$1 AND v.created_at <= $2 AND ($3='' OR b.search @@ to_tsquery('english', $3))
|
|
ORDER BY v.belief_id, v.version DESC`, mind, at, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []VersionAt
|
|
for rows.Next() {
|
|
var v VersionAt
|
|
if err := rows.Scan(&v.BeliefID, &v.Proposition, &v.Status, &v.Confidence, &v.Version, &v.RecordedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, v)
|
|
}
|
|
sortVersions(out)
|
|
if len(out) > limit {
|
|
out = out[:limit]
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func sortVersions(vs []VersionAt) {
|
|
for i := 1; i < len(vs); i++ {
|
|
for j := i; j > 0 && vs[j].Confidence > vs[j-1].Confidence; j-- {
|
|
vs[j], vs[j-1] = vs[j-1], vs[j]
|
|
}
|
|
}
|
|
}
|