265 lines
7.0 KiB
Go
265 lines
7.0 KiB
Go
package llm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"knowledge-graph-backend/internal/model"
|
|
"testing"
|
|
|
|
"github.com/tmc/langchaingo/llms"
|
|
)
|
|
|
|
type mockModel struct {
|
|
generateContent func(ctx context.Context, messages []llms.MessageContent, opts ...llms.CallOption) (*llms.ContentResponse, error)
|
|
}
|
|
|
|
func (m *mockModel) Call(ctx context.Context, prompt string, opts ...llms.CallOption) (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
func (m *mockModel) GenerateContent(ctx context.Context, messages []llms.MessageContent, opts ...llms.CallOption) (*llms.ContentResponse, error) {
|
|
if m.generateContent != nil {
|
|
return m.generateContent(ctx, messages, opts...)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func TestAgent_ChatWithTools_DirectResponse(t *testing.T) {
|
|
mock := &mockModel{
|
|
generateContent: func(ctx context.Context, messages []llms.MessageContent, opts ...llms.CallOption) (*llms.ContentResponse, error) {
|
|
return &llms.ContentResponse{
|
|
Choices: []*llms.ContentChoice{
|
|
{Content: "hello from agent"},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
client := &Client{model: mock}
|
|
executor := NewToolExecutor(&mockGraphService{})
|
|
agent := NewAgent(client, executor)
|
|
|
|
result, err := agent.ChatWithTools(context.Background(), "system", []llms.MessageContent{
|
|
llms.TextParts(llms.ChatMessageTypeHuman, "hi"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result != "hello from agent" {
|
|
t.Errorf("expected 'hello from agent', got %q", result)
|
|
}
|
|
}
|
|
|
|
func TestAgent_ChatWithTools_ToolCallThenResponse(t *testing.T) {
|
|
callCount := 0
|
|
mock := &mockModel{
|
|
generateContent: func(ctx context.Context, messages []llms.MessageContent, opts ...llms.CallOption) (*llms.ContentResponse, error) {
|
|
callCount++
|
|
if callCount == 1 {
|
|
return &llms.ContentResponse{
|
|
Choices: []*llms.ContentChoice{
|
|
{
|
|
ToolCalls: []llms.ToolCall{
|
|
{
|
|
ID: "call_1",
|
|
FunctionCall: &llms.FunctionCall{
|
|
Name: "search_nodes",
|
|
Arguments: `{"query":"AI"}`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
return &llms.ContentResponse{
|
|
Choices: []*llms.ContentChoice{
|
|
{Content: "found AI nodes"},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
graphSvc := &mockGraphService{
|
|
searchNodes: func(query string) []model.Node {
|
|
return []model.Node{{ID: "1", Label: "AI"}}
|
|
},
|
|
}
|
|
|
|
client := &Client{model: mock}
|
|
executor := NewToolExecutor(graphSvc)
|
|
agent := NewAgent(client, executor)
|
|
|
|
result, err := agent.ChatWithTools(context.Background(), "system", []llms.MessageContent{
|
|
llms.TextParts(llms.ChatMessageTypeHuman, "search for AI"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result != "found AI nodes" {
|
|
t.Errorf("expected 'found AI nodes', got %q", result)
|
|
}
|
|
if callCount != 2 {
|
|
t.Errorf("expected 2 GenerateContent calls, got %d", callCount)
|
|
}
|
|
}
|
|
|
|
func TestAgent_ChatWithTools_EmptyResponse(t *testing.T) {
|
|
mock := &mockModel{
|
|
generateContent: func(ctx context.Context, messages []llms.MessageContent, opts ...llms.CallOption) (*llms.ContentResponse, error) {
|
|
return &llms.ContentResponse{Choices: []*llms.ContentChoice{}}, nil
|
|
},
|
|
}
|
|
|
|
client := &Client{model: mock}
|
|
executor := NewToolExecutor(&mockGraphService{})
|
|
agent := NewAgent(client, executor)
|
|
|
|
_, err := agent.ChatWithTools(context.Background(), "system", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty response, got nil")
|
|
}
|
|
}
|
|
|
|
func TestAgent_ChatWithTools_NilResponse(t *testing.T) {
|
|
mock := &mockModel{
|
|
generateContent: func(ctx context.Context, messages []llms.MessageContent, opts ...llms.CallOption) (*llms.ContentResponse, error) {
|
|
return nil, nil
|
|
},
|
|
}
|
|
|
|
client := &Client{model: mock}
|
|
executor := NewToolExecutor(&mockGraphService{})
|
|
agent := NewAgent(client, executor)
|
|
|
|
_, err := agent.ChatWithTools(context.Background(), "system", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for nil response, got nil")
|
|
}
|
|
}
|
|
|
|
func TestAgent_ChatWithTools_GenerateError(t *testing.T) {
|
|
mock := &mockModel{
|
|
generateContent: func(ctx context.Context, messages []llms.MessageContent, opts ...llms.CallOption) (*llms.ContentResponse, error) {
|
|
return nil, fmt.Errorf("network error")
|
|
},
|
|
}
|
|
|
|
client := &Client{model: mock}
|
|
executor := NewToolExecutor(&mockGraphService{})
|
|
agent := NewAgent(client, executor)
|
|
|
|
_, err := agent.ChatWithTools(context.Background(), "system", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestAgent_ChatWithTools_ToolExecutionError_ContinuesLoop(t *testing.T) {
|
|
callCount := 0
|
|
mock := &mockModel{
|
|
generateContent: func(ctx context.Context, messages []llms.MessageContent, opts ...llms.CallOption) (*llms.ContentResponse, error) {
|
|
callCount++
|
|
if callCount == 1 {
|
|
return &llms.ContentResponse{
|
|
Choices: []*llms.ContentChoice{
|
|
{
|
|
ToolCalls: []llms.ToolCall{
|
|
{
|
|
ID: "call_1",
|
|
FunctionCall: &llms.FunctionCall{
|
|
Name: "delete_node",
|
|
Arguments: `{"id":"nonexistent"}`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
return &llms.ContentResponse{
|
|
Choices: []*llms.ContentChoice{
|
|
{Content: "handled error"},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
graphSvc := &mockGraphService{
|
|
deleteNode: func(id string) error {
|
|
return fmt.Errorf("node not found")
|
|
},
|
|
}
|
|
|
|
client := &Client{model: mock}
|
|
executor := NewToolExecutor(graphSvc)
|
|
agent := NewAgent(client, executor)
|
|
|
|
result, err := agent.ChatWithTools(context.Background(), "system", []llms.MessageContent{
|
|
llms.TextParts(llms.ChatMessageTypeHuman, "delete nonexistent"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result != "handled error" {
|
|
t.Errorf("expected 'handled error', got %q", result)
|
|
}
|
|
}
|
|
|
|
func TestAgent_ChatWithTools_MaxIterations(t *testing.T) {
|
|
mock := &mockModel{
|
|
generateContent: func(ctx context.Context, messages []llms.MessageContent, opts ...llms.CallOption) (*llms.ContentResponse, error) {
|
|
return &llms.ContentResponse{
|
|
Choices: []*llms.ContentChoice{
|
|
{
|
|
ToolCalls: []llms.ToolCall{
|
|
{
|
|
ID: "call_loop",
|
|
FunctionCall: &llms.FunctionCall{
|
|
Name: "search_nodes",
|
|
Arguments: `{"query":"loop"}`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
graphSvc := &mockGraphService{
|
|
searchNodes: func(query string) []model.Node {
|
|
return []model.Node{}
|
|
},
|
|
}
|
|
|
|
client := &Client{model: mock}
|
|
executor := NewToolExecutor(graphSvc)
|
|
agent := NewAgent(client, executor)
|
|
|
|
_, err := agent.ChatWithTools(context.Background(), "system", nil)
|
|
if err == nil {
|
|
t.Fatal("expected max iterations error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestAgent_ChatWithTools_ContextCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
mock := &mockModel{
|
|
generateContent: func(ctx context.Context, messages []llms.MessageContent, opts ...llms.CallOption) (*llms.ContentResponse, error) {
|
|
return nil, ctx.Err()
|
|
},
|
|
}
|
|
|
|
client := &Client{model: mock}
|
|
executor := NewToolExecutor(&mockGraphService{})
|
|
agent := NewAgent(client, executor)
|
|
|
|
_, err := agent.ChatWithTools(ctx, "system", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error due to cancelled context, got nil")
|
|
}
|
|
}
|