feat: add beliefs with evidence-derived confidence, contradictions and corrections
Phase 6. Beliefs are propositions with a prior set by epistemic kind and a confidence computed from typed evidence: sigmoid(logit(prior) + slope × (support × diversity − opposition)), where support from a single source is discounted so repetition cannot manufacture certainty, and bounds keep the mind away from absolute certainty. Every evaluation stores its breakdown and appends a version row protected by the append-only trigger; the belief page shows the arithmetic, evidence for and against, alternatives, contradictions and history. Contradiction detection is deterministic first (negation plus token overlap): a hit emits contradiction_detected, links both beliefs and makes each counter-evidence for the other so both trend to contested. Operators can assert beliefs (their statement is the evidence), add evidence, mark a belief wrong (pinned rejected, history kept) and forbid an inference; do-not-infer corrections block matching hypotheses. Retracting a memory retracts the evidence that cited it and re-evaluates the beliefs concerned. The associate process proposes low-confidence causal hypotheses from co-occurring notable observations, and the hypothesis handler turns admitted ones into beliefs with automatic alternatives, closing demo scenarios 1 and 2 without any model call.
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/andydixon/noema/internal/api"
|
||||
"github.com/andydixon/noema/internal/auth"
|
||||
"github.com/andydixon/noema/internal/beliefs"
|
||||
"github.com/andydixon/noema/pkg/concepts"
|
||||
"github.com/andydixon/noema/pkg/events"
|
||||
"github.com/andydixon/noema/pkg/ids"
|
||||
)
|
||||
|
||||
func (u *UI) beliefRoutes(m *http.ServeMux) {
|
||||
m.Handle("GET /minds/{mind}/beliefs", u.require(auth.RoleObserver, u.beliefsPage))
|
||||
m.Handle("POST /minds/{mind}/beliefs", u.require(auth.RoleOperator, u.beliefAssert))
|
||||
m.Handle("GET /minds/{mind}/beliefs/{id}", u.require(auth.RoleObserver, u.beliefDetail))
|
||||
m.Handle("POST /minds/{mind}/beliefs/{id}/evidence", u.require(auth.RoleOperator, u.beliefEvidence))
|
||||
m.Handle("POST /minds/{mind}/beliefs/{id}/alternatives", u.require(auth.RoleOperator, u.beliefAlternative))
|
||||
m.Handle("POST /minds/{mind}/beliefs/{id}/retract", u.require(auth.RoleOperator, u.beliefRetract))
|
||||
m.Handle("POST /minds/{mind}/corrections", u.require(auth.RoleOperator, u.correctionCreate))
|
||||
}
|
||||
|
||||
func (u *UI) beliefsPage(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := u.mind(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
f := api.ParseBeliefFilter(q)
|
||||
f.Limit = 200
|
||||
list, err := u.deps.Beliefs.List(r.Context(), m.ID, f)
|
||||
if err != nil {
|
||||
u.errorPage(w, r, 500, "Could not list beliefs.")
|
||||
return
|
||||
}
|
||||
flat := map[string]string{}
|
||||
for k := range q {
|
||||
flat[k] = q.Get(k)
|
||||
}
|
||||
stats, _ := u.deps.Beliefs.Stats(r.Context(), m.ID)
|
||||
corr, _ := u.deps.Beliefs.Corrections(r.Context(), m.ID, "")
|
||||
u.render(w, r, "beliefs", m.DisplayName+" · Beliefs", map[string]any{"Mind": m, "Beliefs": list, "Q": flat, "Stats": stats, "Corrections": corr})
|
||||
}
|
||||
|
||||
func (u *UI) beliefAssert(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := u.mind(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
r.ParseForm() //nolint:errcheck
|
||||
weight, _ := strconv.ParseFloat(r.PostFormValue("weight"), 64)
|
||||
res, err := u.deps.Beliefs.Create(r.Context(), u.deps.DB, u.emitF(), beliefs.New{MindID: m.ID, Proposition: r.PostFormValue("proposition"), Subject: r.PostFormValue("subject"),
|
||||
Epistemic: concepts.Epistemic(r.PostFormValue("epistemic")), Source: "operator",
|
||||
Evidence: []beliefs.EvidenceInput{{Direction: "for", Kind: "operator", Source: "operator:" + username(r), Weight: weight, Credibility: events.TrustOperator.Credibility(), Note: "asserted by operator " + username(r)}}})
|
||||
if err != nil {
|
||||
u.flash(w, r, "error", err.Error(), "/minds/"+m.Slug+"/beliefs")
|
||||
return
|
||||
}
|
||||
u.deps.Audit.Log(r.Context(), "belief.create", "belief", res.Belief.ID.String(), map[string]any{"mind": m.Slug, "proposition": res.Belief.Proposition})
|
||||
u.flash(w, r, "ok", "Belief recorded with confidence "+strconv.FormatFloat(res.Belief.Confidence, 'f', 2, 64)+".", "/minds/"+m.Slug+"/beliefs/"+res.Belief.ID.String())
|
||||
}
|
||||
|
||||
func (u *UI) beliefDetail(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := u.mind(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, err := ids.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
u.errorPage(w, r, 404, "No such belief.")
|
||||
return
|
||||
}
|
||||
d, err := u.belF(r, m.ID, id)
|
||||
if err != nil {
|
||||
u.errorPage(w, r, 404, "No such belief.")
|
||||
return
|
||||
}
|
||||
u.render(w, r, "belief_detail", "Belief", map[string]any{"Mind": m, "Detail": d})
|
||||
}
|
||||
|
||||
func (u *UI) beliefEvidence(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := u.mind(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, err := ids.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
u.errorPage(w, r, 404, "No such belief.")
|
||||
return
|
||||
}
|
||||
r.ParseForm() //nolint:errcheck
|
||||
weight, _ := strconv.ParseFloat(r.PostFormValue("weight"), 64)
|
||||
note := r.PostFormValue("note")
|
||||
if _, err := u.deps.Beliefs.AddEvidence(r.Context(), u.deps.DB, u.emitF(), m.ID, id, beliefs.EvidenceInput{Direction: r.PostFormValue("direction"), Kind: "operator", Source: "operator:" + username(r), Weight: weight, Credibility: events.TrustOperator.Credibility(), Note: note}, "operator evidence: "+note, ids.Nil, ids.Nil); err != nil {
|
||||
u.flash(w, r, "error", err.Error(), "/minds/"+m.Slug+"/beliefs/"+id.String())
|
||||
return
|
||||
}
|
||||
u.deps.Audit.Log(r.Context(), "belief.evidence", "belief", id.String(), map[string]any{"direction": r.PostFormValue("direction"), "note": note})
|
||||
u.flash(w, r, "ok", "Evidence added and confidence re-evaluated.", "/minds/"+m.Slug+"/beliefs/"+id.String())
|
||||
}
|
||||
|
||||
func (u *UI) beliefAlternative(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := u.mind(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, err := ids.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
u.errorPage(w, r, 404, "No such belief.")
|
||||
return
|
||||
}
|
||||
if _, err := u.deps.Beliefs.AddAlternative(r.Context(), m.ID, id, r.PostFormValue("proposition"), "operator"); err != nil {
|
||||
u.flash(w, r, "error", err.Error(), "/minds/"+m.Slug+"/beliefs/"+id.String())
|
||||
return
|
||||
}
|
||||
u.flash(w, r, "ok", "Alternative recorded.", "/minds/"+m.Slug+"/beliefs/"+id.String())
|
||||
}
|
||||
|
||||
func (u *UI) beliefRetract(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := u.mind(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, err := ids.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
u.errorPage(w, r, 404, "No such belief.")
|
||||
return
|
||||
}
|
||||
reason := r.PostFormValue("reason")
|
||||
dni := r.PostFormValue("do_not_infer") == "1"
|
||||
if _, err := u.deps.Beliefs.Retract(r.Context(), u.emitF(), m.ID, id, reason, username(r), dni); err != nil {
|
||||
u.flash(w, r, "error", err.Error(), "/minds/"+m.Slug+"/beliefs/"+id.String())
|
||||
return
|
||||
}
|
||||
u.deps.Audit.Log(r.Context(), "belief.retract", "belief", id.String(), map[string]any{"reason": reason, "do_not_infer": dni})
|
||||
u.flash(w, r, "ok", "Belief marked wrong. Its history is preserved.", "/minds/"+m.Slug+"/beliefs/"+id.String())
|
||||
}
|
||||
|
||||
func (u *UI) correctionCreate(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := u.mind(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
stmt, reason := r.PostFormValue("statement"), r.PostFormValue("reason")
|
||||
evs, err := u.deps.Events.Append(r.Context(), events.New{MindID: m.ID, Type: events.OperatorAction, Source: "operator", Summary: "Operator " + username(r) + " forbade inference: " + stmt,
|
||||
Salience: 0.6, Confidence: 1, Metadata: map[string]any{"action": "correction.do_not_infer", "actor": username(r)}, Payload: map[string]any{"statement": stmt, "reason": reason}})
|
||||
if err != nil {
|
||||
u.flash(w, r, "error", "Could not record.", "/minds/"+m.Slug+"/beliefs")
|
||||
return
|
||||
}
|
||||
if _, err := u.deps.Beliefs.AddCorrection(r.Context(), u.deps.DB, m.ID, beliefs.Correction{Kind: "do_not_infer", Statement: stmt, Reason: reason, CreatedBy: username(r), EventID: evs[0].ID}); err != nil {
|
||||
u.flash(w, r, "error", err.Error(), "/minds/"+m.Slug+"/beliefs")
|
||||
return
|
||||
}
|
||||
u.deps.Audit.Log(r.Context(), "correction.create", "mind", m.ID.String(), map[string]any{"kind": "do_not_infer", "statement": stmt})
|
||||
u.flash(w, r, "ok", "The mind will not infer that again.", "/minds/"+m.Slug+"/beliefs")
|
||||
}
|
||||
@@ -120,6 +120,11 @@ func (u *UI) memoryRetract(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
u.deps.Events.OperatorAction(r.Context(), m.ID, username(r), "memory.retract", "Operator "+username(r)+" marked a memory inaccurate: "+reason, map[string]any{"memory_id": id, "reason": reason})
|
||||
u.deps.Audit.Log(r.Context(), "memory.retract", "memory", id.String(), map[string]any{"reason": reason})
|
||||
if affected, err := u.deps.Beliefs.RetractEvidence(r.Context(), u.deps.DB, m.ID, "memory", string(id)); err == nil {
|
||||
for _, bid := range affected {
|
||||
u.deps.Beliefs.Reevaluate(r.Context(), u.deps.DB, u.emitF(), m.ID, bid, "supporting memory retracted by operator") //nolint:errcheck
|
||||
}
|
||||
}
|
||||
u.flash(w, r, "ok", "Memory marked inaccurate. It remains in history.", "/minds/"+m.Slug+"/memory/"+id.String())
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/andydixon/noema/internal/auth"
|
||||
"github.com/andydixon/noema/internal/beliefs"
|
||||
"github.com/andydixon/noema/internal/minds"
|
||||
"github.com/andydixon/noema/internal/storage"
|
||||
"github.com/andydixon/noema/pkg/ids"
|
||||
@@ -98,7 +99,9 @@ func (u *UI) mindOverview(w http.ResponseWriter, r *http.Request) {
|
||||
sm, _ := u.deps.Minds.GetSelfModel(r.Context(), m.ID)
|
||||
st, _ := u.deps.Events.Stats(r.Context(), m.ID, time.Now().Add(-24*time.Hour))
|
||||
ms, _ := u.deps.Memory.Stats(r.Context(), m.ID)
|
||||
u.render(w, r, "mind", m.DisplayName, map[string]any{"Mind": m, "Brief": m.Personality.Brief(), "SelfModel": sm, "Stats": st, "MemStats": ms})
|
||||
bs, _ := u.deps.Beliefs.Stats(r.Context(), m.ID)
|
||||
top, _ := u.deps.Beliefs.List(r.Context(), m.ID, beliefs.Filter{Limit: 8})
|
||||
u.render(w, r, "mind", m.DisplayName, map[string]any{"Mind": m, "Brief": m.Personality.Brief(), "SelfModel": sm, "Stats": st, "MemStats": ms, "BeliefStats": bs, "Beliefs": top})
|
||||
}
|
||||
|
||||
func (u *UI) mindStatus(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+9
-2
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/andydixon/noema/internal/app"
|
||||
"github.com/andydixon/noema/internal/attention"
|
||||
"github.com/andydixon/noema/internal/auth"
|
||||
"github.com/andydixon/noema/internal/beliefs"
|
||||
"github.com/andydixon/noema/internal/minds"
|
||||
"github.com/andydixon/noema/internal/version"
|
||||
"github.com/andydixon/noema/pkg/concepts"
|
||||
@@ -36,6 +37,8 @@ type UI struct {
|
||||
ctxF func(r *http.Request, id ids.ID) (api.EventContext, error) // event forensic context, shared with api
|
||||
wsF func(r *http.Request, m minds.Mind) (api.WorkspaceView, error)
|
||||
memF func(r *http.Request, mind, id ids.ID) (api.MemoryDetail, error)
|
||||
belF func(r *http.Request, mind, id ids.ID) (api.BeliefDetail, error)
|
||||
emitF func() beliefs.Emitter
|
||||
}
|
||||
|
||||
// View is the data passed to every template.
|
||||
@@ -56,7 +59,7 @@ const flashCookie = "noema_flash"
|
||||
|
||||
// New parses templates. In development, when ./web exists on disk, templates
|
||||
// and static files are read from disk so edits show without a rebuild.
|
||||
func New(deps app.Deps, statusF func(r *http.Request) any, ctxF func(r *http.Request, id ids.ID) (api.EventContext, error), wsF func(r *http.Request, m minds.Mind) (api.WorkspaceView, error), memF func(r *http.Request, mind, id ids.ID) (api.MemoryDetail, error)) (*UI, error) {
|
||||
func New(deps app.Deps, statusF func(r *http.Request) any, ctxF func(r *http.Request, id ids.ID) (api.EventContext, error), wsF func(r *http.Request, m minds.Mind) (api.WorkspaceView, error), memF func(r *http.Request, mind, id ids.ID) (api.MemoryDetail, error), belF func(r *http.Request, mind, id ids.ID) (api.BeliefDetail, error), emitF func() beliefs.Emitter) (*UI, error) {
|
||||
tmplFS, staticFS := web.Templates(), web.Static()
|
||||
if !deps.Config.Production() {
|
||||
if st, err := os.Stat("web/templates"); err == nil && st.IsDir() {
|
||||
@@ -64,7 +67,7 @@ func New(deps app.Deps, statusF func(r *http.Request) any, ctxF func(r *http.Req
|
||||
deps.Log.Info("ui: serving templates and static files from ./web (development)")
|
||||
}
|
||||
}
|
||||
u := &UI{deps: deps, static: staticFS, statusF: statusF, ctxF: ctxF, wsF: wsF, memF: memF}
|
||||
u := &UI{deps: deps, static: staticFS, statusF: statusF, ctxF: ctxF, wsF: wsF, memF: memF, belF: belF, emitF: emitF}
|
||||
if err := u.parse(tmplFS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -94,6 +97,9 @@ func (u *UI) parse(fsys fs.FS) error {
|
||||
"memoryKinds": func() []concepts.MemoryKind { return concepts.AllMemoryKinds },
|
||||
"epistemicKinds": func() []concepts.Epistemic { return concepts.AllEpistemic },
|
||||
"relations": func() []concepts.Relation { return concepts.AllRelations },
|
||||
"beliefStatuses": func() []beliefs.Status {
|
||||
return []beliefs.Status{beliefs.StatusHypothesis, beliefs.StatusTentative, beliefs.StatusLikely, beliefs.StatusAccepted, beliefs.StatusContested, beliefs.StatusRejected, beliefs.StatusSuperseded}
|
||||
},
|
||||
"trunc": func(s string, n int) string {
|
||||
if len(s) > n {
|
||||
return s[:n] + "…"
|
||||
@@ -246,6 +252,7 @@ func (u *UI) Register(m *http.ServeMux) {
|
||||
u.mindRoutes(m)
|
||||
u.eventRoutes(m)
|
||||
u.memoryRoutes(m)
|
||||
u.beliefRoutes(m)
|
||||
|
||||
for _, p := range []struct{ path, title, note string }{
|
||||
{"/approvals", "Approvals", "The approval centre arrives with the capability system."},
|
||||
|
||||
Reference in New Issue
Block a user