(#39) Status answers read the nest #47
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -8,13 +9,43 @@ import (
|
||||
const quillSystem = `You are Quill crow, the owner's assistant and point of contact in a nest of AI crows.
|
||||
Answer the owner in one or two plain sentences. You are text-only: no web, no files, no tools.`
|
||||
|
||||
const quillHistoryCap = 20
|
||||
|
||||
// quillHistory builds a block of the most recent nest messages (excluding the
|
||||
// requesting message), formatted as "[author|kind] text" with text truncated
|
||||
// to 200 runes. Returns a single line when no earlier messages exist.
|
||||
func quillHistory(all []Message, excludeID int) string {
|
||||
var filtered []Message
|
||||
for _, m := range all {
|
||||
if m.ID != excludeID {
|
||||
filtered = append(filtered, m)
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return "The nest has no earlier messages."
|
||||
}
|
||||
if len(filtered) > quillHistoryCap {
|
||||
filtered = filtered[len(filtered)-quillHistoryCap:]
|
||||
}
|
||||
var sb strings.Builder
|
||||
for _, m := range filtered {
|
||||
text := []rune(m.Text)
|
||||
if len(text) > 200 {
|
||||
text = text[:200]
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("[%s|%s] %s\n", m.Author, m.Kind, string(text)))
|
||||
}
|
||||
return strings.TrimRight(sb.String(), "\n")
|
||||
}
|
||||
|
||||
// respondQuill answers a message addressed to Quill, the assistant service
|
||||
// role (CHATROOM_SPEC section 6). D1: one in-character reply through the
|
||||
// flock. Reading the nest for status (D2) and relaying (D3) extend this file.
|
||||
// role (CHATROOM_SPEC section 6). D2: reads the nest for status before
|
||||
// composing its reply.
|
||||
func (s *Server) respondQuill(nest Nest, req Message) {
|
||||
budget := &Budget{name: "quill-reply", limit: s.p.G.TokenBudgetPerItem}
|
||||
system := systemFor("Quill", quillSystem, false)
|
||||
prompt := "FROM: " + req.Author + "\nMESSAGE: " + req.Text
|
||||
history := quillHistory(s.msgs.All(nest.ID), req.ID)
|
||||
prompt := history + "\n---\nFROM: " + req.Author + "\nMESSAGE: " + req.Text
|
||||
answer, err := s.p.Flock.ask(s.ctx, nil, "Quill", system, prompt, budget)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
+117
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -95,3 +96,119 @@ func TestQuillNotInNestCrows(t *testing.T) {
|
||||
t.Fatal("Quill must not need or earn a worker trust record")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuillPromptContainsNestHistory(t *testing.T) {
|
||||
ts, _, qb := newQuillServer(t, "Hugin finished the build already.")
|
||||
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
|
||||
|
||||
// 1. owner says something that sets up context
|
||||
code, out := post(t, ts.URL+"/api/nests/"+id+"/messages",
|
||||
`{"author":"owner","to":"Hugin","kind":"say","text":"build the widget factory"}`)
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("post say: %d", code)
|
||||
}
|
||||
sayMsgID := out["msg_id"].(string)
|
||||
|
||||
// 2. Hugin delivers an answer
|
||||
code, _ = post(t, ts.URL+"/api/nests/"+id+"/messages",
|
||||
`{"author":"Hugin","kind":"deliver","text":"the widget factory is live","refers_to":"`+sayMsgID+`"}`)
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("post deliver: %d", code)
|
||||
}
|
||||
|
||||
// 3. owner asks Quill for status
|
||||
code, _ = post(t, ts.URL+"/api/nests/"+id+"/messages",
|
||||
`{"author":"owner","to":"Quill","kind":"request","text":"where did that get to"}`)
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("post request to Quill: %d", code)
|
||||
}
|
||||
|
||||
// Wait for Quill's reply (3 prior + 1 Quill reply = 4)
|
||||
msgs := awaitMessages(t, ts, id, 4)
|
||||
var quillReply map[string]any
|
||||
for _, m := range msgs {
|
||||
if m["author"] == "Quill" {
|
||||
quillReply = m
|
||||
}
|
||||
}
|
||||
if quillReply == nil {
|
||||
t.Fatalf("Quill never answered: %v", msgs)
|
||||
}
|
||||
|
||||
qb.mu.Lock()
|
||||
defer qb.mu.Unlock()
|
||||
if len(qb.pairs) != 1 {
|
||||
t.Fatalf("want exactly one model call, got %d", len(qb.pairs))
|
||||
}
|
||||
prompt := qb.pairs[0][1]
|
||||
if !strings.Contains(prompt, "build the widget factory") {
|
||||
t.Fatalf("prompt must contain the owner's earlier message: %q", prompt)
|
||||
}
|
||||
if !strings.Contains(prompt, "the widget factory is live") {
|
||||
t.Fatalf("prompt must contain Hugin's deliver: %q", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuillEmptyNestSaysQuiet(t *testing.T) {
|
||||
ts, _, qb := newQuillServer(t, "All quiet here.")
|
||||
id := newNest(t, ts, "Quiet", `"Hugin"`)
|
||||
|
||||
// Only message is the request to Quill — history excluding it is empty
|
||||
code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages",
|
||||
`{"author":"owner","to":"Quill","kind":"request","text":"any updates?"}`)
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("post: %d", code)
|
||||
}
|
||||
awaitMessages(t, ts, id, 2)
|
||||
|
||||
qb.mu.Lock()
|
||||
defer qb.mu.Unlock()
|
||||
if len(qb.pairs) != 1 {
|
||||
t.Fatalf("want exactly one model call, got %d", len(qb.pairs))
|
||||
}
|
||||
prompt := qb.pairs[0][1]
|
||||
if !strings.Contains(prompt, "no earlier messages") {
|
||||
t.Fatalf("prompt must state the nest is empty: %q", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuillHistoryCap(t *testing.T) {
|
||||
ts, _, qb := newQuillServer(t, "got it")
|
||||
id := newNest(t, ts, "Busy", `"Hugin"`)
|
||||
|
||||
// Post 25 filler messages from owner
|
||||
for i := 0; i < 25; i++ {
|
||||
code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages",
|
||||
fmt.Sprintf(`{"author":"owner","kind":"say","text":"filler line %d"}`, i))
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("post filler %d: %d", i, code)
|
||||
}
|
||||
}
|
||||
|
||||
// Ask Quill
|
||||
code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages",
|
||||
`{"author":"owner","to":"Quill","kind":"request","text":"status"}`)
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("post to Quill: %d", code)
|
||||
}
|
||||
awaitMessages(t, ts, id, 27)
|
||||
|
||||
qb.mu.Lock()
|
||||
defer qb.mu.Unlock()
|
||||
if len(qb.pairs) != 1 {
|
||||
t.Fatalf("want exactly one model call, got %d", len(qb.pairs))
|
||||
}
|
||||
prompt := qb.pairs[0][1]
|
||||
historyLines := 0
|
||||
for _, line := range strings.Split(prompt, "\n") {
|
||||
if strings.HasPrefix(line, "[") {
|
||||
historyLines++
|
||||
}
|
||||
}
|
||||
if historyLines > 20 {
|
||||
t.Fatalf("history lines must be capped at 20, got %d\nprompt:\n%s", historyLines, prompt)
|
||||
}
|
||||
if historyLines == 0 {
|
||||
t.Fatalf("expected some history lines, got none")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user