A statement made to a mind is now three things kept apart: the observation that it arrived, an append-only assertion (attributed actor, stance, certainty, modality, negation, reported or quoted speech, hypothetical status, interpretation confidence and notes), and, only for factual assertions, one piece of evidence for a proposition belief attributed to whoever actually stands behind it. Reported speech is attributed to the person reported and weighed as second-hand; hypotheticals, questions and imperatives create no belief; expressed doubt counts against; a possibly sarcastic remark is recorded with low interpretation confidence rather than guessed at. Interpretation is deterministic code; a model may enrich the claim list but never sets stance or confidence. Association no longer treats conversational turns as world events: hypotheticals, questions, requests and consecutive turns by one speaker cannot seed causal hypotheses. Adds internal/epistemics, migration 0018, the assertions API, assertions on belief pages and in belief detail, a shared end-to-end test harness (tests/harness), end-to-end tests for eight speech patterns, and updates the milestone test to the new provenance.
116 lines
3.9 KiB
Go
116 lines
3.9 KiB
Go
package cognition
|
|
|
|
import (
|
|
"encoding/json"
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/andydixon/noema/internal/attention"
|
|
"github.com/andydixon/noema/internal/brain"
|
|
"github.com/andydixon/noema/internal/epistemics"
|
|
"github.com/andydixon/noema/internal/workspace"
|
|
"github.com/andydixon/noema/pkg/events"
|
|
"github.com/andydixon/noema/pkg/ids"
|
|
)
|
|
|
|
// Associate proposes low-confidence causal hypotheses when two notable
|
|
// observations arrive close together ("deployment at 09:10, latency at
|
|
// 09:12"). It is deliberately weak: confidence comes from the belief layer's
|
|
// arithmetic over the two observations, never from the wording.
|
|
type Associate struct {
|
|
Window time.Duration // how far back an earlier observation may lie
|
|
}
|
|
|
|
// Name implements brain.Process.
|
|
func (Associate) Name() string { return "associate" }
|
|
|
|
// Observe implements brain.Process.
|
|
func (a Associate) Observe(_ context.Context, s *brain.State) ([]brain.Candidate, error) {
|
|
if s.Services.Beliefs == nil {
|
|
return nil, nil
|
|
}
|
|
window := a.Window
|
|
if window <= 0 {
|
|
window = 15 * time.Minute
|
|
}
|
|
var out []brain.Candidate
|
|
for _, ev := range s.NewEvents {
|
|
if strings.HasPrefix(ev.Source, "brain") || ev.Type != events.Observation {
|
|
continue
|
|
}
|
|
laterToks := workspace.Tokenise(ev.Summary)
|
|
laterU := urgency(laterToks)
|
|
if laterU < 0.4 {
|
|
continue
|
|
}
|
|
// Speech is not a world event. A hypothetical or a question cannot be a
|
|
// cause or an effect, and two turns by the same speaker in one
|
|
// conversation are one person talking, not a sequence of happenings.
|
|
speaker := ""
|
|
if ev.Metadata["conversation_id"] != nil {
|
|
var p events.ObservationPayload
|
|
if json.Unmarshal(ev.Payload, &p) == nil {
|
|
speaker = p.Actor
|
|
}
|
|
if !worldly(speaker, ev.Summary) {
|
|
continue
|
|
}
|
|
}
|
|
for _, it := range s.Workspace.WorkingMemory {
|
|
if it.Kind != "observation" || it.SourceEvent.IsZero() || it.SourceEvent == ev.ID {
|
|
continue
|
|
}
|
|
if speaker != "" && strings.HasPrefix(it.Content, speaker+": ") {
|
|
continue
|
|
}
|
|
if !worldly("", it.Content) {
|
|
continue
|
|
}
|
|
if ev.OccurredAt.Sub(it.TouchedAt) > window || it.TouchedAt.After(ev.OccurredAt) {
|
|
continue
|
|
}
|
|
earlierToks := workspace.Tokenise(it.Content)
|
|
if urgency(earlierToks) < 0.3 && workspace.Overlap(earlierToks, toSet(laterToks)) < 0.2 {
|
|
continue
|
|
}
|
|
pair := []string{string(it.SourceEvent), string(ev.ID)}
|
|
sort.Strings(pair)
|
|
prop := truncate(it.Content, 100) + " may have caused: " + truncate(ev.Summary, 100)
|
|
out = append(out, brain.Candidate{Kind: "hypothesis", Subject: strings.Join(pair, "+"), Summary: "hypothesis: " + prop,
|
|
Content: map[string]any{"proposition": prop, "cause_event": string(it.SourceEvent), "effect_event": string(ev.ID), "gap_seconds": int(ev.OccurredAt.Sub(it.TouchedAt).Seconds()), "source_event": string(ev.ID)},
|
|
Features: attention.Features{Salience: 0.4 + 0.5*ev.Salience, Novelty: 0.85, Consequence: 0.8, Urgency: laterU * 0.6, Confidence: 0.3, ContextRelevance: 0.6, OperatorPriority: 0.5},
|
|
Confidence: 0.3, SourceEvents: []ids.ID{ev.ID, it.SourceEvent},
|
|
Evidence: []events.EvidenceRef{{Kind: "event", ID: string(it.SourceEvent), Note: "earlier"}, {Kind: "event", ID: string(ev.ID), Note: "later"}}})
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func toSet(ws []string) map[string]struct{} {
|
|
m := make(map[string]struct{}, len(ws))
|
|
for _, w := range ws {
|
|
m[w] = struct{}{}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// worldly reports whether an utterance describes something that happened,
|
|
// as opposed to a hypothetical, a question or a request, which cannot stand
|
|
// as a cause or an effect.
|
|
func worldly(speaker, summary string) bool {
|
|
text := summary
|
|
if speaker != "" {
|
|
text = strings.TrimPrefix(text, speaker+": ")
|
|
} else if i := strings.Index(text, ": "); i > 0 && i < 40 {
|
|
text = text[i+2:]
|
|
}
|
|
in := epistemics.Interpret(speaker, text)
|
|
switch in.Modality {
|
|
case epistemics.ModalityHypothetical, epistemics.ModalityQuestion, epistemics.ModalityImperative:
|
|
return false
|
|
}
|
|
return true
|
|
}
|