yao/agent/search/nlp/querydsl/generator.go
Max f97bb408d2 Refactor Search Execution and Enhance Result Handling
- Updated the executeAutoSearch method to improve the handling of search results, ensuring better data capture and processing.
- Enhanced the Search type to include additional metadata for improved debugging and user feedback.
- Revised related tests to align with the new search execution logic and ensure comprehensive coverage of changes.
- Updated documentation to reflect modifications in search result handling and execution processes.
2025-12-16 10:55:09 +08:00

170 lines
5.3 KiB
Go

// Package querydsl provides QueryDSL generation from natural language for DB search
// Supports three modes via uses.querydsl configuration:
// - "builtin": Template-based generation (no external dependencies)
// - "<assistant-id>": Delegate to an LLM-powered assistant for high-quality generation
// - "mcp:<server>.<tool>": Call external MCP tool
//
// For production use cases requiring high accuracy, use Agent or MCP mode.
package querydsl
import (
"strings"
"github.com/yaoapp/gou/query/gou"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/search/types"
)
// Generator generates QueryDSL from natural language
// Mode is determined by uses.querydsl configuration
type Generator struct {
usesQueryDSL string // "builtin", "<assistant-id>", "mcp:<server>.<tool>"
config *types.QueryDSLConfig // QueryDSL generation options
}
// NewGenerator creates a new QueryDSL generator
// usesQueryDSL: value from uses.querydsl config
// cfg: QueryDSL generation options from search config
func NewGenerator(usesQueryDSL string, cfg *types.QueryDSLConfig) *Generator {
return &Generator{
usesQueryDSL: usesQueryDSL,
config: cfg,
}
}
// Generate generates QueryDSL from natural language based on configured mode
// Returns a QueryDSL ready for execution
func (g *Generator) Generate(ctx *context.Context, input *Input) (*Result, error) {
var result *Result
var err error
switch {
case g.usesQueryDSL == "builtin" || g.usesQueryDSL == "":
result, err = g.builtinGenerate(input)
case strings.HasPrefix(g.usesQueryDSL, "mcp:"):
result, err = g.mcpGenerate(ctx, input)
default:
// Assume it's an assistant ID for Agent mode
result, err = g.agentGenerate(ctx, input)
}
if err != nil {
return nil, err
}
// Validate generated DSL against allowed fields whitelist
if result != nil && result.DSL != nil && len(input.AllowedFields) > 0 {
result = g.validateFields(result, input.AllowedFields)
}
return result, nil
}
// builtinGenerate uses template-based generation
// This is a lightweight implementation with no external dependencies.
// For better results, use Agent or MCP mode.
func (g *Generator) builtinGenerate(input *Input) (*Result, error) {
generator := NewBuiltinGenerator()
return generator.Generate(input)
}
// agentGenerate delegates to an LLM-powered assistant
// The assistant can understand context and generate semantically correct QueryDSL
func (g *Generator) agentGenerate(ctx *context.Context, input *Input) (*Result, error) {
provider := NewAgentProvider(g.usesQueryDSL)
return provider.Generate(ctx, input)
}
// mcpGenerate calls an external MCP tool
// Format: "mcp:<server>.<tool>"
func (g *Generator) mcpGenerate(ctx *context.Context, input *Input) (*Result, error) {
mcpRef := strings.TrimPrefix(g.usesQueryDSL, "mcp:")
provider, err := NewMCPProvider(mcpRef)
if err != nil {
// Fallback to builtin on invalid MCP format
return g.builtinGenerate(input)
}
return provider.Generate(ctx, input)
}
// validateFields validates that all fields in the generated DSL are in the allowed list
// If a field is not allowed, it's removed and a warning is added
func (g *Generator) validateFields(result *Result, allowedFields []string) *Result {
if result.DSL == nil {
return result
}
// Build allowed fields set for fast lookup
allowed := make(map[string]bool)
for _, f := range allowedFields {
allowed[f] = true
}
var removedFields []string
// Validate Select fields
if len(result.DSL.Select) > 0 {
validSelect := make([]gou.Expression, 0, len(result.DSL.Select))
for _, expr := range result.DSL.Select {
if allowed[expr.Field] {
validSelect = append(validSelect, expr)
} else if expr.Field != "" {
removedFields = append(removedFields, "select:"+expr.Field)
}
}
result.DSL.Select = validSelect
}
// Validate Where fields (recursive)
result.DSL.Wheres = g.validateWheres(result.DSL.Wheres, allowed, &removedFields)
// Validate Order fields
if len(result.DSL.Orders) > 0 {
validOrders := make(gou.Orders, 0, len(result.DSL.Orders))
for _, order := range result.DSL.Orders {
if order.Field != nil && allowed[order.Field.Field] {
validOrders = append(validOrders, order)
} else if order.Field != nil && order.Field.Field != "" {
removedFields = append(removedFields, "order:"+order.Field.Field)
}
}
result.DSL.Orders = validOrders
}
// Add warnings for removed fields
if len(removedFields) > 0 {
warning := "removed fields not in allowed list: " + strings.Join(removedFields, ", ")
result.Warnings = append(result.Warnings, warning)
}
return result
}
// validateWheres recursively validates where conditions
func (g *Generator) validateWheres(wheres []gou.Where, allowed map[string]bool, removedFields *[]string) []gou.Where {
if len(wheres) == 0 {
return wheres
}
validWheres := make([]gou.Where, 0, len(wheres))
for _, w := range wheres {
// Check if the field is allowed
fieldAllowed := true
if w.Field != nil && w.Field.Field != "" {
if !allowed[w.Field.Field] {
*removedFields = append(*removedFields, "where:"+w.Field.Field)
fieldAllowed = false
}
}
if fieldAllowed {
// Recursively validate nested wheres
if len(w.Wheres) > 0 {
w.Wheres = g.validateWheres(w.Wheres, allowed, removedFields)
}
validWheres = append(validWheres, w)
}
}
return validWheres
}