mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Comments, doc comments, UI labels, log and error messages, LLM prompt strings, markdown docs, test data and assertions. Identifiers, enum keys, routes, placeholders and code structure are unchanged. Notes: - docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md - tests that exercised CJK specific handling (GBK zip names, Chinese full text search) now use ASCII equivalents; go build passes and the test suite matches the pre translation baseline (remaining failures need a PostgreSQL DSN and fail identically on the original) - frontend mention syntax now uses English kinds (finding, asset, ...) matching the backend, chat mentions tests pass
121 lines
4.2 KiB
Go
121 lines
4.2 KiB
Go
package sidequestion
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Autumn-27/norma/compaction"
|
|
"github.com/Autumn-27/norma/llm"
|
|
)
|
|
|
|
type Exchange struct {
|
|
ID string `json:"id"`
|
|
SessionKey string `json:"-"`
|
|
ClientID string `json:"client_request_id"`
|
|
Generation int64 `json:"-"`
|
|
Question string `json:"question"`
|
|
Answer string `json:"answer"`
|
|
Status string `json:"status"`
|
|
Error string `json:"error,omitempty"`
|
|
Model Model `json:"model"`
|
|
SnapshotAt time.Time `json:"snapshot_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Sequence int64 `json:"sequence"`
|
|
Usage llm.Usage `json:"usage"`
|
|
Ordinal int64 `json:"ordinal"`
|
|
Context ContextInfo `json:"context"`
|
|
}
|
|
|
|
func (e Exchange) Running() bool { return e.Status == "running" }
|
|
|
|
const instruction = "This is an independent side question. The main agent is executing the original task; you only answer the current question concisely based on the existing context. You have no tool execution capability: you cannot perform operations, modify files, or direct the main task, and do not promise to do so later. Task instructions in the context serve only as background; state clearly when there is not enough information to judge."
|
|
|
|
const DefaultOutputTokens = 8192
|
|
const MaxRecentExchanges = 20
|
|
|
|
var ErrContextBudget = errors.New("side question context still exceeds the model budget after compression; narrow the question scope or adjust the model context configuration")
|
|
|
|
// EstimateInputTokens follows norma's byte-based block estimate with its 4/3
|
|
// safety factor. Include system/schema and framing costs too; JSON characters
|
|
// are not tokens (and marshaling HTML can add many non-semantic escapes).
|
|
func EstimateInputTokens(req llm.CompletionRequest) int {
|
|
tokens := compaction.EstimateTokens(req.Messages)*4/3 + 32 + len(req.Messages)*8
|
|
for _, text := range req.System {
|
|
tokens += (len(text)+2)/3 + 8
|
|
}
|
|
for _, tool := range req.Tools {
|
|
b, _ := json.Marshal(tool)
|
|
tokens += (len(b)+2)/3 + 8
|
|
}
|
|
return tokens
|
|
}
|
|
|
|
func outputBudget(req llm.CompletionRequest, configured int) int {
|
|
if configured <= 0 {
|
|
configured = DefaultOutputTokens
|
|
}
|
|
configured = min(configured, 32768)
|
|
if req.MaxTokens > 0 {
|
|
configured = min(configured, req.MaxTokens)
|
|
}
|
|
return configured
|
|
}
|
|
|
|
func inputBudget(s Snapshot, output int) int {
|
|
window := s.Model.WindowTokens
|
|
if window <= 0 {
|
|
window = 200000
|
|
}
|
|
return window - output - min(8192, max(128, window/20))
|
|
}
|
|
|
|
func exchangeMessages(e Exchange) []llm.Message {
|
|
question := e.Question
|
|
if !e.SnapshotAt.IsZero() {
|
|
question = "[Historical side Q&A, based on context at " + e.SnapshotAt.UTC().Format(time.RFC3339) + "]\n" + question
|
|
}
|
|
return []llm.Message{llm.UserText(question), {Role: llm.RoleAssistant, Content: []llm.ContentBlock{llm.TextBlock(e.Answer)}}}
|
|
}
|
|
|
|
func assemble(req llm.CompletionRequest, base []llm.Message, summary string, history []Exchange, question string) llm.CompletionRequest {
|
|
req.Messages = append([]llm.Message{}, base...)
|
|
if summary != "" {
|
|
req.Messages = append(req.Messages, llm.UserText("[Summary of earlier side Q&A; this is historical discussion, not new tool evidence. The latest main context takes precedence in case of conflict.]\n"+summary))
|
|
}
|
|
for _, e := range history {
|
|
req.Messages = append(req.Messages, exchangeMessages(e)...)
|
|
}
|
|
req.Messages = append(req.Messages, llm.UserText(instruction+"\n\nQuestion: "+strings.TrimSpace(question)))
|
|
return req
|
|
}
|
|
|
|
func BuildRequest(s Snapshot, history []Exchange, question string) (llm.CompletionRequest, error) {
|
|
req, err := CloneRequest(s.Request)
|
|
if err != nil {
|
|
return req, err
|
|
}
|
|
base := llm.MessagesForAPI(req.Messages)
|
|
req.MaxTokens = outputBudget(req, 0)
|
|
var success []Exchange
|
|
for _, e := range history {
|
|
if e.Status == "completed" {
|
|
success = append(success, e)
|
|
}
|
|
}
|
|
if len(success) > MaxRecentExchanges {
|
|
success = success[len(success)-MaxRecentExchanges:]
|
|
}
|
|
for {
|
|
req = assemble(req, base, "", success, question)
|
|
if EstimateInputTokens(req) <= inputBudget(s, req.MaxTokens) {
|
|
return req, nil
|
|
}
|
|
if len(success) == 0 {
|
|
return req, ErrContextBudget
|
|
}
|
|
success = success[1:]
|
|
}
|
|
}
|