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")
|
||||
|
||||
Reference in New Issue
Block a user