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
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:
@@ -0,0 +1,234 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/models/chat"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type wikiPageRepairChat struct {
|
||||
templateCaptureChatModel
|
||||
id string
|
||||
call func(context.Context, []chat.Message, *chat.ChatOptions) (*types.ChatResponse, error)
|
||||
}
|
||||
|
||||
func (m *wikiPageRepairChat) GetModelID() string { return m.id }
|
||||
func (m *wikiPageRepairChat) Chat(ctx context.Context, messages []chat.Message, opts *chat.ChatOptions) (*types.ChatResponse, error) {
|
||||
return m.call(ctx, messages, opts)
|
||||
}
|
||||
|
||||
func pageRepairInput() map[string]string {
|
||||
return map[string]string{
|
||||
"HasAdditions": "1", "HasRetractions": "1", "PageSlug": "concept/alpha", "PageTitle": "Alpha", "PageType": "concept",
|
||||
"ExistingContent": "Existing fact is not valid unless condition A holds.",
|
||||
"SharedSourceContexts": "Source context and uncertainty.", "NewContent": "Source A states 42, except under condition B.",
|
||||
"DeletedContent": "Obsolete assertion.", "RemainingSourcesContent": "Preserved source B.",
|
||||
"AvailableSlugs": "ref-1 (Alpha)", "Language": "English", "CustomInstructions": "Keep attribution.", "InstructionScope": "wiki_content",
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiPageOutputRepairRegeneratesWholePageWithIdenticalScope(t *testing.T) {
|
||||
svc := &wikiIngestService{}
|
||||
input := pageRepairInput()
|
||||
before := make(map[string]string, len(input))
|
||||
for key, value := range input {
|
||||
before[key] = value
|
||||
}
|
||||
var requests [][]chat.Message
|
||||
var prefixes []string
|
||||
model := &wikiPageRepairChat{id: t.Name(), call: func(ctx context.Context, messages []chat.Message, opts *chat.ChatOptions) (*types.ChatResponse, error) {
|
||||
requests = append(requests, append([]chat.Message(nil), messages...))
|
||||
purpose, prefix := types.LLMCallMetadataFromContext(ctx)
|
||||
require.Equal(t, "wiki_page_modify", purpose)
|
||||
prefixes = append(prefixes, prefix)
|
||||
require.Equal(t, wikiLLMMaxTokens, opts.MaxTokens)
|
||||
if len(requests) == 1 {
|
||||
return &types.ChatResponse{Content: "PARTIAL_OUTPUT_MUST_NOT_BE_REUSED", FinishReason: "length"}, nil
|
||||
}
|
||||
return &types.ChatResponse{Content: "SUMMARY: Complete revision\n# Alpha\nNot valid unless condition A holds; source A states 42 except under condition B.", FinishReason: "stop"}, nil
|
||||
}}
|
||||
got, err := svc.generateWikiPageModification(context.Background(), model, input)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, got, "Complete revision")
|
||||
require.Len(t, requests, 2)
|
||||
require.Equal(t, before, input)
|
||||
require.Equal(t, requests[0][0], requests[1][0], "base system prompt must remain unchanged")
|
||||
require.Equal(t, requests[0][1].Content+"\n\n"+wikiPageRepairInstruction, requests[1][1].Content)
|
||||
require.Equal(t, prefixes[0], prefixes[1], "page source prefix remains reusable")
|
||||
require.NotContains(t, requests[1][1].Content, "PARTIAL_OUTPUT_MUST_NOT_BE_REUSED")
|
||||
}
|
||||
|
||||
func TestWikiPageOutputRepairSuccessNeedsNoSecondCall(t *testing.T) {
|
||||
calls := 0
|
||||
model := &wikiScriptedChat{id: t.Name(), call: func(context.Context, []chat.Message) (*types.ChatResponse, error) {
|
||||
calls++
|
||||
return &types.ChatResponse{Content: "SUMMARY: Complete\nComplete body", FinishReason: "stop"}, nil
|
||||
}}
|
||||
got, err := (&wikiIngestService{}).generateWikiPageModification(context.Background(), model, pageRepairInput())
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, got)
|
||||
require.Equal(t, 1, calls)
|
||||
}
|
||||
|
||||
func TestWikiPageOutputRepairStopsAfterOneFailedRegeneration(t *testing.T) {
|
||||
for _, response := range []*types.ChatResponse{
|
||||
{Content: "still partial", FinishReason: "length"},
|
||||
{Content: "still partial", FinishReason: "max_tokens"},
|
||||
{Content: "still partial", FinishReason: "max_output_tokens"},
|
||||
{Content: "still partial", FinishReason: types.FinishReasonIncomplete},
|
||||
{Content: " ", FinishReason: "stop"},
|
||||
{Content: "WIKI_PAGE_REPAIR_UNSAFE", FinishReason: "stop"},
|
||||
{Content: "SUMMARY: Summary without body", FinishReason: "stop"},
|
||||
{Content: "SUMMARY: \nBody without required summary", FinishReason: "stop"},
|
||||
{Content: "Missing required summary line", FinishReason: "stop"},
|
||||
{Content: "I cannot complete the requested page.", FinishReason: "stop"},
|
||||
{Content: strings.Repeat("x", 12001), FinishReason: "stop"},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%s-%d", response.FinishReason, len(response.Content)), func(t *testing.T) {
|
||||
calls := 0
|
||||
model := &wikiScriptedChat{id: t.Name(), call: func(context.Context, []chat.Message) (*types.ChatResponse, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return &types.ChatResponse{Content: "partial", FinishReason: "length"}, nil
|
||||
}
|
||||
return response, nil
|
||||
}}
|
||||
got, err := (&wikiIngestService{}).generateWikiPageModification(context.Background(), model, pageRepairInput())
|
||||
require.Error(t, err)
|
||||
require.Empty(t, got)
|
||||
require.Equal(t, 2, calls)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiPageSummaryOnlyDoesNotOverwritePublishedPage(t *testing.T) {
|
||||
for _, repaired := range []string{"SUMMARY: Only a summary", "Missing required SUMMARY line", "I cannot complete the requested page.", "SUMMARY: \nMissing summary"} {
|
||||
t.Run(repaired, func(t *testing.T) {
|
||||
db := openWikiCheckpointDB(t, filepath.Join(t.TempDir(), "checkpoints.sqlite"))
|
||||
original := &types.WikiPage{ID: "page", TenantID: 7, KnowledgeBaseID: "kb", Slug: "concept/alpha", Title: "Alpha", PageType: types.WikiPageTypeConcept,
|
||||
Status: types.WikiPageStatusPublished, Content: "Original supported content", Summary: "Original summary", SourceRefs: types.StringArray{"old-doc"}}
|
||||
before := *original
|
||||
pages := &checkpointWikiPages{pages: map[string]*types.WikiPage{original.Slug: original}}
|
||||
svc := &wikiIngestService{checkpointDB: db, wikiService: pages, knowledgeSvc: &checkpointKnowledge{}, chunkRepo: &wikiEvidenceChunksRepo{chunks: []*types.Chunk{wikiEvidenceChunk()}}}
|
||||
batch := &WikiBatchContext{SlugTitleMany: func(context.Context, []string) map[string]string { return nil }}
|
||||
batch.checkpointScopes.Store("doc", &wikiCheckpointScope{7, "kb", "doc", "unchanged-fingerprint"})
|
||||
calls := 0
|
||||
model := &wikiScriptedChat{id: t.Name(), call: func(context.Context, []chat.Message) (*types.ChatResponse, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return &types.ChatResponse{Content: "SUMMARY: Incomplete first page", FinishReason: "stop"}, nil
|
||||
}
|
||||
return &types.ChatResponse{Content: repaired, FinishReason: "stop"}, nil
|
||||
}}
|
||||
update := SlugUpdate{KnowledgeID: "doc", SourceRef: "doc", Slug: original.Slug, Type: types.WikiPageTypeConcept, DocTitle: "Source", SourceChunks: []string{"source-chunk"}, Item: extractedItem{Name: "Alpha", Slug: original.Slug, Details: "New fact"}}
|
||||
changed, _, failed, err := svc.reduceSlugUpdates(context.Background(), model, "kb", original.Slug, []SlugUpdate{update}, 7, batch, nil)
|
||||
require.Error(t, err)
|
||||
require.False(t, changed)
|
||||
require.True(t, failed)
|
||||
require.Equal(t, 2, calls)
|
||||
require.Zero(t, pages.writes)
|
||||
require.Equal(t, before, *pages.pages[original.Slug])
|
||||
var checkpoints int64
|
||||
require.NoError(t, db.Model(&wikiIngestCheckpoint{}).Where("stage = ?", "page").Count(&checkpoints).Error)
|
||||
require.Zero(t, checkpoints)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiPageOutputRepairDoesNotMultiplyProviderFailures(t *testing.T) {
|
||||
for _, afterOutputFailure := range []bool{false, true} {
|
||||
for _, failure := range []string{"API request failed with status 401", "API request failed with status 402", "API request failed with status 403", "API request failed with status 429", "connection closed", "timeout"} {
|
||||
if failure == "timeout" && !afterOutputFailure {
|
||||
continue // The first request retains its existing transient transport policy.
|
||||
}
|
||||
t.Run(fmt.Sprintf("repair-%t-%s", afterOutputFailure, failure), func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), types.TenantIDContextKey, uint64(700))
|
||||
calls := 0
|
||||
model := &wikiScriptedChat{id: t.Name(), call: func(context.Context, []chat.Message) (*types.ChatResponse, error) {
|
||||
calls++
|
||||
if afterOutputFailure && calls == 1 {
|
||||
return &types.ChatResponse{Content: "partial", FinishReason: "length"}, nil
|
||||
}
|
||||
return nil, errors.New(failure)
|
||||
}}
|
||||
key := wikiProviderCircuitKey(700, model.id)
|
||||
t.Cleanup(func() { wikiProviderBlocks.Delete(key); wikiProviderSlots.Delete(key) })
|
||||
got, err := (&wikiIngestService{}).generateWikiPageModification(ctx, model, pageRepairInput())
|
||||
require.Error(t, err)
|
||||
require.Empty(t, got)
|
||||
expected := 1
|
||||
if afterOutputFailure {
|
||||
expected++
|
||||
}
|
||||
require.Equal(t, expected, calls)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiPageOutputRepairHonorsCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
calls := 0
|
||||
model := &wikiScriptedChat{id: t.Name(), call: func(context.Context, []chat.Message) (*types.ChatResponse, error) {
|
||||
calls++
|
||||
cancel()
|
||||
return &types.ChatResponse{Content: "partial", FinishReason: "length"}, nil
|
||||
}}
|
||||
got, err := (&wikiIngestService{}).generateWikiPageModification(ctx, model, pageRepairInput())
|
||||
require.Error(t, err)
|
||||
require.Empty(t, got)
|
||||
require.Equal(t, 1, calls)
|
||||
}
|
||||
|
||||
func TestWikiPageOutputRepairFailurePreservesPublishedPageAndCheckpoint(t *testing.T) {
|
||||
for _, repairsSuccessfully := range []bool{false, true} {
|
||||
t.Run(fmt.Sprint(repairsSuccessfully), func(t *testing.T) {
|
||||
db := openWikiCheckpointDB(t, filepath.Join(t.TempDir(), "checkpoints.sqlite"))
|
||||
original := &types.WikiPage{ID: "page", TenantID: 7, KnowledgeBaseID: "kb", Slug: "concept/alpha", Title: "Alpha", PageType: types.WikiPageTypeConcept,
|
||||
Status: types.WikiPageStatusPublished, Content: "Original supported content", Summary: "Original summary", SourceRefs: types.StringArray{"old-doc"}}
|
||||
before := *original
|
||||
pages := &checkpointWikiPages{pages: map[string]*types.WikiPage{original.Slug: original}}
|
||||
svc := &wikiIngestService{checkpointDB: db, wikiService: pages, knowledgeSvc: &checkpointKnowledge{}, chunkRepo: &wikiEvidenceChunksRepo{chunks: []*types.Chunk{wikiEvidenceChunk()}}}
|
||||
batch := &WikiBatchContext{SlugTitleMany: func(context.Context, []string) map[string]string { return nil }}
|
||||
batch.checkpointScopes.Store("doc", &wikiCheckpointScope{7, "kb", "doc", "unchanged-fingerprint"})
|
||||
calls := 0
|
||||
model := &wikiScriptedChat{id: t.Name(), call: func(context.Context, []chat.Message) (*types.ChatResponse, error) {
|
||||
calls++
|
||||
if repairsSuccessfully && calls == 2 {
|
||||
return &types.ChatResponse{Content: "SUMMARY: Complete revised summary\nOriginal supported content. New fact has a condition.", FinishReason: "stop"}, nil
|
||||
}
|
||||
return &types.ChatResponse{Content: "partial", FinishReason: "length"}, nil
|
||||
}}
|
||||
update := SlugUpdate{KnowledgeID: "doc", SourceRef: "doc", Slug: original.Slug, Type: types.WikiPageTypeConcept, DocTitle: "Source", SourceChunks: []string{"source-chunk"}, Item: extractedItem{Name: "Alpha", Slug: original.Slug, Details: "New fact has a condition."}}
|
||||
changed, _, failed, err := svc.reduceSlugUpdates(context.Background(), model, "kb", original.Slug, []SlugUpdate{update}, 7, batch, nil)
|
||||
require.Equal(t, 2, calls)
|
||||
var checkpoints int64
|
||||
require.NoError(t, db.Model(&wikiIngestCheckpoint{}).Where("stage = ?", "page").Count(&checkpoints).Error)
|
||||
if repairsSuccessfully {
|
||||
require.NoError(t, err)
|
||||
require.True(t, changed)
|
||||
require.False(t, failed)
|
||||
require.Equal(t, 1, pages.writes)
|
||||
require.Equal(t, int64(1), checkpoints)
|
||||
require.Contains(t, pages.pages[original.Slug].Content, "Original supported content")
|
||||
require.Contains(t, pages.pages[original.Slug].SourceRefs, "old-doc")
|
||||
require.Contains(t, pages.pages[original.Slug].SourceRefs, "doc")
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.False(t, changed)
|
||||
require.True(t, failed)
|
||||
require.Zero(t, pages.writes)
|
||||
require.Zero(t, checkpoints)
|
||||
require.Equal(t, before, *pages.pages[original.Slug])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user