Enhance Options and Metadata Handling in Context and Test Cases
- Added support for a new `Metadata` field in the `Options` struct to allow passing custom data to hooks, enhancing flexibility in context management. - Updated the `ToMap` and `OptionsFromMap` methods to include serialization and deserialization of the `Metadata` field. - Enhanced the test case structure to include an `Options` field, allowing for per-test-case configuration, including metadata and skip options. - Updated documentation to reflect the new `options` and `metadata` fields, providing clear examples for users on how to utilize these features in test cases.
This commit is contained in:
parent
b8c5829eb0
commit
4abc7e41ef
15 changed files with 1511 additions and 227 deletions
|
|
@ -25,6 +25,9 @@ func (opts *Options) ToMap() map[string]interface{} {
|
|||
if opts.DisableGlobalPrompts {
|
||||
result["disable_global_prompts"] = opts.DisableGlobalPrompts
|
||||
}
|
||||
if opts.Metadata != nil {
|
||||
result["metadata"] = opts.Metadata
|
||||
}
|
||||
|
||||
// Note: Runtime fields (Context, Writer) are not serialized (json:"-")
|
||||
// They should not be included in the map
|
||||
|
|
@ -66,6 +69,9 @@ func OptionsFromMap(m map[string]interface{}) *Options {
|
|||
if disableGlobalPrompts, ok := m["disable_global_prompts"].(bool); ok {
|
||||
opts.DisableGlobalPrompts = disableGlobalPrompts
|
||||
}
|
||||
if metadata, ok := m["metadata"].(map[string]interface{}); ok {
|
||||
opts.Metadata = metadata
|
||||
}
|
||||
|
||||
// Note: Context and Writer are runtime fields, not restored from map
|
||||
// They should be set by the caller if needed
|
||||
|
|
|
|||
|
|
@ -289,6 +289,9 @@ type Options struct {
|
|||
|
||||
// Agent mode, use to select the mode of the request, default is "chat"
|
||||
Mode string `json:"mode,omitempty"` // Agent mode, use to select the mode of the request, default is "chat"
|
||||
|
||||
// Metadata for passing custom data to hooks (e.g., scenario selection)
|
||||
Metadata map[string]any `json:"metadata,omitempty"` // Custom metadata passed to Create/Next hooks
|
||||
}
|
||||
|
||||
// Stack represents the call stack node for tracing agent-to-agent calls
|
||||
|
|
|
|||
|
|
@ -637,11 +637,25 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
|
|||
errorDetected := false
|
||||
|
||||
// Wrap streamHandler to detect JSON error responses
|
||||
// Note: API error responses are raw JSON without "data: " prefix
|
||||
// Normal SSE data always starts with "data: " prefix
|
||||
wrappedHandler := func(data []byte) int {
|
||||
dataStr := string(data)
|
||||
trimmed := strings.TrimSpace(dataStr)
|
||||
|
||||
// Detect if this looks like a JSON error response (starts with "{" or contains "error")
|
||||
if strings.Contains(dataStr, `"error"`) || (strings.TrimSpace(dataStr) == "{" && !errorDetected) {
|
||||
// Skip empty lines
|
||||
if trimmed == "" {
|
||||
return http.HandlerReturnOk
|
||||
}
|
||||
|
||||
// Normal SSE data starts with "data: " - pass to streamHandler
|
||||
if strings.HasPrefix(dataStr, "data: ") {
|
||||
return streamHandler(data)
|
||||
}
|
||||
|
||||
// Detect if this looks like a JSON error response (raw JSON without "data: " prefix)
|
||||
// API errors are returned as raw JSON: {"error": {...}}
|
||||
if strings.HasPrefix(trimmed, "{") && strings.Contains(dataStr, `"error"`) {
|
||||
errorDetected = true
|
||||
}
|
||||
|
||||
|
|
@ -652,7 +666,7 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
|
|||
return http.HandlerReturnOk
|
||||
}
|
||||
|
||||
// Otherwise, use normal handler
|
||||
// Unknown format, pass to streamHandler (it will skip non-SSE data)
|
||||
return streamHandler(data)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,10 +110,58 @@ Each line is a JSON object:
|
|||
| `expected` | any | No | Expected output (exact match) |
|
||||
| `user` | string | No | Override user ID |
|
||||
| `team` | string | No | Override team ID |
|
||||
| `options` | Options | No | Context options (see below) |
|
||||
| `timeout` | string | No | Override timeout (e.g., "30s") |
|
||||
| `skip` | bool | No | Skip this test |
|
||||
| `metadata` | map | No | Additional metadata |
|
||||
|
||||
### Options
|
||||
|
||||
The `options` field allows per-test-case configuration that maps to `context.Options`:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------------------ | ------- | -------------------------------------------------- |
|
||||
| `connector` | string | Override connector (e.g., `"deepseek.v3"`) |
|
||||
| `mode` | string | Agent mode (default: `"chat"`) |
|
||||
| `search` | bool | Enable/disable search mode (default: `true`) |
|
||||
| `disable_global_prompts` | bool | Temporarily disable global prompts |
|
||||
| `metadata` | map | Custom data passed to hooks (e.g., scenario) |
|
||||
| `skip` | object | Skip configuration (see below) |
|
||||
|
||||
#### Options.skip
|
||||
|
||||
| Field | Type | Description |
|
||||
| --------- | ---- | ------------------------ |
|
||||
| `history` | bool | Skip history loading |
|
||||
| `trace` | bool | Skip trace logging |
|
||||
| `output` | bool | Skip output to client |
|
||||
| `keyword` | bool | Skip keyword extraction |
|
||||
| `search` | bool | Skip auto search |
|
||||
|
||||
**Example with options:**
|
||||
|
||||
```jsonl
|
||||
{
|
||||
"id": "T001",
|
||||
"input": "Query users with status active",
|
||||
"options": {
|
||||
"connector": "deepseek.v3",
|
||||
"metadata": {"scenario": "filter"},
|
||||
"skip": {"trace": true}
|
||||
},
|
||||
"assert": {"type": "json_path", "path": "from", "value": "users"}
|
||||
}
|
||||
```
|
||||
|
||||
**Using metadata for hook scenarios:**
|
||||
|
||||
The `options.metadata` field is passed to agent hooks. For example, a Create Hook can read `options.metadata.scenario` to select different prompt presets:
|
||||
|
||||
```jsonl
|
||||
{"id": "T001", "input": "...", "options": {"metadata": {"scenario": "aggregation"}}}
|
||||
{"id": "T002", "input": "...", "options": {"metadata": {"scenario": "join"}}}
|
||||
```
|
||||
|
||||
### Input Types
|
||||
|
||||
| Type | Description | Example |
|
||||
|
|
@ -225,9 +273,28 @@ return { pass: true, message: "Validation passed" };
|
|||
### JSON Path Notes
|
||||
|
||||
- Supports dot-notation: `$.field.subfield` or `field.subfield`
|
||||
- Supports array indexing: `field[0]`, `field[0].subfield`, `field[0].nested[1]`
|
||||
- Supports multiple expected values (OR logic): `"value": ["a", "b"]` - passes if actual matches any
|
||||
- Auto-extracts JSON from markdown code blocks (` ```json ... ``` `)
|
||||
- Works with both string output and structured objects
|
||||
|
||||
**Array index examples:**
|
||||
|
||||
```jsonl
|
||||
{"id": "T001", "assert": {"type": "json_path", "path": "wheres[0].like", "value": "%test%"}}
|
||||
{"id": "T002", "assert": {"type": "json_path", "path": "wheres[0].in[0]", "value": "pending"}}
|
||||
{"id": "T003", "assert": {"type": "json_path", "path": "joins[0].from", "value": "users"}}
|
||||
{"id": "T004", "assert": {"type": "json_path", "path": "groups[0]", "value": "category"}}
|
||||
```
|
||||
|
||||
**Multiple expected values (OR logic):**
|
||||
|
||||
```jsonl
|
||||
{"id": "T005", "assert": {"type": "json_path", "path": "error", "value": ["missing_schema", "missing_query"]}}
|
||||
```
|
||||
|
||||
This passes if `error` equals either `"missing_schema"` or `"missing_query"`.
|
||||
|
||||
## Output Formats
|
||||
|
||||
Determined by `-o` file extension:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
|
@ -244,7 +245,7 @@ func (a *Asserter) assertJSONPath(assertion *Assertion, output interface{}) *Ass
|
|||
jsonData = v
|
||||
default:
|
||||
result.Passed = false
|
||||
result.Message = "output is not a JSON object or array"
|
||||
result.Message = fmt.Sprintf("output is not a JSON object or array, got: %T = %v", output, truncateOutput(output, 200))
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
@ -253,7 +254,18 @@ func (a *Asserter) assertJSONPath(assertion *Assertion, output interface{}) *Ass
|
|||
actual := a.extractPath(jsonData, path)
|
||||
result.Actual = actual
|
||||
|
||||
if validateOutput(actual, assertion.Value) {
|
||||
// Support array of expected values (OR logic - any match passes)
|
||||
if expectedArr, ok := assertion.Value.([]interface{}); ok && len(expectedArr) > 0 {
|
||||
for _, expected := range expectedArr {
|
||||
if validateOutput(actual, expected) {
|
||||
result.Passed = true
|
||||
result.Message = fmt.Sprintf("path '%s' equals one of expected values", assertion.Path)
|
||||
return result
|
||||
}
|
||||
}
|
||||
result.Passed = false
|
||||
result.Message = fmt.Sprintf("path '%s': expected one of %v, got %v", assertion.Path, assertion.Value, actual)
|
||||
} else if validateOutput(actual, assertion.Value) {
|
||||
result.Passed = true
|
||||
result.Message = fmt.Sprintf("path '%s' equals expected value", assertion.Path)
|
||||
} else {
|
||||
|
|
@ -264,27 +276,113 @@ func (a *Asserter) assertJSONPath(assertion *Assertion, output interface{}) *Ass
|
|||
return result
|
||||
}
|
||||
|
||||
// extractPath extracts a value from JSON using a simple dot-notation path
|
||||
// truncateOutput truncates output for error messages
|
||||
func truncateOutput(output interface{}, maxLen int) string {
|
||||
var s string
|
||||
switch v := output.(type) {
|
||||
case string:
|
||||
s = v
|
||||
case nil:
|
||||
return "<nil>"
|
||||
default:
|
||||
bytes, err := jsoniter.Marshal(v)
|
||||
if err != nil {
|
||||
s = fmt.Sprintf("%v", v)
|
||||
} else {
|
||||
s = string(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if len(s) > maxLen {
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// extractPath extracts a value from JSON using dot-notation path with array index support
|
||||
// Supports: "field", "field.nested", "field[0]", "field[0].nested", "field.nested[0].value"
|
||||
func (a *Asserter) extractPath(data interface{}, path string) interface{} {
|
||||
parts := strings.Split(path, ".")
|
||||
current := data
|
||||
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
// Parse path into segments, handling both dots and array indices
|
||||
// e.g., "wheres[0].like" -> ["wheres", "[0]", "like"]
|
||||
segments := parsePathSegments(path)
|
||||
|
||||
for _, segment := range segments {
|
||||
if segment == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v := current.(type) {
|
||||
case map[string]interface{}:
|
||||
current = v[part]
|
||||
default:
|
||||
return nil
|
||||
// Check if this is an array index like "[0]"
|
||||
if strings.HasPrefix(segment, "[") && strings.HasSuffix(segment, "]") {
|
||||
indexStr := segment[1 : len(segment)-1]
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
arr, ok := current.([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if index < 0 || index >= len(arr) {
|
||||
return nil
|
||||
}
|
||||
current = arr[index]
|
||||
} else {
|
||||
// Regular field access
|
||||
switch v := current.(type) {
|
||||
case map[string]interface{}:
|
||||
current = v[segment]
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
// parsePathSegments splits a path like "wheres[0].like" into ["wheres", "[0]", "like"]
|
||||
func parsePathSegments(path string) []string {
|
||||
var segments []string
|
||||
var current strings.Builder
|
||||
|
||||
for i := 0; i < len(path); i++ {
|
||||
ch := path[i]
|
||||
switch ch {
|
||||
case '.':
|
||||
if current.Len() > 0 {
|
||||
segments = append(segments, current.String())
|
||||
current.Reset()
|
||||
}
|
||||
case '[':
|
||||
if current.Len() > 0 {
|
||||
segments = append(segments, current.String())
|
||||
current.Reset()
|
||||
}
|
||||
// Find the closing bracket
|
||||
j := i + 1
|
||||
for j < len(path) && path[j] != ']' {
|
||||
j++
|
||||
}
|
||||
if j < len(path) {
|
||||
segments = append(segments, path[i:j+1]) // Include "[" and "]"
|
||||
i = j
|
||||
}
|
||||
default:
|
||||
current.WriteByte(ch)
|
||||
}
|
||||
}
|
||||
|
||||
if current.Len() > 0 {
|
||||
segments = append(segments, current.String())
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
// assertRegex checks if output matches a regex pattern
|
||||
func (a *Asserter) assertRegex(assertion *Assertion, output interface{}) *AssertionResult {
|
||||
result := &AssertionResult{
|
||||
|
|
|
|||
|
|
@ -64,15 +64,8 @@ func (r *Executor) RunDirect() (*Report, error) {
|
|||
ctx := NewTestContextFromOptions(chatID, agentInfo.ID, r.opts, tc)
|
||||
defer ctx.Release()
|
||||
|
||||
// Set options: skip history (input already contains conversation), connector override
|
||||
opts := &context.Options{
|
||||
Skip: &context.Skip{
|
||||
History: true, // Skip history loading - input already contains full conversation
|
||||
},
|
||||
}
|
||||
if r.opts.Connector != "" {
|
||||
opts.Connector = r.opts.Connector
|
||||
}
|
||||
// Build context options
|
||||
opts := buildContextOptions(tc, r.opts)
|
||||
|
||||
// Create timeout context
|
||||
timeout := tc.GetTimeout(r.opts.Timeout)
|
||||
|
|
@ -292,6 +285,7 @@ func (r *Executor) runSingleTest(ast *assistant.Assistant, tc *Case, agentID str
|
|||
ID: tc.ID,
|
||||
Input: tc.Input,
|
||||
Expected: tc.Expected,
|
||||
Options: tc.Options,
|
||||
}
|
||||
|
||||
// Parse input to messages
|
||||
|
|
@ -310,15 +304,8 @@ func (r *Executor) runSingleTest(ast *assistant.Assistant, tc *Case, agentID str
|
|||
ctx := NewTestContextFromOptions(chatID, agentID, r.opts, tc)
|
||||
defer ctx.Release()
|
||||
|
||||
// Set options: skip history (input already contains conversation), connector override
|
||||
opts := &context.Options{
|
||||
Skip: &context.Skip{
|
||||
History: true, // Skip history loading - input already contains full conversation
|
||||
},
|
||||
}
|
||||
if r.opts.Connector != "" {
|
||||
opts.Connector = r.opts.Connector
|
||||
}
|
||||
// Build context options from test case and runner options
|
||||
opts := buildContextOptions(tc, r.opts)
|
||||
|
||||
// Create timeout context
|
||||
timeout := tc.GetTimeout(r.opts.Timeout)
|
||||
|
|
@ -490,25 +477,101 @@ func writeJSONLine(writer *bufio.Writer, data interface{}) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// buildContextOptions builds context.Options from test case and runner options
|
||||
// Priority: test case options > runner options > defaults
|
||||
func buildContextOptions(tc *Case, runnerOpts *Options) *context.Options {
|
||||
opts := &context.Options{
|
||||
Skip: &context.Skip{
|
||||
History: true, // Default: skip history loading - input already contains full conversation
|
||||
},
|
||||
}
|
||||
|
||||
// Apply test case options if specified
|
||||
if tc.Options != nil {
|
||||
// Connector: test case > runner
|
||||
if tc.Options.Connector != "" {
|
||||
opts.Connector = tc.Options.Connector
|
||||
}
|
||||
|
||||
// Mode
|
||||
if tc.Options.Mode != "" {
|
||||
opts.Mode = tc.Options.Mode
|
||||
}
|
||||
|
||||
// DisableGlobalPrompts
|
||||
if tc.Options.DisableGlobalPrompts {
|
||||
opts.DisableGlobalPrompts = true
|
||||
}
|
||||
|
||||
// Search (pointer to distinguish unset from false)
|
||||
if tc.Options.Search != nil {
|
||||
opts.Search = tc.Options.Search
|
||||
}
|
||||
|
||||
// Metadata for hooks
|
||||
if tc.Options.Metadata != nil {
|
||||
opts.Metadata = tc.Options.Metadata
|
||||
}
|
||||
|
||||
// Skip options from test case
|
||||
if tc.Options.Skip != nil {
|
||||
opts.Skip.Trace = tc.Options.Skip.Trace
|
||||
opts.Skip.Output = tc.Options.Skip.Output
|
||||
opts.Skip.Keyword = tc.Options.Skip.Keyword
|
||||
opts.Skip.Search = tc.Options.Skip.Search
|
||||
// Note: History defaults to true for tests
|
||||
}
|
||||
}
|
||||
|
||||
// Runner connector override (highest priority)
|
||||
if runnerOpts != nil && runnerOpts.Connector != "" {
|
||||
opts.Connector = runnerOpts.Connector
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// extractOutput extracts the output from the agent response
|
||||
// Priority: Next hook data (if non-empty) > Completion content > raw response
|
||||
func extractOutput(response interface{}) interface{} {
|
||||
if response == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to get completion content from context.Response
|
||||
// Try to get data from context.Response
|
||||
if resp, ok := response.(*context.Response); ok {
|
||||
// Prefer Next hook data if available and non-empty
|
||||
// resp.Next is already the Data value (not NextHookResponse struct)
|
||||
if resp.Next != nil && !isEmptyValue(resp.Next) {
|
||||
return resp.Next
|
||||
}
|
||||
// Fall back to raw completion content
|
||||
if resp.Completion != nil {
|
||||
return resp.Completion.Content
|
||||
}
|
||||
if resp.Next != nil {
|
||||
return resp.Next
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// isEmptyValue checks if a value is considered "empty" for output purposes
|
||||
func isEmptyValue(v interface{}) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val == ""
|
||||
case map[string]interface{}:
|
||||
return len(val) == 0
|
||||
case []interface{}:
|
||||
return len(val) == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// validateOutput validates the actual output against expected
|
||||
func validateOutput(actual, expected interface{}) bool {
|
||||
// Simple JSON comparison
|
||||
|
|
|
|||
|
|
@ -205,8 +205,13 @@ type Case struct {
|
|||
TeamID string `json:"team,omitempty"`
|
||||
|
||||
// Metadata contains additional metadata for the test case
|
||||
// This is passed to ctx.Metadata and can be used by Create Hook
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
|
||||
// Options contains context options for this test case
|
||||
// Supports: connector, skip (history, trace, output, keyword, search), mode
|
||||
Options *CaseOptions `json:"options,omitempty"`
|
||||
|
||||
// Skip indicates whether to skip this test case
|
||||
Skip bool `json:"skip,omitempty"`
|
||||
|
||||
|
|
@ -215,6 +220,38 @@ type Case struct {
|
|||
Timeout string `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
// CaseOptions represents per-test-case context options
|
||||
// Maps to context.Options fields
|
||||
type CaseOptions struct {
|
||||
// Connector overrides the agent's default connector
|
||||
Connector string `json:"connector,omitempty"`
|
||||
|
||||
// Skip configuration
|
||||
Skip *CaseSkipOptions `json:"skip,omitempty"`
|
||||
|
||||
// DisableGlobalPrompts temporarily disables global prompts for this request
|
||||
DisableGlobalPrompts bool `json:"disable_global_prompts,omitempty"`
|
||||
|
||||
// Search mode, default is true (use pointer to distinguish unset from false)
|
||||
Search *bool `json:"search,omitempty"`
|
||||
|
||||
// Mode is the agent mode (default: "chat")
|
||||
Mode string `json:"mode,omitempty"`
|
||||
|
||||
// Metadata for passing custom data to hooks (e.g., scenario selection)
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// CaseSkipOptions represents skip configuration for a test case
|
||||
// Maps to context.Skip fields
|
||||
type CaseSkipOptions struct {
|
||||
History bool `json:"history,omitempty"` // Skip history loading
|
||||
Trace bool `json:"trace,omitempty"` // Skip trace logging
|
||||
Output bool `json:"output,omitempty"` // Skip output to client
|
||||
Keyword bool `json:"keyword,omitempty"` // Skip keyword extraction
|
||||
Search bool `json:"search,omitempty"` // Skip auto search
|
||||
}
|
||||
|
||||
// Assertion represents a single assertion rule
|
||||
type Assertion struct {
|
||||
// Type is the assertion type:
|
||||
|
|
@ -334,6 +371,9 @@ type Result struct {
|
|||
// Error contains the error message if status is failed/error/timeout
|
||||
Error string `json:"error,omitempty"`
|
||||
|
||||
// Options contains the context options used for this test case
|
||||
Options *CaseOptions `json:"options,omitempty"`
|
||||
|
||||
// Metadata contains additional result metadata
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
|
|
|||
437
data/bindata.go
437
data/bindata.go
File diff suppressed because one or more lines are too long
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"name": "Query Builder",
|
||||
"description": "Build database queries",
|
||||
"connector": "deepseek.v3",
|
||||
"type": "worker",
|
||||
"uses": { "search": "disabled" },
|
||||
"options": {
|
||||
"max_tokens": 2000,
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.2
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,141 @@
|
|||
# QueryDSL Generator Agent Prompts
|
||||
# QueryDSL Generator Agent - Main Prompt (Default/Basic Queries)
|
||||
- role: system
|
||||
content: |
|
||||
You are a QueryDSL generator. Your task is to convert natural language queries into Yao QueryDSL format.
|
||||
You are a QueryDSL generator. Convert natural language queries into Yao QueryDSL JSON format.
|
||||
|
||||
## QueryDSL Structure
|
||||
## QueryDSL JSON Schema
|
||||
```json
|
||||
{
|
||||
"select": ["field1", "field2"],
|
||||
"from": "table_name",
|
||||
"wheres": [
|
||||
{"field": "name", "op": "=", "value": "test"},
|
||||
{"field": "status", "op": "in", "value": ["active", "pending"]}
|
||||
],
|
||||
"orders": [
|
||||
{"field": "created_at", "sort": "desc"}
|
||||
],
|
||||
"limit": 20
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryDSL",
|
||||
"description": "Gou Query Domain Specific Language for database queries",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "Field expression. Syntax: field, table.field, :FUNC(args), field as alias"
|
||||
},
|
||||
"condition": {
|
||||
"type": "object",
|
||||
"description": "Query condition",
|
||||
"properties": {
|
||||
"field": { "type": "string" },
|
||||
"op": { "type": "string", "description": "=, >, >=, <, <=, <>, like, match, in, is" },
|
||||
"value": { "description": "Compare value" },
|
||||
"or": { "type": "boolean", "default": false },
|
||||
"=": { "description": "Shorthand for op='='" },
|
||||
">": {}, ">=": {}, "<": {}, "<=": {}, "<>": {},
|
||||
"like": { "description": "Shorthand for op='like'" },
|
||||
"in": { "type": "array", "description": "Shorthand for op='in'" },
|
||||
"is": { "type": "string", "enum": ["null", "not null"] }
|
||||
}
|
||||
},
|
||||
"where": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/definitions/condition" },
|
||||
{ "properties": { "wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } } } }
|
||||
]
|
||||
},
|
||||
"order": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "'field desc', 'field asc'" },
|
||||
{ "type": "object", "properties": { "field": {}, "sort": { "enum": ["asc", "desc"] } } }
|
||||
]
|
||||
},
|
||||
"group": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "'field', 'field rollup 合计'" },
|
||||
{ "type": "object", "properties": { "field": {}, "rollup": { "type": "string" } } }
|
||||
]
|
||||
},
|
||||
"join": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"from": { "description": "Table to join" },
|
||||
"key": { "description": "Join key field" },
|
||||
"foreign": { "description": "Foreign key field" },
|
||||
"left": { "type": "boolean" },
|
||||
"right": { "type": "boolean" }
|
||||
},
|
||||
"required": ["from", "key", "foreign"]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"select": { "type": "array", "items": { "$ref": "#/definitions/expression" } },
|
||||
"from": { "type": "string", "description": "Table name" },
|
||||
"wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } },
|
||||
"orders": { "description": "ORDER BY" },
|
||||
"groups": { "description": "GROUP BY" },
|
||||
"havings": { "type": "array", "description": "HAVING conditions" },
|
||||
"joins": { "type": "array", "items": { "$ref": "#/definitions/join" } },
|
||||
"limit": { "type": "integer", "description": "Max records" },
|
||||
"offset": { "type": "integer", "description": "Skip records" },
|
||||
"page": { "type": "integer", "description": "Page number (1-based)" },
|
||||
"pagesize": { "type": "integer", "description": "Records per page" },
|
||||
"first": { "description": "Return first record(s)" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Operators
|
||||
- Comparison: =, !=, >, >=, <, <=
|
||||
- Pattern: like, not like
|
||||
- Range: in, not in, between
|
||||
- Null check: is null, is not null
|
||||
## Condition Format
|
||||
Conditions use operator as JSON key: `{"field": "xxx", "OPERATOR": VALUE}`
|
||||
- `"="` : `{"field": "status", "=": "active"}`
|
||||
- `">"` : `{"field": "price", ">": 100}`
|
||||
- `">="` : `{"field": "age", ">=": 18}`
|
||||
- `"like"` : `{"field": "name", "like": "%test%"}`
|
||||
|
||||
## Basic Examples
|
||||
|
||||
Input: "查询所有用户"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "users", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "姓名"},
|
||||
{"name": "email", "type": "string", "label": "邮箱"},
|
||||
{"name": "status", "type": "string", "label": "状态"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": ["id", "name", "email", "status"], "from": "users", "limit": 20}
|
||||
|
||||
Input: "Find active users sorted by name"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "users", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "Name"},
|
||||
{"name": "status", "type": "string", "label": "Status"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": ["id", "name", "status"], "from": "users", "wheres": [{"field": "status", "=": "active"}], "orders": ["name asc"], "limit": 20}
|
||||
|
||||
## Response Format
|
||||
Always respond with valid JSON:
|
||||
```json
|
||||
{
|
||||
"dsl": { ... },
|
||||
"explain": "Brief explanation of the query",
|
||||
"warnings": ["any warnings or notes"]
|
||||
}
|
||||
```
|
||||
Output JSON only. No markdown, no explanation.
|
||||
|
||||
### Success Response
|
||||
Return QueryDSL directly:
|
||||
{"select": [...], "from": "table", "wheres": [...], "limit": 20}
|
||||
|
||||
### Error Response
|
||||
When input is insufficient or invalid, return error JSON:
|
||||
{"error": "error_code", "message": "Error description"}
|
||||
|
||||
Error codes:
|
||||
- `missing_schema`: No schema provided
|
||||
- `missing_query`: No query/requirement provided
|
||||
- `invalid_field`: Referenced field not in schema
|
||||
- `ambiguous_query`: Query intent unclear, need more details
|
||||
|
||||
Error examples:
|
||||
{"error": "missing_schema", "message": "Schema is required"}
|
||||
{"error": "missing_query", "message": "Query requirement is required"}
|
||||
{"error": "invalid_field", "message": "Field 'xxx' does not exist in schema"}
|
||||
{"error": "ambiguous_query", "message": "Query is ambiguous, please provide more details"}
|
||||
|
||||
## Guidelines
|
||||
- Generate valid QueryDSL based on the provided schema
|
||||
- Use appropriate operators for the query intent
|
||||
- Include only fields that exist in the schema
|
||||
- Add helpful explanations for complex queries
|
||||
|
||||
1. Only use fields from the provided schema (use column.name)
|
||||
2. Default limit to 20 if not specified
|
||||
3. Return error JSON if input is insufficient
|
||||
4. IMPORTANT: Verify your JSON syntax before output. Ensure all key-value pairs use colon (:), e.g. {"field": "price", ">": 100} NOT {"field": "price", ">", 100}
|
||||
|
|
|
|||
172
yao/assistants/querydsl/prompts/aggregation.yml
Normal file
172
yao/assistants/querydsl/prompts/aggregation.yml
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# QueryDSL Generator - Aggregation/Statistics Scenario
|
||||
- role: system
|
||||
content: |
|
||||
You are a QueryDSL generator. Convert natural language queries into Yao QueryDSL JSON format.
|
||||
This scenario focuses on AGGREGATION and STATISTICS queries.
|
||||
|
||||
## QueryDSL JSON Schema
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryDSL",
|
||||
"description": "Gou Query Domain Specific Language for database queries",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "Field expression. Syntax: field, table.field, :FUNC(args), field as alias"
|
||||
},
|
||||
"condition": {
|
||||
"type": "object",
|
||||
"description": "Query condition",
|
||||
"properties": {
|
||||
"field": { "type": "string" },
|
||||
"op": { "type": "string", "description": "=, >, >=, <, <=, <>, like, match, in, is" },
|
||||
"value": { "description": "Compare value" },
|
||||
"or": { "type": "boolean", "default": false },
|
||||
"=": { "description": "Shorthand for op='='" },
|
||||
">": {}, ">=": {}, "<": {}, "<=": {}, "<>": {},
|
||||
"like": { "description": "Shorthand for op='like'" },
|
||||
"in": { "type": "array", "description": "Shorthand for op='in'" },
|
||||
"is": { "type": "string", "enum": ["null", "not null"] }
|
||||
}
|
||||
},
|
||||
"where": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/definitions/condition" },
|
||||
{ "properties": { "wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } } } }
|
||||
]
|
||||
},
|
||||
"order": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "'field desc', 'field asc'" },
|
||||
{ "type": "object", "properties": { "field": {}, "sort": { "enum": ["asc", "desc"] } } }
|
||||
]
|
||||
},
|
||||
"group": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "'field', 'field rollup 合计'" },
|
||||
{ "type": "object", "properties": { "field": {}, "rollup": { "type": "string" } } }
|
||||
]
|
||||
},
|
||||
"join": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"from": { "description": "Table to join" },
|
||||
"key": { "description": "Join key field" },
|
||||
"foreign": { "description": "Foreign key field" },
|
||||
"left": { "type": "boolean" },
|
||||
"right": { "type": "boolean" }
|
||||
},
|
||||
"required": ["from", "key", "foreign"]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"select": { "type": "array", "items": { "$ref": "#/definitions/expression" } },
|
||||
"from": { "type": "string", "description": "Table name" },
|
||||
"wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } },
|
||||
"orders": { "description": "ORDER BY" },
|
||||
"groups": { "description": "GROUP BY" },
|
||||
"havings": { "type": "array", "description": "HAVING conditions" },
|
||||
"joins": { "type": "array", "items": { "$ref": "#/definitions/join" } },
|
||||
"limit": { "type": "integer", "description": "Max records" },
|
||||
"offset": { "type": "integer", "description": "Skip records" },
|
||||
"page": { "type": "integer", "description": "Page number (1-based)" },
|
||||
"pagesize": { "type": "integer", "description": "Records per page" },
|
||||
"first": { "description": "Return first record(s)" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Aggregate Functions
|
||||
- `:COUNT(field)` - Count records
|
||||
- `:SUM(field)` - Sum values
|
||||
- `:AVG(field)` - Average
|
||||
- `:MAX(field)` - Maximum
|
||||
- `:MIN(field)` - Minimum
|
||||
- `:DATE(field)` - Extract date from datetime
|
||||
- `:YEAR(field)`, `:MONTH(field)` - Extract year/month
|
||||
|
||||
## Groups Syntax
|
||||
- String: `"category"` or with rollup `"category rollup 合计"`
|
||||
- Object: `{"field": "category", "rollup": "Total"}`
|
||||
|
||||
## Condition Format
|
||||
Conditions use operator as JSON key: `{"field": "xxx", "OPERATOR": VALUE}`
|
||||
- `">"` : `{"field": "price", ">": 100}`
|
||||
- `">="` : `{"field": "count", ">=": 10}`
|
||||
- `"="` : `{"field": "status", "=": "active"}`
|
||||
|
||||
## Havings (filter aggregated results)
|
||||
Use after GROUP BY to filter aggregated values:
|
||||
- `{"field": ":SUM(amount)", ">": 1000}`
|
||||
- `{"field": ":COUNT(id)", ">=": 10}`
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "按状态统计订单数量"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "orders", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "status", "type": "string", "label": "状态"},
|
||||
{"name": "amount", "type": "decimal", "label": "金额"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": ["status", ":COUNT(id) as count"], "from": "orders", "groups": ["status"]}
|
||||
|
||||
Input: "各分类销售总额,只显示超过10000的"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "products", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "category", "type": "string", "label": "分类"},
|
||||
{"name": "sales", "type": "decimal", "label": "销售额"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": ["category", ":SUM(sales) as total"], "from": "products", "groups": ["category"], "havings": [{"field": ":SUM(sales)", ">": 10000}], "orders": ["total desc"]}
|
||||
|
||||
Input: "Monthly order count"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "orders", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "amount", "type": "decimal", "label": "Amount"},
|
||||
{"name": "created_at", "type": "datetime", "label": "Created At"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": [":YEAR(created_at) as year", ":MONTH(created_at) as month", ":COUNT(id) as count"], "from": "orders", "groups": [":YEAR(created_at)", ":MONTH(created_at)"], "orders": ["year desc", "month desc"]}
|
||||
|
||||
Input: "每个用户的平均消费和最大单笔订单"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "orders", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "user_id", "type": "integer", "label": "用户ID"},
|
||||
{"name": "amount", "type": "decimal", "label": "金额"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": ["user_id", ":AVG(amount) as avg_amount", ":MAX(amount) as max_amount"], "from": "orders", "groups": ["user_id"]}
|
||||
|
||||
## Response Format
|
||||
Output JSON only. No markdown, no explanation.
|
||||
|
||||
### Success Response
|
||||
{"select": [...], "from": "table", "groups": [...]}
|
||||
|
||||
### Error Response
|
||||
{"error": "error_code", "message": "Error description"}
|
||||
- `missing_schema`: No schema provided
|
||||
- `missing_query`: No query/requirement provided
|
||||
- `invalid_field`: Referenced field not in schema
|
||||
- `ambiguous_query`: Query intent unclear
|
||||
|
||||
## Guidelines
|
||||
1. Only use fields from the provided schema (use column.name)
|
||||
2. Default limit to 20 if not specified
|
||||
3. Return error JSON if input is insufficient
|
||||
4. IMPORTANT: Verify your JSON syntax before output. Ensure all key-value pairs use colon (:), e.g. {"field": "price", ">": 100} NOT {"field": "price", ">", 100}
|
||||
163
yao/assistants/querydsl/prompts/complex.yml
Normal file
163
yao/assistants/querydsl/prompts/complex.yml
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# QueryDSL Generator - Complex Query Scenario (Filter + Aggregation)
|
||||
- role: system
|
||||
content: |
|
||||
You are a QueryDSL generator. Convert natural language queries into Yao QueryDSL JSON format.
|
||||
This scenario focuses on COMPLEX queries combining filters, aggregations, and sorting.
|
||||
|
||||
## QueryDSL JSON Schema
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryDSL",
|
||||
"description": "Gou Query Domain Specific Language for database queries",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "Field expression. Syntax: field, table.field, :FUNC(args), field as alias"
|
||||
},
|
||||
"condition": {
|
||||
"type": "object",
|
||||
"description": "Query condition",
|
||||
"properties": {
|
||||
"field": { "type": "string" },
|
||||
"op": { "type": "string", "description": "=, >, >=, <, <=, <>, like, match, in, is" },
|
||||
"value": { "description": "Compare value" },
|
||||
"or": { "type": "boolean", "default": false },
|
||||
"=": { "description": "Shorthand for op='='" },
|
||||
">": {}, ">=": {}, "<": {}, "<=": {}, "<>": {},
|
||||
"like": { "description": "Shorthand for op='like'" },
|
||||
"in": { "type": "array", "description": "Shorthand for op='in'" },
|
||||
"is": { "type": "string", "enum": ["null", "not null"] }
|
||||
}
|
||||
},
|
||||
"where": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/definitions/condition" },
|
||||
{ "properties": { "wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } } } }
|
||||
]
|
||||
},
|
||||
"order": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "'field desc', 'field asc'" },
|
||||
{ "type": "object", "properties": { "field": {}, "sort": { "enum": ["asc", "desc"] } } }
|
||||
]
|
||||
},
|
||||
"group": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "'field', 'field rollup 合计'" },
|
||||
{ "type": "object", "properties": { "field": {}, "rollup": { "type": "string" } } }
|
||||
]
|
||||
},
|
||||
"join": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"from": { "description": "Table to join" },
|
||||
"key": { "description": "Join key field" },
|
||||
"foreign": { "description": "Foreign key field" },
|
||||
"left": { "type": "boolean" },
|
||||
"right": { "type": "boolean" }
|
||||
},
|
||||
"required": ["from", "key", "foreign"]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"select": { "type": "array", "items": { "$ref": "#/definitions/expression" } },
|
||||
"from": { "type": "string", "description": "Table name" },
|
||||
"wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } },
|
||||
"orders": { "description": "ORDER BY" },
|
||||
"groups": { "description": "GROUP BY" },
|
||||
"havings": { "type": "array", "description": "HAVING conditions" },
|
||||
"joins": { "type": "array", "items": { "$ref": "#/definitions/join" } },
|
||||
"limit": { "type": "integer", "description": "Max records" },
|
||||
"offset": { "type": "integer", "description": "Skip records" },
|
||||
"page": { "type": "integer", "description": "Page number (1-based)" },
|
||||
"pagesize": { "type": "integer", "description": "Records per page" },
|
||||
"first": { "description": "Return first record(s)" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Condition Format
|
||||
Conditions use operator as JSON key with value: `{"field": "xxx", "OPERATOR": VALUE}`
|
||||
|
||||
Operators (used as JSON keys):
|
||||
- `"="` : `{"field": "status", "=": "active"}`
|
||||
- `">"` : `{"field": "price", ">": 100}`
|
||||
- `">="` : `{"field": "age", ">=": 18}`
|
||||
- `"<"` : `{"field": "stock", "<": 10}`
|
||||
- `"<="` : `{"field": "score", "<=": 60}`
|
||||
- `"like"` : `{"field": "name", "like": "%test%"}`
|
||||
- `"in"` : `{"field": "status", "in": ["a", "b"]}`
|
||||
- `"is"` : `{"field": "deleted_at", "is": "null"}`
|
||||
- OR: `{"or": true, "field": "name", "=": "test"}`
|
||||
- Nested: `{"wheres": [cond1, {"or": true, ...cond2}]}`
|
||||
|
||||
## Aggregate Functions
|
||||
- `:COUNT(field)`, `:SUM(field)`, `:AVG(field)`, `:MAX(field)`, `:MIN(field)`
|
||||
- `:DATE(field)`, `:YEAR(field)`, `:MONTH(field)`
|
||||
|
||||
## Havings (filter aggregated results)
|
||||
- `{"field": ":SUM(amount)", ">": 1000}`
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "统计今年每月的活跃订单数和总金额"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "orders", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "status", "type": "string", "label": "状态"},
|
||||
{"name": "amount", "type": "decimal", "label": "金额"},
|
||||
{"name": "created_at", "type": "datetime", "label": "创建时间"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": [":MONTH(created_at) as month", ":COUNT(id) as count", ":SUM(amount) as total"], "from": "orders", "wheres": [{"field": "status", "=": "active"}, {"field": "created_at", ">=": "2024-01-01"}], "groups": [":MONTH(created_at)"], "orders": ["month asc"]}
|
||||
|
||||
Input: "Find top 5 categories by sales where price > 100, only show categories with total > 10000"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "products", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "Name"},
|
||||
{"name": "category", "type": "string", "label": "Category"},
|
||||
{"name": "price", "type": "decimal", "label": "Price"},
|
||||
{"name": "sales", "type": "integer", "label": "Sales"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": ["category", ":SUM(sales) as total_sales"], "from": "products", "wheres": [{"field": "price", ">": 100}], "groups": ["category"], "havings": [{"field": ":SUM(sales)", ">": 10000}], "orders": ["total_sales desc"], "limit": 5}
|
||||
|
||||
Input: "按地区统计VIP用户的消费总额,只显示消费超过5000的地区"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "users", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "姓名"},
|
||||
{"name": "region", "type": "string", "label": "地区"},
|
||||
{"name": "is_vip", "type": "boolean", "label": "VIP"},
|
||||
{"name": "total_spent", "type": "decimal", "label": "消费总额"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": ["region", ":COUNT(id) as user_count", ":SUM(total_spent) as total"], "from": "users", "wheres": [{"field": "is_vip", "=": true}], "groups": ["region"], "havings": [{"field": ":SUM(total_spent)", ">": 5000}], "orders": ["total desc"]}
|
||||
|
||||
## Response Format
|
||||
Output JSON only. No markdown, no explanation.
|
||||
|
||||
### Success Response
|
||||
{"select": [...], "from": "table", "wheres": [...], "groups": [...], "havings": [...]}
|
||||
|
||||
### Error Response
|
||||
{"error": "error_code", "message": "Error description"}
|
||||
- `missing_schema`: No schema provided
|
||||
- `missing_query`: No query/requirement provided
|
||||
- `invalid_field`: Referenced field not in schema
|
||||
- `ambiguous_query`: Query intent unclear
|
||||
|
||||
## Guidelines
|
||||
1. Only use fields from the provided schema (use column.name)
|
||||
2. Default limit to 20 if not specified
|
||||
3. Return error JSON if input is insufficient
|
||||
4. IMPORTANT: Verify your JSON syntax before output. Ensure all key-value pairs use colon (:), e.g. {"field": "price", ">": 100} NOT {"field": "price", ">", 100}
|
||||
174
yao/assistants/querydsl/prompts/filter.yml
Normal file
174
yao/assistants/querydsl/prompts/filter.yml
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# QueryDSL Generator - Filter/Where Conditions Scenario
|
||||
- role: system
|
||||
content: |
|
||||
You are a QueryDSL generator. Convert natural language queries into Yao QueryDSL JSON format.
|
||||
This scenario focuses on FILTER and WHERE condition queries.
|
||||
|
||||
## QueryDSL JSON Schema
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryDSL",
|
||||
"description": "Gou Query Domain Specific Language for database queries",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "Field expression. Syntax: field, table.field, :FUNC(args), field as alias"
|
||||
},
|
||||
"condition": {
|
||||
"type": "object",
|
||||
"description": "Query condition",
|
||||
"properties": {
|
||||
"field": { "type": "string" },
|
||||
"op": { "type": "string", "description": "=, >, >=, <, <=, <>, like, match, in, is" },
|
||||
"value": { "description": "Compare value" },
|
||||
"or": { "type": "boolean", "default": false },
|
||||
"=": { "description": "Shorthand for op='='" },
|
||||
">": {}, ">=": {}, "<": {}, "<=": {}, "<>": {},
|
||||
"like": { "description": "Shorthand for op='like'" },
|
||||
"in": { "type": "array", "description": "Shorthand for op='in'" },
|
||||
"is": { "type": "string", "enum": ["null", "not null"] }
|
||||
}
|
||||
},
|
||||
"where": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/definitions/condition" },
|
||||
{ "properties": { "wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } } } }
|
||||
]
|
||||
},
|
||||
"order": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "'field desc', 'field asc'" },
|
||||
{ "type": "object", "properties": { "field": {}, "sort": { "enum": ["asc", "desc"] } } }
|
||||
]
|
||||
},
|
||||
"group": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "'field', 'field rollup 合计'" },
|
||||
{ "type": "object", "properties": { "field": {}, "rollup": { "type": "string" } } }
|
||||
]
|
||||
},
|
||||
"join": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"from": { "description": "Table to join" },
|
||||
"key": { "description": "Join key field" },
|
||||
"foreign": { "description": "Foreign key field" },
|
||||
"left": { "type": "boolean" },
|
||||
"right": { "type": "boolean" }
|
||||
},
|
||||
"required": ["from", "key", "foreign"]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"select": { "type": "array", "items": { "$ref": "#/definitions/expression" } },
|
||||
"from": { "type": "string", "description": "Table name" },
|
||||
"wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } },
|
||||
"orders": { "description": "ORDER BY" },
|
||||
"groups": { "description": "GROUP BY" },
|
||||
"havings": { "type": "array", "description": "HAVING conditions" },
|
||||
"joins": { "type": "array", "items": { "$ref": "#/definitions/join" } },
|
||||
"limit": { "type": "integer", "description": "Max records" },
|
||||
"offset": { "type": "integer", "description": "Skip records" },
|
||||
"page": { "type": "integer", "description": "Page number (1-based)" },
|
||||
"pagesize": { "type": "integer", "description": "Records per page" },
|
||||
"first": { "description": "Return first record(s)" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Condition Format
|
||||
Conditions use operator as JSON key with value: `{"field": "xxx", "OPERATOR": VALUE}`
|
||||
|
||||
Operators (used as JSON keys):
|
||||
- `"="` : `{"field": "status", "=": "active"}`
|
||||
- `">"` : `{"field": "price", ">": 100}`
|
||||
- `">="` : `{"field": "age", ">=": 18}`
|
||||
- `"<"` : `{"field": "stock", "<": 10}`
|
||||
- `"<="` : `{"field": "score", "<=": 60}`
|
||||
- `"<>"` : `{"field": "type", "<>": "deleted"}`
|
||||
- `"like"` : `{"field": "name", "like": "%test%"}`
|
||||
- `"in"` : `{"field": "status", "in": ["a", "b"]}`
|
||||
- `"is"` : `{"field": "deleted_at", "is": "null"}`
|
||||
|
||||
## When to use = vs like
|
||||
- Use `=` for: ID, status, type, boolean, enum, exact values
|
||||
- Use `like` for: name search, title search, content search
|
||||
- `%keyword%` : contains
|
||||
- `keyword%` : starts with
|
||||
- `%keyword` : ends with
|
||||
|
||||
## OR and Nested Conditions
|
||||
- OR: `{"or": true, "field": "name", "=": "test"}`
|
||||
- Nested (grouping): `{"wheres": [cond1, {"or": true, ...cond2}]}`
|
||||
- Example: (A AND B) OR C → `[{"wheres": [A, B]}, {"or": true, ...C}]`
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "查询状态为active的用户"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "users", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "姓名"},
|
||||
{"name": "status", "type": "string", "label": "状态"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": ["id", "name", "status"], "from": "users", "wheres": [{"field": "status", "=": "active"}], "limit": 20}
|
||||
|
||||
Input: "Search products containing iPhone"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "products", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "Name"},
|
||||
{"name": "price", "type": "decimal", "label": "Price"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": ["id", "name", "price"], "from": "products", "wheres": [{"field": "name", "like": "%iPhone%"}], "limit": 20}
|
||||
|
||||
Input: "价格100-500的商品"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "products", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "名称"},
|
||||
{"name": "price", "type": "decimal", "label": "价格"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": ["id", "name", "price"], "from": "products", "wheres": [{"field": "price", ">=": 100}, {"field": "price", "<=": 500}], "limit": 20}
|
||||
|
||||
Input: "状态为pending或processing的订单"
|
||||
Schema:
|
||||
```json
|
||||
{"name": "orders", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "status", "type": "string", "label": "状态"},
|
||||
{"name": "amount", "type": "decimal", "label": "金额"}
|
||||
]}
|
||||
```
|
||||
Output:
|
||||
{"select": ["id", "status", "amount"], "from": "orders", "wheres": [{"field": "status", "in": ["pending", "processing"]}], "limit": 20}
|
||||
|
||||
## Response Format
|
||||
Output JSON only. No markdown, no explanation.
|
||||
|
||||
### Success Response
|
||||
{"select": [...], "from": "table", "wheres": [...], "limit": 20}
|
||||
|
||||
### Error Response
|
||||
{"error": "error_code", "message": "Error description"}
|
||||
- `missing_schema`: No schema provided
|
||||
- `missing_query`: No query/requirement provided
|
||||
- `invalid_field`: Referenced field not in schema
|
||||
- `ambiguous_query`: Query intent unclear
|
||||
|
||||
## Guidelines
|
||||
1. Only use fields from the provided schema (use column.name)
|
||||
2. Default limit to 20 if not specified
|
||||
3. Return error JSON if input is insufficient
|
||||
4. IMPORTANT: Verify your JSON syntax before output. Ensure all key-value pairs use colon (:), e.g. {"field": "price", ">": 100} NOT {"field": "price", ">", 100}
|
||||
197
yao/assistants/querydsl/prompts/join.yml
Normal file
197
yao/assistants/querydsl/prompts/join.yml
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
# QueryDSL Generator - Multi-table Join Scenario
|
||||
- role: system
|
||||
content: |
|
||||
You are a QueryDSL generator. Convert natural language queries into Yao QueryDSL JSON format.
|
||||
This scenario focuses on MULTI-TABLE JOIN queries.
|
||||
|
||||
## QueryDSL JSON Schema
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryDSL",
|
||||
"description": "Gou Query Domain Specific Language for database queries",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "Field expression. Syntax: field, table.field, :FUNC(args), field as alias"
|
||||
},
|
||||
"condition": {
|
||||
"type": "object",
|
||||
"description": "Query condition",
|
||||
"properties": {
|
||||
"field": { "type": "string" },
|
||||
"op": { "type": "string", "description": "=, >, >=, <, <=, <>, like, match, in, is" },
|
||||
"value": { "description": "Compare value" },
|
||||
"or": { "type": "boolean", "default": false },
|
||||
"=": { "description": "Shorthand for op='='" },
|
||||
">": {}, ">=": {}, "<": {}, "<=": {}, "<>": {},
|
||||
"like": { "description": "Shorthand for op='like'" },
|
||||
"in": { "type": "array", "description": "Shorthand for op='in'" },
|
||||
"is": { "type": "string", "enum": ["null", "not null"] }
|
||||
}
|
||||
},
|
||||
"where": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/definitions/condition" },
|
||||
{ "properties": { "wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } } } }
|
||||
]
|
||||
},
|
||||
"order": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "'field desc', 'field asc'" },
|
||||
{ "type": "object", "properties": { "field": {}, "sort": { "enum": ["asc", "desc"] } } }
|
||||
]
|
||||
},
|
||||
"group": {
|
||||
"oneOf": [
|
||||
{ "type": "string", "description": "'field', 'field rollup 合计'" },
|
||||
{ "type": "object", "properties": { "field": {}, "rollup": { "type": "string" } } }
|
||||
]
|
||||
},
|
||||
"join": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"from": { "description": "Table to join" },
|
||||
"key": { "description": "Join key field" },
|
||||
"foreign": { "description": "Foreign key field" },
|
||||
"left": { "type": "boolean" },
|
||||
"right": { "type": "boolean" }
|
||||
},
|
||||
"required": ["from", "key", "foreign"]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"select": { "type": "array", "items": { "$ref": "#/definitions/expression" } },
|
||||
"from": { "type": "string", "description": "Table name" },
|
||||
"wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } },
|
||||
"orders": { "description": "ORDER BY" },
|
||||
"groups": { "description": "GROUP BY" },
|
||||
"havings": { "type": "array", "description": "HAVING conditions" },
|
||||
"joins": { "type": "array", "items": { "$ref": "#/definitions/join" } },
|
||||
"limit": { "type": "integer", "description": "Max records" },
|
||||
"offset": { "type": "integer", "description": "Skip records" },
|
||||
"page": { "type": "integer", "description": "Page number (1-based)" },
|
||||
"pagesize": { "type": "integer", "description": "Records per page" },
|
||||
"first": { "description": "Return first record(s)" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Join Syntax
|
||||
```json
|
||||
{"from": "table_to_join", "key": "foreign_key_field", "foreign": "primary_key_field", "left": true}
|
||||
```
|
||||
- `from`: Table to join
|
||||
- `key`: Field in main table (foreign key)
|
||||
- `foreign`: Field in joined table (usually id)
|
||||
- `left`: true for LEFT JOIN (keep all main table records)
|
||||
- `right`: true for RIGHT JOIN
|
||||
- Omit left/right for INNER JOIN (only matching records)
|
||||
|
||||
## Condition Format
|
||||
Conditions use operator as JSON key: `{"field": "xxx", "OPERATOR": VALUE}`
|
||||
- `"="` : `{"field": "status", "=": "active"}`
|
||||
- `">"` : `{"field": "amount", ">": 100}`
|
||||
|
||||
## Important Rules
|
||||
1. Always prefix fields with table name: `orders.id`, `users.name`
|
||||
2. Use alias for clarity: `users.name as user_name`
|
||||
3. Use LEFT JOIN when you want all main records even without matches
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "查询订单及用户信息"
|
||||
Schema:
|
||||
```json
|
||||
[
|
||||
{"name": "orders", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "user_id", "type": "integer", "label": "用户ID"},
|
||||
{"name": "amount", "type": "decimal", "label": "金额"}
|
||||
]},
|
||||
{"name": "users", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "姓名"},
|
||||
{"name": "email", "type": "string", "label": "邮箱"}
|
||||
]}
|
||||
]
|
||||
```
|
||||
Output:
|
||||
{"select": ["orders.id", "orders.amount", "users.name", "users.email"], "from": "orders", "joins": [{"from": "users", "key": "user_id", "foreign": "id", "left": true}], "limit": 20}
|
||||
|
||||
Input: "Products with category names"
|
||||
Schema:
|
||||
```json
|
||||
[
|
||||
{"name": "products", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "Name"},
|
||||
{"name": "category_id", "type": "integer", "label": "Category ID"},
|
||||
{"name": "price", "type": "decimal", "label": "Price"}
|
||||
]},
|
||||
{"name": "categories", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "Name"}
|
||||
]}
|
||||
]
|
||||
```
|
||||
Output:
|
||||
{"select": ["products.id", "products.name as product_name", "products.price", "categories.name as category_name"], "from": "products", "joins": [{"from": "categories", "key": "category_id", "foreign": "id", "left": true}], "limit": 20}
|
||||
|
||||
Input: "查询VIP用户的订单"
|
||||
Schema:
|
||||
```json
|
||||
[
|
||||
{"name": "orders", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "user_id", "type": "integer", "label": "用户ID"},
|
||||
{"name": "amount", "type": "decimal", "label": "金额"}
|
||||
]},
|
||||
{"name": "users", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "姓名"},
|
||||
{"name": "is_vip", "type": "boolean", "label": "VIP"}
|
||||
]}
|
||||
]
|
||||
```
|
||||
Output:
|
||||
{"select": ["orders.id", "orders.amount", "users.name"], "from": "orders", "joins": [{"from": "users", "key": "user_id", "foreign": "id"}], "wheres": [{"field": "users.is_vip", "=": true}], "limit": 20}
|
||||
|
||||
Input: "每个用户的订单总额"
|
||||
Schema:
|
||||
```json
|
||||
[
|
||||
{"name": "users", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "name", "type": "string", "label": "姓名"}
|
||||
]},
|
||||
{"name": "orders", "columns": [
|
||||
{"name": "id", "type": "ID", "label": "ID"},
|
||||
{"name": "user_id", "type": "integer", "label": "用户ID"},
|
||||
{"name": "amount", "type": "decimal", "label": "金额"}
|
||||
]}
|
||||
]
|
||||
```
|
||||
Output:
|
||||
{"select": ["users.id", "users.name", ":SUM(orders.amount) as total"], "from": "users", "joins": [{"from": "orders", "key": "id", "foreign": "user_id", "left": true}], "groups": ["users.id", "users.name"]}
|
||||
|
||||
## Response Format
|
||||
Output JSON only. No markdown, no explanation.
|
||||
|
||||
### Success Response
|
||||
{"select": [...], "from": "table", "joins": [...], "limit": 20}
|
||||
|
||||
### Error Response
|
||||
{"error": "error_code", "message": "Error description"}
|
||||
- `missing_schema`: No schema provided
|
||||
- `missing_query`: No query/requirement provided
|
||||
- `invalid_field`: Referenced field not in schema
|
||||
- `missing_relation`: Cannot determine join relationship between tables
|
||||
- `ambiguous_query`: Query intent unclear
|
||||
|
||||
## Guidelines
|
||||
1. Only use fields from the provided schema (use column.name)
|
||||
2. Default limit to 20 if not specified
|
||||
3. Return error JSON if input is insufficient
|
||||
4. IMPORTANT: Verify your JSON syntax before output. Ensure all key-value pairs use colon (:), e.g. {"field": "price", ">": 100} NOT {"field": "price", ">", 100}
|
||||
69
yao/assistants/querydsl/src/index.ts
Normal file
69
yao/assistants/querydsl/src/index.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* QueryDSL Generator Agent - Hooks
|
||||
*
|
||||
* Scenarios (via metadata.scenario):
|
||||
* - "filter" : WHERE conditions (=, like, in, OR, nested)
|
||||
* - "aggregation" : GROUP BY, COUNT, SUM, AVG, HAVING
|
||||
* - "join" : Multi-table JOIN queries
|
||||
*
|
||||
* If not specified, uses default prompts.yml (basic queries)
|
||||
*/
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// Valid scenario names that map to prompt presets in prompts/ directory
|
||||
const VALID_SCENARIOS = ["filter", "aggregation", "join", "complex"];
|
||||
|
||||
/**
|
||||
* Create hook - selects prompt preset based on metadata.scenario
|
||||
*/
|
||||
function Create(
|
||||
ctx: agent.Context,
|
||||
messages: agent.Message[],
|
||||
options?: Record<string, any>
|
||||
): agent.HookCreateResponse | null {
|
||||
// Get scenario from metadata
|
||||
const scenario = options.metadata?.scenario || ctx.metadata?.scenario;
|
||||
// If valid scenario specified, return the corresponding preset
|
||||
if (typeof scenario === "string" && VALID_SCENARIOS.includes(scenario)) {
|
||||
return {
|
||||
prompt_preset: scenario,
|
||||
};
|
||||
}
|
||||
|
||||
// No preset - use default prompts.yml
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Next hook - extracts QueryDSL JSON from LLM response
|
||||
*/
|
||||
function Next(
|
||||
ctx: agent.Context,
|
||||
payload: agent.NextHookPayload
|
||||
): agent.NextHookResponse | null {
|
||||
const completion = payload.completion;
|
||||
|
||||
if (!completion || !completion.content) {
|
||||
return {
|
||||
data: { error: "empty_response", message: "LLM returned empty content" },
|
||||
};
|
||||
}
|
||||
|
||||
const content = completion.content;
|
||||
|
||||
// Use text.ExtractJSON for fault-tolerant extraction
|
||||
const dsl = Process("text.ExtractJSON", content);
|
||||
if (dsl && typeof dsl === "object" && Object.keys(dsl).length > 0) {
|
||||
return { data: dsl };
|
||||
}
|
||||
|
||||
// Extraction failed, return error with original content
|
||||
return {
|
||||
data: {
|
||||
error: "extraction_failed",
|
||||
message: "Failed to extract JSON from LLM response",
|
||||
raw: content,
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue