Files
2026-08-15 02:06:44 +08:00

221 lines
5.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package multiagent
import (
"encoding/json"
"strings"
"cyberstrike-ai/internal/agent"
"cyberstrike-ai/internal/einomcp"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/schema"
)
type einoRunResultBuilderConfig struct {
OrchMode string
EmptyHint string
RunMessages *einoRunMessageAccumulator
AssistantOutput *einoAssistantOutputAccumulator
SnapshotMCPIDs func() []string
ModelFacingTrace func() []adk.Message
}
type einoRunResultBuilder struct {
cfg einoRunResultBuilderConfig
}
func newEinoRunResultBuilder(cfg einoRunResultBuilderConfig) *einoRunResultBuilder {
return &einoRunResultBuilder{cfg: cfg}
}
func (b *einoRunResultBuilder) BuildPartial(runErr error) (*RunResult, error) {
if b == nil || b.cfg.RunMessages == nil || !b.cfg.RunMessages.HasNewMessages() {
return nil, runErr
}
return b.build(true), runErr
}
func (b *einoRunResultBuilder) BuildFinal() *RunResult {
if b == nil {
return &RunResult{}
}
return b.build(false)
}
func (b *einoRunResultBuilder) build(partial bool) *RunResult {
var runMsgs []adk.Message
if b.cfg.RunMessages != nil {
runMsgs = b.cfg.RunMessages.Messages()
}
var lastAssistant string
var lastPlanExecuteExecutor string
if b.cfg.AssistantOutput != nil {
lastAssistant = b.cfg.AssistantOutput.LastAssistant()
lastPlanExecuteExecutor = b.cfg.AssistantOutput.LastPlanExecuteExecutor()
}
var modelFacing []adk.Message
if b.cfg.ModelFacingTrace != nil {
modelFacing = b.cfg.ModelFacingTrace()
}
var ids []string
if b.cfg.SnapshotMCPIDs != nil {
ids = b.cfg.SnapshotMCPIDs()
}
return buildEinoRunResultFromAccumulated(
b.cfg.OrchMode,
runMsgs,
modelFacing,
lastAssistant,
lastPlanExecuteExecutor,
b.cfg.EmptyHint,
ids,
partial,
)
}
func einoPartialRunLastOutputHint() string {
return "[执行未正常结束(用户停止、超时或异常)。续跑时请基于上文已产生的工具与结果继续,勿重复已完成步骤。]\n" +
"[Run ended abnormally; continue from the trace above without repeating completed steps.]"
}
func buildEinoRunResultFromAccumulated(
orchMode string,
runAccumulatedMsgs []adk.Message,
persistMsgs []adk.Message,
lastAssistant string,
lastPlanExecuteExecutor string,
emptyHint string,
mcpIDs []string,
partial bool,
) *RunResult {
traceForJSON := persistMsgs
traceJSON := ""
if len(traceForJSON) > 0 {
traceForJSON = markModelFacingTraceForPersistence(traceForJSON)
if histJSON, err := json.Marshal(traceForJSON); err == nil {
traceJSON = string(histJSON)
}
}
cleaned := strings.TrimSpace(lastAssistant)
if orchMode == "plan_execute" {
if e := strings.TrimSpace(lastPlanExecuteExecutor); e != "" {
cleaned = e
} else {
cleaned = UnwrapPlanExecuteUserText(cleaned)
}
}
if cleaned == "" {
if fb := strings.TrimSpace(einoExtractFallbackAssistantFromMsgs(runAccumulatedMsgs)); fb != "" {
cleaned = fb
}
}
cleaned = dedupeRepeatedParagraphs(cleaned, 80)
cleaned = dedupeParagraphsByLineFingerprint(cleaned, 100)
const maxResponseRunes = 100000
if rs := []rune(cleaned); len(rs) > maxResponseRunes {
cleaned = string(rs[:maxResponseRunes]) + "\n\n... (response truncated / 响应已截断)"
}
lastOut := cleaned
resp := cleaned
if partial && cleaned == "" {
lastOut = einoPartialRunLastOutputHint()
resp = emptyHint
}
out := &RunResult{
Response: resp,
MCPExecutionIDs: mcpIDs,
LastAgentTraceInput: traceJSON,
LastAgentTraceOutput: lastOut,
}
if !partial && out.Response == "" {
out.Response = emptyHint
out.LastAgentTraceOutput = out.Response
}
return out
}
func markModelFacingTraceForPersistence(msgs []adk.Message) []adk.Message {
out := cloneADKMessagesForTrace(msgs)
if len(out) == 0 || out[0] == nil {
return out
}
if out[0].Extra == nil {
out[0].Extra = make(map[string]any, 1)
}
out[0].Extra[agent.ModelFacingTraceVersionKey] = 1
return out
}
// einoExtractFallbackAssistantFromMsgs 在「主通道未产出助手正文」时,从 Eino ADK 轨迹中回填用户可见回复。
// 典型场景:监督者仅调用 exitfinal_result 落在 Tool 消息中),或工具结果已写入历史但 lastAssistant 未更新。
//
// 优先级:最后一次 exit 工具输出 → 最后一条含 exit 的助手 tool_calls 参数中的 final_result。
func einoExtractFallbackAssistantFromMsgs(msgs []adk.Message) string {
for i := len(msgs) - 1; i >= 0; i-- {
m := msgs[i]
if m == nil || m.Role != schema.Tool {
continue
}
if !strings.EqualFold(strings.TrimSpace(m.ToolName), adk.ToolInfoExit.Name) {
continue
}
content := strings.TrimSpace(m.Content)
if content == "" || strings.HasPrefix(content, einomcp.ToolErrorPrefix) {
continue
}
return content
}
for i := len(msgs) - 1; i >= 0; i-- {
m := msgs[i]
if m == nil || m.Role != schema.Assistant {
continue
}
if s := einoExtractExitFinalFromAssistantToolCalls(m); s != "" {
return s
}
}
return ""
}
func einoExtractExitFinalFromAssistantToolCalls(msg *schema.Message) string {
if msg == nil || len(msg.ToolCalls) == 0 {
return ""
}
for i := len(msg.ToolCalls) - 1; i >= 0; i-- {
tc := msg.ToolCalls[i]
if !strings.EqualFold(strings.TrimSpace(tc.Function.Name), adk.ToolInfoExit.Name) {
continue
}
if s := einoParseExitFinalResultArguments(tc.Function.Arguments); s != "" {
return s
}
}
return ""
}
func einoParseExitFinalResultArguments(arguments string) string {
arguments = strings.TrimSpace(arguments)
if arguments == "" {
return ""
}
var wrap struct {
FinalResult json.RawMessage `json:"final_result"`
}
if err := json.Unmarshal([]byte(arguments), &wrap); err != nil || len(wrap.FinalResult) == 0 {
return ""
}
var s string
if err := json.Unmarshal(wrap.FinalResult, &s); err == nil {
return strings.TrimSpace(s)
}
var anyVal interface{}
if err := json.Unmarshal(wrap.FinalResult, &anyVal); err != nil {
return ""
}
b, err := json.Marshal(anyVal)
if err != nil {
return ""
}
return strings.TrimSpace(string(b))
}