A belief with no live evidence on either side now derives the status 'unknown' instead of sitting tentatively at its prior. Withdrawing every piece of evidence leaves the proposition unknown, never rejected, and introspection answers 'I do not know whether X' rather than quoting the prior. Hypotheses keep their own status because they are proposals by construction. Migration 0019 widens the status constraint; the docs define every epistemic number and how absence of evidence is handled.
116 lines
3.9 KiB
Go
116 lines
3.9 KiB
Go
package cognition
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"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
|
|
}
|