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 {
result["disable_global_prompts"] = opts.DisableGlobalPrompts
}
if opts.Metadata != nil {
result["metadata"] = opts.Metadata
}
// Note: Runtime fields (Context, Writer) are not serialized (json:"-")
// They should not be included in the map
@ -66,6 +69,9 @@ func OptionsFromMap(m map[string]interface{}) *Options {
if disableGlobalPrompts, ok := m["disable_global_prompts"].(bool); ok {
opts.DisableGlobalPrompts = disableGlobalPrompts
}
if metadata, ok := m["metadata"].(map[string]interface{}); ok {
opts.Metadata = metadata
}
// Note: Context and Writer are runtime fields, not restored from map
// They should be set by the caller if needed

View file

@ -289,6 +289,9 @@ type Options struct {
// Agent mode, use to select the mode of the request, default is "chat"
Mode string `json:"mode,omitempty"` // Agent mode, use to select the mode of the request, default is "chat"
// Metadata for passing custom data to hooks (e.g., scenario selection)
Metadata map[string]any `json:"metadata,omitempty"` // Custom metadata passed to Create/Next hooks
}
// Stack represents the call stack node for tracing agent-to-agent calls

View file

@ -637,11 +637,25 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
errorDetected := false
// Wrap streamHandler to detect JSON error responses
// Note: API error responses are raw JSON without "data: " prefix
// Normal SSE data always starts with "data: " prefix
wrappedHandler := func(data []byte) int {
dataStr := string(data)
trimmed := strings.TrimSpace(dataStr)
// Detect if this looks like a JSON error response (starts with "{" or contains "error")
if strings.Contains(dataStr, `"error"`) || (strings.TrimSpace(dataStr) == "{" && !errorDetected) {
// Skip empty lines
if trimmed == "" {
return http.HandlerReturnOk
}
// Normal SSE data starts with "data: " - pass to streamHandler
if strings.HasPrefix(dataStr, "data: ") {
return streamHandler(data)
}
// Detect if this looks like a JSON error response (raw JSON without "data: " prefix)
// API errors are returned as raw JSON: {"error": {...}}
if strings.HasPrefix(trimmed, "{") && strings.Contains(dataStr, `"error"`) {
errorDetected = true
}
@ -652,7 +666,7 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
return http.HandlerReturnOk
}
// Otherwise, use normal handler
// Unknown format, pass to streamHandler (it will skip non-SSE data)
return streamHandler(data)
}

View file

@ -110,10 +110,58 @@ Each line is a JSON object:
| `expected` | any | No | Expected output (exact match) |
| `user` | string | No | Override user ID |
| `team` | string | No | Override team ID |
| `options` | Options | No | Context options (see below) |
| `timeout` | string | No | Override timeout (e.g., "30s") |
| `skip` | bool | No | Skip this test |
| `metadata` | map | No | Additional metadata |
### Options
The `options` field allows per-test-case configuration that maps to `context.Options`:
| Field | Type | Description |
| ------------------------ | ------- | -------------------------------------------------- |
| `connector` | string | Override connector (e.g., `"deepseek.v3"`) |
| `mode` | string | Agent mode (default: `"chat"`) |
| `search` | bool | Enable/disable search mode (default: `true`) |
| `disable_global_prompts` | bool | Temporarily disable global prompts |
| `metadata` | map | Custom data passed to hooks (e.g., scenario) |
| `skip` | object | Skip configuration (see below) |
#### Options.skip
| Field | Type | Description |
| --------- | ---- | ------------------------ |
| `history` | bool | Skip history loading |
| `trace` | bool | Skip trace logging |
| `output` | bool | Skip output to client |
| `keyword` | bool | Skip keyword extraction |
| `search` | bool | Skip auto search |
**Example with options:**
```jsonl
{
"id": "T001",
"input": "Query users with status active",
"options": {
"connector": "deepseek.v3",
"metadata": {"scenario": "filter"},
"skip": {"trace": true}
},
"assert": {"type": "json_path", "path": "from", "value": "users"}
}
```
**Using metadata for hook scenarios:**
The `options.metadata` field is passed to agent hooks. For example, a Create Hook can read `options.metadata.scenario` to select different prompt presets:
```jsonl
{"id": "T001", "input": "...", "options": {"metadata": {"scenario": "aggregation"}}}
{"id": "T002", "input": "...", "options": {"metadata": {"scenario": "join"}}}
```
### Input Types
| Type | Description | Example |
@ -225,9 +273,28 @@ return { pass: true, message: "Validation passed" };
### JSON Path Notes
- Supports dot-notation: `$.field.subfield` or `field.subfield`
- Supports array indexing: `field[0]`, `field[0].subfield`, `field[0].nested[1]`
- Supports multiple expected values (OR logic): `"value": ["a", "b"]` - passes if actual matches any
- Auto-extracts JSON from markdown code blocks (` ```json ... ``` `)
- Works with both string output and structured objects
**Array index examples:**
```jsonl
{"id": "T001", "assert": {"type": "json_path", "path": "wheres[0].like", "value": "%test%"}}
{"id": "T002", "assert": {"type": "json_path", "path": "wheres[0].in[0]", "value": "pending"}}
{"id": "T003", "assert": {"type": "json_path", "path": "joins[0].from", "value": "users"}}
{"id": "T004", "assert": {"type": "json_path", "path": "groups[0]", "value": "category"}}
```
**Multiple expected values (OR logic):**
```jsonl
{"id": "T005", "assert": {"type": "json_path", "path": "error", "value": ["missing_schema", "missing_query"]}}
```
This passes if `error` equals either `"missing_schema"` or `"missing_query"`.
## Output Formats
Determined by `-o` file extension:

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
jsoniter "github.com/json-iterator/go"
@ -244,7 +245,7 @@ func (a *Asserter) assertJSONPath(assertion *Assertion, output interface{}) *Ass
jsonData = v
default:
result.Passed = false
result.Message = "output is not a JSON object or array"
result.Message = fmt.Sprintf("output is not a JSON object or array, got: %T = %v", output, truncateOutput(output, 200))
return result
}
@ -253,7 +254,18 @@ func (a *Asserter) assertJSONPath(assertion *Assertion, output interface{}) *Ass
actual := a.extractPath(jsonData, path)
result.Actual = actual
if validateOutput(actual, assertion.Value) {
// Support array of expected values (OR logic - any match passes)
if expectedArr, ok := assertion.Value.([]interface{}); ok && len(expectedArr) > 0 {
for _, expected := range expectedArr {
if validateOutput(actual, expected) {
result.Passed = true
result.Message = fmt.Sprintf("path '%s' equals one of expected values", assertion.Path)
return result
}
}
result.Passed = false
result.Message = fmt.Sprintf("path '%s': expected one of %v, got %v", assertion.Path, assertion.Value, actual)
} else if validateOutput(actual, assertion.Value) {
result.Passed = true
result.Message = fmt.Sprintf("path '%s' equals expected value", assertion.Path)
} else {
@ -264,27 +276,113 @@ func (a *Asserter) assertJSONPath(assertion *Assertion, output interface{}) *Ass
return result
}
// extractPath extracts a value from JSON using a simple dot-notation path
// truncateOutput truncates output for error messages
func truncateOutput(output interface{}, maxLen int) string {
var s string
switch v := output.(type) {
case string:
s = v
case nil:
return "<nil>"
default:
bytes, err := jsoniter.Marshal(v)
if err != nil {
s = fmt.Sprintf("%v", v)
} else {
s = string(bytes)
}
}
if len(s) > maxLen {
return s[:maxLen] + "..."
}
return s
}
// extractPath extracts a value from JSON using dot-notation path with array index support
// Supports: "field", "field.nested", "field[0]", "field[0].nested", "field.nested[0].value"
func (a *Asserter) extractPath(data interface{}, path string) interface{} {
parts := strings.Split(path, ".")
current := data
for _, part := range parts {
if part == "" {
// Parse path into segments, handling both dots and array indices
// e.g., "wheres[0].like" -> ["wheres", "[0]", "like"]
segments := parsePathSegments(path)
for _, segment := range segments {
if segment == "" {
continue
}
switch v := current.(type) {
case map[string]interface{}:
current = v[part]
default:
return nil
// Check if this is an array index like "[0]"
if strings.HasPrefix(segment, "[") && strings.HasSuffix(segment, "]") {
indexStr := segment[1 : len(segment)-1]
index, err := strconv.Atoi(indexStr)
if err != nil {
return nil
}
arr, ok := current.([]interface{})
if !ok {
return nil
}
if index < 0 || index >= len(arr) {
return nil
}
current = arr[index]
} else {
// Regular field access
switch v := current.(type) {
case map[string]interface{}:
current = v[segment]
default:
return nil
}
}
}
return current
}
// parsePathSegments splits a path like "wheres[0].like" into ["wheres", "[0]", "like"]
func parsePathSegments(path string) []string {
var segments []string
var current strings.Builder
for i := 0; i < len(path); i++ {
ch := path[i]
switch ch {
case '.':
if current.Len() > 0 {
segments = append(segments, current.String())
current.Reset()
}
case '[':
if current.Len() > 0 {
segments = append(segments, current.String())
current.Reset()
}
// Find the closing bracket
j := i + 1
for j < len(path) && path[j] != ']' {
j++
}
if j < len(path) {
segments = append(segments, path[i:j+1]) // Include "[" and "]"
i = j
}
default:
current.WriteByte(ch)
}
}
if current.Len() > 0 {
segments = append(segments, current.String())
}
return segments
}
// assertRegex checks if output matches a regex pattern
func (a *Asserter) assertRegex(assertion *Assertion, output interface{}) *AssertionResult {
result := &AssertionResult{

View file

@ -64,15 +64,8 @@ func (r *Executor) RunDirect() (*Report, error) {
ctx := NewTestContextFromOptions(chatID, agentInfo.ID, r.opts, tc)
defer ctx.Release()
// Set options: skip history (input already contains conversation), connector override
opts := &context.Options{
Skip: &context.Skip{
History: true, // Skip history loading - input already contains full conversation
},
}
if r.opts.Connector != "" {
opts.Connector = r.opts.Connector
}
// Build context options
opts := buildContextOptions(tc, r.opts)
// Create timeout context
timeout := tc.GetTimeout(r.opts.Timeout)
@ -292,6 +285,7 @@ func (r *Executor) runSingleTest(ast *assistant.Assistant, tc *Case, agentID str
ID: tc.ID,
Input: tc.Input,
Expected: tc.Expected,
Options: tc.Options,
}
// Parse input to messages
@ -310,15 +304,8 @@ func (r *Executor) runSingleTest(ast *assistant.Assistant, tc *Case, agentID str
ctx := NewTestContextFromOptions(chatID, agentID, r.opts, tc)
defer ctx.Release()
// Set options: skip history (input already contains conversation), connector override
opts := &context.Options{
Skip: &context.Skip{
History: true, // Skip history loading - input already contains full conversation
},
}
if r.opts.Connector != "" {
opts.Connector = r.opts.Connector
}
// Build context options from test case and runner options
opts := buildContextOptions(tc, r.opts)
// Create timeout context
timeout := tc.GetTimeout(r.opts.Timeout)
@ -490,25 +477,101 @@ func writeJSONLine(writer *bufio.Writer, data interface{}) error {
return err
}
// buildContextOptions builds context.Options from test case and runner options
// Priority: test case options > runner options > defaults
func buildContextOptions(tc *Case, runnerOpts *Options) *context.Options {
opts := &context.Options{
Skip: &context.Skip{
History: true, // Default: skip history loading - input already contains full conversation
},
}
// Apply test case options if specified
if tc.Options != nil {
// Connector: test case > runner
if tc.Options.Connector != "" {
opts.Connector = tc.Options.Connector
}
// Mode
if tc.Options.Mode != "" {
opts.Mode = tc.Options.Mode
}
// DisableGlobalPrompts
if tc.Options.DisableGlobalPrompts {
opts.DisableGlobalPrompts = true
}
// Search (pointer to distinguish unset from false)
if tc.Options.Search != nil {
opts.Search = tc.Options.Search
}
// Metadata for hooks
if tc.Options.Metadata != nil {
opts.Metadata = tc.Options.Metadata
}
// Skip options from test case
if tc.Options.Skip != nil {
opts.Skip.Trace = tc.Options.Skip.Trace
opts.Skip.Output = tc.Options.Skip.Output
opts.Skip.Keyword = tc.Options.Skip.Keyword
opts.Skip.Search = tc.Options.Skip.Search
// Note: History defaults to true for tests
}
}
// Runner connector override (highest priority)
if runnerOpts != nil && runnerOpts.Connector != "" {
opts.Connector = runnerOpts.Connector
}
return opts
}
// extractOutput extracts the output from the agent response
// Priority: Next hook data (if non-empty) > Completion content > raw response
func extractOutput(response interface{}) interface{} {
if response == nil {
return nil
}
// Try to get completion content from context.Response
// Try to get data from context.Response
if resp, ok := response.(*context.Response); ok {
// Prefer Next hook data if available and non-empty
// resp.Next is already the Data value (not NextHookResponse struct)
if resp.Next != nil && !isEmptyValue(resp.Next) {
return resp.Next
}
// Fall back to raw completion content
if resp.Completion != nil {
return resp.Completion.Content
}
if resp.Next != nil {
return resp.Next
}
}
return response
}
// isEmptyValue checks if a value is considered "empty" for output purposes
func isEmptyValue(v interface{}) bool {
if v == nil {
return true
}
switch val := v.(type) {
case string:
return val == ""
case map[string]interface{}:
return len(val) == 0
case []interface{}:
return len(val) == 0
}
return false
}
// validateOutput validates the actual output against expected
func validateOutput(actual, expected interface{}) bool {
// Simple JSON comparison

View file

@ -205,8 +205,13 @@ type Case struct {
TeamID string `json:"team,omitempty"`
// Metadata contains additional metadata for the test case
// This is passed to ctx.Metadata and can be used by Create Hook
Metadata map[string]interface{} `json:"metadata,omitempty"`
// Options contains context options for this test case
// Supports: connector, skip (history, trace, output, keyword, search), mode
Options *CaseOptions `json:"options,omitempty"`
// Skip indicates whether to skip this test case
Skip bool `json:"skip,omitempty"`
@ -215,6 +220,38 @@ type Case struct {
Timeout string `json:"timeout,omitempty"`
}
// CaseOptions represents per-test-case context options
// Maps to context.Options fields
type CaseOptions struct {
// Connector overrides the agent's default connector
Connector string `json:"connector,omitempty"`
// Skip configuration
Skip *CaseSkipOptions `json:"skip,omitempty"`
// DisableGlobalPrompts temporarily disables global prompts for this request
DisableGlobalPrompts bool `json:"disable_global_prompts,omitempty"`
// Search mode, default is true (use pointer to distinguish unset from false)
Search *bool `json:"search,omitempty"`
// Mode is the agent mode (default: "chat")
Mode string `json:"mode,omitempty"`
// Metadata for passing custom data to hooks (e.g., scenario selection)
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// CaseSkipOptions represents skip configuration for a test case
// Maps to context.Skip fields
type CaseSkipOptions struct {
History bool `json:"history,omitempty"` // Skip history loading
Trace bool `json:"trace,omitempty"` // Skip trace logging
Output bool `json:"output,omitempty"` // Skip output to client
Keyword bool `json:"keyword,omitempty"` // Skip keyword extraction
Search bool `json:"search,omitempty"` // Skip auto search
}
// Assertion represents a single assertion rule
type Assertion struct {
// Type is the assertion type:
@ -334,6 +371,9 @@ type Result struct {
// Error contains the error message if status is failed/error/timeout
Error string `json:"error,omitempty"`
// Options contains the context options used for this test case
Options *CaseOptions `json:"options,omitempty"`
// Metadata contains additional result metadata
Metadata map[string]interface{} `json:"metadata,omitempty"`
}

File diff suppressed because one or more lines are too long

View file

@ -1,10 +1,11 @@
{
"name": "Query Builder",
"description": "Build database queries",
"connector": "deepseek.v3",
"type": "worker",
"uses": { "search": "disabled" },
"options": {
"max_tokens": 2000,
"max_tokens": 8192,
"temperature": 0.2
}
}

View file

@ -1,43 +1,141 @@
# QueryDSL Generator Agent Prompts
# QueryDSL Generator Agent - Main Prompt (Default/Basic Queries)
- role: system
content: |
You are a QueryDSL generator. Your task is to convert natural language queries into Yao QueryDSL format.
You are a QueryDSL generator. Convert natural language queries into Yao QueryDSL JSON format.
## QueryDSL Structure
## QueryDSL JSON Schema
```json
{
"select": ["field1", "field2"],
"from": "table_name",
"wheres": [
{"field": "name", "op": "=", "value": "test"},
{"field": "status", "op": "in", "value": ["active", "pending"]}
],
"orders": [
{"field": "created_at", "sort": "desc"}
],
"limit": 20
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QueryDSL",
"description": "Gou Query Domain Specific Language for database queries",
"type": "object",
"definitions": {
"expression": {
"type": "string",
"description": "Field expression. Syntax: field, table.field, :FUNC(args), field as alias"
},
"condition": {
"type": "object",
"description": "Query condition",
"properties": {
"field": { "type": "string" },
"op": { "type": "string", "description": "=, >, >=, <, <=, <>, like, match, in, is" },
"value": { "description": "Compare value" },
"or": { "type": "boolean", "default": false },
"=": { "description": "Shorthand for op='='" },
">": {}, ">=": {}, "<": {}, "<=": {}, "<>": {},
"like": { "description": "Shorthand for op='like'" },
"in": { "type": "array", "description": "Shorthand for op='in'" },
"is": { "type": "string", "enum": ["null", "not null"] }
}
},
"where": {
"allOf": [
{ "$ref": "#/definitions/condition" },
{ "properties": { "wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } } } }
]
},
"order": {
"oneOf": [
{ "type": "string", "description": "'field desc', 'field asc'" },
{ "type": "object", "properties": { "field": {}, "sort": { "enum": ["asc", "desc"] } } }
]
},
"group": {
"oneOf": [
{ "type": "string", "description": "'field', 'field rollup 合计'" },
{ "type": "object", "properties": { "field": {}, "rollup": { "type": "string" } } }
]
},
"join": {
"type": "object",
"properties": {
"from": { "description": "Table to join" },
"key": { "description": "Join key field" },
"foreign": { "description": "Foreign key field" },
"left": { "type": "boolean" },
"right": { "type": "boolean" }
},
"required": ["from", "key", "foreign"]
}
},
"properties": {
"select": { "type": "array", "items": { "$ref": "#/definitions/expression" } },
"from": { "type": "string", "description": "Table name" },
"wheres": { "type": "array", "items": { "$ref": "#/definitions/where" } },
"orders": { "description": "ORDER BY" },
"groups": { "description": "GROUP BY" },
"havings": { "type": "array", "description": "HAVING conditions" },
"joins": { "type": "array", "items": { "$ref": "#/definitions/join" } },
"limit": { "type": "integer", "description": "Max records" },
"offset": { "type": "integer", "description": "Skip records" },
"page": { "type": "integer", "description": "Page number (1-based)" },
"pagesize": { "type": "integer", "description": "Records per page" },
"first": { "description": "Return first record(s)" }
}
}
```
## Supported Operators
- Comparison: =, !=, >, >=, <, <=
- Pattern: like, not like
- Range: in, not in, between
- Null check: is null, is not null
## Condition Format
Conditions use operator as JSON key: `{"field": "xxx", "OPERATOR": VALUE}`
- `"="` : `{"field": "status", "=": "active"}`
- `">"` : `{"field": "price", ">": 100}`
- `">="` : `{"field": "age", ">=": 18}`
- `"like"` : `{"field": "name", "like": "%test%"}`
## Basic Examples
Input: "查询所有用户"
Schema:
```json
{"name": "users", "columns": [
{"name": "id", "type": "ID", "label": "ID"},
{"name": "name", "type": "string", "label": "姓名"},
{"name": "email", "type": "string", "label": "邮箱"},
{"name": "status", "type": "string", "label": "状态"}
]}
```
Output:
{"select": ["id", "name", "email", "status"], "from": "users", "limit": 20}
Input: "Find active users sorted by name"
Schema:
```json
{"name": "users", "columns": [
{"name": "id", "type": "ID", "label": "ID"},
{"name": "name", "type": "string", "label": "Name"},
{"name": "status", "type": "string", "label": "Status"}
]}
```
Output:
{"select": ["id", "name", "status"], "from": "users", "wheres": [{"field": "status", "=": "active"}], "orders": ["name asc"], "limit": 20}
## Response Format
Always respond with valid JSON:
```json
{
"dsl": { ... },
"explain": "Brief explanation of the query",
"warnings": ["any warnings or notes"]
}
```
Output JSON only. No markdown, no explanation.
### Success Response
Return QueryDSL directly:
{"select": [...], "from": "table", "wheres": [...], "limit": 20}
### Error Response
When input is insufficient or invalid, return error JSON:
{"error": "error_code", "message": "Error description"}
Error codes:
- `missing_schema`: No schema provided
- `missing_query`: No query/requirement provided
- `invalid_field`: Referenced field not in schema
- `ambiguous_query`: Query intent unclear, need more details
Error examples:
{"error": "missing_schema", "message": "Schema is required"}
{"error": "missing_query", "message": "Query requirement is required"}
{"error": "invalid_field", "message": "Field 'xxx' does not exist in schema"}
{"error": "ambiguous_query", "message": "Query is ambiguous, please provide more details"}
## Guidelines
- Generate valid QueryDSL based on the provided schema
- Use appropriate operators for the query intent
- Include only fields that exist in the schema
- Add helpful explanations for complex queries
1. Only use fields from the provided schema (use column.name)
2. Default limit to 20 if not specified
3. Return error JSON if input is insufficient
4. IMPORTANT: Verify your JSON syntax before output. Ensure all key-value pairs use colon (:), e.g. {"field": "price", ">": 100} NOT {"field": "price", ">", 100}

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