mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-10 22:33:48 +02:00
291 lines
8.4 KiB
Go
291 lines
8.4 KiB
Go
package multiagent
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/cloudwego/eino/adk"
|
|
"github.com/cloudwego/eino/schema"
|
|
)
|
|
|
|
var (
|
|
summarizationAnalysisBlockRegex = regexp.MustCompile(`(?is)<analysis>\s*.*?\s*</analysis>`)
|
|
summarizationSummaryBlockRegex = regexp.MustCompile(`(?is)<summary>\s*(.*?)\s*</summary>`)
|
|
userIntentLedgerBlockRegex = regexp.MustCompile(`(?is)<original_user_intent_ledger>\s*(.*?)\s*</original_user_intent_ledger>`)
|
|
userIntentLedgerSectionRegex = regexp.MustCompile(`(?is)\s*## 原始用户输入与约束账本(系统保真)\s*<original_user_intent_ledger>\s*.*?\s*</original_user_intent_ledger>\s*`)
|
|
)
|
|
|
|
const (
|
|
userIntentLedgerStartMarker = "<original_user_intent_ledger>"
|
|
userIntentLedgerEndMarker = "</original_user_intent_ledger>"
|
|
)
|
|
|
|
// stripAnalysisFromSummarizationMessage removes the <analysis> block from a post-processed
|
|
// Eino summary user message. Analysis helps one-shot generation quality but should not
|
|
// occupy continuation context after compaction.
|
|
func stripAnalysisFromSummarizationMessage(msg adk.Message) adk.Message {
|
|
if msg == nil {
|
|
return msg
|
|
}
|
|
cloned := *msg
|
|
if cloned.Content != "" {
|
|
cloned.Content = stripAnalysisFromSummarizationText(cloned.Content)
|
|
}
|
|
if len(cloned.UserInputMultiContent) > 0 {
|
|
parts := make([]schema.MessageInputPart, len(cloned.UserInputMultiContent))
|
|
copy(parts, cloned.UserInputMultiContent)
|
|
// Only the first text part carries model output plus Eino preamble/transcript path.
|
|
for i := range parts {
|
|
if parts[i].Type != schema.ChatMessagePartTypeText || parts[i].Text == "" {
|
|
continue
|
|
}
|
|
if i == 0 {
|
|
parts[i].Text = stripAnalysisFromSummarizationText(parts[i].Text)
|
|
}
|
|
break
|
|
}
|
|
cloned.UserInputMultiContent = parts
|
|
}
|
|
return &cloned
|
|
}
|
|
|
|
func stripAnalysisFromSummarizationText(text string) string {
|
|
text = strings.TrimSpace(text)
|
|
if text == "" {
|
|
return text
|
|
}
|
|
stripped := strings.TrimSpace(summarizationAnalysisBlockRegex.ReplaceAllString(text, ""))
|
|
if stripped == "" {
|
|
return text
|
|
}
|
|
return stripped
|
|
}
|
|
|
|
// extractSummarizationSummaryBody returns the inner text of the last <summary> block when present.
|
|
// Used by tests and optional strict compaction paths.
|
|
func extractSummarizationSummaryBody(text string) (string, bool) {
|
|
text = strings.TrimSpace(text)
|
|
if text == "" {
|
|
return "", false
|
|
}
|
|
all := summarizationSummaryBlockRegex.FindAllStringSubmatch(text, -1)
|
|
if len(all) == 0 || len(all[len(all)-1]) < 2 {
|
|
return "", false
|
|
}
|
|
body := strings.TrimSpace(all[len(all)-1][1])
|
|
if body == "" {
|
|
return "", false
|
|
}
|
|
return body, true
|
|
}
|
|
|
|
// buildOriginalUserIntentLedgerMessage returns a deterministic, host-generated
|
|
// context anchor for raw user inputs. Keep it separate from the model-generated
|
|
// working summary so compaction can rewrite the summary without rewriting user intent.
|
|
func buildOriginalUserIntentLedgerMessage(originalMessages []adk.Message, maxRunes, entryMaxRunes int) adk.Message {
|
|
ledger := buildOriginalUserIntentLedger(originalMessages, maxRunes, entryMaxRunes)
|
|
if strings.TrimSpace(ledger) == "" {
|
|
return nil
|
|
}
|
|
return schema.SystemMessage(wrapUserIntentLedger(ledger))
|
|
}
|
|
|
|
func mergeMessageIntoLeadingSystem(msgs []adk.Message, msg adk.Message) []adk.Message {
|
|
if msg == nil {
|
|
return msgs
|
|
}
|
|
for i, existing := range msgs {
|
|
if existing == nil || existing.Role != schema.System {
|
|
continue
|
|
}
|
|
cloned := *existing
|
|
cloned.Content = strings.TrimSpace(strings.TrimSpace(cloned.Content) + "\n\n" + strings.TrimSpace(msg.Content))
|
|
out := make([]adk.Message, len(msgs))
|
|
copy(out, msgs)
|
|
out[i] = &cloned
|
|
return out
|
|
}
|
|
out := make([]adk.Message, 0, len(msgs)+1)
|
|
out = append(out, msg)
|
|
out = append(out, msgs...)
|
|
return out
|
|
}
|
|
|
|
func stripOriginalUserIntentLedgerFromMessages(msgs []adk.Message) []adk.Message {
|
|
if len(msgs) == 0 {
|
|
return msgs
|
|
}
|
|
out := make([]adk.Message, 0, len(msgs))
|
|
for _, msg := range msgs {
|
|
if msg == nil {
|
|
continue
|
|
}
|
|
cloned := *msg
|
|
cloned.Content = stripOriginalUserIntentLedgerFromText(cloned.Content)
|
|
if len(cloned.UserInputMultiContent) > 0 {
|
|
parts := make([]schema.MessageInputPart, len(cloned.UserInputMultiContent))
|
|
copy(parts, cloned.UserInputMultiContent)
|
|
for i := range parts {
|
|
if parts[i].Type == schema.ChatMessagePartTypeText {
|
|
parts[i].Text = stripOriginalUserIntentLedgerFromText(parts[i].Text)
|
|
}
|
|
}
|
|
cloned.UserInputMultiContent = parts
|
|
}
|
|
if strings.TrimSpace(cloned.Content) == "" && len(cloned.UserInputMultiContent) == 0 && len(cloned.ToolCalls) == 0 && cloned.ReasoningContent == "" {
|
|
continue
|
|
}
|
|
out = append(out, &cloned)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func buildOriginalUserIntentLedger(msgs []adk.Message, maxRunes, entryMaxRunes int) string {
|
|
entries := collectOriginalUserIntentEntries(msgs)
|
|
if len(entries) == 0 {
|
|
return ""
|
|
}
|
|
var sb strings.Builder
|
|
for i, entry := range entries {
|
|
line := fmt.Sprintf("- [U%03d] %s\n", i+1, sanitizeUserIntentLedgerEntry(entry, entryMaxRunes))
|
|
if maxRunes > 0 && utf8RuneLen(sb.String())+utf8RuneLen(line) > maxRunes {
|
|
sb.WriteString("- [...truncated] 用户原始输入账本超过预算;完整压缩前记录见 summarization transcript。\n")
|
|
break
|
|
}
|
|
sb.WriteString(line)
|
|
}
|
|
return strings.TrimSpace(sb.String())
|
|
}
|
|
|
|
func collectOriginalUserIntentEntries(msgs []adk.Message) []string {
|
|
seen := make(map[string]struct{})
|
|
entries := make([]string, 0, 16)
|
|
add := func(s string) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" || isSyntheticContinuationUserText(s) {
|
|
return
|
|
}
|
|
key := normalizeUserIntentLedgerText(s)
|
|
if key == "" {
|
|
return
|
|
}
|
|
if _, ok := seen[key]; ok {
|
|
return
|
|
}
|
|
seen[key] = struct{}{}
|
|
entries = append(entries, s)
|
|
}
|
|
for _, msg := range msgs {
|
|
if msg == nil {
|
|
continue
|
|
}
|
|
for _, entry := range extractExistingUserIntentLedgerEntries(messageTextForLedger(msg)) {
|
|
add(entry)
|
|
}
|
|
if msg.Role == schema.User {
|
|
add(adkUserMessageText(msg))
|
|
}
|
|
}
|
|
return entries
|
|
}
|
|
|
|
func messageTextForLedger(msg adk.Message) string {
|
|
if msg == nil {
|
|
return ""
|
|
}
|
|
var b strings.Builder
|
|
if strings.TrimSpace(msg.Content) != "" {
|
|
b.WriteString(msg.Content)
|
|
}
|
|
for _, part := range msg.UserInputMultiContent {
|
|
if part.Type != schema.ChatMessagePartTypeText || strings.TrimSpace(part.Text) == "" {
|
|
continue
|
|
}
|
|
if b.Len() > 0 {
|
|
b.WriteByte('\n')
|
|
}
|
|
b.WriteString(part.Text)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func extractExistingUserIntentLedgerEntries(text string) []string {
|
|
matches := userIntentLedgerBlockRegex.FindAllStringSubmatch(text, -1)
|
|
if len(matches) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(matches))
|
|
for _, match := range matches {
|
|
if len(match) < 2 {
|
|
continue
|
|
}
|
|
for _, line := range strings.Split(match[1], "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "- [...truncated]") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "- [U") {
|
|
if idx := strings.Index(line, "]"); idx >= 0 && idx+1 < len(line) {
|
|
line = strings.TrimSpace(line[idx+1:])
|
|
}
|
|
}
|
|
if line != "" {
|
|
out = append(out, line)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func stripOriginalUserIntentLedgerFromText(text string) string {
|
|
text = strings.TrimSpace(text)
|
|
if text == "" {
|
|
return ""
|
|
}
|
|
text = userIntentLedgerSectionRegex.ReplaceAllString(text, "\n")
|
|
text = userIntentLedgerBlockRegex.ReplaceAllString(text, "\n")
|
|
return strings.TrimSpace(text)
|
|
}
|
|
|
|
func wrapUserIntentLedger(ledger string) string {
|
|
return strings.TrimSpace("## 原始用户输入与约束账本(系统保真)\n" +
|
|
userIntentLedgerStartMarker + "\n" +
|
|
strings.TrimSpace(ledger) + "\n" +
|
|
userIntentLedgerEndMarker)
|
|
}
|
|
|
|
func sanitizeUserIntentLedgerEntry(s string, maxRunes int) string {
|
|
s = strings.ReplaceAll(strings.TrimSpace(s), userIntentLedgerStartMarker, "[ledger-start]")
|
|
s = strings.ReplaceAll(s, userIntentLedgerEndMarker, "[ledger-end]")
|
|
s = strings.Join(strings.Fields(s), " ")
|
|
if maxRunes > 0 && utf8RuneLen(s) > maxRunes {
|
|
return truncateUserIntentLedgerRunes(s, maxRunes) + "..."
|
|
}
|
|
return s
|
|
}
|
|
|
|
func normalizeUserIntentLedgerText(s string) string {
|
|
return strings.Join(strings.Fields(strings.TrimSpace(s)), " ")
|
|
}
|
|
|
|
func isSyntheticContinuationUserText(s string) bool {
|
|
return strings.Contains(s, continuationSessionMarker) ||
|
|
strings.Contains(s, "【系统自动续跑 / Auto resume】")
|
|
}
|
|
|
|
func utf8RuneLen(s string) int {
|
|
return len([]rune(s))
|
|
}
|
|
|
|
func truncateUserIntentLedgerRunes(s string, n int) string {
|
|
if n <= 0 {
|
|
return ""
|
|
}
|
|
runes := []rune(s)
|
|
if len(runes) <= n {
|
|
return s
|
|
}
|
|
return string(runes[:n])
|
|
}
|