feat(epistemics): record assertions between observations and beliefs
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.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package cognition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -43,10 +45,29 @@ func (a Associate) Observe(_ context.Context, s *brain.State) ([]brain.Candidate
|
||||
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
|
||||
}
|
||||
@@ -74,3 +95,21 @@ func toSet(ws []string) map[string]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
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/andydixon/noema/internal/beliefs"
|
||||
"github.com/andydixon/noema/internal/brain"
|
||||
"github.com/andydixon/noema/internal/conversation"
|
||||
"github.com/andydixon/noema/internal/epistemics"
|
||||
"github.com/andydixon/noema/internal/graph"
|
||||
"github.com/andydixon/noema/internal/memory"
|
||||
"github.com/andydixon/noema/internal/predictions"
|
||||
@@ -27,6 +28,7 @@ type Handlers struct {
|
||||
Beliefs *beliefs.Store
|
||||
Conversations *conversation.Store
|
||||
Graph *graph.Store
|
||||
Assertions *epistemics.Store // nil disables assertion records (claims still become evidence)
|
||||
}
|
||||
|
||||
// Register installs the handlers on a registry.
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/andydixon/noema/internal/beliefs"
|
||||
"github.com/andydixon/noema/internal/brain"
|
||||
"github.com/andydixon/noema/internal/conversation"
|
||||
"github.com/andydixon/noema/internal/epistemics"
|
||||
evstore "github.com/andydixon/noema/internal/events"
|
||||
"github.com/andydixon/noema/internal/llm"
|
||||
"github.com/andydixon/noema/internal/memory"
|
||||
@@ -90,27 +91,57 @@ func (h Handlers) perceive(ctx context.Context, c *brain.Cycle, t workspace.Thou
|
||||
Evidence: []events.EvidenceRef{{Kind: "memory", ID: string(mem.ID)}, {Kind: "event", ID: srcID, Note: "source"}}, Metadata: map[string]any{"memory_id": mem.ID, "kind": mem.Kind}})
|
||||
}
|
||||
}
|
||||
// Claims become beliefs; the message is their evidence.
|
||||
// Every claim is first an assertion: the certain fact that someone took a
|
||||
// stance. Only factual assertions (direct or reported) then feed a
|
||||
// proposition belief, as one piece of evidence attributed to whoever
|
||||
// actually stands behind it; hypotheticals, questions and imperatives
|
||||
// never do, and expressed doubt counts against.
|
||||
var claimed []BeliefRef
|
||||
var asserted []AssertionRef
|
||||
if h.Beliefs != nil {
|
||||
for _, cl := range a.Claims {
|
||||
w := 0.6
|
||||
if cl.Certainty == "hedged" {
|
||||
w = 0.3
|
||||
in := epistemics.Interpret(actor, cl.Proposition)
|
||||
if cl.Certainty == "hedged" && in.Certainty == epistemics.CertaintyAsserted {
|
||||
in.Certainty = epistemics.CertaintyHedged
|
||||
}
|
||||
if a.Intent == "correction" {
|
||||
w += 0.2
|
||||
var beliefID ids.ID
|
||||
var res beliefs.Result
|
||||
if in.CreatesBelief() {
|
||||
w := in.EvidenceWeight()
|
||||
if a.Intent == "correction" && !in.Reported {
|
||||
w = min(1, w+0.2)
|
||||
}
|
||||
attributed := in.AttributedTo
|
||||
cred := trust.Credibility()
|
||||
if in.Reported {
|
||||
cred = events.TrustLow.Credibility() + 0.2 // second-hand through a trusted speaker
|
||||
}
|
||||
ev := []beliefs.EvidenceInput{{Direction: "for", Kind: "assertion", RefID: "", Source: "asserted-by:" + attributed, Weight: w, Credibility: cred,
|
||||
Note: describeAssertion(actor, in), Derivation: beliefs.DerivationAssertion, Family: beliefs.FamilyForActor(attributed), RootIDs: []string{srcID}}}
|
||||
if dw := in.DoubtWeight(); dw > 0 {
|
||||
ev = append(ev, beliefs.EvidenceInput{Direction: "against", Kind: "assertion", Source: "doubted-by:" + actor, Weight: dw, Credibility: trust.Credibility(),
|
||||
Note: actor + " doubts it", Derivation: beliefs.DerivationAssertion, Family: beliefs.FamilyForActor(actor), RootIDs: []string{srcID}})
|
||||
}
|
||||
var err error
|
||||
res, err = h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: mind.ID, Proposition: in.Proposition, Epistemic: concepts.Claim, Source: "conversation", OriginEvent: perc.ID, Causation: perc.ID, Correlation: c.State.CycleID, Evidence: ev,
|
||||
Detail: map[string]any{"attributed_to": attributed, "reported": in.Reported, "interpretation_confidence": in.Confidence}})
|
||||
if err != nil {
|
||||
c.Emit(events.New{Type: events.Metacognition, Source: "brain.perceive", Summary: "claim not recorded: " + err.Error(), Salience: 0.3, Confidence: 1, Metadata: map[string]any{"check": "claim_rejected"}})
|
||||
} else {
|
||||
beliefID = res.Belief.ID
|
||||
claimed = append(claimed, BeliefRef{ID: res.Belief.ID, Proposition: res.Belief.Proposition, Confidence: res.Belief.Confidence, Status: string(res.Belief.Status)})
|
||||
}
|
||||
}
|
||||
res, err := h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: mind.ID, Proposition: cl.Proposition, Epistemic: concepts.Claim, Source: "conversation", OriginEvent: perc.ID, Causation: perc.ID, Correlation: c.State.CycleID,
|
||||
Evidence: []beliefs.EvidenceInput{{Direction: "for", Kind: "event", RefID: srcID, Source: "said-by:" + actor, Weight: w, Credibility: trust.Credibility(), Note: actor + " " + cl.Certainty + " it",
|
||||
Derivation: beliefs.DerivationAssertion, Family: beliefs.FamilyForActor(actor), RootIDs: []string{srcID}}}})
|
||||
if err != nil {
|
||||
c.Emit(events.New{Type: events.Metacognition, Source: "brain.perceive", Summary: "claim not recorded: " + err.Error(), Salience: 0.3, Confidence: 1, Metadata: map[string]any{"check": "claim_rejected"}})
|
||||
continue
|
||||
if h.Assertions != nil {
|
||||
rec, err := h.Assertions.Record(ctx, c.Tx, epistemics.New{MindID: mind.ID, Actor: actor, ActorKind: string(trust), Interpretation: in, Subject: subjectKey(in.Proposition), SourceEvent: ids.ID(srcID), ConversationID: ids.ID(convID), MessageID: ids.ID(msgID), Context: truncate(text, 300), Trust: string(trust), BeliefID: beliefID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
asserted = append(asserted, AssertionRef{ID: rec.ID, AttributedTo: rec.AttributedTo, Proposition: rec.Proposition, Stance: rec.Stance, Modality: rec.Modality, Reported: rec.Reported, Hypothetical: rec.Hypothetical, Interpretation: rec.Interpretation, BeliefID: beliefID})
|
||||
}
|
||||
claimed = append(claimed, BeliefRef{ID: res.Belief.ID, Proposition: res.Belief.Proposition, Confidence: res.Belief.Confidence, Status: string(res.Belief.Status)})
|
||||
}
|
||||
}
|
||||
_ = asserted
|
||||
refs := map[string]ids.ID{"memory": memRef}
|
||||
for _, b := range claimed {
|
||||
refs["belief"] = b.ID
|
||||
@@ -449,3 +480,29 @@ func firstNonEmpty(a, b string) string {
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// AssertionRef is the response intent's view of a recorded assertion.
|
||||
type AssertionRef struct {
|
||||
ID ids.ID `json:"id"`
|
||||
AttributedTo string `json:"attributed_to"`
|
||||
Proposition string `json:"proposition"`
|
||||
Stance string `json:"stance"`
|
||||
Modality string `json:"modality"`
|
||||
Reported bool `json:"reported"`
|
||||
Hypothetical bool `json:"hypothetical"`
|
||||
Interpretation float64 `json:"interpretation_confidence"`
|
||||
BeliefID ids.ID `json:"belief_id,omitempty"`
|
||||
}
|
||||
|
||||
// describeAssertion words the evidence note so the belief page says who
|
||||
// stood behind the proposition and how the mind read them.
|
||||
func describeAssertion(speaker string, in epistemics.Interpretation) string {
|
||||
switch {
|
||||
case in.Reported && in.Quoted:
|
||||
return speaker + " quoted " + in.AttributedTo + " (" + in.Certainty + ")"
|
||||
case in.Reported:
|
||||
return speaker + " reported that " + in.AttributedTo + " holds this (" + in.Certainty + ", read with " + fmt.Sprintf("%.0f%%", in.Confidence*100) + " interpretation confidence)"
|
||||
default:
|
||||
return speaker + " " + in.Certainty + " it (read with " + fmt.Sprintf("%.0f%%", in.Confidence*100) + " interpretation confidence)"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user