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
168 lines
6.6 KiB
Go
168 lines
6.6 KiB
Go
package workbench
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/Tencent/WeKnora/internal/accesspolicy"
|
||
"github.com/Tencent/WeKnora/internal/agent/token"
|
||
"github.com/Tencent/WeKnora/internal/models/chat"
|
||
"github.com/Tencent/WeKnora/internal/types"
|
||
)
|
||
|
||
const qualitySystem = `你是内置跨稿质检员。审查已采用正文的逻辑、先修依赖、衔接、重复和论据一致性。严重逻辑断裂或重复为 blocking,措辞建议为 suggestion。不得编造问题或审查范围。严格输出 JSON {"passed":true,"summary":"中文总结","issues":[{"segment_ids":["实际段落编号"],"severity":"blocking 或 suggestion","problem":"具体问题","suggestion":"具体修改建议"}]}。无 blocking 时 passed 必须为 true,否则 false。`
|
||
|
||
func manuscriptPrompt(segments []Segment, advice string) string {
|
||
return "本次仅审查以下实际采用的正文。用户可以主动跳过内容段,编号不连续或段数少于原计划本身不是缺陷,不得要求恢复已跳过段;若确有未完成承诺,应建议在已采用段内补足或修正预告。\n已采用正文:" + compact(segments) + "\n用户补充意见:" + advice
|
||
}
|
||
|
||
// Every accepted segment is reviewed in full, and adjacent windows overlap by
|
||
// one complete segment. A single segment that cannot fit still fails explicitly.
|
||
func manuscriptWindows(c *completionRequest, segments []Segment, advice string) ([][]Segment, error) {
|
||
fits := func(rows []Segment) bool {
|
||
messages := append(c.messages[:0:0], c.messages...)
|
||
messages[1].Content = manuscriptPrompt(rows, advice)
|
||
return c.estimator.EstimateMessages(messages) <= c.inputLimit
|
||
}
|
||
if fits(segments) {
|
||
return [][]Segment{segments}, nil
|
||
}
|
||
windows := [][]Segment{}
|
||
current := []Segment{}
|
||
for _, segment := range segments {
|
||
if !fits([]Segment{segment}) {
|
||
return nil, fmt.Errorf("%w: 模型上下文容量不足以容纳单个内容段,请为质检选择更大上下文模型或拆分该段", ErrInvalid)
|
||
}
|
||
if len(current) > 0 && !fits(append(append([]Segment(nil), current...), segment)) {
|
||
windows = append(windows, current)
|
||
current = []Segment{current[len(current)-1]}
|
||
if !fits(append(current, segment)) {
|
||
return nil, fmt.Errorf("%w: 相邻正文的上下文容量不足,无法完整检查衔接,请选择更大上下文模型", ErrInvalid)
|
||
}
|
||
}
|
||
current = append(current, segment)
|
||
}
|
||
if len(current) > 0 {
|
||
windows = append(windows, current)
|
||
}
|
||
return windows, nil
|
||
}
|
||
|
||
func mergeQualityReports(reports []QualityReport, accepted []Segment) (*QualityReport, error) {
|
||
merged := &QualityReport{Passed: true, Issues: []QualityIssue{}}
|
||
seen := map[string]bool{}
|
||
for i, report := range reports {
|
||
merged.Summary += fmt.Sprintf("检查 %d:%s\n", i+1, report.Summary)
|
||
for _, issue := range report.Issues {
|
||
key := compact(issue)
|
||
if !seen[key] {
|
||
seen[key] = true
|
||
merged.Issues = append(merged.Issues, issue)
|
||
}
|
||
if issue.Severity == "blocking" {
|
||
merged.Passed = false
|
||
}
|
||
}
|
||
}
|
||
if err := validateQuality(merged, accepted); err != nil {
|
||
return nil, err
|
||
}
|
||
return merged, nil
|
||
}
|
||
|
||
func (s *Service) prepareQualityCompletion(ctx context.Context, agentID string) (*completionRequest, error) {
|
||
if !accesspolicy.Allowed(ctx, "content.review") {
|
||
return nil, ErrForbidden
|
||
}
|
||
snapshot, frozen := snapshotFromContext(ctx, agentID)
|
||
if !frozen || snapshot.ModelID == "" {
|
||
return s.prepareCompletion(ctx, agentID, qualitySystem, "")
|
||
}
|
||
// Use the task's recorded model for review, without loading private skills,
|
||
// persona, memory, or knowledge that the reviewer cannot otherwise access.
|
||
metadata, err := s.models.GetModelByID(ctx, snapshot.ModelID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if metadata == nil {
|
||
return nil, fmt.Errorf("%w: 任务的质检模型已不可用,请更新任务配置", ErrInvalid)
|
||
}
|
||
estimator, err := token.NewEstimator()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
contextSize := types.AgentMaxContextTokens(0, metadata.Parameters.ContextWindow)
|
||
if snapshot.ContextWindow > 0 {
|
||
contextSize = min(contextSize, snapshot.ContextWindow)
|
||
}
|
||
outputLimit := snapshot.OutputLimit
|
||
if outputLimit <= 0 {
|
||
outputLimit = types.DefaultMaxCompletionTokens(types.AgentModeQuickAnswer, "")
|
||
}
|
||
if metadata.Parameters.MaxOutputTokens > 0 {
|
||
outputLimit = min(outputLimit, metadata.Parameters.MaxOutputTokens)
|
||
}
|
||
agent := &types.CustomAgent{ID: agentID, Config: types.CustomAgentConfig{ModelID: snapshot.ModelID, Temperature: snapshot.Temperature, Thinking: snapshot.Thinking}}
|
||
c := &completionRequest{agent: agent, estimator: estimator, contextSize: contextSize,
|
||
inputLimit: contextSize - contextSize*15/100 - outputLimit, outputLimit: outputLimit,
|
||
messages: []chat.Message{{Role: "system", Content: qualitySystem}, {Role: "user"}}, jsonOutput: true}
|
||
return c, c.validateBudget()
|
||
}
|
||
|
||
func (s *Service) reviewManuscript(ctx context.Context, agentID string, accepted []Segment, advice string) (*QualityReport, error) {
|
||
c, err := s.prepareQualityCompletion(ctx, agentID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
windows, err := manuscriptWindows(c, accepted, advice)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
reports := []QualityReport{}
|
||
for i, window := range windows {
|
||
if lease, ok := ctx.Value(leaseContextKey{}).(jobLease); ok {
|
||
if err := s.progress(ctx, lease.ID, 40+40*i/len(windows), fmt.Sprintf("跨稿质检:第 %d/%d 组,完整检查正文及相邻衔接", i+1, len(windows))); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
c.messages[1].Content = manuscriptPrompt(window, advice)
|
||
c.jsonOutput = true
|
||
text, err := s.runCompletion(ctx, c)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
report, err := parseJSON[QualityReport](text)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if err := validateQuality(&report, window); err != nil {
|
||
return nil, err
|
||
}
|
||
reports = append(reports, report)
|
||
}
|
||
report, err := mergeQualityReports(reports, accepted)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(windows) > 1 {
|
||
report.Summary = fmt.Sprintf("共 %d 组,全部已采用正文逐组完整质检,相邻组保留完整衔接段;远距离语义一致性仍需结合整稿复核。\n", len(windows)) + report.Summary
|
||
}
|
||
return report, nil
|
||
}
|
||
|
||
func composeAccepted(accepted []Segment) string {
|
||
parts := make([]string, 0, len(accepted))
|
||
for _, segment := range accepted {
|
||
parts = append(parts, segment.Content)
|
||
}
|
||
return strings.Join(parts, "\n\n")
|
||
}
|
||
|
||
func (s *Service) composeManuscript(ctx context.Context, agentID string, accepted []Segment, advice string) (string, error) {
|
||
if strings.TrimSpace(advice) != "" {
|
||
return "", fmt.Errorf("%w: 组合成稿保留原文;请先在对应内容段应用修改建议并重新质检", ErrInvalid)
|
||
}
|
||
return composeAccepted(accepted), nil
|
||
}
|