yao/agent/content/pdf/pdf_test.go
Max 195391afaa Enhance PDF and Content Processing with New File Handling Features
- Added support for parsing various file types (PDF, DOCX, PPTX) in the content processing pipeline, allowing for more flexible content extraction.
- Implemented a new method to convert file attachments to raw text when content parsing is skipped, improving performance for internal calls.
- Introduced loading message suppression for image processing to enhance user experience during PDF analysis.
- Updated the PDF handler to cache processed text and manage loading messages effectively, ensuring smoother interactions during content retrieval.
- Enhanced error handling and logging for PDF processing, improving traceability and debugging capabilities.
2025-12-29 15:20:38 +08:00

362 lines
9.5 KiB
Go

package pdf_test
import (
stdContext "context"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/yao/agent/content/pdf"
contentTypes "github.com/yaoapp/yao/agent/content/types"
agentContext "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/output/message"
"github.com/yaoapp/yao/agent/testutils"
oauthTypes "github.com/yaoapp/yao/openapi/oauth/types"
)
// Test files directory (relative to yao-dev-app)
const testFilesDir = "assistants/tests/vision-helper/tests"
// newTestContext creates a Context for testing with commonly used fields pre-populated
func newTestContext(capabilities *openai.Capabilities) *agentContext.Context {
authorized := &oauthTypes.AuthorizedInfo{
Subject: "test-user",
ClientID: "test-client-id",
UserID: "test-user-123",
TeamID: "test-team-456",
TenantID: "test-tenant-789",
}
ctx := agentContext.New(stdContext.Background(), authorized, "test-chat")
ctx.AssistantID = "test-assistant"
ctx.Locale = "en-us"
ctx.Theme = "light"
ctx.Client = agentContext.Client{
Type: "web",
UserAgent: "TestAgent/1.0",
IP: "127.0.0.1",
}
ctx.Referer = agentContext.RefererAPI
ctx.Accept = agentContext.AcceptWebCUI
ctx.Route = ""
ctx.Metadata = make(map[string]interface{})
ctx.Capabilities = capabilities
ctx.IDGenerator = message.NewIDGenerator()
return ctx
}
// newTestOptions creates test options with the given capabilities
func newTestOptions(capabilities *openai.Capabilities, completionOptions *agentContext.CompletionOptions) *contentTypes.Options {
return &contentTypes.Options{
Capabilities: capabilities,
CompletionOptions: completionOptions,
}
}
// getTestFilePath returns the full path to a test file
func getTestFilePath(filename string) string {
yaoRoot := os.Getenv("YAO_TEST_APPLICATION")
if yaoRoot == "" {
yaoRoot = os.Getenv("YAO_ROOT")
}
return filepath.Join(yaoRoot, testFilesDir, filename)
}
// TestParseWithMissingURL tests parsing PDF with missing URL
func TestParseWithMissingURL(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
capabilities := &openai.Capabilities{
Vision: "openai",
}
options := newTestOptions(capabilities, nil)
ctx := newTestContext(capabilities)
// Create content with nil File
content := agentContext.ContentPart{
Type: agentContext.ContentFile,
File: nil,
}
handler := pdf.New(options)
_, _, err := handler.Parse(ctx, content)
assert.Error(t, err)
assert.Contains(t, err.Error(), "missing URL")
}
// TestParseWithEmptyURL tests parsing PDF with empty URL
func TestParseWithEmptyURL(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
capabilities := &openai.Capabilities{
Vision: "openai",
}
options := newTestOptions(capabilities, nil)
ctx := newTestContext(capabilities)
// Create content with empty URL
content := agentContext.ContentPart{
Type: agentContext.ContentFile,
File: &agentContext.FileAttachment{
URL: "",
Filename: "test.pdf",
},
}
handler := pdf.New(options)
_, _, err := handler.Parse(ctx, content)
assert.Error(t, err)
assert.Contains(t, err.Error(), "missing URL")
}
// TestParseWithLocalPDFAndVisionSupport tests parsing a local PDF file when model supports vision
func TestParseWithLocalPDFAndVisionSupport(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
// Check if test file exists
pdfPath := getTestFilePath("test.pdf")
if _, err := os.Stat(pdfPath); os.IsNotExist(err) {
t.Skipf("Test PDF file not found: %s", pdfPath)
}
// Create capabilities with vision support
capabilities := &openai.Capabilities{
Vision: "openai",
}
options := newTestOptions(capabilities, nil)
ctx := newTestContext(capabilities)
// Create content with local file path
content := agentContext.ContentPart{
Type: agentContext.ContentFile,
File: &agentContext.FileAttachment{
URL: pdfPath,
Filename: "test.pdf",
},
}
handler := pdf.New(options)
result, refs, err := handler.Parse(ctx, content)
// Should succeed - PDF converted to images
assert.NoError(t, err)
assert.Nil(t, refs)
// When model supports vision, Parse returns the first image_url part
// Use ParseMulti to get all pages as separate image_url parts
assert.Equal(t, agentContext.ContentImageURL, result.Type)
assert.NotNil(t, result.ImageURL)
assert.NotEmpty(t, result.ImageURL.URL)
t.Logf("PDF parse result type: %s, URL prefix: %s...", result.Type, result.ImageURL.URL[:50])
}
// TestParseWithLocalPDFAndVisionAgent tests parsing a local PDF file using vision agent
func TestParseWithLocalPDFAndVisionAgent(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
// Check if test file exists
pdfPath := getTestFilePath("test.pdf")
if _, err := os.Stat(pdfPath); os.IsNotExist(err) {
t.Skipf("Test PDF file not found: %s", pdfPath)
}
// Create capabilities WITHOUT vision support
capabilities := &openai.Capabilities{
Vision: nil,
}
// Configure to use vision agent
completionOptions := &agentContext.CompletionOptions{
Uses: &agentContext.Uses{
Vision: "tests.vision-test", // Use our test vision agent
},
}
options := newTestOptions(capabilities, completionOptions)
ctx := newTestContext(capabilities)
// Create content with local file path
content := agentContext.ContentPart{
Type: agentContext.ContentFile,
File: &agentContext.FileAttachment{
URL: pdfPath,
Filename: "test.pdf",
},
}
handler := pdf.New(options)
result, refs, err := handler.Parse(ctx, content)
// Should succeed - PDF converted to images and processed by vision agent
assert.NoError(t, err)
assert.Nil(t, refs)
assert.Equal(t, agentContext.ContentText, result.Type)
assert.NotEmpty(t, result.Text)
t.Logf("PDF parse result (via vision agent): %s", result.Text)
}
// TestParseMultiWithLocalPDF tests ParseMulti which returns separate parts for each page
func TestParseMultiWithLocalPDF(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
// Check if test file exists
pdfPath := getTestFilePath("test.pdf")
if _, err := os.Stat(pdfPath); os.IsNotExist(err) {
t.Skipf("Test PDF file not found: %s", pdfPath)
}
// Create capabilities with vision support
capabilities := &openai.Capabilities{
Vision: "openai",
}
options := newTestOptions(capabilities, nil)
ctx := newTestContext(capabilities)
// Create content with local file path
content := agentContext.ContentPart{
Type: agentContext.ContentFile,
File: &agentContext.FileAttachment{
URL: pdfPath,
Filename: "test.pdf",
},
}
handler := pdf.New(options)
parts, refs, err := handler.ParseMulti(ctx, content)
// Should succeed and return at least one part (one per page)
assert.NoError(t, err)
assert.Nil(t, refs)
assert.NotEmpty(t, parts)
t.Logf("PDF ParseMulti returned %d parts", len(parts))
// When model supports vision, each part should be image_url type
for i, part := range parts {
assert.Equal(t, agentContext.ContentImageURL, part.Type)
assert.NotNil(t, part.ImageURL)
t.Logf(" Part %d: type=%s, has URL=%v", i+1, part.Type, part.ImageURL != nil && part.ImageURL.URL != "")
}
}
// TestParseWithUnsupportedSource tests parsing PDF with unsupported source
func TestParseWithUnsupportedSource(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
capabilities := &openai.Capabilities{
Vision: "openai",
}
options := newTestOptions(capabilities, nil)
ctx := newTestContext(capabilities)
// Create content with HTTP URL (not implemented)
content := agentContext.ContentPart{
Type: agentContext.ContentFile,
File: &agentContext.FileAttachment{
URL: "https://example.com/test.pdf",
Filename: "test.pdf",
},
}
handler := pdf.New(options)
_, _, err := handler.Parse(ctx, content)
assert.Error(t, err)
assert.Contains(t, err.Error(), "HTTP URL fetch not implemented")
}
// TestParseWithNonExistentFile tests parsing PDF with non-existent file
func TestParseWithNonExistentFile(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
capabilities := &openai.Capabilities{
Vision: "openai",
}
options := newTestOptions(capabilities, nil)
ctx := newTestContext(capabilities)
// Create content with non-existent file
content := agentContext.ContentPart{
Type: agentContext.ContentFile,
File: &agentContext.FileAttachment{
URL: "/non/existent/path/test.pdf",
Filename: "test.pdf",
},
}
handler := pdf.New(options)
_, _, err := handler.Parse(ctx, content)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported PDF source")
}
// TestSilentLoadingOption tests that SilentLoading option is respected
func TestSilentLoadingOption(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
capabilities := &openai.Capabilities{
Vision: "openai",
}
// Create options with SilentLoading enabled
options := &contentTypes.Options{
Capabilities: capabilities,
SilentLoading: true,
}
// This test just verifies the option can be set
// The actual behavior is tested in the image handler tests
handler := pdf.New(options)
assert.NotNil(t, handler)
assert.True(t, options.SilentLoading)
}