(#22) Jev mode classification for nest requests #29

Merged
jacisjake merged 2 commits from task/22-mode-classification into main 2026-10-28 11:26:58 +00:00
3 changed files with 131 additions and 47 deletions
Showing only changes of commit 9205cece65 - Show all commits

No files matched your search

+58
View File
@@ -155,6 +155,64 @@ func deref(f *float64) float64 {
return *f return *f
} }
// modeQuestions classifies how a request should be taken up (Epic C). The
// escape option "other" keeps Jev from forcing a wrong class; mode is discrete,
// so choice, not score (JEV_STRATEGIES.md strategy 3).
var modeQuestions = map[string]any{
"mode": map[string]any{
"type": "choice",
"instructions": "How should this chatroom request be taken up?",
"criteria": map[string]string{
"solo": "Simple: one crow answers it directly.",
"conspiracy": "A judgment call worth several distinct readings: several crows take the same request from their different strengths.",
"split": "Large with distinct parts: separate subtasks, one owner each.",
"other": "None of these fits well.",
},
},
}
// ClassifyMode returns the mode when Jev is set and its choice is at or above
// the band. "other" reads as unsure (not ok) so callers fall back to a header.
func (j *JevClient) ClassifyMode(ctx context.Context, text string) (string, map[string]float64, bool) {
if j == nil {
return "", nil, false
}
answers, _, _, err := j.SystemOne(ctx, text, modeQuestions)
if err != nil {
return "", nil, false
}
a := answers["mode"]
if a.Choice == "" || a.Choice == "other" || a.Confidence == nil || *a.Confidence < j.threshold {
return "", nil, false
}
return a.Choice, a.Probabilities, true
}
// ClassifyMode classifies a request under the same guardrails as any call:
// flock cap, timeout and flight log, with the probabilities recorded. Returns
// the mode and ok; below-band or absent Jev means not ok.
func (f *Flock) ClassifyMode(ctx context.Context, text string, budgets ...*Budget) (string, bool) {
if f.jev == nil {
return "", false
}
for _, b := range budgets {
if err := b.Check(); err != nil {
return "", false
}
}
f.sem <- struct{}{}
defer func() { <-f.sem }()
cctx, cancel := context.WithTimeout(ctx, f.g.CrowTimeout)
defer cancel()
start := time.Now()
mode, probs, ok := f.jev.ClassifyMode(cctx, text)
e := Event{Kind: "mode", Item: text, Crow: "Sentry", Model: f.jev.model, Ms: time.Since(start).Milliseconds(), Prob: probs, Mode: mode}
if f.log != nil {
f.log.Write(e)
}
return mode, ok
}
func (c JevCall) confident(threshold float64) bool { func (c JevCall) confident(threshold float64) bool {
return c.ModeConf >= threshold && c.TaskTypeConf >= threshold return c.ModeConf >= threshold && c.TaskTypeConf >= threshold
} }
+61 -38
View File
@@ -42,20 +42,33 @@ func (s *Server) getNestMessages(w http.ResponseWriter, r *http.Request) {
all := s.msgs.All(nestID) all := s.msgs.All(nestID)
out := make([]map[string]any, 0, len(all)) out := make([]map[string]any, 0, len(all))
for _, m := range all { for _, m := range all {
out = append(out, map[string]any{ out = append(out, nestRecord(m))
"msg_id": m.MsgID(),
"kind": m.Kind,
"author": m.Author,
"text": m.Text,
"to": m.To,
"refers_to": m.RefersTo,
"tagged_by": m.TaggedBy,
"ts": m.Timestamp,
})
} }
writeJSON(w, http.StatusOK, out) writeJSON(w, http.StatusOK, out)
} }
// nestRecord is the EVENTS.md wire shape for one message.
func nestRecord(m Message) map[string]any {
rec := map[string]any{
"msg_id": m.MsgID(),
"kind": m.Kind,
"author": m.Author,
"text": m.Text,
"to": m.To,
"refers_to": m.RefersTo,
"tagged_by": m.TaggedBy,
"ts": m.Timestamp,
}
if m.Mode != "" {
rec["mode"] = m.Mode
rec["mode_tagged_by"] = m.ModeTagged
}
if m.Invited > 0 {
rec["invited"] = m.Invited
}
return rec
}
func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) {
nestID := r.PathValue("nest_id") nestID := r.PathValue("nest_id")
nest, found := s.nests.Get(nestID) nest, found := s.nests.Get(nestID)
@@ -68,6 +81,7 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) {
Author string `json:"author"` Author string `json:"author"`
Text string `json:"text"` Text string `json:"text"`
Kind string `json:"kind"` Kind string `json:"kind"`
Mode string `json:"mode"`
To string `json:"to"` To string `json:"to"`
RefersTo string `json:"refers_to"` RefersTo string `json:"refers_to"`
} }
@@ -115,15 +129,20 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) {
} }
} }
// Mode (Epic C, same rule shape as kind): Jev first, header escape, "" when unsure.
mode, modeBy := s.tagMode(kind, body.Mode, body.Text)
msg := Message{ msg := Message{
NestID: nestID, NestID: nestID,
Kind: kind, Kind: kind,
Author: body.Author, Author: body.Author,
Text: body.Text, Text: body.Text,
To: body.To, To: body.To,
RefersTo: body.RefersTo, RefersTo: body.RefersTo,
TaggedBy: taggedBy, TaggedBy: taggedBy,
Timestamp: time.Now().UTC().Format(time.RFC3339), Mode: mode,
ModeTagged: modeBy,
Timestamp: time.Now().UTC().Format(time.RFC3339),
} }
record, err := s.msgs.Append(msg) record, err := s.msgs.Append(msg)
if err != nil { if err != nil {
@@ -139,16 +158,27 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) {
go s.respondToRequest(nest, record) go s.respondToRequest(nest, record)
} }
writeJSON(w, http.StatusCreated, map[string]any{ writeJSON(w, http.StatusCreated, nestRecord(record))
"msg_id": record.MsgID(), }
"kind": record.Kind,
"author": record.Author, var modePostable = map[string]bool{"solo": true, "conspiracy": true, "split": true}
"text": record.Text,
"to": record.To, // tagMode classifies a request's mode (Jev first, header escape). Only requests
"refers_to": record.RefersTo, // carry a mode; other kinds get "".
"tagged_by": record.TaggedBy, func (s *Server) tagMode(kind, header, text string) (string, string) {
"ts": record.Timestamp, if kind != "request" {
}) return "", ""
}
if m, ok := s.p.Flock.ClassifyMode(s.ctx, text); ok {
if modePostable[header] && header != m {
return m, "disagreement"
}
return m, "jev"
}
if modePostable[header] {
return header, "header"
}
return "", "default"
} }
// tagKind runs the A1 rule: Jev first; Jev unsure -> the poster's header; // tagKind runs the A1 rule: Jev first; Jev unsure -> the poster's header;
@@ -169,16 +199,9 @@ func (s *Server) tagKind(header, text string) (string, string) {
} }
func (s *Server) emitNestMessage(m Message) { func (s *Server) emitNestMessage(m Message) {
s.hub.PublishNest(m.NestID, "nest.message", map[string]any{ rec := nestRecord(m)
"nest_id": m.NestID, rec["nest_id"] = m.NestID
"msg_id": m.MsgID(), s.hub.PublishNest(m.NestID, "nest.message", rec)
"kind": m.Kind,
"author": m.Author,
"text": m.Text,
"to": m.To,
"refers_to": m.RefersTo,
"tagged_by": m.TaggedBy,
})
} }
// respondToRequest is the must-respond rule: the addressed crow answers with // respondToRequest is the must-respond rule: the addressed crow answers with
+12 -9
View File
@@ -13,15 +13,18 @@ import (
// contract. IDs are assigned by the store; MsgID is "<nest_id>-<n>" and is // contract. IDs are assigned by the store; MsgID is "<nest_id>-<n>" and is
// what the contract exposes. // what the contract exposes.
type Message struct { type Message struct {
NestID string `json:"nest_id"` NestID string `json:"nest_id"`
ID int `json:"id"` ID int `json:"id"`
Kind string `json:"kind"` Kind string `json:"kind"`
Author string `json:"author"` Author string `json:"author"`
Text string `json:"text"` Text string `json:"text"`
To string `json:"to,omitempty"` To string `json:"to,omitempty"`
RefersTo string `json:"refers_to,omitempty"` RefersTo string `json:"refers_to,omitempty"`
TaggedBy string `json:"tagged_by,omitempty"` TaggedBy string `json:"tagged_by,omitempty"`
Timestamp string `json:"ts"` Mode string `json:"mode,omitempty"` // solo | conspiracy | split (requests only, Epic C)
ModeTagged string `json:"mode_tagged_by,omitempty"`
Invited int `json:"invited,omitempty"`
Timestamp string `json:"ts"`
} }
func (m Message) MsgID() string { return fmt.Sprintf("%s-%d", m.NestID, m.ID) } func (m Message) MsgID() string { return fmt.Sprintf("%s-%d", m.NestID, m.ID) }