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:
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/andydixon/noema/internal/consolidation"
|
||||
"github.com/andydixon/noema/internal/conversation"
|
||||
"github.com/andydixon/noema/internal/dreaming"
|
||||
"github.com/andydixon/noema/internal/epistemics"
|
||||
evstore "github.com/andydixon/noema/internal/events"
|
||||
"github.com/andydixon/noema/internal/goals"
|
||||
"github.com/andydixon/noema/internal/graph"
|
||||
@@ -63,6 +64,7 @@ func testServer(t *testing.T) (*httptest.Server, string) {
|
||||
deps.Integrations = &integrations.Manager{Store: integrations.NewStore(pool), Events: deps.Events, Graph: deps.Graph, Log: log, Pollers: map[integrations.Kind]integrations.Poller{}}
|
||||
deps.Dreaming = &dreaming.Service{DB: pool, Memory: deps.Memory, Beliefs: deps.Beliefs, Events: deps.Events, Log: log}
|
||||
deps.Snapshots = &snapshots.Store{DB: pool, Events: deps.Events, Minds: deps.Minds}
|
||||
deps.Assertions = epistemics.NewStore(pool)
|
||||
deps.Narrative = narrative.NewStore(pool)
|
||||
deps.Notifications = notifications.NewStore(pool)
|
||||
deps.Consolidation = &consolidation.Service{DB: pool, Memory: deps.Memory, Beliefs: deps.Beliefs, Events: deps.Events, Minds: deps.Minds, Log: log}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/andydixon/noema/internal/auth"
|
||||
"github.com/andydixon/noema/internal/epistemics"
|
||||
"github.com/andydixon/noema/pkg/ids"
|
||||
)
|
||||
|
||||
// assertionRoutes expose who asserted what to a mind, kept apart from what
|
||||
// the mind believes.
|
||||
func (s *Server) assertionRoutes() {
|
||||
m := s.mux
|
||||
m.Handle("GET /api/v1/minds/{mind}/assertions", require(auth.RoleObserver, s.assertionsList))
|
||||
m.Handle("GET /api/v1/minds/{mind}/assertions/actors", require(auth.RoleObserver, s.assertionActors))
|
||||
}
|
||||
|
||||
func (s *Server) assertionsList(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := s.mind(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
f := epistemics.Filter{AttributedTo: q.Get("actor"), Stance: q.Get("stance"), Limit: intParam(q.Get("limit"), 100)}
|
||||
if b := q.Get("belief"); b != "" {
|
||||
id, err := ids.Parse(b)
|
||||
if err != nil {
|
||||
fail(w, r, ValidationError("bad belief id"))
|
||||
return
|
||||
}
|
||||
f.BeliefID = id
|
||||
}
|
||||
list, err := s.deps.Assertions.List(r.Context(), m.ID, f)
|
||||
if err != nil {
|
||||
fail(w, r, err)
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []epistemics.Assertion{}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": list})
|
||||
}
|
||||
|
||||
func (s *Server) assertionActors(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := s.mind(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
list, err := s.deps.Assertions.Actors(r.Context(), m.ID)
|
||||
if err != nil {
|
||||
fail(w, r, err)
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []epistemics.ActorSummary{}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": list})
|
||||
}
|
||||
|
||||
// intParam parses a bounded positive integer query value.
|
||||
func intParam(v string, def int) int {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n <= 0 {
|
||||
return def
|
||||
}
|
||||
return n
|
||||
}
|
||||
+16
-8
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/andydixon/noema/internal/auth"
|
||||
"github.com/andydixon/noema/internal/beliefs"
|
||||
"github.com/andydixon/noema/internal/epistemics"
|
||||
"github.com/andydixon/noema/internal/storage"
|
||||
"github.com/andydixon/noema/pkg/concepts"
|
||||
"github.com/andydixon/noema/pkg/events"
|
||||
@@ -129,14 +130,15 @@ func firstNonEmpty(a, b string) string {
|
||||
|
||||
// BeliefDetail is the full picture of one belief.
|
||||
type BeliefDetail struct {
|
||||
Belief beliefs.Belief `json:"belief"`
|
||||
Breakdown beliefs.Breakdown `json:"breakdown"`
|
||||
Evidence []beliefs.EvidenceRow `json:"evidence"`
|
||||
Versions []beliefs.Version `json:"versions"`
|
||||
Alternatives []beliefs.Alternative `json:"alternatives"`
|
||||
Links []beliefs.Link `json:"links"`
|
||||
Events []events.Event `json:"events"`
|
||||
Origin *events.Event `json:"origin,omitempty"`
|
||||
Belief beliefs.Belief `json:"belief"`
|
||||
Breakdown beliefs.Breakdown `json:"breakdown"`
|
||||
Evidence []beliefs.EvidenceRow `json:"evidence"`
|
||||
Versions []beliefs.Version `json:"versions"`
|
||||
Alternatives []beliefs.Alternative `json:"alternatives"`
|
||||
Links []beliefs.Link `json:"links"`
|
||||
Events []events.Event `json:"events"`
|
||||
Origin *events.Event `json:"origin,omitempty"`
|
||||
Assertions []epistemics.Assertion `json:"assertions"` // who stated this proposition, with what stance
|
||||
}
|
||||
|
||||
// BeliefDetail assembles the detail; shared with the UI.
|
||||
@@ -156,6 +158,12 @@ func (s *Server) BeliefDetail(r *http.Request, mind, id ids.ID) (BeliefDetail, e
|
||||
d.Versions, _ = s.deps.Beliefs.Versions(ctx, mind, id)
|
||||
d.Alternatives, _ = s.deps.Beliefs.Alternatives(ctx, mind, id)
|
||||
d.Links, _ = s.deps.Beliefs.Links(ctx, mind, id)
|
||||
if s.deps.Assertions != nil {
|
||||
d.Assertions, _ = s.deps.Assertions.List(ctx, mind, epistemics.Filter{BeliefID: id, Limit: 50})
|
||||
}
|
||||
if d.Assertions == nil {
|
||||
d.Assertions = []epistemics.Assertion{}
|
||||
}
|
||||
d.Events, _ = s.deps.Events.DerivedFrom(ctx, "belief", string(id), 100)
|
||||
if !b.OriginEvent.IsZero() {
|
||||
if e, err := s.deps.Events.Get(ctx, b.OriginEvent); err == nil {
|
||||
|
||||
@@ -2626,6 +2626,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/minds/{mind}/assertions": {
|
||||
"get": {
|
||||
"operationId": "assertionsList",
|
||||
"summary": "list assertions: who took what stance on which proposition",
|
||||
"x-required-role": "observer",
|
||||
"tags": [
|
||||
"minds"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "mind",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "mind slug or id"
|
||||
},
|
||||
{
|
||||
"name": "actor",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "belief",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stance",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/minds/{mind}/assertions/actors": {
|
||||
"get": {
|
||||
"operationId": "assertionActors",
|
||||
"summary": "summarise asserting actors",
|
||||
"x-required-role": "observer",
|
||||
"tags": [
|
||||
"minds"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "mind",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "mind slug or id"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/minds/{mind}/beliefs": {
|
||||
"get": {
|
||||
"operationId": "beliefsList",
|
||||
|
||||
@@ -79,6 +79,7 @@ func (s *Server) routes() {
|
||||
s.integrationRoutes()
|
||||
s.dreamRoutes()
|
||||
s.studyRoutes()
|
||||
s.assertionRoutes()
|
||||
|
||||
m.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, r, http.StatusNotFound, "not_found", "no such endpoint")
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/andydixon/noema/internal/consolidation"
|
||||
"github.com/andydixon/noema/internal/conversation"
|
||||
"github.com/andydixon/noema/internal/dreaming"
|
||||
"github.com/andydixon/noema/internal/epistemics"
|
||||
"github.com/andydixon/noema/internal/events"
|
||||
"github.com/andydixon/noema/internal/experiments"
|
||||
"github.com/andydixon/noema/internal/goals"
|
||||
@@ -52,6 +53,7 @@ type Deps struct {
|
||||
Brain *brain.Manager
|
||||
Memory *memory.Store
|
||||
Beliefs *beliefs.Store
|
||||
Assertions *epistemics.Store
|
||||
Conversations *conversation.Store
|
||||
Goals *goals.Store
|
||||
Motivation *motivation.Store
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
// Package epistemics keeps apart three things a careless system conflates:
|
||||
// the observation that somebody said something, the assertion they made,
|
||||
// and any belief about whether what they said is true.
|
||||
//
|
||||
// An Assertion is the durable, append-only record that an actor took a
|
||||
// stance on a proposition. Its confidence is about interpretation ("how sure
|
||||
// are we that Andy asserted X?"), never about X. Whether X is believed is a
|
||||
// separate question answered by the belief layer from evidence, of which the
|
||||
// assertion is one row with a weight that depends on stance, modality and
|
||||
// how clearly it was understood. Reported speech attributes the stance to
|
||||
// the person reported, hypotheticals create no belief at all, doubt counts
|
||||
// against, and a possibly sarcastic remark is recorded with low
|
||||
// interpretation confidence rather than guessed at.
|
||||
package epistemics
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Stances an actor can take towards a proposition.
|
||||
const (
|
||||
StanceAsserts = "asserts"
|
||||
StanceDenies = "denies"
|
||||
StanceDoubts = "doubts"
|
||||
StanceQuestions = "questions"
|
||||
StanceSupposes = "supposes"
|
||||
StanceReports = "reports" // the speaker relays somebody else's stance
|
||||
)
|
||||
|
||||
// Certainty the speaker expressed.
|
||||
const (
|
||||
CertaintyAsserted = "asserted"
|
||||
CertaintyHedged = "hedged"
|
||||
CertaintyUncertain = "uncertain"
|
||||
CertaintyEmphatic = "emphatic"
|
||||
)
|
||||
|
||||
// Modality of the utterance.
|
||||
const (
|
||||
ModalityFactual = "factual"
|
||||
ModalityHypothetical = "hypothetical"
|
||||
ModalityReported = "reported"
|
||||
ModalityQuoted = "quoted"
|
||||
ModalityImperative = "imperative"
|
||||
ModalityQuestion = "question"
|
||||
)
|
||||
|
||||
// Interpretation is the deterministic reading of one claim sentence.
|
||||
type Interpretation struct {
|
||||
Proposition string `json:"proposition"` // the proposition as attributed (reporting frame removed)
|
||||
AttributedTo string `json:"attributed_to"` // whose stance this is
|
||||
Stance string `json:"stance"` // stance of AttributedTo towards Proposition
|
||||
SpeakerStance string `json:"speaker_stance"` // stance of the speaker when they report somebody else ("" = same)
|
||||
Certainty string `json:"certainty"`
|
||||
Modality string `json:"modality"`
|
||||
Negated bool `json:"negated"`
|
||||
Reported bool `json:"reported"`
|
||||
ReportedActor string `json:"reported_actor,omitempty"`
|
||||
Quoted bool `json:"quoted"`
|
||||
Hypothetical bool `json:"hypothetical"`
|
||||
Confidence float64 `json:"interpretation_confidence"` // how sure the reading itself is
|
||||
Notes []string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
reportVerbs = `(?:says?|said|saying|thinks?|thought|thinking|believes?|believed|claims?|claimed|claiming|told (?:me|us)|telling (?:me|us)|reckons?|insists?|insisting|mentioned|reported|argues?|arguing|swears?|maintains?)`
|
||||
reReported = regexp.MustCompile(`(?i)^(?:apparently[, ]+|well[, ]+)?([A-Z][\w'.-]*(?: [A-Z][\w'.-]*)?)\s+` + reportVerbs + `\s+(?:that\s+)?(.+)$`)
|
||||
reQuoted = regexp.MustCompile(`(?i)^([A-Z][\w'.-]*(?: [A-Z][\w'.-]*)?)\s+` + reportVerbs + `[:,]?\s*["“](.+?)["”]\.?$`)
|
||||
reDiffuse = regexp.MustCompile(`(?i)^(?:people|everyone|everybody|they|folks|some people|others|the team|the internet)\s+(?:keep |keeps |always |all )?` + reportVerbs + `\s+(?:that\s+)?(.+)$`)
|
||||
reHypothet = regexp.MustCompile(`(?i)^(?:imagine|suppose|supposing|assume|assuming|let'?s say|what if|hypothetically|pretend|say)\b[,:]?\s*(?:that\s+)?(.+)$`)
|
||||
reConditional = regexp.MustCompile(`(?i)^if\s+(.+?),\s*(?:then\s+)?(.+)$`)
|
||||
reDisagree = regexp.MustCompile(`(?i)^(.+?)[,;]?\s+(?:but|though|although)\s+(?:i|we)\s+(?:don'?t|do not|didn'?t|can'?t|cannot|never)\s+(?:believe|buy|trust|think so|agree)(?:\s+(?:her|him|them|it|that|this))?\.?$`)
|
||||
reDoubtSuffix = regexp.MustCompile(`(?i)^(.+?)[,;]?\s+(?:which|and that|but that)\s+(?:is|was|seems)\s+(?:nonsense|rubbish|wrong|false|unlikely|doubtful)\.?$`)
|
||||
reNegation = regexp.MustCompile(`(?i)\b(?:not|no|never|isn't|isnt|aren't|wasn't|weren't|doesn't|don't|didn't|cannot|can't|won't|wouldn't|shouldn't|nobody|nothing|none)\b`)
|
||||
reEmphatic = regexp.MustCompile(`(?i)\b(?:definitely|certainly|absolutely|without doubt|i'?m (?:sure|certain)|for sure|no doubt|clearly)\b`)
|
||||
reHedge = regexp.MustCompile(`(?i)\b(?:i think|i believe|maybe|perhaps|possibly|might|probably|not sure|i guess|seems|i suspect|apparently|likely|sort of|kind of)\b`)
|
||||
reSarcasm = regexp.MustCompile(`(?i)^(?:oh (?:yes|yeah|sure|right)|yeah,? right|sure,? and|of course|great,)\b|\b(?:absolute|total|utter|complete)\s+(?:genius|hero|legend|star|expert|joy|delight)\b|\b(?:as always|as usual|what a surprise|shock horror)\b`)
|
||||
reImperative = regexp.MustCompile(`(?i)^(?:please\s+)?(?:do|make|run|go|check|tell|show|remember|note|delete|fetch|read|stop|start|send|install)\b`)
|
||||
)
|
||||
|
||||
// Interpret reads one claim sentence spoken by speaker.
|
||||
func Interpret(speaker, sentence string) Interpretation {
|
||||
s := strings.TrimSpace(sentence)
|
||||
s = strings.TrimRight(s, " .!")
|
||||
in := Interpretation{Proposition: s, AttributedTo: speaker, Stance: StanceAsserts, Certainty: CertaintyAsserted, Modality: ModalityFactual, Confidence: 0.85}
|
||||
lower := strings.ToLower(s)
|
||||
if strings.HasSuffix(sentence, "?") {
|
||||
in.Stance, in.Modality, in.Confidence = StanceQuestions, ModalityQuestion, 0.9
|
||||
return in
|
||||
}
|
||||
if reImperative.MatchString(s) {
|
||||
in.Modality, in.Stance, in.Confidence = ModalityImperative, StanceSupposes, 0.7
|
||||
in.Notes = append(in.Notes, "imperative: a request, not a claim about the world")
|
||||
return in
|
||||
}
|
||||
// Hypotheticals and conditionals create no commitment.
|
||||
if m := reHypothet.FindStringSubmatch(s); m != nil {
|
||||
in.Proposition, in.Hypothetical, in.Modality, in.Stance, in.Confidence = strings.TrimSpace(m[1]), true, ModalityHypothetical, StanceSupposes, 0.85
|
||||
in.Notes = append(in.Notes, "hypothetical framing; no commitment to the proposition")
|
||||
in.Negated = reNegation.MatchString(in.Proposition)
|
||||
return in
|
||||
}
|
||||
if m := reConditional.FindStringSubmatch(s); m != nil {
|
||||
in.Hypothetical, in.Modality, in.Stance, in.Confidence = true, ModalityHypothetical, StanceSupposes, 0.7
|
||||
in.Notes = append(in.Notes, "conditional; the consequent is asserted only under the antecedent")
|
||||
return in
|
||||
}
|
||||
// Disagreement with reported speech: "Sarah says X, but I don't believe her".
|
||||
if m := reDisagree.FindStringSubmatch(s); m != nil {
|
||||
inner := Interpret(speaker, m[1])
|
||||
inner.SpeakerStance = StanceDoubts
|
||||
inner.Notes = append(inner.Notes, "speaker distances themselves from the reported claim")
|
||||
if !inner.Reported {
|
||||
// "X, but I don't believe it": the speaker relays a claim without an author.
|
||||
inner.Reported, inner.ReportedActor, inner.AttributedTo, inner.Modality = true, "unspecified", "unspecified", ModalityReported
|
||||
inner.Confidence = minf(inner.Confidence, 0.6)
|
||||
}
|
||||
return inner
|
||||
}
|
||||
if m := reDoubtSuffix.FindStringSubmatch(s); m != nil {
|
||||
inner := Interpret(speaker, m[1])
|
||||
inner.SpeakerStance = StanceDoubts
|
||||
inner.Notes = append(inner.Notes, "speaker rejects the claim")
|
||||
return inner
|
||||
}
|
||||
// Quoted and reported speech attribute the stance to the person quoted.
|
||||
if m := reQuoted.FindStringSubmatch(s); m != nil {
|
||||
in.ReportedActor, in.Proposition = m[1], strings.TrimSpace(m[2])
|
||||
in.Reported, in.Quoted, in.Modality, in.AttributedTo, in.Confidence = true, true, ModalityQuoted, m[1], 0.8
|
||||
in.SpeakerStance = StanceReports
|
||||
in.Negated = reNegation.MatchString(in.Proposition)
|
||||
in.Certainty = certaintyOf(in.Proposition)
|
||||
return in
|
||||
}
|
||||
if m := reDiffuse.FindStringSubmatch(s); m != nil {
|
||||
in.ReportedActor, in.Proposition = "unspecified people", strings.TrimSpace(m[1])
|
||||
in.Reported, in.Modality, in.AttributedTo, in.Confidence = true, ModalityReported, "unspecified people", 0.6
|
||||
in.SpeakerStance, in.Certainty = StanceReports, CertaintyUncertain
|
||||
in.Notes = append(in.Notes, "diffuse attribution; nobody in particular stands behind it")
|
||||
in.Negated = reNegation.MatchString(in.Proposition)
|
||||
return in
|
||||
}
|
||||
if m := reReported.FindStringSubmatch(s); m != nil && !strings.EqualFold(m[1], "I") {
|
||||
in.ReportedActor, in.Proposition = m[1], strings.TrimSpace(m[2])
|
||||
in.Reported, in.Modality, in.AttributedTo, in.Confidence = true, ModalityReported, m[1], 0.75
|
||||
in.SpeakerStance = StanceReports
|
||||
in.Negated = reNegation.MatchString(in.Proposition)
|
||||
in.Certainty = certaintyOf(in.Proposition)
|
||||
if strings.Contains(strings.ToLower(m[0]), "think") || strings.Contains(strings.ToLower(m[0]), "believe") {
|
||||
in.Notes = append(in.Notes, "reports a belief, not an observation")
|
||||
}
|
||||
return in
|
||||
}
|
||||
// Plain statement by the speaker.
|
||||
in.Negated = reNegation.MatchString(lower)
|
||||
in.Certainty = certaintyOf(s)
|
||||
if reSarcasm.MatchString(s) {
|
||||
in.Confidence = 0.4
|
||||
in.Notes = append(in.Notes, "possible sarcasm or irony; literal reading uncertain")
|
||||
}
|
||||
if in.Negated && in.Stance == StanceAsserts {
|
||||
in.Notes = append(in.Notes, "negated proposition asserted as stated")
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
func certaintyOf(s string) string {
|
||||
switch {
|
||||
case reEmphatic.MatchString(s):
|
||||
return CertaintyEmphatic
|
||||
case reHedge.MatchString(s):
|
||||
return CertaintyHedged
|
||||
}
|
||||
return CertaintyAsserted
|
||||
}
|
||||
|
||||
// CreatesBelief reports whether an interpretation should feed a proposition
|
||||
// belief at all. Questions, imperatives and hypotheticals never do.
|
||||
func (in Interpretation) CreatesBelief() bool {
|
||||
switch in.Modality {
|
||||
case ModalityQuestion, ModalityImperative, ModalityHypothetical:
|
||||
return false
|
||||
}
|
||||
return in.Stance == StanceAsserts && strings.TrimSpace(in.Proposition) != ""
|
||||
}
|
||||
|
||||
// EvidenceWeight is the weight the assertion contributes as evidence for
|
||||
// its proposition, before source credibility: how firmly the attributed
|
||||
// actor stood behind it, discounted by how surely the reading itself holds.
|
||||
func (in Interpretation) EvidenceWeight() float64 {
|
||||
w := 0.5
|
||||
switch in.Certainty {
|
||||
case CertaintyEmphatic:
|
||||
w = 0.55
|
||||
case CertaintyHedged:
|
||||
w = 0.3
|
||||
case CertaintyUncertain:
|
||||
w = 0.2
|
||||
}
|
||||
if in.Reported {
|
||||
w *= 0.5 // second-hand: the mind did not hear the attributed actor
|
||||
}
|
||||
return round(w * in.Confidence)
|
||||
}
|
||||
|
||||
// DoubtWeight is the counter-weight the speaker's expressed doubt adds.
|
||||
func (in Interpretation) DoubtWeight() float64 {
|
||||
if in.SpeakerStance != StanceDoubts {
|
||||
return 0
|
||||
}
|
||||
return round(0.3 * in.Confidence)
|
||||
}
|
||||
|
||||
func minf(a, b float64) float64 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func round(v float64) float64 { return float64(int(v*1000+0.5)) / 1000 }
|
||||
@@ -0,0 +1,62 @@
|
||||
package epistemics
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestInterpretSpeechPatterns(t *testing.T) {
|
||||
cases := []struct {
|
||||
name, sentence string
|
||||
wantAttributed, wantStance string
|
||||
wantModality string
|
||||
wantReported, wantHypothet bool
|
||||
wantNegated, wantBelief bool
|
||||
wantSpeakerStance string
|
||||
maxConfidence, minConfidence float64
|
||||
}{
|
||||
{"direct factual", "Steve steals laptops.", "andy", StanceAsserts, ModalityFactual, false, false, false, true, "", 1, 0.8},
|
||||
{"reported belief", "Alice thinks the server is compromised.", "Alice", StanceAsserts, ModalityReported, true, false, false, true, StanceReports, 1, 0.6},
|
||||
{"diffuse report", "People keep saying PostgreSQL corrupts data.", "unspecified people", StanceAsserts, ModalityReported, true, false, false, true, StanceReports, 0.7, 0.4},
|
||||
{"hypothetical", "Imagine Bob murdered somebody.", "andy", StanceSupposes, ModalityHypothetical, false, true, false, false, "", 1, 0.5},
|
||||
{"conditional", "If the disk fills up, the database stops accepting writes.", "andy", StanceSupposes, ModalityHypothetical, false, true, false, false, "", 1, 0.5},
|
||||
{"disagreement", "Sarah says the network is down, but I don't believe her.", "Sarah", StanceAsserts, ModalityReported, true, false, false, true, StanceDoubts, 1, 0.5},
|
||||
{"quoted", `Dave said "the backups finished at 03:00".`, "Dave", StanceAsserts, ModalityQuoted, true, false, false, true, StanceReports, 1, 0.6},
|
||||
{"negated", "Atlas is not the primary database host.", "andy", StanceAsserts, ModalityFactual, false, false, true, true, "", 1, 0.8},
|
||||
{"sarcasm suspected", "Oh yes, Dave is an absolute genius.", "andy", StanceAsserts, ModalityFactual, false, false, false, true, "", 0.5, 0},
|
||||
{"question", "Is atlas the primary host?", "andy", StanceQuestions, ModalityQuestion, false, false, false, false, "", 1, 0},
|
||||
{"imperative", "Please check the disk on server-01.", "andy", StanceSupposes, ModalityImperative, false, false, false, false, "", 1, 0},
|
||||
{"hedged", "I think the migration caused the outage.", "andy", StanceAsserts, ModalityFactual, false, false, false, true, "", 1, 0.8},
|
||||
{"first person not reported", "I said the migration was fine.", "andy", StanceAsserts, ModalityFactual, false, false, false, true, "", 1, 0.8},
|
||||
}
|
||||
for _, c := range cases {
|
||||
in := Interpret("andy", c.sentence)
|
||||
if in.AttributedTo != c.wantAttributed || in.Stance != c.wantStance || in.Modality != c.wantModality || in.Reported != c.wantReported || in.Hypothetical != c.wantHypothet || in.Negated != c.wantNegated {
|
||||
t.Errorf("%s: %+v", c.name, in)
|
||||
}
|
||||
if in.CreatesBelief() != c.wantBelief {
|
||||
t.Errorf("%s: CreatesBelief=%v want %v", c.name, in.CreatesBelief(), c.wantBelief)
|
||||
}
|
||||
if in.SpeakerStance != c.wantSpeakerStance {
|
||||
t.Errorf("%s: speaker stance %q want %q", c.name, in.SpeakerStance, c.wantSpeakerStance)
|
||||
}
|
||||
if in.Confidence > c.maxConfidence || in.Confidence < c.minConfidence {
|
||||
t.Errorf("%s: interpretation confidence %.2f outside [%.2f, %.2f]", c.name, in.Confidence, c.minConfidence, c.maxConfidence)
|
||||
}
|
||||
}
|
||||
// Weights: direct > hedged > reported; sarcasm discounts; doubt counts against.
|
||||
direct, hedged, reported := Interpret("a", "The server is down."), Interpret("a", "I think the server is down."), Interpret("a", "Alice says the server is down.")
|
||||
if !(direct.EvidenceWeight() > hedged.EvidenceWeight() && hedged.EvidenceWeight() > reported.EvidenceWeight()*1.2) {
|
||||
t.Fatalf("weights: direct=%.3f hedged=%.3f reported=%.3f", direct.EvidenceWeight(), hedged.EvidenceWeight(), reported.EvidenceWeight())
|
||||
}
|
||||
if s := Interpret("a", "Oh yes, Dave is an absolute genius."); s.EvidenceWeight() >= direct.EvidenceWeight()/2 {
|
||||
t.Fatalf("sarcasm not discounted: %.3f", s.EvidenceWeight())
|
||||
}
|
||||
if d := Interpret("a", "Sarah says the network is down, but I don't believe her."); d.DoubtWeight() == 0 || d.EvidenceWeight() == 0 {
|
||||
t.Fatalf("disagreement should record both the reported claim and the doubt: %+v", d)
|
||||
}
|
||||
if h := Interpret("a", "Imagine Bob murdered somebody."); h.EvidenceWeight() != 0.5*0.85 && !h.Hypothetical {
|
||||
t.Fatal("hypothetical weight irrelevant but flag must be set")
|
||||
}
|
||||
// Reported speech keeps the proposition, not the frame.
|
||||
if r := Interpret("a", "Alice thinks the server is compromised."); r.Proposition != "the server is compromised" {
|
||||
t.Fatalf("proposition should drop the reporting frame: %q", r.Proposition)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package epistemics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/andydixon/noema/internal/storage"
|
||||
"github.com/andydixon/noema/pkg/ids"
|
||||
)
|
||||
|
||||
// Assertion is the append-only record that an actor took a stance.
|
||||
type Assertion struct {
|
||||
ID ids.ID `json:"id"`
|
||||
MindID ids.ID `json:"mind_id"`
|
||||
Actor string `json:"actor"`
|
||||
ActorKind string `json:"actor_kind"`
|
||||
AttributedTo string `json:"attributed_to"`
|
||||
Proposition string `json:"proposition"`
|
||||
Subject string `json:"subject"`
|
||||
Stance string `json:"stance"`
|
||||
Certainty string `json:"certainty"`
|
||||
Modality string `json:"modality"`
|
||||
Negated bool `json:"negated"`
|
||||
Reported bool `json:"reported"`
|
||||
ReportedActor string `json:"reported_actor,omitempty"`
|
||||
Quoted bool `json:"quoted"`
|
||||
Hypothetical bool `json:"hypothetical"`
|
||||
Interpretation float64 `json:"interpretation_confidence"`
|
||||
Notes []string `json:"notes"`
|
||||
SourceEvent ids.ID `json:"source_event,omitempty"`
|
||||
ConversationID ids.ID `json:"conversation_id,omitempty"`
|
||||
MessageID ids.ID `json:"message_id,omitempty"`
|
||||
Context string `json:"context,omitempty"`
|
||||
Trust string `json:"trust"`
|
||||
BeliefID ids.ID `json:"belief_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// New is the input to Record.
|
||||
type New struct {
|
||||
MindID ids.ID
|
||||
Actor string
|
||||
ActorKind string
|
||||
Interpretation Interpretation
|
||||
Subject string
|
||||
SourceEvent ids.ID
|
||||
ConversationID ids.ID
|
||||
MessageID ids.ID
|
||||
Context string
|
||||
Trust string
|
||||
BeliefID ids.ID
|
||||
}
|
||||
|
||||
// Store persists assertions.
|
||||
type Store struct{ db *pgxpool.Pool }
|
||||
|
||||
// NewStore creates a store.
|
||||
func NewStore(db *pgxpool.Pool) *Store { return &Store{db: db} }
|
||||
|
||||
const cols = `id, mind_id, actor, actor_kind, attributed_to, proposition, subject, stance, certainty, modality, negated, reported, reported_actor, quoted, hypothetical, interpretation_confidence, interpretation_notes, source_event, conversation_id, message_id, context, trust, belief_id, created_at`
|
||||
|
||||
// Record writes an assertion inside q.
|
||||
func (s *Store) Record(ctx context.Context, q storage.Querier, n New) (Assertion, error) {
|
||||
in := n.Interpretation
|
||||
a := Assertion{ID: ids.New(), MindID: n.MindID, Actor: n.Actor, ActorKind: firstNonEmpty(n.ActorKind, "user"), AttributedTo: firstNonEmpty(in.AttributedTo, n.Actor), Proposition: in.Proposition, Subject: n.Subject,
|
||||
Stance: in.Stance, Certainty: in.Certainty, Modality: in.Modality, Negated: in.Negated, Reported: in.Reported, ReportedActor: in.ReportedActor, Quoted: in.Quoted, Hypothetical: in.Hypothetical,
|
||||
Interpretation: in.Confidence, Notes: in.Notes, SourceEvent: n.SourceEvent, ConversationID: n.ConversationID, MessageID: n.MessageID, Context: n.Context, Trust: firstNonEmpty(n.Trust, "user"), BeliefID: n.BeliefID, CreatedAt: time.Now()}
|
||||
if a.Notes == nil {
|
||||
a.Notes = []string{}
|
||||
}
|
||||
if in.SpeakerStance != "" && in.SpeakerStance != a.Stance && a.AttributedTo != n.Actor {
|
||||
// The speaker's own stance is recorded separately so "Andy reports that
|
||||
// Sarah asserts X" and "Andy doubts X" are both retrievable.
|
||||
a.Notes = append(a.Notes, "speaker stance: "+in.SpeakerStance)
|
||||
}
|
||||
_, err := q.Exec(ctx, `INSERT INTO assertions (`+cols+`) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24)`,
|
||||
a.ID, a.MindID, a.Actor, a.ActorKind, a.AttributedTo, a.Proposition, a.Subject, a.Stance, a.Certainty, a.Modality, a.Negated, a.Reported, a.ReportedActor, a.Quoted, a.Hypothetical,
|
||||
a.Interpretation, a.Notes, nullID(a.SourceEvent), nullID(a.ConversationID), nullID(a.MessageID), a.Context, a.Trust, nullID(a.BeliefID), a.CreatedAt)
|
||||
return a, err
|
||||
}
|
||||
|
||||
// Filter narrows a listing.
|
||||
type Filter struct {
|
||||
AttributedTo string
|
||||
BeliefID ids.ID
|
||||
Stance string
|
||||
Limit int
|
||||
}
|
||||
|
||||
// List returns assertions for a mind, newest first.
|
||||
func (s *Store) List(ctx context.Context, mind ids.ID, f Filter) ([]Assertion, error) {
|
||||
if f.Limit <= 0 || f.Limit > 500 {
|
||||
f.Limit = 100
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM assertions WHERE mind_id=$1 AND ($2='' OR lower(attributed_to)=lower($2)) AND ($3::uuid IS NULL OR belief_id=$3) AND ($4='' OR stance=$4) ORDER BY created_at DESC LIMIT $5`,
|
||||
mind, f.AttributedTo, nullID(f.BeliefID), f.Stance, f.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Assertion
|
||||
for rows.Next() {
|
||||
a, err := scan(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Actors summarises who has asserted things to a mind.
|
||||
type ActorSummary struct {
|
||||
AttributedTo string `json:"attributed_to"`
|
||||
Assertions int `json:"assertions"`
|
||||
Reported int `json:"reported"`
|
||||
MeanClarity float64 `json:"mean_interpretation_confidence"`
|
||||
}
|
||||
|
||||
// Actors lists asserting actors for a mind.
|
||||
func (s *Store) Actors(ctx context.Context, mind ids.ID) ([]ActorSummary, error) {
|
||||
rows, err := s.db.Query(ctx, `SELECT attributed_to, count(*), count(*) FILTER (WHERE reported), avg(interpretation_confidence) FROM assertions WHERE mind_id=$1 GROUP BY attributed_to ORDER BY count(*) DESC LIMIT 200`, mind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []ActorSummary
|
||||
for rows.Next() {
|
||||
var a ActorSummary
|
||||
if err := rows.Scan(&a.AttributedTo, &a.Assertions, &a.Reported, &a.MeanClarity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func scan(row pgx.Row) (Assertion, error) {
|
||||
var a Assertion
|
||||
var src, conv, msg, bel *string
|
||||
if err := row.Scan(&a.ID, &a.MindID, &a.Actor, &a.ActorKind, &a.AttributedTo, &a.Proposition, &a.Subject, &a.Stance, &a.Certainty, &a.Modality, &a.Negated, &a.Reported, &a.ReportedActor, &a.Quoted, &a.Hypothetical,
|
||||
&a.Interpretation, &a.Notes, &src, &conv, &msg, &a.Context, &a.Trust, &bel, &a.CreatedAt); err != nil {
|
||||
return a, storage.Translate(err)
|
||||
}
|
||||
a.SourceEvent, a.ConversationID, a.MessageID, a.BeliefID = idOf(src), idOf(conv), idOf(msg), idOf(bel)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func nullID(id ids.ID) *string {
|
||||
if id.IsZero() {
|
||||
return nil
|
||||
}
|
||||
s := string(id)
|
||||
return &s
|
||||
}
|
||||
|
||||
func idOf(s *string) ids.ID {
|
||||
if s == nil {
|
||||
return ids.Nil
|
||||
}
|
||||
return ids.ID(*s)
|
||||
}
|
||||
|
||||
func firstNonEmpty(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/andydixon/noema/internal/capabilities"
|
||||
"github.com/andydixon/noema/internal/cognition"
|
||||
"github.com/andydixon/noema/internal/conversation"
|
||||
"github.com/andydixon/noema/internal/epistemics"
|
||||
evstore "github.com/andydixon/noema/internal/events"
|
||||
"github.com/andydixon/noema/internal/goals"
|
||||
"github.com/andydixon/noema/internal/graph"
|
||||
@@ -53,7 +54,7 @@ func NewHarness(t *testing.T) *Harness {
|
||||
}
|
||||
reg := brain.NewRegistry().AddProcess(cognition.Attend{}).AddProcess(cognition.Retrieve{}).AddProcess(cognition.Associate{}).AddProcess(cognition.Perceive{}).
|
||||
AddProcess(cognition.GoalMonitor{}).AddProcess(cognition.Curiosity{}).AddProcess(cognition.Metacognition{}).AddProcess(cognition.Act{})
|
||||
cognition.Handlers{Memory: mem, Beliefs: bs, Conversations: conversation.NewStore(pool), Graph: graph.NewStore(pool)}.Register(reg)
|
||||
cognition.Handlers{Memory: mem, Beliefs: bs, Conversations: conversation.NewStore(pool), Graph: graph.NewStore(pool), Assertions: epistemics.NewStore(pool)}.Register(reg)
|
||||
cfg := brain.Config{DB: pool, Events: es, Minds: ms, Registry: reg, Pool: scheduler.NewPool(2), Log: log, Metrics: observability.NewMetrics(),
|
||||
Services: brain.Services{Events: es, Memory: mem, Beliefs: bs, Goals: goals.NewStore(pool), Motivation: motivation.NewStore(pool), Predictions: predictions.NewStore(pool), Capabilities: caps}}
|
||||
return &Harness{Pool: pool, Events: es, Minds: ms, Snaps: &snapshots.Store{DB: pool, Events: es, Minds: ms}, Brain: cfg}
|
||||
|
||||
Reference in New Issue
Block a user