mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-15 15:40:38 +02:00
221 lines
7.4 KiB
Go
221 lines
7.4 KiB
Go
package multiagent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"cyberstrike-ai/internal/config"
|
|
|
|
localbk "github.com/cloudwego/eino-ext/adk/backend/local"
|
|
"github.com/cloudwego/eino/adk"
|
|
"github.com/cloudwego/eino/components/tool"
|
|
"github.com/cloudwego/eino/schema"
|
|
)
|
|
|
|
func TestReductionCacheRootDir(t *testing.T) {
|
|
got := reductionCacheRootDir("", "proj-1", "conv-1")
|
|
want := filepath.Join("tmp", "reduction", "projects", "proj-1")
|
|
if got != want {
|
|
t.Fatalf("project scope: got %q want %q", got, want)
|
|
}
|
|
got = reductionCacheRootDir("", "", "conv-abc")
|
|
want = filepath.Join("tmp", "reduction", "conversations", "conv-abc")
|
|
if got != want {
|
|
t.Fatalf("conversation scope: got %q want %q", got, want)
|
|
}
|
|
custom := reductionCacheRootDir("/data/cache", "p1", "c1")
|
|
if !strings.HasSuffix(custom, filepath.Join("projects", "p1")) {
|
|
t.Fatalf("custom base should still scope by project, got %q", custom)
|
|
}
|
|
}
|
|
|
|
func TestBuildAgenticReductionMiddlewareClearsOldAgenticToolResult(t *testing.T) {
|
|
ctx := context.Background()
|
|
loc, err := localbk.NewBackend(ctx, &localbk.Config{})
|
|
if err != nil {
|
|
t.Fatalf("NewBackend: %v", err)
|
|
}
|
|
root := t.TempDir()
|
|
mw, err := buildAgenticReductionMiddleware(ctx, config.MultiAgentEinoMiddlewareConfig{
|
|
ReductionRootDir: root,
|
|
ReductionMaxTokensForClear: 1,
|
|
}, "", "conv-1", loc, nil)
|
|
if err != nil {
|
|
t.Fatalf("buildAgenticReductionMiddleware: %v", err)
|
|
}
|
|
oldText := strings.Repeat("old-tool-output-", 20)
|
|
newText := strings.Repeat("new-tool-output-", 20)
|
|
state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{
|
|
Messages: []*schema.AgenticMessage{
|
|
agenticAssistantToolCall("old-call", "execute", `{"command":"old"}`),
|
|
agenticToolResult("old-call", "execute", oldText),
|
|
agenticAssistantToolCall("new-call", "execute", `{"command":"new"}`),
|
|
agenticToolResult("new-call", "execute", newText),
|
|
},
|
|
}
|
|
_, out, err := mw.BeforeModelRewriteState(ctx, state, nil)
|
|
if err != nil {
|
|
t.Fatalf("BeforeModelRewriteState: %v", err)
|
|
}
|
|
oldGot := out.Messages[1].ContentBlocks[0].FunctionToolResult.Content[0].Text.Text
|
|
newGot := out.Messages[3].ContentBlocks[0].FunctionToolResult.Content[0].Text.Text
|
|
if oldGot == oldText {
|
|
t.Fatal("agentic reduction did not clear old oversized tool result")
|
|
}
|
|
if !strings.Contains(oldGot, "read_file") {
|
|
t.Fatalf("cleared content should mention read_file, got %q", oldGot)
|
|
}
|
|
if newGot != newText {
|
|
t.Fatalf("latest tool result should be retained, got %q", newGot)
|
|
}
|
|
}
|
|
|
|
func agenticAssistantToolCall(callID, name, arguments string) *schema.AgenticMessage {
|
|
return &schema.AgenticMessage{
|
|
Role: schema.AgenticRoleTypeAssistant,
|
|
ContentBlocks: []*schema.ContentBlock{schema.NewContentBlock(&schema.FunctionToolCall{
|
|
CallID: callID,
|
|
Name: name,
|
|
Arguments: arguments,
|
|
})},
|
|
}
|
|
}
|
|
|
|
func agenticToolResult(callID, name, text string) *schema.AgenticMessage {
|
|
return &schema.AgenticMessage{
|
|
Role: schema.AgenticRoleTypeUser,
|
|
ContentBlocks: []*schema.ContentBlock{schema.NewContentBlock(&schema.FunctionToolResult{
|
|
CallID: callID,
|
|
Name: name,
|
|
Content: []*schema.FunctionToolResultContentBlock{{
|
|
Type: schema.FunctionToolResultContentBlockTypeText,
|
|
Text: &schema.UserInputText{Text: text},
|
|
}},
|
|
})},
|
|
}
|
|
}
|
|
|
|
func TestBuildAgenticReductionMiddlewareHandlesSingleAgenticToolResult(t *testing.T) {
|
|
ctx := context.Background()
|
|
loc, err := localbk.NewBackend(ctx, &localbk.Config{})
|
|
if err != nil {
|
|
t.Fatalf("NewBackend: %v", err)
|
|
}
|
|
mw, err := buildAgenticReductionMiddleware(ctx, config.MultiAgentEinoMiddlewareConfig{
|
|
ReductionRootDir: t.TempDir(),
|
|
ReductionMaxTokensForClear: 1,
|
|
}, "", "conv-1", loc, nil)
|
|
if err != nil {
|
|
t.Fatalf("buildAgenticReductionMiddleware: %v", err)
|
|
}
|
|
state := &adk.TypedChatModelAgentState[*schema.AgenticMessage]{
|
|
Messages: []*schema.AgenticMessage{
|
|
{
|
|
Role: schema.AgenticRoleTypeUser,
|
|
ContentBlocks: []*schema.ContentBlock{schema.NewContentBlock(&schema.FunctionToolResult{
|
|
CallID: "call-1",
|
|
Name: "execute",
|
|
Content: []*schema.FunctionToolResultContentBlock{{
|
|
Type: schema.FunctionToolResultContentBlockTypeText,
|
|
Text: &schema.UserInputText{Text: strings.Repeat("tool-output-", 20)},
|
|
}},
|
|
})},
|
|
},
|
|
},
|
|
}
|
|
_, out, err := mw.BeforeModelRewriteState(ctx, state, nil)
|
|
if err != nil {
|
|
t.Fatalf("BeforeModelRewriteState: %v", err)
|
|
}
|
|
got := out.Messages[0].ContentBlocks[0].FunctionToolResult.Content[0].Text.Text
|
|
if got != strings.Repeat("tool-output-", 20) {
|
|
t.Fatalf("single retained tool result should not be cleared, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestPrependEinoAgenticMiddlewaresRespectsReductionPlacement(t *testing.T) {
|
|
ctx := context.Background()
|
|
loc, err := localbk.NewBackend(ctx, &localbk.Config{})
|
|
if err != nil {
|
|
t.Fatalf("NewBackend: %v", err)
|
|
}
|
|
patchToolCalls := false
|
|
mw := &config.MultiAgentEinoMiddlewareConfig{
|
|
ReductionEnable: true,
|
|
ReductionRootDir: t.TempDir(),
|
|
ReductionMaxTokensForClear: 100,
|
|
PatchToolCalls: &patchToolCalls,
|
|
}
|
|
_, mainHandlers, _, err := prependEinoAgenticMiddlewares(ctx, mw, einoMWMain, nil, loc, "", "conv-1", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("prepend main: %v", err)
|
|
}
|
|
if len(mainHandlers) != 1 {
|
|
t.Fatalf("main handlers = %d, want reduction", len(mainHandlers))
|
|
}
|
|
_, subHandlers, _, err := prependEinoAgenticMiddlewares(ctx, mw, einoMWSub, nil, loc, "", "conv-1", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("prepend sub: %v", err)
|
|
}
|
|
if len(subHandlers) != 0 {
|
|
t.Fatalf("sub handlers = %d, want skipped when reduction_sub_agents=false", len(subHandlers))
|
|
}
|
|
mw.ReductionSubAgents = true
|
|
_, subHandlers, _, err = prependEinoAgenticMiddlewares(ctx, mw, einoMWSub, nil, loc, "", "conv-1", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("prepend sub enabled: %v", err)
|
|
}
|
|
if len(subHandlers) != 1 {
|
|
t.Fatalf("sub handlers = %d, want reduction when reduction_sub_agents=true", len(subHandlers))
|
|
}
|
|
}
|
|
|
|
func TestPrependEinoAgenticMiddlewaresMountsToolSearchAndPatchToolCalls(t *testing.T) {
|
|
ctx := context.Background()
|
|
mw := &config.MultiAgentEinoMiddlewareConfig{
|
|
ToolSearchEnable: true,
|
|
ToolSearchMinTools: 20,
|
|
ToolSearchAlwaysVisible: 5,
|
|
}
|
|
outTools, handlers, toolSearchActive, err := prependEinoAgenticMiddlewares(ctx, mw, einoMWMain, stubTools(25), nil, "", "conv-test", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("prependEinoAgenticMiddlewares: %v", err)
|
|
}
|
|
if !toolSearchActive {
|
|
t.Fatal("agentic tool_search should be active")
|
|
}
|
|
if len(outTools) != 5 {
|
|
t.Fatalf("mounted tools = %d, want static visible tools only", len(outTools))
|
|
}
|
|
if len(handlers) != 2 {
|
|
t.Fatalf("handlers = %d, want patchtoolcalls + toolsearch", len(handlers))
|
|
}
|
|
}
|
|
|
|
type stubTool struct{ name string }
|
|
|
|
func (s stubTool) Info(_ context.Context) (*schema.ToolInfo, error) {
|
|
return &schema.ToolInfo{Name: s.name}, nil
|
|
}
|
|
|
|
func TestSplitToolsForToolSearch(t *testing.T) {
|
|
mk := func(n int) []tool.BaseTool {
|
|
out := make([]tool.BaseTool, n)
|
|
for i := 0; i < n; i++ {
|
|
out[i] = stubTool{name: fmt.Sprintf("t%d", i)}
|
|
}
|
|
return out
|
|
}
|
|
static, dynamic, ok := splitToolsForToolSearch(mk(4), 3)
|
|
if ok || len(static) != 4 || dynamic != nil {
|
|
t.Fatalf("expected no split when len<=alwaysVisible+1, got ok=%v static=%d dynamic=%v", ok, len(static), dynamic)
|
|
}
|
|
static, dynamic, ok = splitToolsForToolSearch(mk(20), 5)
|
|
if !ok || len(static) != 5 || len(dynamic) != 15 {
|
|
t.Fatalf("expected split 5+15, got ok=%v static=%d dynamic=%d", ok, len(static), len(dynamic))
|
|
}
|
|
}
|