WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
@@ -0,0 +1,243 @@
package memory
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/Tencent/WeKnora/internal/types"
"github.com/stretchr/testify/require"
)
// The distillation prompt is the part of this feature nobody can review by
// reading it: whether a rule helps is an empirical question. This file is the
// harness for answering it.
//
// It has two modes. By default it runs offline and only checks that the golden
// set is well-formed and that the prompt actually carries what each case needs,
// which is cheap and keeps the file honest. Given a real model it scores the
// prompt against the set:
//
// WEKNORA_MEMORY_EVAL_MODEL=<model id> \
// WEKNORA_MEMORY_EVAL_BASE_URL=... WEKNORA_MEMORY_EVAL_API_KEY=... \
// go test ./internal/application/service/memory/ -run TestPromptEval -v
//
// Scores are printed per case and in total. There is no pass threshold on
// purpose: the number is only meaningful compared against the previous run of
// the same set, and baking in a bar would just make people lower it.
type evalExistingNote struct {
Kind string `json:"kind"`
Topic string `json:"topic"`
Content string `json:"content"`
}
type evalCase struct {
Name string `json:"name"`
Context []string `json:"context"`
Lines []string `json:"lines"`
Existing []evalExistingNote `json:"existing"`
ExpectCountMin *int `json:"expect_count_min"`
ExpectCountMax *int `json:"expect_count_max"`
ExpectKinds []string `json:"expect_kinds"`
ExpectActions []string `json:"expect_actions"`
ExpectTargets []int `json:"expect_targets"`
ExpectSubstrings []string `json:"expect_substrings"`
RejectSubstrings []string `json:"reject_substrings"`
ExpectExpiry bool `json:"expect_expiry"`
}
type evalSet struct {
Cases []evalCase `json:"cases"`
}
func loadEvalSet(t *testing.T) evalSet {
t.Helper()
raw, err := os.ReadFile("evalset.json")
require.NoError(t, err)
var set evalSet
require.NoError(t, json.Unmarshal(raw, &set))
require.NotEmpty(t, set.Cases)
return set
}
// segmentForCase renders a case the same way a real run would, so the harness
// grades the prompt the product actually sends.
func segmentForCase(c evalCase) transcriptSegment {
base := time.Now().Add(-time.Hour)
segment := transcriptSegment{sessionID: "eval", context: c.Context}
for i, line := range c.Lines {
segment.lines = append(segment.lines, transcriptLine{
sessionID: "eval",
messageID: fmt.Sprintf("eval-%d", i+1),
at: base.Add(time.Duration(i) * time.Minute),
content: line,
})
}
return segment
}
func existingForCase(c evalCase) []*types.MemoryItem {
items := make([]*types.MemoryItem, 0, len(c.Existing))
for _, note := range c.Existing {
items = append(items, &types.MemoryItem{
Kind: note.Kind, Topic: note.Topic, Content: note.Content,
})
}
return items
}
// TestEvalSetIsWellFormed runs everywhere and keeps the golden set from rotting:
// a case whose text never reaches the prompt grades nothing.
func TestEvalSetIsWellFormed(t *testing.T) {
set := loadEvalSet(t)
seen := make(map[string]struct{}, len(set.Cases))
for _, c := range set.Cases {
require.NotEmpty(t, c.Name)
_, duplicate := seen[c.Name]
require.False(t, duplicate, "duplicate case name %q", c.Name)
seen[c.Name] = struct{}{}
require.NotEmpty(t, c.Lines, "case %q has nothing to extract from", c.Name)
prompt := buildExtractionPrompt(segmentForCase(c), existingForCase(c), nil, nil, "")
for _, line := range c.Lines {
require.Contains(t, prompt, line, "case %q: line missing from the prompt", c.Name)
}
for _, line := range c.Context {
require.Contains(t, prompt, line, "case %q: context missing from the prompt", c.Name)
}
for _, note := range c.Existing {
require.Contains(t, prompt, note.Content,
"case %q: existing note missing from the prompt", c.Name)
}
for i := range c.ExpectTargets {
require.Less(t, c.ExpectTargets[i], len(c.Existing),
"case %q expects target %d but supplies fewer notes", c.Name, c.ExpectTargets[i])
}
}
}
// evalResult is one graded case.
type evalResult struct {
name string
passed bool
failures []string
}
func gradeCase(c evalCase, decisions []extractionDecision) evalResult {
result := evalResult{name: c.name(), passed: true}
kept := make([]extractionDecision, 0, len(decisions))
for _, decision := range decisions {
action := strings.ToLower(strings.TrimSpace(decision.Action))
if action == "" || action == "none" || action == "noop" {
continue
}
kept = append(kept, decision)
}
fail := func(format string, args ...any) {
result.passed = false
result.failures = append(result.failures, fmt.Sprintf(format, args...))
}
if c.ExpectCountMin != nil && len(kept) < *c.ExpectCountMin {
fail("expected at least %d memories, got %d", *c.ExpectCountMin, len(kept))
}
if c.ExpectCountMax != nil && len(kept) > *c.ExpectCountMax {
fail("expected at most %d memories, got %d", *c.ExpectCountMax, len(kept))
}
blob := strings.ToLower(decisionsBlob(kept))
for _, want := range c.ExpectSubstrings {
if !strings.Contains(blob, strings.ToLower(want)) {
fail("missing %q", want)
}
}
for _, reject := range c.RejectSubstrings {
if strings.Contains(blob, strings.ToLower(reject)) {
fail("should not have recorded %q", reject)
}
}
for _, kind := range c.ExpectKinds {
if !hasField(kept, func(d extractionDecision) bool {
return strings.EqualFold(d.Kind, kind)
}) {
fail("no memory of kind %q", kind)
}
}
for _, action := range c.ExpectActions {
if !hasField(kept, func(d extractionDecision) bool {
return strings.EqualFold(d.Action, action)
}) {
fail("no %q action", action)
}
}
for _, target := range c.ExpectTargets {
if !hasField(kept, func(d extractionDecision) bool {
return d.Target != nil && *d.Target == target
}) {
fail("no decision targeting note %d", target)
}
}
if c.ExpectExpiry && !hasField(kept, func(d extractionDecision) bool {
return parseExpiry(d.ExpiresAt) != nil
}) {
fail("no usable expires_at")
}
return result
}
func (c evalCase) name() string { return c.Name }
func decisionsBlob(decisions []extractionDecision) string {
var builder strings.Builder
for _, decision := range decisions {
fmt.Fprintf(&builder, "%s %s %s %s %s\n",
decision.Action, decision.Kind, decision.Topic, decision.Content, decision.ExpiresAt)
}
return builder.String()
}
func hasField(decisions []extractionDecision, match func(extractionDecision) bool) bool {
for _, decision := range decisions {
if match(decision) {
return true
}
}
return false
}
// TestPromptEval scores the prompt against the golden set using a real model.
// Skipped unless WEKNORA_MEMORY_EVAL_MODEL is set.
func TestPromptEval(t *testing.T) {
modelID := strings.TrimSpace(os.Getenv("WEKNORA_MEMORY_EVAL_MODEL"))
if modelID == "" {
t.Skip("set WEKNORA_MEMORY_EVAL_MODEL (plus base URL / API key) to score the prompt")
}
chatModel, err := newEvalChatModel(modelID)
require.NoError(t, err)
set := loadEvalSet(t)
passed := 0
for _, c := range set.Cases {
prompt := buildExtractionPrompt(segmentForCase(c), existingForCase(c), nil, nil, "")
decisions, err := runEvalExtraction(context.Background(), chatModel, prompt)
if err != nil {
t.Errorf("case %q: %v", c.Name, err)
continue
}
result := gradeCase(c, decisions)
if result.passed {
passed++
t.Logf("PASS %s", result.name)
continue
}
t.Errorf("FAIL %s\n %s", result.name, strings.Join(result.failures, "\n "))
}
t.Logf("prompt eval: %d/%d cases passed with model %s", passed, len(set.Cases), modelID)
}