The target is a same-origin path built from a stored mind slug and a parsed UUID.
549 lines
18 KiB
Go
549 lines
18 KiB
Go
package ui
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/andydixon/noema/internal/agency"
|
|
"github.com/andydixon/noema/internal/auth"
|
|
evstore "github.com/andydixon/noema/internal/events"
|
|
"github.com/andydixon/noema/internal/experiments"
|
|
"github.com/andydixon/noema/internal/planning"
|
|
"github.com/andydixon/noema/internal/research"
|
|
"github.com/andydixon/noema/internal/volition"
|
|
"github.com/andydixon/noema/pkg/events"
|
|
"github.com/andydixon/noema/pkg/ids"
|
|
)
|
|
|
|
func (u *UI) volitionRoutes(m *http.ServeMux) {
|
|
m.Handle("GET /minds/{mind}/wants", u.require(auth.RoleObserver, u.wantsPage))
|
|
m.Handle("GET /minds/{mind}/intentions", u.require(auth.RoleObserver, u.intentionsPage))
|
|
m.Handle("GET /minds/{mind}/intentions/{id}", u.require(auth.RoleObserver, u.intentionPage))
|
|
m.Handle("POST /minds/{mind}/intentions/{id}/abandon", u.require(auth.RoleOperator, u.intentionAbandon))
|
|
m.Handle("GET /intentions/{id}", u.require(auth.RoleObserver, u.intentionRedirect))
|
|
m.Handle("GET /minds/{mind}/commitments", u.require(auth.RoleObserver, u.commitmentsPage))
|
|
m.Handle("POST /minds/{mind}/commitments", u.require(auth.RoleOperator, u.commitmentCreate))
|
|
m.Handle("POST /minds/{mind}/commitments/{id}/close", u.require(auth.RoleOperator, u.commitmentClose))
|
|
m.Handle("GET /minds/{mind}/plans", u.require(auth.RoleObserver, u.plansPage))
|
|
m.Handle("GET /minds/{mind}/plans/{id}", u.require(auth.RoleObserver, u.planPage))
|
|
m.Handle("POST /minds/{mind}/plans/{id}/cancel", u.require(auth.RoleOperator, u.planCancel))
|
|
m.Handle("POST /minds/{mind}/plans/{id}/steps/{step}/retry", u.require(auth.RoleOperator, u.planStepRetry))
|
|
m.Handle("GET /minds/{mind}/agency", u.require(auth.RoleObserver, u.agencyPage))
|
|
m.Handle("GET /minds/{mind}/why", u.require(auth.RoleObserver, u.whyPage))
|
|
m.Handle("GET /minds/{mind}/research", u.require(auth.RoleObserver, u.researchPage))
|
|
m.Handle("GET /research", u.require(auth.RoleObserver, u.researchIndex))
|
|
}
|
|
|
|
type wantRow struct {
|
|
volition.Want
|
|
Eff float64
|
|
Conflicts []conflictView
|
|
}
|
|
|
|
type conflictView struct {
|
|
Other, Kind, Outcome, Rule string
|
|
Preferred bool
|
|
At time.Time
|
|
}
|
|
|
|
func (u *UI) wantsPage(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
ctx := r.Context()
|
|
all, _ := u.deps.Volition.Wants(ctx, nil, m.ID, nil, 500)
|
|
byID := map[ids.ID]volition.Want{}
|
|
for _, x := range all {
|
|
byID[x.ID] = x
|
|
}
|
|
conflicts, _ := u.deps.Volition.Conflicts(ctx, m.ID, "", 500)
|
|
now := time.Now()
|
|
var active, closed []wantRow
|
|
for _, x := range all {
|
|
row := wantRow{Want: x, Eff: x.Effective(now)}
|
|
for _, c := range conflicts {
|
|
other := ids.Nil
|
|
switch x.ID {
|
|
case c.WantA:
|
|
other = c.WantB
|
|
case c.WantB:
|
|
other = c.WantA
|
|
default:
|
|
continue
|
|
}
|
|
rule, _ := c.Rationale["rule"].(string)
|
|
row.Conflicts = append(row.Conflicts, conflictView{Other: byID[other].Description, Kind: c.Kind, Outcome: c.Resolution + " (" + c.Status + ")", Rule: rule, Preferred: c.Preferred == x.ID, At: c.CreatedAt})
|
|
}
|
|
if x.Status == volition.WantActive {
|
|
active = append(active, row)
|
|
} else {
|
|
closed = append(closed, row)
|
|
}
|
|
}
|
|
sort.SliceStable(active, func(i, j int) bool { return active[i].Eff > active[j].Eff })
|
|
goalTitles := map[ids.ID]string{}
|
|
if gs, err := u.deps.Goals.List(ctx, m.ID, nil, 300); err == nil {
|
|
for _, g := range gs {
|
|
goalTitles[g.ID] = g.Title
|
|
}
|
|
}
|
|
u.render(w, r, "wants", m.DisplayName+" · Wants", map[string]any{"Mind": m, "Active": active, "Closed": closed, "Goals": goalTitles, "HalfLife": volition.WantHalfLife.String(), "Threshold": volition.WantThreshold})
|
|
}
|
|
|
|
func (u *UI) intentionsPage(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
all, _ := u.deps.Volition.Intentions(r.Context(), nil, m.ID, nil, 500)
|
|
groups := map[string][]volition.Intention{}
|
|
for _, it := range all {
|
|
groups[string(it.Status)] = append(groups[string(it.Status)], it)
|
|
}
|
|
order := []string{"active", "blocked", "suspended", "proposed", "fulfilled", "abandoned", "superseded"}
|
|
notFormed, _ := u.deps.Volition.Decisions(r.Context(), m.ID, volition.DecisionFilter{Kind: "non_formation", Limit: 30})
|
|
u.render(w, r, "intentions", m.DisplayName+" · Intentions", map[string]any{"Mind": m, "Groups": groups, "Order": order, "NotFormed": notFormed})
|
|
}
|
|
|
|
func (u *UI) intentionRedirect(w http.ResponseWriter, r *http.Request) {
|
|
id, err := ids.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
u.errorPage(w, r, http.StatusNotFound, "No such intention.")
|
|
return
|
|
}
|
|
it, err := u.deps.Volition.GetIntentionAny(r.Context(), id)
|
|
if err != nil {
|
|
u.errorPage(w, r, http.StatusNotFound, "No such intention.")
|
|
return
|
|
}
|
|
m, err := u.deps.Minds.Get(r.Context(), it.MindID)
|
|
if err != nil {
|
|
u.errorPage(w, r, http.StatusNotFound, "No such mind.")
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/minds/"+m.Slug+"/intentions/"+string(id), http.StatusSeeOther) // #nosec G710 -- same-origin path built from a stored slug and a parsed UUID
|
|
}
|
|
|
|
// dagLevel is one column of a plan's dependency graph.
|
|
type dagLevel struct {
|
|
Level int
|
|
Steps []planning.Step
|
|
}
|
|
|
|
func planLevels(p planning.Plan) []dagLevel {
|
|
level := map[string]int{}
|
|
byKey := map[string]planning.Step{}
|
|
for _, s := range p.Steps {
|
|
byKey[s.Key] = s
|
|
}
|
|
var depth func(k string, seen map[string]bool) int
|
|
depth = func(k string, seen map[string]bool) int {
|
|
if l, ok := level[k]; ok {
|
|
return l
|
|
}
|
|
if seen[k] {
|
|
return 0
|
|
}
|
|
seen[k] = true
|
|
l := 0
|
|
for _, d := range byKey[k].DependsOn {
|
|
if x := depth(d, seen) + 1; x > l {
|
|
l = x
|
|
}
|
|
}
|
|
level[k] = l
|
|
return l
|
|
}
|
|
maxL := 0
|
|
for _, s := range p.Steps {
|
|
if l := depth(s.Key, map[string]bool{}); l > maxL {
|
|
maxL = l
|
|
}
|
|
}
|
|
out := make([]dagLevel, maxL+1)
|
|
for i := range out {
|
|
out[i].Level = i
|
|
}
|
|
for _, s := range p.Steps {
|
|
out[level[s.Key]].Steps = append(out[level[s.Key]].Steps, s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (u *UI) intentionPage(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, http.StatusNotFound, "No such intention.")
|
|
return
|
|
}
|
|
d, err := u.studyF().IntentionDetail(r, m.ID, id)
|
|
if err != nil {
|
|
u.errorPage(w, r, http.StatusNotFound, "No such intention.")
|
|
return
|
|
}
|
|
formation, _ := json.MarshalIndent(d.Intention.Formation, "", " ")
|
|
var levels [][]dagLevel
|
|
for _, p := range d.Plans {
|
|
levels = append(levels, planLevels(p))
|
|
}
|
|
timeline, _ := u.studyF().AgencyTimeline(r, m.ID, 200, id)
|
|
u.render(w, r, "intention", m.DisplayName+" · Intention", map[string]any{"Mind": m, "D": d, "Formation": string(formation), "Levels": levels, "Timeline": timeline})
|
|
}
|
|
|
|
func (u *UI) intentionAbandon(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, _ := ids.Parse(r.PathValue("id"))
|
|
r.ParseForm() //nolint:errcheck
|
|
to := "/minds/" + m.Slug + "/intentions/" + string(id)
|
|
if _, err := u.studyF().AbandonIntention(r, m.ID, id, r.PostFormValue("reason")); err != nil {
|
|
u.flash(w, r, "error", err.Error(), to)
|
|
return
|
|
}
|
|
u.flash(w, r, "ok", "Intention abandoned; its plan was cancelled and open requests withdrawn.", to)
|
|
}
|
|
|
|
func (u *UI) commitmentsPage(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
all, _ := u.deps.Volition.Commitments(r.Context(), nil, m.ID, nil, 500)
|
|
var active, closed []volition.Commitment
|
|
for _, c := range all {
|
|
if c.Status == "active" {
|
|
active = append(active, c)
|
|
} else {
|
|
closed = append(closed, c)
|
|
}
|
|
}
|
|
conflicts := volition.CommitmentConflicts(active, time.Now())
|
|
u.render(w, r, "commitments", m.DisplayName+" · Commitments", map[string]any{"Mind": m, "Active": active, "Closed": closed, "Conflicts": conflicts,
|
|
"Kinds": []string{"operator_instruction", "promise", "responsibility", "value_rule", "relationship", "prior_decision", "policy", "scheduled"}})
|
|
}
|
|
|
|
func (u *UI) commitmentCreate(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
r.ParseForm() //nolint:errcheck
|
|
to := "/minds/" + m.Slug + "/commitments"
|
|
split := func(k string) []string {
|
|
var out []string
|
|
for _, x := range strings.Split(r.PostFormValue(k), ",") {
|
|
if x = strings.TrimSpace(x); x != "" {
|
|
out = append(out, x)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
n := volition.NewCommitment{MindID: m.ID, Description: r.PostFormValue("description"), Kind: volition.CommitmentKind(r.PostFormValue("kind")), Polarity: r.PostFormValue("polarity"), Unless: r.PostFormValue("unless"),
|
|
Beneficiary: r.PostFormValue("beneficiary"), Matcher: volition.Matcher{Capabilities: split("capabilities"), Targets: split("targets"), Access: r.PostFormValue("access"), MinRisk: r.PostFormValue("min_risk")}}
|
|
n.Strength, _ = strconv.ParseFloat(r.PostFormValue("strength"), 64)
|
|
n.Priority, _ = strconv.ParseFloat(r.PostFormValue("priority"), 64)
|
|
if v := r.PostFormValue("due_at"); v != "" {
|
|
if t, err := time.ParseInLocation("2006-01-02T15:04", v, time.Local); err == nil {
|
|
n.DueAt = &t
|
|
}
|
|
}
|
|
if _, err := u.studyF().CreateCommitment(r, n); err != nil {
|
|
u.flash(w, r, "error", err.Error(), to)
|
|
return
|
|
}
|
|
u.flash(w, r, "ok", "Commitment recorded. It shapes what the mind intends; it never grants authority.", to)
|
|
}
|
|
|
|
func (u *UI) commitmentClose(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, _ := ids.Parse(r.PathValue("id"))
|
|
r.ParseForm() //nolint:errcheck
|
|
to := "/minds/" + m.Slug + "/commitments"
|
|
if _, err := u.studyF().CloseCommitment(r, m.ID, id, r.PostFormValue("status"), r.PostFormValue("reason")); err != nil {
|
|
u.flash(w, r, "error", err.Error(), to)
|
|
return
|
|
}
|
|
u.flash(w, r, "ok", "Commitment closed.", to)
|
|
}
|
|
|
|
func (u *UI) plansPage(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
list, _ := u.deps.Plans.List(r.Context(), m.ID, nil, 200)
|
|
u.render(w, r, "plans", m.DisplayName+" · Plans", map[string]any{"Mind": m, "Plans": list})
|
|
}
|
|
|
|
func (u *UI) planPage(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, http.StatusNotFound, "No such plan.")
|
|
return
|
|
}
|
|
p, err := u.deps.Plans.Get(r.Context(), nil, id)
|
|
if err != nil || p.MindID != m.ID {
|
|
u.errorPage(w, r, http.StatusNotFound, "No such plan.")
|
|
return
|
|
}
|
|
it, _ := u.deps.Volition.GetIntention(r.Context(), nil, m.ID, p.IntentionID)
|
|
revisions, _ := u.deps.Plans.ForIntention(r.Context(), nil, p.IntentionID)
|
|
validation, _ := json.MarshalIndent(p.Validation, "", " ")
|
|
u.render(w, r, "plan", m.DisplayName+" · Plan", map[string]any{"Mind": m, "Plan": p, "Intention": it, "Levels": planLevels(p), "Revisions": revisions, "Validation": string(validation)})
|
|
}
|
|
|
|
func (u *UI) planCancel(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, _ := ids.Parse(r.PathValue("id"))
|
|
to := "/minds/" + m.Slug + "/plans/" + string(id)
|
|
p, err := u.deps.Plans.Get(r.Context(), nil, id)
|
|
if err != nil || p.MindID != m.ID {
|
|
u.flash(w, r, "error", "No such plan.", to)
|
|
return
|
|
}
|
|
r.ParseForm() //nolint:errcheck
|
|
if err := u.deps.Executor.Cancel(r.Context(), id, "cancelled by operator "+username(r)+": "+r.PostFormValue("reason")); err != nil {
|
|
u.flash(w, r, "error", err.Error(), to)
|
|
return
|
|
}
|
|
u.deps.Audit.Log(r.Context(), "plan.cancel", "plan", id.String(), map[string]any{"mind": m.ID})
|
|
u.flash(w, r, "ok", "Plan cancelled.", to)
|
|
}
|
|
|
|
func (u *UI) planStepRetry(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, _ := ids.Parse(r.PathValue("id"))
|
|
to := "/minds/" + m.Slug + "/plans/" + string(id)
|
|
p, err := u.deps.Plans.Get(r.Context(), nil, id)
|
|
if err != nil || p.MindID != m.ID {
|
|
u.flash(w, r, "error", "No such plan.", to)
|
|
return
|
|
}
|
|
if err := u.studyF().RetryStep(r, p, r.PathValue("step")); err != nil {
|
|
u.flash(w, r, "error", err.Error(), to)
|
|
return
|
|
}
|
|
u.flash(w, r, "ok", "Step released; the executor will pick it up.", to)
|
|
}
|
|
|
|
// stageOf names the agency stage an event belongs to.
|
|
func stageOf(t events.Type) string {
|
|
switch t {
|
|
case events.WantCreated, events.WantSuppressed, events.WantConflictDetected:
|
|
return "wanted"
|
|
case events.IntentionFormed:
|
|
return "intended"
|
|
case events.CommitmentCreated, events.CommitmentDue, events.CommitmentConflict, events.CommitmentFulfilled, events.CommitmentBroken, events.CommitmentRevoked:
|
|
return "committed"
|
|
case events.PlanCreated, events.PlanReplanned:
|
|
return "planned"
|
|
case events.ToolRequested:
|
|
return "requested"
|
|
case events.ToolAuthorised, events.ToolDenied:
|
|
return "approved"
|
|
case events.PlanStepStarted, events.PlanStepCompleted, events.PlanStepFailed, events.ToolExecuted, events.ToolFailed:
|
|
return "acted"
|
|
case events.PredictionCreated, events.PredictionResolved, events.PredictionError:
|
|
return "observed"
|
|
}
|
|
return "reconsidered"
|
|
}
|
|
|
|
type timelineRow struct {
|
|
Event events.Event
|
|
Stage string
|
|
Why string
|
|
}
|
|
|
|
func (u *UI) agencyPage(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
corr, _ := ids.Parse(r.URL.Query().Get("intention"))
|
|
evs, _ := u.studyF().AgencyTimeline(r, m.ID, 300, corr)
|
|
var rows []timelineRow
|
|
for _, ev := range evs {
|
|
row := timelineRow{Event: ev, Stage: stageOf(ev.Type)}
|
|
for _, k := range []string{"execution_id", "approval_id", "request_id", "intention_id"} {
|
|
if v, _ := ev.Metadata[k].(string); v != "" {
|
|
row.Why = "/minds/" + m.Slug + "/why?kind=event&id=" + string(ev.ID)
|
|
break
|
|
}
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
u.render(w, r, "agency", m.DisplayName+" · Agency", map[string]any{"Mind": m, "Rows": rows, "Intention": corr})
|
|
}
|
|
|
|
func (u *UI) whyPage(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := ids.Parse(r.URL.Query().Get("id"))
|
|
if err != nil {
|
|
u.errorPage(w, r, http.StatusBadRequest, "Choose an action, approval, request, plan step, intention or event to explain.")
|
|
return
|
|
}
|
|
kind := r.URL.Query().Get("kind")
|
|
if kind == "" {
|
|
kind = "event"
|
|
}
|
|
x, err := u.deps.Agency.Explain(r.Context(), m.ID, agency.Ref{Kind: kind, ID: id})
|
|
if err != nil {
|
|
u.errorPage(w, r, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
var attribution *agency.Attribution
|
|
if kind == "event" {
|
|
if a, err := u.deps.Agency.Attribute(r.Context(), m.ID, id); err == nil {
|
|
attribution = &a
|
|
}
|
|
}
|
|
u.render(w, r, "why", m.DisplayName+" · Why did this happen?", map[string]any{"Mind": m, "X": x, "Attribution": attribution})
|
|
}
|
|
|
|
func (u *UI) researchPage(w http.ResponseWriter, r *http.Request) {
|
|
m, ok := u.mind(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
ctx := r.Context()
|
|
since := time.Now().Add(-30 * 24 * time.Hour)
|
|
metrics, _ := u.deps.Research.Agency(ctx, m.ID, since)
|
|
calibration, _ := u.deps.Research.MetacognitiveCalibration(ctx, m.ID, time.Now().Add(-90*24*time.Hour))
|
|
introspection, _ := u.deps.Research.List(ctx, m.ID, "introspection_accuracy", since, 50)
|
|
claims, _ := u.deps.Research.List(ctx, m.ID, "action_claim_accuracy", since, 50)
|
|
labels := map[string]int{}
|
|
for _, x := range introspection {
|
|
labels[x.Label]++
|
|
}
|
|
// Self-model accuracy now.
|
|
var selfAcc *float64
|
|
var selfErr []string
|
|
if sm, err := u.deps.Minds.GetSelfModel(ctx, m.ID); err == nil && len(sm.Capabilities) > 0 {
|
|
if inv, err := u.deps.Capabilities.Inventory(ctx, m.ID); err == nil {
|
|
eff, stated := map[string]string{}, map[string]string{}
|
|
for _, i := range inv {
|
|
eff[i.Capability.Name] = string(i.Effect)
|
|
if i.Capability.Locked {
|
|
eff[i.Capability.Name] = "locked"
|
|
}
|
|
}
|
|
for _, c := range sm.Capabilities {
|
|
stated[c.Name] = c.Policy
|
|
}
|
|
a, wrong := research.SelfModelAccuracy(stated, eff)
|
|
selfAcc, selfErr = &a, wrong
|
|
}
|
|
}
|
|
// Attribution of recent outcomes (resolved expectations).
|
|
attr := map[string]int{}
|
|
if outs, err := u.deps.Events.Query(ctx, eventsFilterTypes(m.ID, events.PredictionResolved, events.PredictionError)); err == nil {
|
|
for i, ev := range outs {
|
|
if i >= 50 {
|
|
break
|
|
}
|
|
if a, err := u.deps.Agency.Attribute(ctx, m.ID, ev.ID); err == nil {
|
|
attr[a.Class]++
|
|
}
|
|
}
|
|
}
|
|
metrics.Attribution = attr
|
|
var exps []experiments.Experiment
|
|
if u.deps.Experiments != nil {
|
|
exps, _ = u.deps.Experiments.List(ctx, 200)
|
|
}
|
|
var related []experimentRow
|
|
for _, e := range exps {
|
|
sn, err := u.deps.Snapshots.Get(ctx, e.SnapshotID)
|
|
if err != nil || sn.MindID != m.ID {
|
|
continue
|
|
}
|
|
related = append(related, experimentRowOf(e))
|
|
}
|
|
bySection := map[string][]research.Measure{}
|
|
var sections []string
|
|
for _, x := range research.Measures {
|
|
if _, ok := bySection[x.Section]; !ok {
|
|
sections = append(sections, x.Section)
|
|
}
|
|
bySection[x.Section] = append(bySection[x.Section], x)
|
|
}
|
|
u.render(w, r, "research", m.DisplayName+" · Research", map[string]any{"Mind": m, "Metrics": metrics, "Calibration": calibration, "Introspection": introspection, "IntrospectionLabels": labels, "Claims": claims,
|
|
"SelfAccuracy": selfAcc, "SelfErrors": selfErr, "Experiments": related, "Sections": sections, "Measures": bySection, "Disclaimer": research.Disclaimer})
|
|
}
|
|
|
|
type experimentRow struct {
|
|
E experiments.Experiment
|
|
Protocol string
|
|
Repro string
|
|
Variants int
|
|
First int
|
|
Share float64
|
|
}
|
|
|
|
func experimentRowOf(e experiments.Experiment) experimentRow {
|
|
res := experiments.ResultsOf(e)
|
|
row := experimentRow{E: e, Protocol: "custom", Repro: res.Reproducibility.Status, Variants: len(res.Variants), First: -1}
|
|
if e.Definition.Protocol != nil && e.Definition.Protocol.Kind != "" {
|
|
row.Protocol = e.Definition.Protocol.Kind
|
|
}
|
|
if res.Comparison != nil {
|
|
row.First, row.Share = res.Comparison.FirstDivergentStimulus, res.Comparison.ArchitectureShare
|
|
}
|
|
return row
|
|
}
|
|
|
|
func (u *UI) researchIndex(w http.ResponseWriter, r *http.Request) {
|
|
var exps []experiments.Experiment
|
|
if u.deps.Experiments != nil {
|
|
exps, _ = u.deps.Experiments.List(r.Context(), 200)
|
|
}
|
|
byProtocol := map[string][]experimentRow{}
|
|
for _, e := range exps {
|
|
row := experimentRowOf(e)
|
|
byProtocol[row.Protocol] = append(byProtocol[row.Protocol], row)
|
|
}
|
|
list, _ := u.deps.Minds.List(r.Context(), false)
|
|
bySection := map[string][]research.Measure{}
|
|
var sections []string
|
|
for _, x := range research.Measures {
|
|
if _, ok := bySection[x.Section]; !ok {
|
|
sections = append(sections, x.Section)
|
|
}
|
|
bySection[x.Section] = append(bySection[x.Section], x)
|
|
}
|
|
u.render(w, r, "research_index", "Cognitive research", map[string]any{"ByProtocol": byProtocol, "Protocols": []string{"global_workspace", "ablation", "self_model", "counterfactual", "model_comparison", "conflict", "custom"},
|
|
"Minds": list, "Sections": sections, "Measures": bySection, "Disclaimer": research.Disclaimer})
|
|
}
|
|
|
|
func eventsFilterTypes(mind ids.ID, types ...events.Type) evstore.Filter {
|
|
return evstore.Filter{MindID: mind, Types: types, Limit: 100}
|
|
}
|