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 {
|
if opts.DisableGlobalPrompts {
|
||||||
result["disable_global_prompts"] = 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:"-")
|
// Note: Runtime fields (Context, Writer) are not serialized (json:"-")
|
||||||
// They should not be included in the map
|
// 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 {
|
if disableGlobalPrompts, ok := m["disable_global_prompts"].(bool); ok {
|
||||||
opts.DisableGlobalPrompts = disableGlobalPrompts
|
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
|
// Note: Context and Writer are runtime fields, not restored from map
|
||||||
// They should be set by the caller if needed
|
// 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"
|
// 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"
|
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
|
// 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
|
errorDetected := false
|
||||||
|
|
||||||
// Wrap streamHandler to detect JSON error responses
|
// 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 {
|
wrappedHandler := func(data []byte) int {
|
||||||
dataStr := string(data)
|
dataStr := string(data)
|
||||||
|
trimmed := strings.TrimSpace(dataStr)
|
||||||
|
|
||||||
// Detect if this looks like a JSON error response (starts with "{" or contains "error")
|
// Skip empty lines
|
||||||
if strings.Contains(dataStr, `"error"`) || (strings.TrimSpace(dataStr) == "{" && !errorDetected) {
|
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
|
errorDetected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -652,7 +666,7 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
|
||||||
return http.HandlerReturnOk
|
return http.HandlerReturnOk
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, use normal handler
|
// Unknown format, pass to streamHandler (it will skip non-SSE data)
|
||||||
return streamHandler(data)
|
return streamHandler(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,10 +110,58 @@ Each line is a JSON object:
|
||||||
| `expected` | any | No | Expected output (exact match) |
|
| `expected` | any | No | Expected output (exact match) |
|
||||||
| `user` | string | No | Override user ID |
|
| `user` | string | No | Override user ID |
|
||||||
| `team` | string | No | Override team ID |
|
| `team` | string | No | Override team ID |
|
||||||
|
| `options` | Options | No | Context options (see below) |
|
||||||
| `timeout` | string | No | Override timeout (e.g., "30s") |
|
| `timeout` | string | No | Override timeout (e.g., "30s") |
|
||||||
| `skip` | bool | No | Skip this test |
|
| `skip` | bool | No | Skip this test |
|
||||||
| `metadata` | map | No | Additional metadata |
|
| `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
|
### Input Types
|
||||||
|
|
||||||
| Type | Description | Example |
|
| Type | Description | Example |
|
||||||
|
|
@ -225,9 +273,28 @@ return { pass: true, message: "Validation passed" };
|
||||||
### JSON Path Notes
|
### JSON Path Notes
|
||||||
|
|
||||||
- Supports dot-notation: `$.field.subfield` or `field.subfield`
|
- 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 ... ``` `)
|
- Auto-extracts JSON from markdown code blocks (` ```json ... ``` `)
|
||||||
- Works with both string output and structured objects
|
- 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
|
## Output Formats
|
||||||
|
|
||||||
Determined by `-o` file extension:
|
Determined by `-o` file extension:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
|
@ -244,7 +245,7 @@ func (a *Asserter) assertJSONPath(assertion *Assertion, output interface{}) *Ass
|
||||||
jsonData = v
|
jsonData = v
|
||||||
default:
|
default:
|
||||||
result.Passed = false
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,7 +254,18 @@ func (a *Asserter) assertJSONPath(assertion *Assertion, output interface{}) *Ass
|
||||||
actual := a.extractPath(jsonData, path)
|
actual := a.extractPath(jsonData, path)
|
||||||
result.Actual = actual
|
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.Passed = true
|
||||||
result.Message = fmt.Sprintf("path '%s' equals expected value", assertion.Path)
|
result.Message = fmt.Sprintf("path '%s' equals expected value", assertion.Path)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -264,27 +276,113 @@ func (a *Asserter) assertJSONPath(assertion *Assertion, output interface{}) *Ass
|
||||||
return result
|
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{} {
|
func (a *Asserter) extractPath(data interface{}, path string) interface{} {
|
||||||
parts := strings.Split(path, ".")
|
|
||||||
current := data
|
current := data
|
||||||
|
|
||||||
for _, part := range parts {
|
// Parse path into segments, handling both dots and array indices
|
||||||
if part == "" {
|
// e.g., "wheres[0].like" -> ["wheres", "[0]", "like"]
|
||||||
|
segments := parsePathSegments(path)
|
||||||
|
|
||||||
|
for _, segment := range segments {
|
||||||
|
if segment == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
switch v := current.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
current = v[part]
|
current = v[segment]
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return current
|
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
|
// assertRegex checks if output matches a regex pattern
|
||||||
func (a *Asserter) assertRegex(assertion *Assertion, output interface{}) *AssertionResult {
|
func (a *Asserter) assertRegex(assertion *Assertion, output interface{}) *AssertionResult {
|
||||||
result := &AssertionResult{
|
result := &AssertionResult{
|
||||||
|
|
|
||||||
|
|
@ -64,15 +64,8 @@ func (r *Executor) RunDirect() (*Report, error) {
|
||||||
ctx := NewTestContextFromOptions(chatID, agentInfo.ID, r.opts, tc)
|
ctx := NewTestContextFromOptions(chatID, agentInfo.ID, r.opts, tc)
|
||||||
defer ctx.Release()
|
defer ctx.Release()
|
||||||
|
|
||||||
// Set options: skip history (input already contains conversation), connector override
|
// Build context options
|
||||||
opts := &context.Options{
|
opts := buildContextOptions(tc, r.opts)
|
||||||
Skip: &context.Skip{
|
|
||||||
History: true, // Skip history loading - input already contains full conversation
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if r.opts.Connector != "" {
|
|
||||||
opts.Connector = r.opts.Connector
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create timeout context
|
// Create timeout context
|
||||||
timeout := tc.GetTimeout(r.opts.Timeout)
|
timeout := tc.GetTimeout(r.opts.Timeout)
|
||||||
|
|
@ -292,6 +285,7 @@ func (r *Executor) runSingleTest(ast *assistant.Assistant, tc *Case, agentID str
|
||||||
ID: tc.ID,
|
ID: tc.ID,
|
||||||
Input: tc.Input,
|
Input: tc.Input,
|
||||||
Expected: tc.Expected,
|
Expected: tc.Expected,
|
||||||
|
Options: tc.Options,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse input to messages
|
// 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)
|
ctx := NewTestContextFromOptions(chatID, agentID, r.opts, tc)
|
||||||
defer ctx.Release()
|
defer ctx.Release()
|
||||||
|
|
||||||
// Set options: skip history (input already contains conversation), connector override
|
// Build context options from test case and runner options
|
||||||
opts := &context.Options{
|
opts := buildContextOptions(tc, r.opts)
|
||||||
Skip: &context.Skip{
|
|
||||||
History: true, // Skip history loading - input already contains full conversation
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if r.opts.Connector != "" {
|
|
||||||
opts.Connector = r.opts.Connector
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create timeout context
|
// Create timeout context
|
||||||
timeout := tc.GetTimeout(r.opts.Timeout)
|
timeout := tc.GetTimeout(r.opts.Timeout)
|
||||||
|
|
@ -490,25 +477,101 @@ func writeJSONLine(writer *bufio.Writer, data interface{}) error {
|
||||||
return err
|
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
|
// extractOutput extracts the output from the agent response
|
||||||
|
// Priority: Next hook data (if non-empty) > Completion content > raw response
|
||||||
func extractOutput(response interface{}) interface{} {
|
func extractOutput(response interface{}) interface{} {
|
||||||
if response == nil {
|
if response == nil {
|
||||||
return 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 {
|
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 {
|
if resp.Completion != nil {
|
||||||
return resp.Completion.Content
|
return resp.Completion.Content
|
||||||
}
|
}
|
||||||
if resp.Next != nil {
|
|
||||||
return resp.Next
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
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
|
// validateOutput validates the actual output against expected
|
||||||
func validateOutput(actual, expected interface{}) bool {
|
func validateOutput(actual, expected interface{}) bool {
|
||||||
// Simple JSON comparison
|
// Simple JSON comparison
|
||||||
|
|
|
||||||
|
|
@ -205,8 +205,13 @@ type Case struct {
|
||||||
TeamID string `json:"team,omitempty"`
|
TeamID string `json:"team,omitempty"`
|
||||||
|
|
||||||
// Metadata contains additional metadata for the test case
|
// 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"`
|
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 indicates whether to skip this test case
|
||||||
Skip bool `json:"skip,omitempty"`
|
Skip bool `json:"skip,omitempty"`
|
||||||
|
|
||||||
|
|
@ -215,6 +220,38 @@ type Case struct {
|
||||||
Timeout string `json:"timeout,omitempty"`
|
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
|
// Assertion represents a single assertion rule
|
||||||
type Assertion struct {
|
type Assertion struct {
|
||||||
// Type is the assertion type:
|
// Type is the assertion type:
|
||||||
|
|
@ -334,6 +371,9 @@ type Result struct {
|
||||||
// Error contains the error message if status is failed/error/timeout
|
// Error contains the error message if status is failed/error/timeout
|
||||||
Error string `json:"error,omitempty"`
|
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 contains additional result metadata
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
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",
|
"name": "Query Builder",
|
||||||
"description": "Build database queries",
|
"description": "Build database queries",
|
||||||
|
"connector": "deepseek.v3",
|
||||||
"type": "worker",
|
"type": "worker",
|
||||||
"uses": { "search": "disabled" },
|
"uses": { "search": "disabled" },
|
||||||
"options": {
|
"options": {
|
||||||
"max_tokens": 2000,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.2
|
"temperature": 0.2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,141 @@
|
||||||
# QueryDSL Generator Agent Prompts
|
# QueryDSL Generator Agent - Main Prompt (Default/Basic Queries)
|
||||||
- role: system
|
- role: system
|
||||||
content: |
|
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
|
```json
|
||||||
{
|
{
|
||||||
"select": ["field1", "field2"],
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"from": "table_name",
|
"title": "QueryDSL",
|
||||||
"wheres": [
|
"description": "Gou Query Domain Specific Language for database queries",
|
||||||
{"field": "name", "op": "=", "value": "test"},
|
"type": "object",
|
||||||
{"field": "status", "op": "in", "value": ["active", "pending"]}
|
"definitions": {
|
||||||
],
|
"expression": {
|
||||||
"orders": [
|
"type": "string",
|
||||||
{"field": "created_at", "sort": "desc"}
|
"description": "Field expression. Syntax: field, table.field, :FUNC(args), field as alias"
|
||||||
],
|
},
|
||||||
"limit": 20
|
"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
|
## Condition Format
|
||||||
- Comparison: =, !=, >, >=, <, <=
|
Conditions use operator as JSON key: `{"field": "xxx", "OPERATOR": VALUE}`
|
||||||
- Pattern: like, not like
|
- `"="` : `{"field": "status", "=": "active"}`
|
||||||
- Range: in, not in, between
|
- `">"` : `{"field": "price", ">": 100}`
|
||||||
- Null check: is null, is not null
|
- `">="` : `{"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
|
## Response Format
|
||||||
Always respond with valid JSON:
|
Output JSON only. No markdown, no explanation.
|
||||||
```json
|
|
||||||
{
|
### Success Response
|
||||||
"dsl": { ... },
|
Return QueryDSL directly:
|
||||||
"explain": "Brief explanation of the query",
|
{"select": [...], "from": "table", "wheres": [...], "limit": 20}
|
||||||
"warnings": ["any warnings or notes"]
|
|
||||||
}
|
### 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
|
## Guidelines
|
||||||
- Generate valid QueryDSL based on the provided schema
|
1. Only use fields from the provided schema (use column.name)
|
||||||
- Use appropriate operators for the query intent
|
2. Default limit to 20 if not specified
|
||||||
- Include only fields that exist in the schema
|
3. Return error JSON if input is insufficient
|
||||||
- Add helpful explanations for complex queries
|
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