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:
Max 2025-12-18 11:03:55 +08:00
parent b8c5829eb0
commit 4abc7e41ef
15 changed files with 1511 additions and 227 deletions

View file

@ -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

View file

@ -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

View file

@ -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)
} }

View file

@ -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:

View file

@ -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
} }
switch v := current.(type) { // Check if this is an array index like "[0]"
case map[string]interface{}: if strings.HasPrefix(segment, "[") && strings.HasSuffix(segment, "]") {
current = v[part] indexStr := segment[1 : len(segment)-1]
default: index, err := strconv.Atoi(indexStr)
return nil 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 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{

View file

@ -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

View file

@ -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"`
} }

File diff suppressed because one or more lines are too long

View file

@ -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
} }
} }

View file

@ -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}

View 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}

View 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}

View 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}

View 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}

View 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,
},
};
}