diff --git a/agent/context/log.go b/agent/context/log.go index 1f555bc6..16658e42 100644 --- a/agent/context/log.go +++ b/agent/context/log.go @@ -378,6 +378,10 @@ func (l *RequestLogger) LLMStart(connector, model string, messageCount int) { // LLMComplete logs the completion of an LLM call func (l *RequestLogger) LLMComplete(tokens int, hasToolCalls bool) { + if l.noop { + return + } + elapsed := time.Since(l.startTime).Round(time.Millisecond) status := "streaming" if hasToolCalls { @@ -397,6 +401,10 @@ func (l *RequestLogger) LLMComplete(tokens int, hasToolCalls bool) { // ToolStart logs the start of tool execution func (l *RequestLogger) ToolStart(toolName string) { + if l.noop { + return + } + if config.IsDevelopment() { fmt.Printf("%s 🔧 Tool: %s%s\n", colorYellow, toolName, colorReset) } else { @@ -406,6 +414,10 @@ func (l *RequestLogger) ToolStart(toolName string) { // ToolComplete logs the completion of tool execution func (l *RequestLogger) ToolComplete(toolName string, success bool) { + if l.noop { + return + } + if config.IsDevelopment() { if success { fmt.Printf("%s ✓ %s completed%s\n", colorGreen, toolName, colorReset) @@ -423,6 +435,10 @@ func (l *RequestLogger) ToolComplete(toolName string, success bool) { // HookStart logs the start of a hook execution func (l *RequestLogger) HookStart(hookName string) { + if l.noop { + return + } + elapsed := time.Since(l.startTime).Round(time.Millisecond) if config.IsDevelopment() { @@ -434,6 +450,10 @@ func (l *RequestLogger) HookStart(hookName string) { // HookComplete logs the completion of a hook func (l *RequestLogger) HookComplete(hookName string) { + if l.noop { + return + } + if config.IsDevelopment() { fmt.Printf("%s ✓ %s done%s\n", colorGreen, hookName, colorReset) } else { @@ -456,6 +476,10 @@ func (l *RequestLogger) Cleanup(resource string) { // HistoryLoad logs history loading func (l *RequestLogger) HistoryLoad(count, maxSize int) { + if l.noop { + return + } + if config.IsDevelopment() { fmt.Printf("%s Loaded %d/%d history messages%s\n", colorGray, count, maxSize, colorReset) } else { @@ -465,6 +489,10 @@ func (l *RequestLogger) HistoryLoad(count, maxSize int) { // HistoryOverlap logs overlap detection func (l *RequestLogger) HistoryOverlap(overlapCount int) { + if l.noop { + return + } + if overlapCount > 0 { if config.IsDevelopment() { fmt.Printf("%s Removed %d overlapping messages%s\n", colorYellow, overlapCount, colorReset) diff --git a/agent/test/DESIGN.md b/agent/test/DESIGN.md new file mode 100644 index 00000000..5af29a12 --- /dev/null +++ b/agent/test/DESIGN.md @@ -0,0 +1,787 @@ +# Agent Test Package Design + +## Overview + +Agent Test Package provides a framework for testing AI agents with structured test cases. +It supports batch testing, report generation, stability analysis, and CI integration. + +### Quick Start + +```bash +# Quick test with a single message (auto-detect agent from current directory) +cd assistants/keyword +yao agent test -i "hello world" + +# Or specify agent explicitly +yao agent test -i "hello world" -n keyword.agent + +# Run tests from JSONL file (auto-detect agent from path) +yao agent test -i assistants/keyword/tests/inputs.jsonl + +# Run with stability analysis (5 runs per test case) +yao agent test -i assistants/keyword/tests/inputs.jsonl --runs 5 + +# Generate HTML report +yao agent test -i assistants/keyword/tests/inputs.jsonl -r report.html -o report.html +``` + +## Usage + +```bash +# Basic usage - auto-detect agent, output to same directory as input +# Output: tests/output-20241217100000.jsonl +yao agent test -i tests/inputs.jsonl + +# Override connector +yao agent test -i tests/inputs.jsonl -c openai.gpt4 + +# Specify agent explicitly +yao agent test -i tests/inputs.jsonl -n my.agent + +# Specify test environment (user and team) +yao agent test -i tests/inputs.jsonl -u test-user -t test-team + +# Run multiple times for stability analysis +yao agent test -i tests/inputs.jsonl --runs 5 + +# Custom timeout per test case (default: 5m) +yao agent test -i tests/inputs.jsonl --timeout 10m + +# Run tests in parallel (4 concurrent test cases) +yao agent test -i tests/inputs.jsonl --parallel 4 + +# Combine parallel and timeout for faster execution +yao agent test -i tests/inputs.jsonl --parallel 8 --timeout 2m + +# Custom output file path +yao agent test -i tests/inputs.jsonl -o /path/to/results.jsonl + +# Use custom reporter agent for personalized report (HTML) +yao agent test -i tests/inputs.jsonl -r report.html -o report.html + +# Use custom reporter agent for personalized report (Markdown) +yao agent test -i tests/inputs.jsonl -r report.markdown -o report.md + +# Full example with all options +yao agent test -i tests/inputs.jsonl \ + -n keyword.agent \ + -c deepseek.v3 \ + -u test-user \ + -t test-team \ + --runs 3 \ + --timeout 10m \ + --parallel 4 \ + -r report.html \ + -o report.html +``` + +### Input Modes + +The `-i` flag supports two input modes: + +**1. JSONL File Mode** - Load test cases from a file: + +```bash +yao agent test -i tests/inputs.jsonl +``` + +**2. Direct Message Mode** - Test with a single message: + +```bash +# Auto-detect agent from current working directory +cd assistants/keyword +yao agent test -i "Extract keywords from this text" + +# Or specify agent explicitly +yao agent test -i "Extract keywords from this text" -n keyword.agent +yao agent test -i "你好世界" -n keyword.agent -c deepseek.v3 +``` + +When using direct message mode: + +- Agent is resolved from current working directory (looks for `package.yao` upward) +- If not found, use `-n` flag to specify agent explicitly +- Output is printed to stdout (or saved to `-o` if specified) +- Useful for quick testing and debugging + +### Default Output Path + +When `-o` is not specified and using JSONL file mode, the output file is automatically generated in the same directory as the input file: + +``` +{input_directory}/output-{timestamp}.jsonl +``` + +Example: + +- Input: `/app/assistants/keyword/tests/inputs.jsonl` +- Output: `/app/assistants/keyword/tests/output-20241217100000.jsonl` + +The timestamp format is `YYYYMMDDHHMMSS` (e.g., `20241217100000` for 2024-12-17 10:00:00). + +When using direct message mode without `-o`, output is printed to stdout. + +## Command Line Options + +| Flag | Long Flag | Description | Default | Example | +| ---- | ------------- | ------------------------------------- | -------------------------- | --------------------------------------- | +| `-i` | `--input` | Input: JSONL file path or message | - | `-i tests/inputs.jsonl` or `-i "hello"` | +| `-o` | `--output` | Path to output file (format by ext) | `output-{timestamp}.jsonl` | `-o report.html` | +| `-n` | `--name` | Explicit agent ID | auto-detect | `-n keyword.agent` | +| `-c` | `--connector` | Override connector | agent default | `-c openai.gpt4` | +| `-u` | `--user` | Test user ID (global override) | "test-user" | `-u admin` | +| `-t` | `--team` | Test team ID (global override) | "test-team" | `-t ops-team` | +| `-r` | `--reporter` | Custom reporter agent ID | - (use built-in) | `-r report.beautiful` | +| | `--runs` | Number of runs for stability analysis | 1 | `--runs 5` | +| | `--timeout` | Default timeout per test case | 5m | `--timeout 10m` | +| | `--parallel` | Number of parallel test cases | 1 | `--parallel 4` | +| `-v` | `--verbose` | Verbose output | false | `-v` | +| | `--fail-fast` | Stop on first failure | false | `--fail-fast` | + +**Notes**: + +- Without `-o` flag, output is saved to `{input_dir}/output-{timestamp}.jsonl` +- Output format is determined by `-o` file extension: `.jsonl`, `.json`, `.md`, `.html` +- Use `-r` to specify a custom reporter agent for personalized report generation + +## Agent Resolution + +The agent is resolved in the following order: + +1. **Explicit specification** (`-n` flag): Use the specified agent ID +2. **Path-based detection**: Traverse up from `tests/inputs.jsonl` to find `package.yao` + +### Path-based Detection Example + +``` +/app/assistants/workers/system/keyword/ +├── package.yao <- Agent definition +├── prompts.yml +├── src/ +│ └── index.ts +└── tests/ + └── inputs.jsonl <- Test input file +``` + +Given input path `/app/assistants/workers/system/keyword/tests/inputs.jsonl`: + +1. Check `/app/assistants/workers/system/keyword/tests/package.yao` - not found +2. Check `/app/assistants/workers/system/keyword/package.yao` - **found!** +3. Load agent from `/app/assistants/workers/system/keyword/` + +## Test Environment + +Agent calls require a `Context` with user and tenant information. The test framework creates a test context with configurable environment: + +```go +// TestEnvironment configures the test execution context +type TestEnvironment struct { + UserID string // User ID for authorized info (-u flag) + TeamID string // Team ID for authorized info (-t flag) + Locale string // Locale (default: "en-us") + ClientType string // Client type (default: "test") + ClientIP string // Client IP (default: "127.0.0.1") + Referer string // Request referer (default: "test") + Accept string // Accept format (default: "standard") +} +``` + +Example context creation (similar to `agent_next_test.go`): + +```go +func newTestContext(env *TestEnvironment, chatID, assistantID string) *context.Context { + authorized := &types.AuthorizedInfo{ + Subject: env.UserID, + UserID: env.UserID, + TeamID: env.TeamID, + } + ctx := context.New(stdContext.Background(), authorized, chatID) + ctx.AssistantID = assistantID + ctx.Locale = env.Locale + ctx.Client = context.Client{ + Type: env.ClientType, + IP: env.ClientIP, + } + ctx.Referer = env.Referer + ctx.Accept = env.Accept + return ctx +} +``` + +## Stability Analysis (Multiple Runs) + +When `--runs N` is specified (N > 1), the framework runs each test case N times and collects stability metrics: + +### Stability Metrics + +| Metric | Description | +| ------------------ | ------------------------------------------ | +| `pass_rate` | Percentage of runs that passed (0-100%) | +| `consistency` | How consistent the outputs are across runs | +| `avg_duration_ms` | Average execution time | +| `min_duration_ms` | Minimum execution time | +| `max_duration_ms` | Maximum execution time | +| `std_deviation_ms` | Standard deviation of execution time | + +### Stability Report Structure + +```json +{ + "summary": { + "total_cases": 42, + "total_runs": 126, + "runs_per_case": 3, + "overall_pass_rate": 95.2, + "stable_cases": 38, + "unstable_cases": 4, + "duration_ms": 45678 + }, + "results": [ + { + "id": "T001", + "runs": 3, + "passed": 3, + "failed": 0, + "pass_rate": 100.0, + "consistency": 1.0, + "stable": true, + "avg_duration_ms": 234, + "min_duration_ms": 210, + "max_duration_ms": 256, + "std_deviation_ms": 18.5, + "run_details": [ + {"run": 1, "status": "passed", "duration_ms": 234, "output": {...}}, + {"run": 2, "status": "passed", "duration_ms": 210, "output": {...}}, + {"run": 3, "status": "passed", "duration_ms": 256, "output": {...}} + ] + }, + { + "id": "T002", + "runs": 3, + "passed": 2, + "failed": 1, + "pass_rate": 66.7, + "consistency": 0.67, + "stable": false, + "run_details": [...] + } + ] +} +``` + +### Stability Classification + +| Pass Rate | Classification | +| --------- | --------------- | +| 100% | Stable | +| 80-99% | Mostly Stable | +| 50-79% | Unstable | +| < 50% | Highly Unstable | + +## Custom Reporter Agent + +By default, the framework outputs JSONL format. You can specify a reporter agent (`-r` flag) for personalized report generation: + +### Reporter Agent Interface + +The reporter agent receives the test results and generates a custom report: + +```json +// Input to reporter agent +{ + "report": { + "summary": {...}, + "results": [...], + "metadata": {...} + }, + "format": "html", // or "markdown", "text" + "options": { + "verbose": true, + "include_outputs": true + } +} +``` + +### Built-in Reporter Agents + +| Agent ID | Description | +| ----------------- | -------------------------------------- | +| `report.json` | JSON format (default, no agent needed) | +| `report.markdown` | Markdown format with tables | +| `report.html` | Interactive HTML report | +| `report.summary` | Brief text summary | + +### Custom Reporter Example + +Create a custom reporter agent at `assistants/reporters/my-reporter/`: + +```yaml +# prompts.yml +- role: system + content: | + You are a test report generator. Generate a beautiful report from test results. + + Output format: HTML with embedded CSS + + Requirements: + - Show summary statistics prominently + - Use color coding (green=pass, red=fail) + - Include charts for stability metrics + - Make it printable +``` + +## Input Format (JSONL) + +Each line in the input file is a JSON object with the following structure: + +```jsonl +{"id": "T001", "input": "Simple text input"} +{"id": "T002", "input": {"role": "user", "content": "Message with role"}} +{"id": "T003", "input": {"role": "user", "content": [{"type": "text", "text": "ContentPart array"}]}} +{"id": "T004", "input": [{"role": "user", "content": "First message"}, {"role": "assistant", "content": "Response"}, {"role": "user", "content": "Follow-up"}]} +{"id": "T005", "input": "Text input", "expected": {"keywords": ["keyword1", "keyword2"]}} +{"id": "T006", "input": "Test with specific user", "user": "admin", "team": "ops-team"} +``` + +### Input Types + +| Type | Description | Example | +| ----------- | ------------------------ | ----------------------------------------------------- | +| `string` | Simple text input | `"Hello world"` | +| `Message` | Single message with role | `{"role": "user", "content": "..."}` | +| `[]Message` | Conversation history | `[{"role": "user", ...}, {"role": "assistant", ...}]` | + +### Fields + +| Field | Type | Required | Description | +| ---------- | ------------------------------ | -------- | ---------------------------------------------------- | +| `id` | string | Yes | Unique test case identifier (e.g., "T001") | +| `input` | string \| Message \| []Message | Yes | Test input | +| `expected` | any | No | Expected output for exact match validation | +| `assert` | Assertion \| []Assertion | No | Custom assertion rules (see Assertions section) | +| `user` | string | No | User ID for this test case (overridden by `-u` flag) | +| `team` | string | No | Team ID for this test case (overridden by `-t` flag) | +| `metadata` | map | No | Additional metadata for the test case | +| `skip` | bool | No | Skip this test case | +| `timeout` | string | No | Override timeout (e.g., "30s", "1m") | + +### Assertions + +The `assert` field allows flexible validation of agent output. If `assert` is defined, it takes precedence over `expected`. + +#### Assertion Types + +| Type | Description | Example | +| -------------- | ----------------------------------------------- | ---------------------------------------------------------------- | +| `equals` | Exact match (default if only `expected` is set) | `{"type": "equals", "value": {"need_search": false}}` | +| `contains` | Output contains the expected string/value | `{"type": "contains", "value": "keyword"}` | +| `not_contains` | Output does not contain the string/value | `{"type": "not_contains", "value": "error"}` | +| `json_path` | Extract value using JSON path and compare | `{"type": "json_path", "path": "$.need_search", "value": false}` | +| `regex` | Match output against regex pattern | `{"type": "regex", "value": "\\d{3}-\\d{4}"}` | +| `type` | Check output type (string, object, array, etc.) | `{"type": "type", "value": "object"}` | +| `script` | Run a custom assertion script | `{"type": "script", "script": "scripts.test.Assert"}` | + +#### Assertion Structure + +```typescript +interface Assertion { + type: string; // Assertion type (required) + value?: any; // Expected value or pattern + path?: string; // JSON path for json_path assertions + script?: string; // Script name for script assertions + message?: string; // Custom failure message + negate?: boolean; // Invert the assertion result +} +``` + +#### Examples + +**Simple contains check:** + +```jsonl +{ + "id": "T001", + "input": "Hello", + "assert": { + "type": "contains", + "value": "need_search" + } +} +``` + +**JSON path validation (for agents returning JSON):** + +```jsonl +{ + "id": "T002", + "input": "What's the weather?", + "assert": { + "type": "json_path", + "path": "$.need_search", + "value": true + } +} +``` + +**Multiple assertions (all must pass):** + +```jsonl +{ + "id": "T003", + "input": "Calculate 2+2", + "assert": [ + { + "type": "json_path", + "path": "$.need_search", + "value": false + }, + { + "type": "json_path", + "path": "$.confidence", + "value": 0.99 + }, + { + "type": "not_contains", + "value": "error" + } + ] +} +``` + +**Custom script assertion:** + +```jsonl +{ + "id": "T004", + "input": "Complex test", + "assert": { + "type": "script", + "script": "scripts.test.ValidateOutput" + } +} +``` + +The script receives `(output, input, expected)` and should return: + +```typescript +// Simple boolean +return true; // or false + +// Or detailed result +return { + pass: true, + message: "Validation passed: output contains expected keywords", +}; +``` + +**Negated assertion:** + +```jsonl +{ + "id": "T005", + "input": "Hello", + "assert": { + "type": "contains", + "value": "error", + "negate": true + } +} +``` + +#### JSON Path Notes + +- Supports simple dot-notation paths: `$.field.subfield` or `field.subfield` +- Automatically extracts JSON from markdown code blocks (e.g., ` ```json ... ``` `) +- Works with both string output and structured objects + +### Environment Override Priority + +The test environment (user/team) is determined by the following priority (highest first): + +1. **Command line flags** (`-u`, `-t`): Global override for all test cases +2. **Test case fields** (`user`, `team`): Per-test case configuration +3. **Default values**: "test-user", "test-team" + +Example: + +```bash +# All tests run as "admin" user in "prod-team", regardless of test case settings +yao agent test -i tests/inputs.jsonl -u admin -t prod-team -o report.json +``` + +```jsonl +# T001 uses default user/team +{"id": "T001", "input": "Hello"} + +# T002 uses specific user/team (unless overridden by -u/-t flags) +{"id": "T002", "input": "Admin action", "user": "admin", "team": "admin-team"} + +# T003 uses specific user only, team uses default +{"id": "T003", "input": "User specific test", "user": "special-user"} +``` + +## Output Format + +### Default: JSONL (without `-r` flag) + +By default (without `-r` flag), the output is JSONL format - one JSON object per line, suitable for streaming and CI integration: + +```jsonl +{"type": "start", "timestamp": "2024-12-17T10:00:00Z", "agent_id": "keyword", "total_cases": 42} +{"type": "result", "id": "T001", "status": "passed", "duration_ms": 234, "output": {"keywords": ["AI", "ML"]}} +{"type": "result", "id": "T002", "status": "passed", "duration_ms": 189, "output": {"keywords": ["cloud"]}} +{"type": "result", "id": "T003", "status": "failed", "duration_ms": 0, "error": "timeout after 30s"} +{"type": "summary", "total": 42, "passed": 40, "failed": 2, "duration_ms": 12345} +``` + +This format is: + +- **Streamable**: Results are output as they complete +- **Parseable**: Each line is valid JSON, easy to process with `jq` or scripts +- **CI-friendly**: Exit code indicates pass/fail status + +### Custom Report (with `-r` flag) + +```json +{ + "summary": { + "total": 42, + "passed": 40, + "failed": 2, + "skipped": 0, + "duration_ms": 12345, + "agent_id": "keyword", + "connector": "deepseek.v3", + "runs_per_case": 1, + "overall_pass_rate": 95.2 + }, + "environment": { + "user_id": "test-user", + "team_id": "test-team", + "locale": "en-us" + }, + "results": [ + { + "id": "T001", + "status": "passed", + "input": "...", + "output": { "keywords": ["AI", "machine learning"] }, + "expected": null, + "duration_ms": 234, + "error": null + } + ], + "metadata": { + "started_at": "2024-12-17T10:00:00Z", + "completed_at": "2024-12-17T10:00:12Z", + "version": "0.10.5" + } +} +``` + +### HTML Report + +Beautiful, interactive HTML report with: + +- Summary statistics (pass/fail/skip counts, duration) +- Stability charts (when runs > 1) +- Filterable test results table +- Expandable input/output details +- Error highlighting +- Export options + +### Markdown Report + +```markdown +# Agent Test Report + +## Summary + +| Metric | Value | +| --------- | ----------- | +| Agent | keyword | +| Connector | deepseek.v3 | +| Total | 42 | +| Passed | 40 | +| Failed | 2 | +| Pass Rate | 95.2% | +| Duration | 12.3s | + +## Environment + +| Setting | Value | +| ------- | --------- | +| User | test-user | +| Team | test-team | +| Locale | en-us | + +## Results + +### ✅ T001 - Passed (234ms) + +... +``` + +## Architecture + +``` +agent/test/ +├── DESIGN.md # This file +├── types.go # Core types and interfaces +├── interfaces.go # Runner and Reporter interfaces +├── runner.go # Test runner implementation +├── loader.go # Test case loader +├── resolver.go # Agent resolver +├── environment.go # Test environment setup +├── stability.go # Stability analysis +└── reporter/ + ├── json.go # JSON reporter + ├── html.go # HTML reporter + ├── markdown.go # Markdown reporter + └── agent.go # Agent-based custom reporter +``` + +## Core Components + +### 1. TestCase + +Represents a single test case loaded from JSONL. + +### 2. TestResult + +Represents the result of running a single test case. + +### 3. TestReport + +Represents the complete test report with summary and results. + +### 4. Runner + +Executes test cases against an agent: + +- Loads test cases from JSONL +- Resolves agent from path or explicit ID +- Creates test context with environment +- Executes each test case (optionally multiple runs) +- Collects results and stability metrics + +### 5. Reporter + +Generates reports in various formats. The format is determined by the `-o` file extension: + +| Extension | Format | Description | +| --------- | -------- | -------------------------- | +| `.jsonl` | JSONL | Streaming, line-by-line | +| `.json` | JSON | Full structured report | +| `.md` | Markdown | Human-readable with tables | +| `.html` | HTML | Interactive web report | + +#### Custom Reporter Agent (`-r` flag) + +When `-r ` is specified, the framework calls the specified agent to generate the report: + +1. Test execution completes, `TestReport` is generated +2. Framework calls the reporter agent with input: + ```json + { + "report": { + /* TestReport object */ + }, + "format": "html", + "options": { "verbose": true } + } + ``` +3. Agent processes the report and returns formatted content +4. Framework writes the returned content to the output file + +Example usage: + +```bash +# Use custom reporter agent to generate a beautiful HTML report +yao agent test -i tests/inputs.jsonl -r report.beautiful -o report.html + +# Use custom reporter agent to generate Slack-formatted summary +yao agent test -i tests/inputs.jsonl -r report.slack -o summary.txt +``` + +This allows for fully customizable report generation using AI agents + +## Configuration + +### Test Options + +```go +type Options struct { + // Input/Output + InputFile string // Path to inputs.jsonl + OutputFile string // Path to output report + + // Agent Selection + AgentID string // Explicit agent ID (optional) + Connector string // Override connector (optional) + + // Test Environment + UserID string // Test user ID (-u flag) + TeamID string // Test team ID (-t flag) + Locale string // Locale (default: "en-us") + + // Execution + Timeout time.Duration // Default timeout per test + Parallel int // Number of parallel tests (default: 1) + Runs int // Number of runs per test case (default: 1) + + // Reporting + ReporterID string // Reporter agent ID for custom report + + // Behavior + Verbose bool // Verbose output + FailFast bool // Stop on first failure +} +``` + +## Exit Codes + +| Code | Description | +| ---- | ------------------- | +| 0 | All tests passed | +| 1 | Some tests failed | +| 2 | Configuration error | +| 3 | Runtime error | + +## CI Integration + +### GitHub Actions Example + +```yaml +- name: Run Agent Tests + run: | + yao agent test -i assistants/keyword/tests/inputs.jsonl \ + -u ci-user -t ci-team \ + --runs 3 \ + -o report.json + +- name: Check Stability + run: | + # Fail if any test has pass rate below 80% + jq -e '.results | all(.pass_rate >= 80)' report.json + +- name: Upload Test Report + uses: actions/upload-artifact@v3 + with: + name: agent-test-report + path: report.json +``` + +### Exit Code Handling + +The command exits with code 1 if any tests fail, making it easy to integrate with CI pipelines. + +## Future Enhancements + +1. **Snapshot Testing**: Compare outputs against saved snapshots +2. **Fuzzing**: Generate random inputs for robustness testing +3. **Coverage**: Track which agent code paths are exercised +4. **Benchmarking**: Performance metrics and regression detection +5. **Diff Reports**: Compare results between runs +6. **Flaky Test Detection**: Automatic identification of unstable tests +7. **Test Prioritization**: Run most important/failing tests first diff --git a/agent/test/README.md b/agent/test/README.md new file mode 100644 index 00000000..daf76222 --- /dev/null +++ b/agent/test/README.md @@ -0,0 +1,381 @@ +# Agent Test Framework + +A testing framework for Yao AI agents with support for assertions, stability analysis, and CI integration. + +## Quick Start + +```bash +# Test with direct message (auto-detect agent from current directory) +cd assistants/keyword +yao agent test -i "Extract keywords from: AI and machine learning" + +# Test with direct message (specify agent explicitly) +yao agent test -i "Hello world" -n workers.system.keyword + +# Test with JSONL file (auto-detect agent from path) +yao agent test -i assistants/keyword/tests/inputs.jsonl + +# Generate HTML report +yao agent test -i tests/inputs.jsonl -o report.html + +# Stability analysis (run each test 5 times) +yao agent test -i tests/inputs.jsonl --runs 5 +``` + +## Input Modes + +The `-i` flag supports two input modes: + +### 1. JSONL File Mode + +Load test cases from a file: + +```bash +yao agent test -i tests/inputs.jsonl +``` + +Agent is auto-detected by traversing up from the input file to find `package.yao`. + +### 2. Direct Message Mode + +Test with a single message: + +```bash +# Auto-detect agent from current working directory +cd assistants/keyword +yao agent test -i "Extract keywords from this text" + +# Or specify agent explicitly +yao agent test -i "Hello" -n workers.system.keyword +``` + +Output is printed to stdout (or saved to `-o` if specified). + +## Command Line Options + +| Flag | Description | Default | +| ------------- | ---------------------------------------- | -------------------------- | +| `-i` | Input: JSONL file path or direct message | (required) | +| `-o` | Output file path | `output-{timestamp}.jsonl` | +| `-n` | Agent ID (optional, auto-detected) | auto-detect | +| `-c` | Override connector | agent default | +| `-u` | Test user ID | `test-user` | +| `-t` | Test team ID | `test-team` | +| `-r` | Reporter agent ID | built-in | +| `--runs` | Runs per test (stability analysis) | 1 | +| `--timeout` | Timeout per test | 5m | +| `--parallel` | Parallel test cases | 1 | +| `-v` | Verbose output | false | +| `--fail-fast` | Stop on first failure | false | + +## Agent Resolution + +The agent is resolved in the following priority order: + +1. **Explicit `-n` flag**: `yao agent test -i "msg" -n my.agent` +2. **Path-based detection**: Traverse up from input file to find `package.yao` +3. **Current directory**: For direct message mode, look for `package.yao` in cwd + +Example directory structure: + +``` +assistants/workers/system/keyword/ +├── package.yao <- Agent definition (auto-detected) +├── prompts.yml +├── src/ +│ └── index.ts +└── tests/ + └── inputs.jsonl <- Input file +``` + +## Input Format (JSONL) + +Each line is a JSON object: + +```jsonl +{"id": "T001", "input": "Simple text"} +{"id": "T002", "input": {"role": "user", "content": "Message with role"}} +{"id": "T003", "input": [{"role": "user", "content": "Hi"}, {"role": "assistant", "content": "Hello"}, {"role": "user", "content": "Follow-up"}]} +{"id": "T004", "input": "Test", "assert": {"type": "json_path", "path": "field", "value": true}} +{"id": "T005", "input": "Skip this", "skip": true} +``` + +### Fields + +| Field | Type | Required | Description | +| ---------- | ------------------------------ | -------- | ------------------------------ | +| `id` | string | Yes | Test case ID | +| `input` | string \| Message \| []Message | Yes | Test input | +| `assert` | Assertion \| []Assertion | No | Assertion rules | +| `expected` | any | No | Expected output (exact match) | +| `user` | string | No | Override user ID | +| `team` | string | No | Override team ID | +| `timeout` | string | No | Override timeout (e.g., "30s") | +| `skip` | bool | No | Skip this test | +| `metadata` | map | No | Additional metadata | + +### Input Types + +| Type | Description | Example | +| ----------- | -------------------- | ----------------------------------------------------- | +| `string` | Simple text | `"Hello world"` | +| `Message` | Single message | `{"role": "user", "content": "..."}` | +| `[]Message` | Conversation history | `[{"role": "user", ...}, {"role": "assistant", ...}]` | + +## Assertions + +Use `assert` for flexible validation. If `assert` is defined, it takes precedence over `expected`. + +### Assertion Types + +| Type | Description | Example | +| -------------- | ----------------------------- | --------------------------------------------------------- | +| `equals` | Exact match | `{"type": "equals", "value": {"key": "val"}}` | +| `contains` | Output contains value | `{"type": "contains", "value": "keyword"}` | +| `not_contains` | Output does not contain value | `{"type": "not_contains", "value": "error"}` | +| `json_path` | Extract JSON path and compare | `{"type": "json_path", "path": "$.field", "value": true}` | +| `regex` | Match regex pattern | `{"type": "regex", "value": "\\d+"}` | +| `type` | Check output type | `{"type": "type", "value": "object"}` | +| `script` | Run custom assertion script | `{"type": "script", "script": "scripts.test.Check"}` | + +### Assertion Options + +| Field | Type | Description | +| --------- | ------ | --------------------------- | +| `type` | string | Assertion type (required) | +| `value` | any | Expected value or pattern | +| `path` | string | JSON path (for `json_path`) | +| `script` | string | Script name (for `script`) | +| `message` | string | Custom failure message | +| `negate` | bool | Invert the result | + +### Examples + +**JSON path validation:** + +```jsonl +{ + "id": "T001", + "input": "What's the weather?", + "assert": { + "type": "json_path", + "path": "need_search", + "value": true + } +} +``` + +**Multiple assertions (all must pass):** + +```jsonl +{ + "id": "T002", + "input": "Hello", + "assert": [ + { + "type": "json_path", + "path": "need_search", + "value": false + }, + { + "type": "not_contains", + "value": "error" + } + ] +} +``` + +**Custom script assertion:** + +```jsonl +{ + "id": "T003", + "input": "Test", + "assert": { + "type": "script", + "script": "scripts.test.Validate" + } +} +``` + +Script receives `(output, input, expected)` and returns: + +```javascript +// Boolean +return true; + +// Or detailed result +return { pass: true, message: "Validation passed" }; +``` + +**Negated assertion:** + +```jsonl +{ + "id": "T004", + "input": "Hello", + "assert": { + "type": "contains", + "value": "error", + "negate": true + } +} +``` + +### JSON Path Notes + +- Supports dot-notation: `$.field.subfield` or `field.subfield` +- Auto-extracts JSON from markdown code blocks (` ```json ... ``` `) +- Works with both string output and structured objects + +## Output Formats + +Determined by `-o` file extension: + +| Extension | Format | Description | +| --------- | -------- | ---------------------- | +| `.jsonl` | JSONL | Streaming (default) | +| `.json` | JSON | Complete structured | +| `.md` | Markdown | Human-readable | +| `.html` | HTML | Interactive web report | + +### Default Output Path + +When `-o` is not specified in file mode: + +``` +{input_directory}/output-{timestamp}.jsonl +``` + +Example: `tests/output-20241217100000.jsonl` + +In direct message mode without `-o`, output is printed to stdout. + +## Stability Analysis + +Run each test multiple times to measure consistency: + +```bash +yao agent test -i tests/inputs.jsonl --runs 5 -o stability.json +``` + +Output includes: + +- Pass rate per test +- Stability classification (stable, mostly_stable, unstable, highly_unstable) +- Average/min/max duration +- Standard deviation + +### Stability Classification + +| Pass Rate | Classification | +| --------- | --------------- | +| 100% | Stable | +| 80-99% | Mostly Stable | +| 50-79% | Unstable | +| < 50% | Highly Unstable | + +## Test Environment + +The test framework creates a context with configurable environment: + +| Setting | Flag | Default | +| ---------- | ---- | ----------- | +| User ID | `-u` | `test-user` | +| Team ID | `-t` | `test-team` | +| Locale | - | `en-us` | +| ClientType | - | `test` | +| ClientIP | - | `127.0.0.1` | + +Priority: Command line flags > Test case fields > Defaults + +## Custom Reporter Agent + +Use `-r` to specify a custom agent for report generation: + +```bash +yao agent test -i tests/inputs.jsonl -r report.beautiful -o report.html +``` + +The reporter agent receives: + +```json +{ + "report": { "summary": {...}, "results": [...] }, + "format": "html", + "options": { "verbose": true } +} +``` + +## CI Integration + +```bash +# Exit code: 0 = all passed, 1 = failures +yao agent test -i tests/inputs.jsonl -o results.jsonl --fail-fast + +# Parse JSONL results +cat results.jsonl | jq 'select(.type == "summary")' +``` + +### GitHub Actions Example + +```yaml +- name: Run Agent Tests + run: | + yao agent test -i assistants/keyword/tests/inputs.jsonl \ + -u ci-user -t ci-team \ + --runs 3 \ + -o report.json + +- name: Check Stability + run: | + jq -e '.results | all(.pass_rate >= 80)' report.json +``` + +## Examples + +```bash +# Quick development test (auto-detect agent) +cd assistants/keyword +yao agent test -i "Extract keywords: AI and ML" + +# Quick development test (specify agent) +yao agent test -i "Hello" -n workers.system.keyword + +# Full test suite with HTML report +yao agent test -i tests/inputs.jsonl -o report.html -v + +# Override connector +yao agent test -i tests/inputs.jsonl -c openai.gpt4 + +# Stability analysis +yao agent test -i tests/inputs.jsonl --runs 10 -o stability.json + +# Parallel execution with timeout +yao agent test -i tests/inputs.jsonl --parallel 4 --timeout 2m + +# Custom test environment +yao agent test -i tests/inputs.jsonl -u admin -t prod-team + +# Custom reporter agent +yao agent test -i tests/inputs.jsonl -r report.beautiful -o custom-report.md + +# Full example with all options +yao agent test -i tests/inputs.jsonl \ + -n keyword.agent \ + -c deepseek.v3 \ + -u test-user \ + -t test-team \ + --runs 3 \ + --timeout 10m \ + --parallel 4 \ + -r report.html \ + -o report.html +``` + +## Exit Codes + +| Code | Description | +| ---- | --------------------------------------------------- | +| 0 | All tests passed | +| 1 | Tests failed, configuration error, or runtime error | diff --git a/agent/test/assert.go b/agent/test/assert.go new file mode 100644 index 00000000..fd486368 --- /dev/null +++ b/agent/test/assert.go @@ -0,0 +1,480 @@ +package test + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou/process" +) + +// Asserter handles test assertions +type Asserter struct{} + +// NewAsserter creates a new asserter +func NewAsserter() *Asserter { + return &Asserter{} +} + +// Validate validates the output against the test case's assertions +// Returns (passed, error message) +func (a *Asserter) Validate(tc *Case, output interface{}) (bool, string) { + // If assert is defined, use assertion rules + if tc.Assert != nil { + return a.validateAssertions(tc, output) + } + + // If expected is defined, use simple comparison + if tc.Expected != nil { + if validateOutput(output, tc.Expected) { + return true, "" + } + return false, "output does not match expected" + } + + // No assertions defined - pass if we got output without error + return true, "" +} + +// validateAssertions validates output against assertion rules +func (a *Asserter) validateAssertions(tc *Case, output interface{}) (bool, string) { + assertions := a.parseAssertions(tc.Assert) + if len(assertions) == 0 { + return true, "" + } + + var failures []string + for _, assertion := range assertions { + result := a.evaluateAssertion(assertion, output, tc.Input) + if !result.Passed { + msg := result.Message + if assertion.Message != "" { + msg = assertion.Message + } + failures = append(failures, msg) + } + } + + if len(failures) > 0 { + return false, strings.Join(failures, "; ") + } + return true, "" +} + +// parseAssertions parses the assert field into a list of assertions +func (a *Asserter) parseAssertions(assert interface{}) []*Assertion { + if assert == nil { + return nil + } + + var assertions []*Assertion + + switch v := assert.(type) { + case map[string]interface{}: + // Single assertion object + assertion := a.mapToAssertion(v) + if assertion != nil { + assertions = append(assertions, assertion) + } + + case []interface{}: + // Array of assertions + for _, item := range v { + if m, ok := item.(map[string]interface{}); ok { + assertion := a.mapToAssertion(m) + if assertion != nil { + assertions = append(assertions, assertion) + } + } + } + + case string: + // Shorthand: just a type name (e.g., "contains") + assertions = append(assertions, &Assertion{Type: v}) + } + + return assertions +} + +// mapToAssertion converts a map to an Assertion +func (a *Asserter) mapToAssertion(m map[string]interface{}) *Assertion { + assertion := &Assertion{} + + if t, ok := m["type"].(string); ok { + assertion.Type = t + } + if v, ok := m["value"]; ok { + assertion.Value = v + } + if p, ok := m["path"].(string); ok { + assertion.Path = p + } + if s, ok := m["script"].(string); ok { + assertion.Script = s + } + if msg, ok := m["message"].(string); ok { + assertion.Message = msg + } + if n, ok := m["negate"].(bool); ok { + assertion.Negate = n + } + + return assertion +} + +// evaluateAssertion evaluates a single assertion +func (a *Asserter) evaluateAssertion(assertion *Assertion, output, input interface{}) *AssertionResult { + result := &AssertionResult{ + Assertion: assertion, + Expected: assertion.Value, + } + + switch assertion.Type { + case "equals", "": + result = a.assertEquals(assertion, output) + case "contains": + result = a.assertContains(assertion, output) + case "not_contains": + result = a.assertNotContains(assertion, output) + case "json_path": + result = a.assertJSONPath(assertion, output) + case "regex": + result = a.assertRegex(assertion, output) + case "type": + result = a.assertType(assertion, output) + case "script": + result = a.assertScript(assertion, output, input) + default: + result.Passed = false + result.Message = fmt.Sprintf("unknown assertion type: %s", assertion.Type) + } + + // Apply negate + if assertion.Negate { + result.Passed = !result.Passed + if result.Passed { + result.Message = "negated assertion passed" + } else { + result.Message = "negated: " + result.Message + } + } + + return result +} + +// assertEquals checks for exact equality +func (a *Asserter) assertEquals(assertion *Assertion, output interface{}) *AssertionResult { + result := &AssertionResult{ + Assertion: assertion, + Actual: output, + Expected: assertion.Value, + } + + if validateOutput(output, assertion.Value) { + result.Passed = true + result.Message = "values are equal" + } else { + result.Passed = false + result.Message = fmt.Sprintf("expected %v, got %v", assertion.Value, output) + } + + return result +} + +// assertContains checks if output contains the expected value +func (a *Asserter) assertContains(assertion *Assertion, output interface{}) *AssertionResult { + result := &AssertionResult{ + Assertion: assertion, + Actual: output, + Expected: assertion.Value, + } + + outputStr := a.toString(output) + expectedStr := a.toString(assertion.Value) + + if strings.Contains(outputStr, expectedStr) { + result.Passed = true + result.Message = fmt.Sprintf("output contains '%s'", expectedStr) + } else { + result.Passed = false + result.Message = fmt.Sprintf("output does not contain '%s'", expectedStr) + } + + return result +} + +// assertNotContains checks if output does not contain the expected value +func (a *Asserter) assertNotContains(assertion *Assertion, output interface{}) *AssertionResult { + result := a.assertContains(assertion, output) + result.Passed = !result.Passed + if result.Passed { + result.Message = fmt.Sprintf("output does not contain '%s'", a.toString(assertion.Value)) + } else { + result.Message = fmt.Sprintf("output should not contain '%s'", a.toString(assertion.Value)) + } + return result +} + +// assertJSONPath extracts a value using JSON path and compares +func (a *Asserter) assertJSONPath(assertion *Assertion, output interface{}) *AssertionResult { + result := &AssertionResult{ + Assertion: assertion, + Expected: assertion.Value, + } + + // Convert output to JSON if needed + var jsonData interface{} + switch v := output.(type) { + case string: + // Try to parse as JSON + if err := jsoniter.Unmarshal([]byte(v), &jsonData); err != nil { + // Try to extract JSON from markdown code blocks + extracted := extractJSONFromText(v) + if extracted != nil { + jsonData = extracted + } else { + result.Passed = false + result.Message = fmt.Sprintf("output is not valid JSON: %s", err.Error()) + return result + } + } + case map[string]interface{}, []interface{}: + jsonData = v + default: + result.Passed = false + result.Message = "output is not a JSON object or array" + return result + } + + // Extract value using simple path (e.g., "$.need_search" or "need_search") + path := strings.TrimPrefix(assertion.Path, "$.") + actual := a.extractPath(jsonData, path) + result.Actual = actual + + if validateOutput(actual, assertion.Value) { + result.Passed = true + result.Message = fmt.Sprintf("path '%s' equals expected value", assertion.Path) + } else { + result.Passed = false + result.Message = fmt.Sprintf("path '%s': expected %v, got %v", assertion.Path, assertion.Value, actual) + } + + return result +} + +// extractPath extracts a value from JSON using a simple dot-notation path +func (a *Asserter) extractPath(data interface{}, path string) interface{} { + parts := strings.Split(path, ".") + current := data + + for _, part := range parts { + if part == "" { + continue + } + + switch v := current.(type) { + case map[string]interface{}: + current = v[part] + default: + return nil + } + } + + return current +} + +// assertRegex checks if output matches a regex pattern +func (a *Asserter) assertRegex(assertion *Assertion, output interface{}) *AssertionResult { + result := &AssertionResult{ + Assertion: assertion, + Actual: output, + Expected: assertion.Value, + } + + pattern, ok := assertion.Value.(string) + if !ok { + result.Passed = false + result.Message = "regex pattern must be a string" + return result + } + + re, err := regexp.Compile(pattern) + if err != nil { + result.Passed = false + result.Message = fmt.Sprintf("invalid regex pattern: %s", err.Error()) + return result + } + + outputStr := a.toString(output) + if re.MatchString(outputStr) { + result.Passed = true + result.Message = fmt.Sprintf("output matches pattern '%s'", pattern) + } else { + result.Passed = false + result.Message = fmt.Sprintf("output does not match pattern '%s'", pattern) + } + + return result +} + +// assertType checks the type of the output +func (a *Asserter) assertType(assertion *Assertion, output interface{}) *AssertionResult { + result := &AssertionResult{ + Assertion: assertion, + Actual: output, + Expected: assertion.Value, + } + + expectedType, ok := assertion.Value.(string) + if !ok { + result.Passed = false + result.Message = "type assertion value must be a string" + return result + } + + actualType := a.getType(output) + result.Actual = actualType + + if actualType == expectedType { + result.Passed = true + result.Message = fmt.Sprintf("output is of type '%s'", expectedType) + } else { + result.Passed = false + result.Message = fmt.Sprintf("expected type '%s', got '%s'", expectedType, actualType) + } + + return result +} + +// getType returns the type name of a value +func (a *Asserter) getType(v interface{}) string { + if v == nil { + return "null" + } + + switch v.(type) { + case string: + return "string" + case float64, float32, int, int64, int32: + return "number" + case bool: + return "boolean" + case []interface{}: + return "array" + case map[string]interface{}: + return "object" + default: + return fmt.Sprintf("%T", v) + } +} + +// assertScript runs a custom assertion script +func (a *Asserter) assertScript(assertion *Assertion, output, input interface{}) *AssertionResult { + result := &AssertionResult{ + Assertion: assertion, + Actual: output, + } + + if assertion.Script == "" { + result.Passed = false + result.Message = "script assertion requires a script name" + return result + } + + // Build script arguments + args := []interface{}{ + output, + input, + assertion.Value, + } + + // Run the script as a process + p, err := process.Of(assertion.Script, args...) + if err != nil { + result.Passed = false + result.Message = fmt.Sprintf("failed to create process: %s", err.Error()) + return result + } + + res, err := p.Exec() + if err != nil { + result.Passed = false + result.Message = fmt.Sprintf("script execution failed: %s", err.Error()) + return result + } + + // Parse script result + // Expected format: { "pass": bool, "message": string } + switch v := res.(type) { + case bool: + result.Passed = v + if v { + result.Message = "script assertion passed" + } else { + result.Message = "script assertion failed" + } + + case map[string]interface{}: + if pass, ok := v["pass"].(bool); ok { + result.Passed = pass + } + if msg, ok := v["message"].(string); ok { + result.Message = msg + } + + default: + result.Passed = false + result.Message = fmt.Sprintf("script returned unexpected type: %T", res) + } + + return result +} + +// toString converts a value to string for comparison +func (a *Asserter) toString(v interface{}) string { + if v == nil { + return "" + } + + switch val := v.(type) { + case string: + return val + case []byte: + return string(val) + default: + b, err := json.Marshal(v) + if err != nil { + return fmt.Sprintf("%v", v) + } + return string(b) + } +} + +// extractJSONFromText tries to extract JSON from text (e.g., markdown code blocks) +func extractJSONFromText(text string) interface{} { + // Try to find JSON in code blocks + patterns := []string{ + "```json\n", + "```\n", + } + + for _, start := range patterns { + if idx := strings.Index(text, start); idx >= 0 { + text = text[idx+len(start):] + if endIdx := strings.Index(text, "```"); endIdx >= 0 { + text = text[:endIdx] + } + break + } + } + + // Try to parse + var result interface{} + if err := jsoniter.Unmarshal([]byte(strings.TrimSpace(text)), &result); err == nil { + return result + } + + return nil +} diff --git a/agent/test/context.go b/agent/test/context.go new file mode 100644 index 00000000..244e7bd5 --- /dev/null +++ b/agent/test/context.go @@ -0,0 +1,63 @@ +package test + +import ( + stdContext "context" + + "github.com/yaoapp/yao/agent/context" + "github.com/yaoapp/yao/agent/output/message" + "github.com/yaoapp/yao/openapi/oauth/types" +) + +// NewTestContext creates a new context for testing +// This is similar to newAgentNextTestContext in agent_next_test.go +// but configurable via Environment +func NewTestContext(chatID, assistantID string, env *Environment) *context.Context { + // Build authorized info from environment + authorized := &types.AuthorizedInfo{ + Subject: env.UserID, + UserID: env.UserID, + TenantID: env.TeamID, + } + + // Create context with standard initialization + ctx := context.New(stdContext.Background(), authorized, chatID) + ctx.ID = chatID + ctx.AssistantID = assistantID + ctx.Locale = env.Locale + ctx.Client = context.Client{ + Type: env.ClientType, + UserAgent: "yao-agent-test/1.0", + IP: env.ClientIP, + } + ctx.Referer = env.Referer + ctx.Accept = context.AcceptStandard + ctx.IDGenerator = message.NewIDGenerator() + ctx.Metadata = make(map[string]interface{}) + + // Initialize interrupt controller + ctx.Interrupt = context.NewInterruptController() + + // Close the default logger created by context.New() and use noop logger + // to suppress LLM debug output during tests + if ctx.Logger != nil { + ctx.Logger.Close() + } + ctx.Logger = context.Noop() + + return ctx +} + +// NewTestContextFromOptions creates a test context from test options and test case +func NewTestContextFromOptions(chatID, assistantID string, opts *Options, tc *Case) *context.Context { + // Get environment from test case (with options override) + env := tc.GetEnvironment(opts) + return NewTestContext(chatID, assistantID, env) +} + +// GenerateChatID generates a unique chat ID for testing +func GenerateChatID(testID string, runNumber int) string { + if runNumber > 1 { + return "test-" + testID + "-run" + string(rune('0'+runNumber)) + } + return "test-" + testID +} diff --git a/agent/test/input.go b/agent/test/input.go new file mode 100644 index 00000000..32df9768 --- /dev/null +++ b/agent/test/input.go @@ -0,0 +1,230 @@ +package test + +import ( + "fmt" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/yao/agent/context" +) + +// ParseInput converts various input formats to []context.Message +// Supported formats: +// - string: converted to single user message +// - map (Message): single message with role and content +// - []interface{} ([]Message): array of messages (conversation history) +func ParseInput(input interface{}) ([]context.Message, error) { + if input == nil { + return nil, fmt.Errorf("input is nil") + } + + switch v := input.(type) { + case string: + // Simple string input -> single user message + return []context.Message{ + { + Role: context.RoleUser, + Content: v, + }, + }, nil + + case map[string]interface{}: + // Single message object + msg, err := parseMessageMap(v) + if err != nil { + return nil, fmt.Errorf("failed to parse message: %w", err) + } + return []context.Message{*msg}, nil + + case []interface{}: + // Array of messages (conversation history) + messages := make([]context.Message, 0, len(v)) + for i, item := range v { + switch m := item.(type) { + case map[string]interface{}: + msg, err := parseMessageMap(m) + if err != nil { + return nil, fmt.Errorf("failed to parse message at index %d: %w", i, err) + } + messages = append(messages, *msg) + default: + return nil, fmt.Errorf("invalid message type at index %d: expected object, got %T", i, item) + } + } + return messages, nil + + default: + return nil, fmt.Errorf("unsupported input type: %T", input) + } +} + +// parseMessageMap converts a map to context.Message +func parseMessageMap(m map[string]interface{}) (*context.Message, error) { + msg := &context.Message{} + + // Parse role (required) + if role, ok := m["role"].(string); ok { + msg.Role = context.MessageRole(role) + } else { + // Default to user role if not specified + msg.Role = context.RoleUser + } + + // Parse content (required) + if content, ok := m["content"]; ok { + msg.Content = content + } else { + return nil, fmt.Errorf("message missing 'content' field") + } + + // Parse optional name + if name, ok := m["name"].(string); ok { + msg.Name = &name + } + + // Parse optional tool_call_id (for tool messages) + if toolCallID, ok := m["tool_call_id"].(string); ok { + msg.ToolCallID = &toolCallID + } + + // Parse optional tool_calls (for assistant messages) + if toolCalls, ok := m["tool_calls"].([]interface{}); ok { + msg.ToolCalls = make([]context.ToolCall, 0, len(toolCalls)) + for _, tc := range toolCalls { + if tcMap, ok := tc.(map[string]interface{}); ok { + toolCall, err := parseToolCall(tcMap) + if err != nil { + return nil, fmt.Errorf("failed to parse tool_call: %w", err) + } + msg.ToolCalls = append(msg.ToolCalls, *toolCall) + } + } + } + + // Parse optional refusal (for assistant messages) + if refusal, ok := m["refusal"].(string); ok { + msg.Refusal = &refusal + } + + return msg, nil +} + +// parseToolCall converts a map to context.ToolCall +func parseToolCall(m map[string]interface{}) (*context.ToolCall, error) { + tc := &context.ToolCall{} + + if id, ok := m["id"].(string); ok { + tc.ID = id + } + + if typ, ok := m["type"].(string); ok { + tc.Type = context.ToolCallType(typ) + } else { + tc.Type = context.ToolTypeFunction + } + + if fn, ok := m["function"].(map[string]interface{}); ok { + if name, ok := fn["name"].(string); ok { + tc.Function.Name = name + } + if args, ok := fn["arguments"].(string); ok { + tc.Function.Arguments = args + } else if args, ok := fn["arguments"].(map[string]interface{}); ok { + // Convert map to JSON string + argsBytes, err := jsoniter.Marshal(args) + if err != nil { + return nil, fmt.Errorf("failed to marshal arguments: %w", err) + } + tc.Function.Arguments = string(argsBytes) + } + } + + return tc, nil +} + +// ExtractTextContent extracts text content from various content formats +// Used for display in reports +func ExtractTextContent(content interface{}) string { + if content == nil { + return "" + } + + switch v := content.(type) { + case string: + return v + + case []interface{}: + // ContentPart array + var texts []string + for _, part := range v { + if partMap, ok := part.(map[string]interface{}); ok { + if partMap["type"] == "text" { + if text, ok := partMap["text"].(string); ok { + texts = append(texts, text) + } + } + } + } + if len(texts) > 0 { + result := texts[0] + for i := 1; i < len(texts); i++ { + result += "\n" + texts[i] + } + return result + } + return fmt.Sprintf("[%d content parts]", len(v)) + + case map[string]interface{}: + // Single ContentPart or Message + if v["type"] == "text" { + if text, ok := v["text"].(string); ok { + return text + } + } + if content, ok := v["content"]; ok { + return ExtractTextContent(content) + } + return fmt.Sprintf("%v", v) + + default: + return fmt.Sprintf("%v", v) + } +} + +// SummarizeInput creates a short summary of the input for display +func SummarizeInput(input interface{}, maxLen int) string { + text := "" + + switch v := input.(type) { + case string: + text = v + + case map[string]interface{}: + if content, ok := v["content"]; ok { + text = ExtractTextContent(content) + } + + case []interface{}: + // Get the last user message for summary + for i := len(v) - 1; i >= 0; i-- { + if msg, ok := v[i].(map[string]interface{}); ok { + if msg["role"] == "user" { + if content, ok := msg["content"]; ok { + text = ExtractTextContent(content) + break + } + } + } + } + if text == "" && len(v) > 0 { + text = fmt.Sprintf("[%d messages]", len(v)) + } + + default: + text = fmt.Sprintf("%v", v) + } + + if maxLen > 0 && len(text) > maxLen { + return text[:maxLen-3] + "..." + } + return text +} diff --git a/agent/test/interfaces.go b/agent/test/interfaces.go new file mode 100644 index 00000000..a5996010 --- /dev/null +++ b/agent/test/interfaces.go @@ -0,0 +1,125 @@ +package test + +import ( + "context" + "io" +) + +// Runner is the interface for test execution +type Runner interface { + // Run executes all test cases and returns the report + Run(ctx context.Context) (*Report, error) + + // RunCase executes a single test case + RunCase(ctx context.Context, tc *Case) (*Result, error) + + // GetAgentInfo returns information about the agent being tested + GetAgentInfo() *AgentInfo + + // SetProgressCallback sets a callback for progress updates + SetProgressCallback(callback ProgressCallback) +} + +// ProgressCallback is called during test execution to report progress +// Parameters: +// - current: current test index (1-based) +// - total: total number of tests +// - result: result of the current test (nil if not yet completed) +type ProgressCallback func(current, total int, result *Result) + +// Reporter is the interface for generating test reports +type Reporter interface { + // Generate generates a report from the test results + Generate(report *Report) error + + // Write writes the report to the given writer + Write(report *Report, w io.Writer) error +} + +// Loader is the interface for loading test cases +type Loader interface { + // Load loads test cases from the input source + Load() ([]*Case, error) + + // LoadFile loads test cases from a JSONL file + LoadFile(path string) ([]*Case, error) +} + +// Resolver is the interface for resolving agent information +type Resolver interface { + // Resolve resolves the agent from options + // Priority: explicit AgentID > path-based detection + Resolve(opts *Options) (*AgentInfo, error) + + // ResolveFromPath resolves the agent by traversing up from the input file path + ResolveFromPath(inputPath string) (*AgentInfo, error) +} + +// Validator is the interface for validating test outputs +type Validator interface { + // Validate compares actual output against expected output + // Returns nil if validation passes, error otherwise + Validate(actual, expected interface{}) error + + // ValidateJSON validates JSON outputs with flexible comparison + ValidateJSON(actual, expected interface{}) error +} + +// OutputAdapter adapts agent output to a comparable format +type OutputAdapter interface { + // Adapt transforms the raw agent output to a normalized format + Adapt(output interface{}) (interface{}, error) +} + +// RunnerFactory creates Runner instances +type RunnerFactory interface { + // Create creates a new Runner with the given options + Create(opts *Options) (Runner, error) +} + +// ReporterFactory creates Reporter instances +type ReporterFactory interface { + // Create creates a new Reporter for the given format + Create(format OutputFormat) (Reporter, error) + + // CreateFromPath creates a Reporter based on output file extension + CreateFromPath(outputPath string) (Reporter, error) +} + +// Hook allows customization of test execution +type Hook interface { + // BeforeAll is called before any tests run + BeforeAll(ctx context.Context, cases []*Case) error + + // BeforeEach is called before each test case + BeforeEach(ctx context.Context, tc *Case) error + + // AfterEach is called after each test case + AfterEach(ctx context.Context, tc *Case, result *Result) error + + // AfterAll is called after all tests complete + AfterAll(ctx context.Context, report *Report) error +} + +// DefaultHook provides a no-op implementation of Hook +type DefaultHook struct{} + +// BeforeAll implements Hook +func (h *DefaultHook) BeforeAll(ctx context.Context, cases []*Case) error { + return nil +} + +// BeforeEach implements Hook +func (h *DefaultHook) BeforeEach(ctx context.Context, tc *Case) error { + return nil +} + +// AfterEach implements Hook +func (h *DefaultHook) AfterEach(ctx context.Context, tc *Case, result *Result) error { + return nil +} + +// AfterAll implements Hook +func (h *DefaultHook) AfterAll(ctx context.Context, report *Report) error { + return nil +} diff --git a/agent/test/loader.go b/agent/test/loader.go new file mode 100644 index 00000000..c648a617 --- /dev/null +++ b/agent/test/loader.go @@ -0,0 +1,141 @@ +package test + +import ( + "bufio" + "fmt" + "os" + "strings" + "time" + + jsoniter "github.com/json-iterator/go" +) + +// JSONLLoader loads test cases from JSONL files +type JSONLLoader struct{} + +// NewLoader creates a new JSONL loader +func NewLoader() Loader { + return &JSONLLoader{} +} + +// Load loads test cases from the default input source +// This is a placeholder - actual implementation would use configured path +func (l *JSONLLoader) Load() ([]*Case, error) { + return nil, fmt.Errorf("Load() requires explicit path, use LoadFile() instead") +} + +// LoadFile loads test cases from a JSONL file +func (l *JSONLLoader) LoadFile(path string) ([]*Case, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open file %s: %w", path, err) + } + defer file.Close() + + var cases []*Case + scanner := bufio.NewScanner(file) + lineNum := 0 + + // Increase buffer size for long lines + const maxCapacity = 1024 * 1024 // 1MB + buf := make([]byte, maxCapacity) + scanner.Buffer(buf, maxCapacity) + + for scanner.Scan() { + lineNum++ + line := strings.TrimSpace(scanner.Text()) + + // Skip empty lines + if line == "" { + continue + } + + // Skip comments (lines starting with //) + if strings.HasPrefix(line, "//") { + continue + } + + var tc Case + if err := jsoniter.UnmarshalFromString(line, &tc); err != nil { + return nil, fmt.Errorf("failed to parse line %d: %w", lineNum, err) + } + + // Validate required fields + if tc.ID == "" { + return nil, fmt.Errorf("line %d: missing required field 'id'", lineNum) + } + if tc.Input == nil { + return nil, fmt.Errorf("line %d (id=%s): missing required field 'input'", lineNum, tc.ID) + } + + cases = append(cases, &tc) + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + + if len(cases) == 0 { + return nil, fmt.Errorf("no test cases found in %s", path) + } + + return cases, nil +} + +// ValidateTestCases validates a slice of test cases +func ValidateTestCases(cases []*Case) error { + ids := make(map[string]bool) + + for i, tc := range cases { + // Check for duplicate IDs + if ids[tc.ID] { + return fmt.Errorf("duplicate test case ID: %s", tc.ID) + } + ids[tc.ID] = true + + // Validate input can be parsed + if _, err := tc.GetMessages(); err != nil { + return fmt.Errorf("test case %s (index %d): invalid input: %w", tc.ID, i, err) + } + + // Validate timeout format if specified + if tc.Timeout != "" { + // GetTimeout returns a duration, parsing error would return default + // We validate by checking if the string is parseable + if _, err := time.ParseDuration(tc.Timeout); err != nil { + return fmt.Errorf("test case %s: invalid timeout format: %s", tc.ID, tc.Timeout) + } + } + } + + return nil +} + +// FilterTestCases filters test cases based on criteria +func FilterTestCases(cases []*Case, filter func(*Case) bool) []*Case { + var result []*Case + for _, tc := range cases { + if filter(tc) { + result = append(result, tc) + } + } + return result +} + +// FilterSkipped returns test cases that are not skipped +func FilterSkipped(cases []*Case) []*Case { + return FilterTestCases(cases, func(tc *Case) bool { + return !tc.Skip + }) +} + +// FilterByIDs returns test cases matching the given IDs +func FilterByIDs(cases []*Case, ids []string) []*Case { + idSet := make(map[string]bool) + for _, id := range ids { + idSet[id] = true + } + return FilterTestCases(cases, func(tc *Case) bool { + return idSet[tc.ID] + }) +} diff --git a/agent/test/output.go b/agent/test/output.go new file mode 100644 index 00000000..1b3fc51b --- /dev/null +++ b/agent/test/output.go @@ -0,0 +1,316 @@ +package test + +import ( + "fmt" + "strings" + "time" + + "github.com/fatih/color" + jsoniter "github.com/json-iterator/go" +) + +// OutputWriter handles colored console output for test execution +type OutputWriter struct { + verbose bool +} + +// NewOutputWriter creates a new output writer +func NewOutputWriter(verbose bool) *OutputWriter { + return &OutputWriter{verbose: verbose} +} + +// Header prints a header section +func (w *OutputWriter) Header(title string) { + fmt.Println() + color.New(color.FgCyan, color.Bold).Println("═══════════════════════════════════════════════════════════════") + color.New(color.FgCyan, color.Bold).Printf(" %s\n", title) + color.New(color.FgCyan, color.Bold).Println("═══════════════════════════════════════════════════════════════") +} + +// SubHeader prints a sub-header +func (w *OutputWriter) SubHeader(title string) { + fmt.Println() + color.New(color.FgWhite, color.Bold).Println("───────────────────────────────────────────────────────────────") + color.New(color.FgWhite, color.Bold).Printf(" %s\n", title) + color.New(color.FgWhite, color.Bold).Println("───────────────────────────────────────────────────────────────") +} + +// Info prints an info message +func (w *OutputWriter) Info(format string, args ...interface{}) { + color.New(color.FgBlue).Printf("ℹ ") + fmt.Printf(format+"\n", args...) +} + +// Success prints a success message +func (w *OutputWriter) Success(format string, args ...interface{}) { + color.New(color.FgGreen).Printf("✓ ") + fmt.Printf(format+"\n", args...) +} + +// Error prints an error message +func (w *OutputWriter) Error(format string, args ...interface{}) { + color.New(color.FgRed).Printf("✗ ") + fmt.Printf(format+"\n", args...) +} + +// Warning prints a warning message +func (w *OutputWriter) Warning(format string, args ...interface{}) { + color.New(color.FgYellow).Printf("⚠ ") + fmt.Printf(format+"\n", args...) +} + +// Skip prints a skip message +func (w *OutputWriter) Skip(format string, args ...interface{}) { + color.New(color.FgYellow).Printf("○ ") + fmt.Printf(format+"\n", args...) +} + +// Verbose prints a verbose message (only if verbose mode is enabled) +func (w *OutputWriter) Verbose(format string, args ...interface{}) { + if w.verbose { + color.New(color.FgHiBlack).Printf(" │ ") + fmt.Printf(format+"\n", args...) + } +} + +// TestStart prints test case start +func (w *OutputWriter) TestStart(id string, input string, runNum int) { + inputPreview := truncateString(input, 50) + if runNum > 1 { + color.New(color.FgWhite).Printf("► [%s] Run %d: ", id, runNum) + } else { + color.New(color.FgWhite).Printf("► [%s] ", id) + } + color.New(color.FgHiBlack).Printf("%s", inputPreview) + fmt.Print(" ") +} + +// TestResult prints test case result +func (w *OutputWriter) TestResult(status Status, duration time.Duration) { + switch status { + case StatusPassed: + color.New(color.FgGreen, color.Bold).Printf("PASSED") + case StatusFailed: + color.New(color.FgRed, color.Bold).Printf("FAILED") + case StatusSkipped: + color.New(color.FgYellow).Printf("SKIPPED") + case StatusError: + color.New(color.FgRed, color.Bold).Printf("ERROR") + case StatusTimeout: + color.New(color.FgRed).Printf("TIMEOUT") + } + color.New(color.FgHiBlack).Printf(" (%s)\n", formatDuration(duration)) +} + +// TestError prints test error details +func (w *OutputWriter) TestError(err string) { + color.New(color.FgRed).Printf(" └─ %s\n", err) +} + +// TestOutput prints test output (verbose mode) +func (w *OutputWriter) TestOutput(output string) { + if w.verbose && output != "" { + outputPreview := truncateString(output, 100) + color.New(color.FgHiBlack).Printf(" └─ Output: %s\n", outputPreview) + } +} + +// Progress prints progress information +func (w *OutputWriter) Progress(current, total int) { + percentage := float64(current) / float64(total) * 100 + color.New(color.FgHiBlack).Printf("\r Progress: %d/%d (%.0f%%)", current, total, percentage) +} + +// Summary prints the test summary +func (w *OutputWriter) Summary(summary *Summary, duration time.Duration) { + w.SubHeader("Summary") + + // Agent info + color.New(color.FgWhite).Printf(" Agent: ") + color.New(color.FgCyan).Printf("%s\n", summary.AgentID) + + if summary.Connector != "" { + color.New(color.FgWhite).Printf(" Connector: ") + color.New(color.FgCyan).Printf("%s\n", summary.Connector) + } + + // Results + color.New(color.FgWhite).Printf(" Total: ") + fmt.Printf("%d\n", summary.Total) + + color.New(color.FgWhite).Printf(" Passed: ") + if summary.Passed > 0 { + color.New(color.FgGreen).Printf("%d\n", summary.Passed) + } else { + fmt.Printf("%d\n", summary.Passed) + } + + color.New(color.FgWhite).Printf(" Failed: ") + if summary.Failed > 0 { + color.New(color.FgRed).Printf("%d\n", summary.Failed) + } else { + fmt.Printf("%d\n", summary.Failed) + } + + if summary.Skipped > 0 { + color.New(color.FgWhite).Printf(" Skipped: ") + color.New(color.FgYellow).Printf("%d\n", summary.Skipped) + } + + if summary.Errors > 0 { + color.New(color.FgWhite).Printf(" Errors: ") + color.New(color.FgRed).Printf("%d\n", summary.Errors) + } + + if summary.Timeouts > 0 { + color.New(color.FgWhite).Printf(" Timeouts: ") + color.New(color.FgRed).Printf("%d\n", summary.Timeouts) + } + + // Pass rate + passRate := float64(0) + if summary.Total > 0 { + passRate = float64(summary.Passed) / float64(summary.Total) * 100 + } + color.New(color.FgWhite).Printf(" Pass Rate: ") + if passRate == 100 { + color.New(color.FgGreen, color.Bold).Printf("%.1f%%\n", passRate) + } else if passRate >= 80 { + color.New(color.FgYellow).Printf("%.1f%%\n", passRate) + } else { + color.New(color.FgRed).Printf("%.1f%%\n", passRate) + } + + // Duration + color.New(color.FgWhite).Printf(" Duration: ") + fmt.Printf("%s\n", formatDuration(duration)) + + // Stability info (if runs > 1) + if summary.RunsPerCase > 1 { + fmt.Println() + color.New(color.FgWhite, color.Bold).Println(" Stability Analysis:") + color.New(color.FgWhite).Printf(" Runs/Case: %d\n", summary.RunsPerCase) + color.New(color.FgWhite).Printf(" Total Runs: %d\n", summary.TotalRuns) + color.New(color.FgWhite).Printf(" Stable Cases: ") + if summary.StableCases == summary.Total { + color.New(color.FgGreen).Printf("%d\n", summary.StableCases) + } else { + color.New(color.FgYellow).Printf("%d\n", summary.StableCases) + } + color.New(color.FgWhite).Printf(" Unstable: ") + if summary.UnstableCases > 0 { + color.New(color.FgRed).Printf("%d\n", summary.UnstableCases) + } else { + fmt.Printf("%d\n", summary.UnstableCases) + } + } +} + +// OutputFile prints the output file path +func (w *OutputWriter) OutputFile(path string) { + fmt.Println() + color.New(color.FgWhite).Printf(" Output: ") + color.New(color.FgCyan).Printf("%s\n", path) +} + +// FinalResult prints the final result banner +func (w *OutputWriter) FinalResult(passed bool) { + fmt.Println() + if passed { + color.New(color.FgGreen, color.Bold).Println("═══════════════════════════════════════════════════════════════") + color.New(color.FgGreen, color.Bold).Println(" ✨ ALL TESTS PASSED ✨") + color.New(color.FgGreen, color.Bold).Println("═══════════════════════════════════════════════════════════════") + } else { + color.New(color.FgRed, color.Bold).Println("═══════════════════════════════════════════════════════════════") + color.New(color.FgRed, color.Bold).Println(" ❌ TESTS FAILED") + color.New(color.FgRed, color.Bold).Println("═══════════════════════════════════════════════════════════════") + } + fmt.Println() +} + +// DirectOutput prints the agent output directly (for development mode) +func (w *OutputWriter) DirectOutput(output interface{}) { + if output == nil { + return + } + + // Try to format as JSON if it's a complex type + switch v := output.(type) { + case string: + fmt.Println(v) + case map[string]interface{}, []interface{}: + // Pretty print JSON + jsonBytes, err := jsoniter.MarshalIndent(v, "", " ") + if err != nil { + fmt.Printf("%v\n", output) + } else { + fmt.Println(string(jsonBytes)) + } + default: + // Try to marshal as JSON + jsonBytes, err := jsoniter.MarshalIndent(output, "", " ") + if err != nil { + fmt.Printf("%v\n", output) + } else { + fmt.Println(string(jsonBytes)) + } + } +} + +// StabilityResult prints stability analysis result for a test case +func (w *OutputWriter) StabilityResult(sr *StabilityResult) { + color.New(color.FgWhite).Printf(" [%s] ", sr.ID) + + // Pass rate + if sr.PassRate == 100 { + color.New(color.FgGreen).Printf("%.0f%%", sr.PassRate) + } else if sr.PassRate >= 80 { + color.New(color.FgYellow).Printf("%.0f%%", sr.PassRate) + } else { + color.New(color.FgRed).Printf("%.0f%%", sr.PassRate) + } + + // Classification + color.New(color.FgHiBlack).Printf(" (%d/%d) ", sr.Passed, sr.Runs) + + switch sr.StabilityClass { + case StabilityStable: + color.New(color.FgGreen).Printf("Stable") + case StabilityMostlyStable: + color.New(color.FgYellow).Printf("Mostly Stable") + case StabilityUnstable: + color.New(color.FgRed).Printf("Unstable") + case StabilityHighlyUnstable: + color.New(color.FgRed, color.Bold).Printf("Highly Unstable") + } + + // Timing + color.New(color.FgHiBlack).Printf(" avg:%.0fms\n", sr.AvgDurationMs) +} + +// Helper functions + +func truncateString(s string, maxLen int) string { + // Remove newlines and extra spaces + s = strings.ReplaceAll(s, "\n", " ") + s = strings.ReplaceAll(s, "\r", "") + s = strings.Join(strings.Fields(s), " ") + + if len(s) <= maxLen { + return s + } + return s[:maxLen-3] + "..." +} + +func formatDuration(d time.Duration) string { + if d < time.Millisecond { + return fmt.Sprintf("%dµs", d.Microseconds()) + } + if d < time.Second { + return fmt.Sprintf("%dms", d.Milliseconds()) + } + if d < time.Minute { + return fmt.Sprintf("%.1fs", d.Seconds()) + } + return fmt.Sprintf("%.1fm", d.Minutes()) +} diff --git a/agent/test/reporter.go b/agent/test/reporter.go new file mode 100644 index 00000000..7e486249 --- /dev/null +++ b/agent/test/reporter.go @@ -0,0 +1,713 @@ +package test + +import ( + "bufio" + "fmt" + "html/template" + "io" + "strings" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/yao/agent/caller" + "github.com/yaoapp/yao/agent/context" +) + +// JSONLReporter generates JSONL format reports (default) +type JSONLReporter struct{} + +// NewJSONLReporter creates a new JSONL reporter +func NewJSONLReporter() *JSONLReporter { + return &JSONLReporter{} +} + +// Generate generates a JSONL report (writes to stdout or file) +func (r *JSONLReporter) Generate(report *Report) error { + return nil // JSONL is written during test execution +} + +// Write writes the report in JSONL format +func (r *JSONLReporter) Write(report *Report, w io.Writer) error { + writer := bufio.NewWriter(w) + defer writer.Flush() + + // Start event + startEvent := map[string]interface{}{ + "type": "start", + "timestamp": report.Metadata.StartedAt.Format(time.RFC3339), + "agent_id": report.Summary.AgentID, + "total_cases": report.Summary.Total, + } + if err := writeJSONLineToWriter(writer, startEvent); err != nil { + return err + } + + // Result events + if report.Results != nil { + for _, result := range report.Results { + resultEvent := map[string]interface{}{ + "type": "result", + "id": result.ID, + "status": result.Status, + "duration_ms": result.DurationMs, + } + if result.Output != nil { + resultEvent["output"] = result.Output + } + if result.Error != "" { + resultEvent["error"] = result.Error + } + if err := writeJSONLineToWriter(writer, resultEvent); err != nil { + return err + } + } + } + + // Stability results + if report.StabilityResults != nil { + for _, sr := range report.StabilityResults { + stabilityEvent := map[string]interface{}{ + "type": "stability", + "id": sr.ID, + "runs": sr.Runs, + "passed": sr.Passed, + "failed": sr.Failed, + "pass_rate": sr.PassRate, + "stable": sr.Stable, + "stability_class": sr.StabilityClass, + "avg_duration_ms": sr.AvgDurationMs, + } + if err := writeJSONLineToWriter(writer, stabilityEvent); err != nil { + return err + } + } + } + + // Summary event + summaryEvent := map[string]interface{}{ + "type": "summary", + "total": report.Summary.Total, + "passed": report.Summary.Passed, + "failed": report.Summary.Failed, + "skipped": report.Summary.Skipped, + "errors": report.Summary.Errors, + "timeouts": report.Summary.Timeouts, + "duration_ms": report.Summary.DurationMs, + } + if report.Summary.RunsPerCase > 1 { + summaryEvent["runs_per_case"] = report.Summary.RunsPerCase + summaryEvent["total_runs"] = report.Summary.TotalRuns + summaryEvent["overall_pass_rate"] = report.Summary.OverallPassRate + summaryEvent["stable_cases"] = report.Summary.StableCases + summaryEvent["unstable_cases"] = report.Summary.UnstableCases + } + return writeJSONLineToWriter(writer, summaryEvent) +} + +// writeJSONLineToWriter writes a JSON line to the writer +func writeJSONLineToWriter(writer *bufio.Writer, data interface{}) error { + line, err := jsoniter.Marshal(data) + if err != nil { + return err + } + _, err = writer.Write(line) + if err != nil { + return err + } + _, err = writer.WriteString("\n") + return err +} + +// JSONReporter generates full JSON format reports +type JSONReporter struct{} + +// NewJSONReporter creates a new JSON reporter +func NewJSONReporter() *JSONReporter { + return &JSONReporter{} +} + +// Generate generates a JSON report +func (r *JSONReporter) Generate(report *Report) error { + return nil +} + +// Write writes the report in JSON format +func (r *JSONReporter) Write(report *Report, w io.Writer) error { + encoder := jsoniter.NewEncoder(w) + encoder.SetIndent("", " ") + return encoder.Encode(report) +} + +// MarkdownReporter generates Markdown format reports +type MarkdownReporter struct{} + +// NewMarkdownReporter creates a new Markdown reporter +func NewMarkdownReporter() *MarkdownReporter { + return &MarkdownReporter{} +} + +// Generate generates a Markdown report +func (r *MarkdownReporter) Generate(report *Report) error { + return nil +} + +// Write writes the report in Markdown format +func (r *MarkdownReporter) Write(report *Report, w io.Writer) error { + var sb strings.Builder + + // Header + sb.WriteString("# Agent Test Report\n\n") + + // Summary + sb.WriteString("## Summary\n\n") + sb.WriteString("| Metric | Value |\n") + sb.WriteString("| ------ | ----- |\n") + sb.WriteString(fmt.Sprintf("| Agent | %s |\n", report.Summary.AgentID)) + if report.Summary.Connector != "" { + sb.WriteString(fmt.Sprintf("| Connector | %s |\n", report.Summary.Connector)) + } + sb.WriteString(fmt.Sprintf("| Total | %d |\n", report.Summary.Total)) + sb.WriteString(fmt.Sprintf("| Passed | %d |\n", report.Summary.Passed)) + sb.WriteString(fmt.Sprintf("| Failed | %d |\n", report.Summary.Failed)) + if report.Summary.Skipped > 0 { + sb.WriteString(fmt.Sprintf("| Skipped | %d |\n", report.Summary.Skipped)) + } + if report.Summary.Errors > 0 { + sb.WriteString(fmt.Sprintf("| Errors | %d |\n", report.Summary.Errors)) + } + if report.Summary.Timeouts > 0 { + sb.WriteString(fmt.Sprintf("| Timeouts | %d |\n", report.Summary.Timeouts)) + } + + passRate := float64(0) + if report.Summary.Total > 0 { + passRate = float64(report.Summary.Passed) / float64(report.Summary.Total) * 100 + } + sb.WriteString(fmt.Sprintf("| Pass Rate | %.1f%% |\n", passRate)) + sb.WriteString(fmt.Sprintf("| Duration | %dms |\n", report.Summary.DurationMs)) + sb.WriteString("\n") + + // Environment + if report.Environment != nil { + sb.WriteString("## Environment\n\n") + sb.WriteString("| Setting | Value |\n") + sb.WriteString("| ------- | ----- |\n") + sb.WriteString(fmt.Sprintf("| User | %s |\n", report.Environment.UserID)) + sb.WriteString(fmt.Sprintf("| Team | %s |\n", report.Environment.TeamID)) + sb.WriteString(fmt.Sprintf("| Locale | %s |\n", report.Environment.Locale)) + sb.WriteString("\n") + } + + // Results + sb.WriteString("## Results\n\n") + + if report.Results != nil { + for _, result := range report.Results { + statusIcon := "✅" + switch result.Status { + case StatusFailed: + statusIcon = "❌" + case StatusError: + statusIcon = "💥" + case StatusTimeout: + statusIcon = "⏱️" + case StatusSkipped: + statusIcon = "⏭️" + } + + sb.WriteString(fmt.Sprintf("### %s %s - %s (%dms)\n\n", statusIcon, result.ID, result.Status, result.DurationMs)) + + if result.Error != "" { + sb.WriteString(fmt.Sprintf("**Error:** %s\n\n", result.Error)) + } + } + } + + // Stability results + if report.StabilityResults != nil { + sb.WriteString("## Stability Analysis\n\n") + sb.WriteString("| ID | Pass Rate | Runs | Status | Avg Duration |\n") + sb.WriteString("| -- | --------- | ---- | ------ | ------------ |\n") + + for _, sr := range report.StabilityResults { + status := string(sr.StabilityClass) + sb.WriteString(fmt.Sprintf("| %s | %.0f%% | %d/%d | %s | %.0fms |\n", + sr.ID, sr.PassRate, sr.Passed, sr.Runs, status, sr.AvgDurationMs)) + } + sb.WriteString("\n") + } + + // Metadata + sb.WriteString("## Metadata\n\n") + sb.WriteString(fmt.Sprintf("- **Started:** %s\n", report.Metadata.StartedAt.Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("- **Completed:** %s\n", report.Metadata.CompletedAt.Format(time.RFC3339))) + if report.Metadata.InputFile != "" { + sb.WriteString(fmt.Sprintf("- **Input File:** %s\n", report.Metadata.InputFile)) + } + if report.Metadata.OutputFile != "" { + sb.WriteString(fmt.Sprintf("- **Output File:** %s\n", report.Metadata.OutputFile)) + } + + _, err := w.Write([]byte(sb.String())) + return err +} + +// HTMLReporter generates HTML format reports +type HTMLReporter struct{} + +// NewHTMLReporter creates a new HTML reporter +func NewHTMLReporter() *HTMLReporter { + return &HTMLReporter{} +} + +// Generate generates an HTML report +func (r *HTMLReporter) Generate(report *Report) error { + return nil +} + +// Write writes the report in HTML format +func (r *HTMLReporter) Write(report *Report, w io.Writer) error { + tmpl, err := template.New("report").Parse(htmlTemplate) + if err != nil { + return fmt.Errorf("failed to parse HTML template: %w", err) + } + + // Calculate pass rate + passRate := float64(0) + if report.Summary.Total > 0 { + passRate = float64(report.Summary.Passed) / float64(report.Summary.Total) * 100 + } + + data := map[string]interface{}{ + "Report": report, + "PassRate": passRate, + } + + return tmpl.Execute(w, data) +} + +// HTML template for reports +const htmlTemplate = ` + + + + + Agent Test Report - {{.Report.Summary.AgentID}} + + + +
+

Agent Test Report

+

{{.Report.Summary.AgentID}} {{if .Report.Summary.Connector}}• {{.Report.Summary.Connector}}{{end}}

+ +
+
+
{{.Report.Summary.Total}}
+
Total Tests
+
+
+
{{.Report.Summary.Passed}}
+
Passed
+
+
+
{{.Report.Summary.Failed}}
+
Failed
+
+
+
{{printf "%.1f" .PassRate}}%
+
Pass Rate
+
+
+
{{.Report.Summary.DurationMs}}ms
+
Duration
+
+
+ +

Test Results

+ + + + + + + + + + + {{range .Report.Results}} + + + + + + + {{end}} + {{range .Report.StabilityResults}} + + + + + + + {{end}} + +
IDStatusDurationDetails
{{.ID}}{{.Status}}{{.DurationMs}}ms + {{if .Error}}
{{.Error}}
{{end}} +
{{.ID}}{{.StabilityClass}}{{printf "%.0f" .AvgDurationMs}}ms avg{{.Passed}}/{{.Runs}} passed ({{printf "%.0f" .PassRate}}%)
+ +

Metadata

+ +
+ +` + +// AgentReporter uses a custom agent to generate reports +type AgentReporter struct { + agentID string + format string + verbose bool + ctx *context.Context // Test context for agent call +} + +// NewAgentReporter creates a new agent-based reporter +func NewAgentReporter(agentID, format string, verbose bool) *AgentReporter { + return &AgentReporter{ + agentID: agentID, + format: format, + verbose: verbose, + } +} + +// SetContext sets the context for agent calls +func (r *AgentReporter) SetContext(ctx *context.Context) { + r.ctx = ctx +} + +// Generate generates a report using the agent +func (r *AgentReporter) Generate(report *Report) error { + return nil +} + +// Write writes the report using the agent +func (r *AgentReporter) Write(report *Report, w io.Writer) error { + // Check if AgentGetterFunc is initialized + if caller.AgentGetterFunc == nil { + return fmt.Errorf("AgentGetterFunc not initialized, cannot call reporter agent") + } + + // Get the reporter agent + agent, err := caller.AgentGetterFunc(r.agentID) + if err != nil { + return fmt.Errorf("failed to get reporter agent %s: %w", r.agentID, err) + } + + // Build input for the reporter agent + input := &ReporterInput{ + Report: report, + Format: r.format, + Options: &ReporterOptions{ + Verbose: r.verbose, + IncludeOutputs: r.verbose, + IncludeInputs: r.verbose, + }, + } + + // Convert input to JSON for the agent + inputJSON, err := jsoniter.Marshal(input) + if err != nil { + return fmt.Errorf("failed to marshal reporter input: %w", err) + } + + // Create message for the agent + messages := []context.Message{ + { + Role: context.RoleUser, + Content: string(inputJSON), + }, + } + + // Create context if not provided + ctx := r.ctx + if ctx == nil { + // Create a minimal context for the reporter agent call + ctx = NewTestContext("reporter", r.agentID, NewEnvironment("", "")) + defer ctx.Release() + } + + // Call the agent with skip options (no history, no output) + options := &context.Options{ + Skip: &context.Skip{ + History: true, + Output: true, + }, + } + + result, err := agent.Stream(ctx, messages, options) + if err != nil { + return fmt.Errorf("reporter agent call failed: %w", err) + } + + // Extract content from result + content, err := r.extractContent(result) + if err != nil { + return fmt.Errorf("failed to extract report content: %w", err) + } + + // Write the content to output + _, err = w.Write([]byte(content)) + if err != nil { + return fmt.Errorf("failed to write report: %w", err) + } + + return nil +} + +// extractContent extracts the report content from the agent's response +func (r *AgentReporter) extractContent(result interface{}) (string, error) { + if result == nil { + return "", fmt.Errorf("agent returned nil result") + } + + // Try to convert to map first (context.Response) + switch v := result.(type) { + case string: + return v, nil + + case *context.Response: + // Extract from completion content + if v.Completion != nil && v.Completion.Content != nil { + return r.contentToString(v.Completion.Content) + } + // Try next field + if v.Next != nil { + return r.contentToString(v.Next) + } + return "", fmt.Errorf("no content in response") + + case map[string]interface{}: + // Check for completion.content + if completion, ok := v["completion"].(map[string]interface{}); ok { + if content, ok := completion["content"]; ok { + return r.contentToString(content) + } + } + // Check for next + if next, ok := v["next"]; ok { + return r.contentToString(next) + } + // Check for content directly + if content, ok := v["content"]; ok { + return r.contentToString(content) + } + // Marshal the whole thing + jsonBytes, _ := jsoniter.Marshal(v) + return string(jsonBytes), nil + + default: + // Try to marshal as JSON + jsonBytes, err := jsoniter.Marshal(result) + if err != nil { + return fmt.Sprintf("%v", result), nil + } + return string(jsonBytes), nil + } +} + +// contentToString converts various content types to string +func (r *AgentReporter) contentToString(content interface{}) (string, error) { + switch v := content.(type) { + case string: + return v, nil + case []byte: + return string(v), nil + default: + jsonBytes, err := jsoniter.Marshal(content) + if err != nil { + return fmt.Sprintf("%v", content), nil + } + return string(jsonBytes), nil + } +} + +// GetReporter returns a reporter based on output format +func GetReporter(format OutputFormat) Reporter { + switch format { + case FormatJSON: + return NewJSONReporter() + case FormatHTML: + return NewHTMLReporter() + case FormatMarkdown: + return NewMarkdownReporter() + default: + return NewJSONLReporter() + } +} + +// GetReporterFromPath returns a reporter based on file extension +func GetReporterFromPath(outputPath string) Reporter { + format := GetOutputFormat(outputPath) + return GetReporter(format) +} + +// GetReporterWithAgent returns an agent-based reporter if agentID is specified, +// otherwise returns a built-in reporter based on output format +func GetReporterWithAgent(agentID, outputPath string, verbose bool) Reporter { + if agentID != "" { + format := GetOutputFormat(outputPath) + return NewAgentReporter(agentID, string(format), verbose) + } + return GetReporterFromPath(outputPath) +} diff --git a/agent/test/resolver.go b/agent/test/resolver.go new file mode 100644 index 00000000..9ccf31ee --- /dev/null +++ b/agent/test/resolver.go @@ -0,0 +1,316 @@ +package test + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + jsoniter "github.com/json-iterator/go" +) + +// PathResolver resolves agent information from file paths +type PathResolver struct{} + +// NewResolver creates a new path resolver +func NewResolver() Resolver { + return &PathResolver{} +} + +// Resolve resolves the agent from options +// Priority: explicit AgentID > path-based detection (from input file or cwd) +func (r *PathResolver) Resolve(opts *Options) (*AgentInfo, error) { + // If explicit agent ID is provided, use it + if opts.AgentID != "" { + return r.ResolveByID(opts.AgentID) + } + + // For file mode, resolve from input file path + if opts.InputMode == InputModeFile { + if opts.Input == "" { + return nil, fmt.Errorf("no agent ID or input file specified") + } + return r.ResolveFromPath(opts.Input) + } + + // For message mode, try to resolve from current working directory + return r.ResolveFromCwd() +} + +// ResolveFromCwd resolves the agent from the current working directory +// It looks for package.yao in the current directory or parent directories +func (r *PathResolver) ResolveFromCwd() (*AgentInfo, error) { + cwd, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("failed to get current working directory: %w", err) + } + + info, err := r.ResolveFromPath(cwd) + if err != nil { + return nil, fmt.Errorf("no agent found in current directory. Use -n to specify agent explicitly") + } + return info, nil +} + +// ResolveFromPath resolves the agent by traversing up from the input file path +// It looks for package.yao in parent directories +func (r *PathResolver) ResolveFromPath(inputPath string) (*AgentInfo, error) { + // Get absolute path + absPath, err := filepath.Abs(inputPath) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path: %w", err) + } + + // Start from the directory containing the input file + dir := filepath.Dir(absPath) + + // Traverse up to find package.yao + for { + packagePath := filepath.Join(dir, "package.yao") + if _, err := os.Stat(packagePath); err == nil { + // Found package.yao + return r.loadAgentFromPath(dir, packagePath) + } + + // Move to parent directory + parent := filepath.Dir(dir) + if parent == dir { + // Reached root, no package.yao found + break + } + dir = parent + } + + return nil, fmt.Errorf("no package.yao found in path hierarchy of %s", inputPath) +} + +// ResolveByID resolves an agent by its ID +// This would integrate with the assistant loading system +func (r *PathResolver) ResolveByID(agentID string) (*AgentInfo, error) { + // This is a placeholder - actual implementation would use assistant.Get() + // For now, return basic info + return &AgentInfo{ + ID: agentID, + Name: agentID, + }, nil +} + +// loadAgentFromPath loads agent information from a package.yao file +func (r *PathResolver) loadAgentFromPath(agentDir, packagePath string) (*AgentInfo, error) { + // Read package.yao + data, err := os.ReadFile(packagePath) + if err != nil { + return nil, fmt.Errorf("failed to read package.yao: %w", err) + } + + // Parse package.yao + var pkg PackageYao + if err := jsoniter.Unmarshal(data, &pkg); err != nil { + return nil, fmt.Errorf("failed to parse package.yao: %w", err) + } + + // Derive agent ID from directory path + agentID := deriveAgentID(agentDir) + + return &AgentInfo{ + ID: agentID, + Name: pkg.Name, + Description: pkg.Description, + Path: agentDir, + Connector: pkg.Connector, + Type: pkg.Type, + }, nil +} + +// PackageYao represents the structure of package.yao +type PackageYao struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Connector string `json:"connector,omitempty"` + Type string `json:"type,omitempty"` + Uses map[string]interface{} `json:"uses,omitempty"` + Options map[string]interface{} `json:"options,omitempty"` +} + +// deriveAgentID derives an agent ID from the directory path +// e.g., /app/assistants/workers/system/keyword -> workers.system.keyword +func deriveAgentID(dir string) string { + // Find "assistants" in path and use everything after it + parts := strings.Split(filepath.ToSlash(dir), "/") + + // Look for "assistants" marker + startIdx := -1 + for i, part := range parts { + if part == "assistants" { + startIdx = i + 1 + break + } + } + + if startIdx == -1 || startIdx >= len(parts) { + // No "assistants" found, use the last directory name + return filepath.Base(dir) + } + + // Join remaining parts with dots + return strings.Join(parts[startIdx:], ".") +} + +// GetOutputFormat determines the output format from file extension +func GetOutputFormat(outputPath string) OutputFormat { + ext := strings.ToLower(filepath.Ext(outputPath)) + switch ext { + case ".json": + return FormatJSON + case ".html", ".htm": + return FormatHTML + case ".md", ".markdown": + return FormatMarkdown + default: + return FormatJSON // Default to JSON + } +} + +// ValidateOptions validates test options +func ValidateOptions(opts *Options) error { + if opts.Input == "" { + return fmt.Errorf("input is required (-i flag)") + } + + // For file mode, check input file exists + if opts.InputMode == InputModeFile { + if _, err := os.Stat(opts.Input); os.IsNotExist(err) { + return fmt.Errorf("input file not found: %s", opts.Input) + } + } + + // Note: For message mode, agent can be resolved from cwd, so no validation here + // The resolver will return an error if agent cannot be found + + // Validate timeout + if opts.Timeout < 0 { + return fmt.Errorf("timeout cannot be negative") + } + + // Validate parallel + if opts.Parallel < 0 { + return fmt.Errorf("parallel cannot be negative") + } + + return nil +} + +// DefaultOptions returns options with default values +func DefaultOptions() *Options { + return &Options{ + Timeout: 5 * time.Minute, // 5 minutes default timeout + Parallel: 1, + Runs: 1, + Verbose: false, + FailFast: false, + } +} + +// DetectInputMode detects the input mode from the input string +// Returns InputModeFile if input looks like a file path, InputModeMessage otherwise +func DetectInputMode(input string) InputMode { + // If input ends with .jsonl or .json, treat as file + if strings.HasSuffix(input, ".jsonl") || strings.HasSuffix(input, ".json") { + return InputModeFile + } + + // If input contains path separator, check if file exists + if strings.Contains(input, string(filepath.Separator)) || strings.Contains(input, "/") { + if _, err := os.Stat(input); err == nil { + return InputModeFile + } + } + + // Otherwise treat as direct message + return InputModeMessage +} + +// MergeOptions merges user options with defaults +func MergeOptions(opts *Options, defaults *Options) *Options { + result := *defaults + + if opts.Input != "" { + result.Input = opts.Input + result.InputMode = DetectInputMode(opts.Input) + } + if opts.OutputFile != "" { + result.OutputFile = opts.OutputFile + } + if opts.AgentID != "" { + result.AgentID = opts.AgentID + } + if opts.Connector != "" { + result.Connector = opts.Connector + } + if opts.UserID != "" { + result.UserID = opts.UserID + } + if opts.TeamID != "" { + result.TeamID = opts.TeamID + } + if opts.Locale != "" { + result.Locale = opts.Locale + } + if opts.Timeout > 0 { + result.Timeout = opts.Timeout + } + if opts.Parallel > 0 { + result.Parallel = opts.Parallel + } + if opts.Runs > 0 { + result.Runs = opts.Runs + } + if opts.ReporterID != "" { + result.ReporterID = opts.ReporterID + } + if opts.Verbose { + result.Verbose = opts.Verbose + } + if opts.FailFast { + result.FailFast = opts.FailFast + } + + return &result +} + +// GenerateDefaultOutputPath generates the default output path based on input file +// Format: {input_directory}/output-{timestamp}.jsonl +// Timestamp format: YYYYMMDDHHMMSS +func GenerateDefaultOutputPath(inputPath string) string { + dir := filepath.Dir(inputPath) + timestamp := time.Now().Format("20060102150405") + filename := fmt.Sprintf("output-%s.jsonl", timestamp) + return filepath.Join(dir, filename) +} + +// ResolveOutputPath resolves the output path based on input mode +// - File mode: generate default path in same directory as input +// - Message mode: return empty string (output to stdout) +// If outputPath is explicitly specified, always use it +func ResolveOutputPath(opts *Options) string { + if opts.OutputFile != "" { + return opts.OutputFile + } + + // For file mode, generate default output path + if opts.InputMode == InputModeFile { + return GenerateDefaultOutputPath(opts.Input) + } + + // For message mode, output to stdout (empty string) + return "" +} + +// CreateTestCaseFromMessage creates a single test case from a direct message +func CreateTestCaseFromMessage(message string) *Case { + return &Case{ + ID: "T001", + Input: message, + } +} diff --git a/agent/test/runner.go b/agent/test/runner.go new file mode 100644 index 00000000..939d2dae --- /dev/null +++ b/agent/test/runner.go @@ -0,0 +1,510 @@ +package test + +import ( + "bufio" + stdContext "context" + "fmt" + "os" + "sync" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/yao/agent/assistant" + "github.com/yaoapp/yao/agent/context" +) + +// Executor executes test cases against an agent +type Executor struct { + opts *Options + output *OutputWriter + resolver Resolver + loader Loader +} + +// NewRunner creates a new test runner +func NewRunner(opts *Options) *Executor { + return &Executor{ + opts: opts, + output: NewOutputWriter(opts.Verbose), + resolver: NewResolver(), + loader: NewLoader(), + } +} + +// Run executes all test cases and returns a report +func (r *Executor) Run() (*Report, error) { + // For direct message mode, use simplified output (development mode) + if r.opts.InputMode == InputModeMessage { + return r.RunDirect() + } + + return r.RunTests() +} + +// RunDirect executes a single direct message and outputs the result directly +// This is optimized for development/debugging scenarios +func (r *Executor) RunDirect() (*Report, error) { + // Resolve agent + agentInfo, err := r.resolver.Resolve(r.opts) + if err != nil { + return nil, fmt.Errorf("failed to resolve agent: %w", err) + } + + // Get assistant + ast, err := assistant.Get(agentInfo.ID) + if err != nil { + return nil, fmt.Errorf("failed to get assistant: %w", err) + } + + // Create test case from message + tc := CreateTestCaseFromMessage(r.opts.Input) + + // Create context + chatID := GenerateChatID(tc.ID, 1) + 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 + } + + // Create timeout context + timeout := tc.GetTimeout(r.opts.Timeout) + timeoutCtx, cancel := stdContext.WithTimeout(ctx.Context, timeout) + defer cancel() + ctx.Context = timeoutCtx + + // Parse input to messages + messages, err := tc.GetMessages() + if err != nil { + return nil, fmt.Errorf("failed to parse input: %w", err) + } + + // Run the agent + response, err := ast.Stream(ctx, messages, opts) + + // Check for timeout + if timeoutCtx.Err() != nil { + return nil, fmt.Errorf("timeout after %s", timeout) + } + + // Check for error + if err != nil { + return nil, err + } + + // Extract and print output directly + output := extractOutput(response) + r.output.DirectOutput(output) + + // Return minimal report (for exit code handling) + return &Report{ + Summary: &Summary{ + Total: 1, + Passed: 1, + AgentID: agentInfo.ID, + }, + }, nil +} + +// RunTests executes test cases from file and generates a report +func (r *Executor) RunTests() (*Report, error) { + startTime := time.Now() + + // Print header + r.output.Header("Agent Test") + + // Resolve agent + agentInfo, err := r.resolver.Resolve(r.opts) + if err != nil { + return nil, fmt.Errorf("failed to resolve agent: %w", err) + } + + r.output.Info("Agent: %s", agentInfo.ID) + if r.opts.Connector != "" { + r.output.Info("Connector: %s (override)", r.opts.Connector) + } else if agentInfo.Connector != "" { + r.output.Info("Connector: %s", agentInfo.Connector) + } + + // Load test cases + var testCases []*Case + + // File mode - load from JSONL + testCases, err = r.loader.LoadFile(r.opts.Input) + if err != nil { + return nil, fmt.Errorf("failed to load test cases: %w", err) + } + r.output.Info("Input: %s (%d test cases)", r.opts.Input, len(testCases)) + + // Filter skipped tests + activeTests := FilterSkipped(testCases) + skippedCount := len(testCases) - len(activeTests) + if skippedCount > 0 { + r.output.Warning("Skipped: %d test cases", skippedCount) + } + + // Print test info + if r.opts.Runs > 1 { + r.output.Info("Runs: %d per test case (stability analysis)", r.opts.Runs) + } + r.output.Info("Timeout: %s", r.opts.Timeout) + if r.opts.Parallel > 1 { + r.output.Info("Parallel: %d", r.opts.Parallel) + } + + // Get assistant + ast, err := assistant.Get(agentInfo.ID) + if err != nil { + return nil, fmt.Errorf("failed to get assistant: %w", err) + } + + // Create report + report := &Report{ + Summary: &Summary{ + Total: len(testCases), + AgentID: agentInfo.ID, + AgentPath: agentInfo.Path, + Connector: r.opts.Connector, + RunsPerCase: r.opts.Runs, + }, + Environment: NewEnvironment(r.opts.UserID, r.opts.TeamID), + Metadata: &ReportMetadata{ + StartedAt: startTime, + InputFile: r.opts.Input, + Options: r.opts, + }, + } + + // Run tests + r.output.SubHeader("Running Tests") + + if r.opts.Runs > 1 { + // Stability testing mode + report.StabilityResults = r.runStabilityTests(ast, activeTests, agentInfo.ID) + r.calculateStabilitySummary(report) + } else { + // Single run mode + report.Results = r.runSingleTests(ast, activeTests, agentInfo.ID) + r.calculateSingleSummary(report) + } + + // Add skipped count + report.Summary.Skipped = skippedCount + + // Complete report + report.Summary.DurationMs = time.Since(startTime).Milliseconds() + report.Metadata.CompletedAt = time.Now() + + // Print summary + r.output.Summary(report.Summary, time.Since(startTime)) + + // Write output + if r.opts.OutputFile != "" { + err = r.writeOutput(report) + if err != nil { + r.output.Error("Failed to write output: %s", err.Error()) + } else { + r.output.OutputFile(r.opts.OutputFile) + } + } + + // Print final result + r.output.FinalResult(!report.HasFailures()) + + return report, nil +} + +// runSingleTests runs each test case once +func (r *Executor) runSingleTests(ast *assistant.Assistant, testCases []*Case, agentID string) []*Result { + results := make([]*Result, 0, len(testCases)) + + if r.opts.Parallel > 1 { + // Parallel execution + results = r.runParallel(ast, testCases, agentID) + } else { + // Sequential execution + for i, tc := range testCases { + result := r.runSingleTest(ast, tc, agentID, 1) + results = append(results, result) + + // Check fail-fast + if r.opts.FailFast && result.Status != StatusPassed && result.Status != StatusSkipped { + r.output.Warning("Stopping due to --fail-fast (failed at test %d/%d)", i+1, len(testCases)) + break + } + } + } + + return results +} + +// runParallel runs tests in parallel +func (r *Executor) runParallel(ast *assistant.Assistant, testCases []*Case, agentID string) []*Result { + results := make([]*Result, len(testCases)) + var wg sync.WaitGroup + sem := make(chan struct{}, r.opts.Parallel) + + for i, tc := range testCases { + wg.Add(1) + go func(idx int, testCase *Case) { + defer wg.Done() + sem <- struct{}{} // Acquire + defer func() { <-sem }() // Release + + results[idx] = r.runSingleTest(ast, testCase, agentID, 1) + }(i, tc) + } + + wg.Wait() + return results +} + +// runSingleTest runs a single test case +func (r *Executor) runSingleTest(ast *assistant.Assistant, tc *Case, agentID string, runNum int) *Result { + // Get input summary for display + inputSummary := SummarizeInput(tc.Input, 50) + r.output.TestStart(tc.ID, inputSummary, runNum) + + startTime := time.Now() + + // Create result + result := &Result{ + ID: tc.ID, + Input: tc.Input, + Expected: tc.Expected, + } + + // Parse input to messages + messages, err := tc.GetMessages() + if err != nil { + result.Status = StatusError + result.Error = fmt.Sprintf("failed to parse input: %s", err.Error()) + result.DurationMs = time.Since(startTime).Milliseconds() + r.output.TestResult(result.Status, time.Since(startTime)) + r.output.TestError(result.Error) + return result + } + + // Create context + chatID := GenerateChatID(tc.ID, runNum) + 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 + } + + // Create timeout context + timeout := tc.GetTimeout(r.opts.Timeout) + timeoutCtx, cancel := stdContext.WithTimeout(ctx.Context, timeout) + defer cancel() + ctx.Context = timeoutCtx + + // Run the test + response, err := ast.Stream(ctx, messages, opts) + + duration := time.Since(startTime) + result.DurationMs = duration.Milliseconds() + + // Check for timeout + if timeoutCtx.Err() != nil { + result.Status = StatusTimeout + result.Error = fmt.Sprintf("timeout after %s", timeout) + r.output.TestResult(result.Status, duration) + r.output.TestError(result.Error) + return result + } + + // Check for error + if err != nil { + result.Status = StatusError + result.Error = err.Error() + r.output.TestResult(result.Status, duration) + r.output.TestError(result.Error) + return result + } + + // Extract output + result.Output = extractOutput(response) + + // Validate result using asserter + asserter := NewAsserter() + passed, errMsg := asserter.Validate(tc, result.Output) + if passed { + result.Status = StatusPassed + } else { + result.Status = StatusFailed + result.Error = errMsg + } + + r.output.TestResult(result.Status, duration) + if result.Status == StatusFailed { + r.output.TestError(result.Error) + } + r.output.TestOutput(fmt.Sprintf("%v", result.Output)) + + return result +} + +// runStabilityTests runs each test case multiple times for stability analysis +func (r *Executor) runStabilityTests(ast *assistant.Assistant, testCases []*Case, agentID string) []*StabilityResult { + results := make([]*StabilityResult, 0, len(testCases)) + + for _, tc := range testCases { + sr := &StabilityResult{ + ID: tc.ID, + Input: tc.Input, + Expected: tc.Expected, + RunDetails: make([]*RunDetail, 0, r.opts.Runs), + } + + // Run multiple times + for run := 1; run <= r.opts.Runs; run++ { + result := r.runSingleTest(ast, tc, agentID, run) + + rd := &RunDetail{ + Run: run, + Status: result.Status, + DurationMs: result.DurationMs, + Output: result.Output, + Error: result.Error, + } + sr.RunDetails = append(sr.RunDetails, rd) + } + + // Calculate stability metrics + sr.CalculateStability() + + // Print stability result + r.output.StabilityResult(sr) + + results = append(results, sr) + + // Check fail-fast + if r.opts.FailFast && !sr.Stable { + r.output.Warning("Stopping due to --fail-fast (unstable test: %s)", tc.ID) + break + } + } + + return results +} + +// calculateSingleSummary calculates summary for single run mode +func (r *Executor) calculateSingleSummary(report *Report) { + for _, result := range report.Results { + switch result.Status { + case StatusPassed: + report.Summary.Passed++ + case StatusFailed: + report.Summary.Failed++ + case StatusError: + report.Summary.Errors++ + case StatusTimeout: + report.Summary.Timeouts++ + } + } +} + +// calculateStabilitySummary calculates summary for stability mode +func (r *Executor) calculateStabilitySummary(report *Report) { + report.Summary.TotalRuns = len(report.StabilityResults) * r.opts.Runs + + var totalPassRate float64 + for _, sr := range report.StabilityResults { + if sr.Stable { + report.Summary.StableCases++ + report.Summary.Passed++ + } else { + report.Summary.UnstableCases++ + report.Summary.Failed++ + } + totalPassRate += sr.PassRate + } + + if len(report.StabilityResults) > 0 { + report.Summary.OverallPassRate = totalPassRate / float64(len(report.StabilityResults)) + } +} + +// writeOutput writes the test report to the output file +func (r *Executor) writeOutput(report *Report) error { + file, err := os.Create(r.opts.OutputFile) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer file.Close() + + // Get reporter based on -r flag or file extension + reporter := GetReporterWithAgent(r.opts.ReporterID, r.opts.OutputFile, r.opts.Verbose) + + // If using agent reporter, set context + if agentReporter, ok := reporter.(*AgentReporter); ok { + // Create a context for the reporter agent call + ctx := NewTestContext("reporter", r.opts.ReporterID, report.Environment) + defer ctx.Release() + agentReporter.SetContext(ctx) + } + + // Write report using the reporter + return reporter.Write(report, file) +} + +// writeJSONLine writes a JSON line to the writer +func writeJSONLine(writer *bufio.Writer, data interface{}) error { + line, err := jsoniter.Marshal(data) + if err != nil { + return err + } + _, err = writer.Write(line) + if err != nil { + return err + } + _, err = writer.WriteString("\n") + return err +} + +// extractOutput extracts the output from the agent response +func extractOutput(response interface{}) interface{} { + if response == nil { + return nil + } + + // Try to get completion content from context.Response + if resp, ok := response.(*context.Response); ok { + if resp.Completion != nil { + return resp.Completion.Content + } + if resp.Next != nil { + return resp.Next + } + } + + return response +} + +// validateOutput validates the actual output against expected +func validateOutput(actual, expected interface{}) bool { + // Simple JSON comparison + actualJSON, err1 := jsoniter.Marshal(actual) + expectedJSON, err2 := jsoniter.Marshal(expected) + + if err1 != nil || err2 != nil { + return false + } + + return string(actualJSON) == string(expectedJSON) +} diff --git a/agent/test/types.go b/agent/test/types.go new file mode 100644 index 00000000..ac7c785a --- /dev/null +++ b/agent/test/types.go @@ -0,0 +1,628 @@ +package test + +import ( + "math" + "time" + + "github.com/yaoapp/yao/agent/context" +) + +// Status represents the status of a test case execution +type Status string + +const ( + // StatusPassed indicates the test passed + StatusPassed Status = "passed" + // StatusFailed indicates the test failed + StatusFailed Status = "failed" + // StatusSkipped indicates the test was skipped + StatusSkipped Status = "skipped" + // StatusError indicates a runtime error occurred + StatusError Status = "error" + // StatusTimeout indicates the test timed out + StatusTimeout Status = "timeout" +) + +// OutputFormat represents the output format for test reports +type OutputFormat string + +const ( + // FormatJSON outputs JSON format (for CI integration) + FormatJSON OutputFormat = "json" + // FormatHTML outputs HTML format (for human review) + FormatHTML OutputFormat = "html" + // FormatMarkdown outputs Markdown format (for documentation) + FormatMarkdown OutputFormat = "markdown" +) + +// StabilityClass represents the stability classification of a test case +type StabilityClass string + +const ( + // StabilityStable indicates 100% pass rate + StabilityStable StabilityClass = "stable" + // StabilityMostlyStable indicates 80-99% pass rate + StabilityMostlyStable StabilityClass = "mostly_stable" + // StabilityUnstable indicates 50-79% pass rate + StabilityUnstable StabilityClass = "unstable" + // StabilityHighlyUnstable indicates < 50% pass rate + StabilityHighlyUnstable StabilityClass = "highly_unstable" +) + +// InputMode represents the input mode for test cases +type InputMode string + +const ( + // InputModeFile indicates input from a JSONL file + InputModeFile InputMode = "file" + // InputModeMessage indicates input from a direct message string + InputModeMessage InputMode = "message" +) + +// Options represents the configuration options for running tests +type Options struct { + // Input/Output + // =============================== + + // Input is the input source: either a file path or a direct message + Input string `json:"input"` + + // InputMode is the input mode (auto-detected from Input) + InputMode InputMode `json:"input_mode"` + + // OutputFile is the path to write the test report + // Format is determined by file extension (.json, .html, .md) + OutputFile string `json:"output_file"` + + // Agent Selection + // =============================== + + // AgentID is the explicit agent ID to test (optional) + // If not set, agent is resolved from InputFile path + AgentID string `json:"agent_id,omitempty"` + + // Connector overrides the agent's default connector (optional) + Connector string `json:"connector,omitempty"` + + // Test Environment + // =============================== + + // UserID is the test user ID (-u flag) + UserID string `json:"user_id,omitempty"` + + // TeamID is the test team ID (-t flag) + TeamID string `json:"team_id,omitempty"` + + // Locale is the locale for the test context (default: "en-us") + Locale string `json:"locale,omitempty"` + + // Execution + // =============================== + + // Timeout is the default timeout for each test case + // Can be overridden per test case + Timeout time.Duration `json:"timeout,omitempty"` + + // Parallel is the number of tests to run in parallel + // Default is 1 (sequential execution) + Parallel int `json:"parallel,omitempty"` + + // Runs is the number of times to run each test case + // Default is 1. When > 1, stability metrics are collected + Runs int `json:"runs,omitempty"` + + // Reporting + // =============================== + + // ReporterID is the reporter agent ID for custom report generation + // If not set, default JSONL format is used + ReporterID string `json:"reporter_id,omitempty"` + + // Behavior + // =============================== + + // Verbose enables verbose output during test execution + Verbose bool `json:"verbose,omitempty"` + + // FailFast stops execution on first failure + FailFast bool `json:"fail_fast,omitempty"` +} + +// Environment configures the test execution context +type Environment struct { + // UserID is the user ID for authorized info (-u flag) + UserID string `json:"user_id"` + + // TeamID is the team ID for authorized info (-t flag) + TeamID string `json:"team_id"` + + // Locale is the locale (default: "en-us") + Locale string `json:"locale"` + + // ClientType is the client type (default: "test") + ClientType string `json:"client_type"` + + // ClientIP is the client IP (default: "127.0.0.1") + ClientIP string `json:"client_ip"` + + // Referer is the request referer (default: "test") + Referer string `json:"referer"` + + // Accept is the accept format (default: "standard") + Accept string `json:"accept"` +} + +// NewEnvironment creates a new test environment with defaults +func NewEnvironment(userID, teamID string) *Environment { + env := &Environment{ + UserID: userID, + TeamID: teamID, + Locale: "en-us", + ClientType: "test", + ClientIP: "127.0.0.1", + Referer: "test", + Accept: "standard", + } + + // Apply defaults if not set + if env.UserID == "" { + env.UserID = "test-user" + } + if env.TeamID == "" { + env.TeamID = "test-team" + } + + return env +} + +// Case represents a single test case loaded from JSONL +type Case struct { + // ID is the unique identifier for this test case (e.g., "T001") + ID string `json:"id"` + + // Input is the test input, can be: + // - string: simple text input + // - map (Message): single message with role and content + // - []map ([]Message): conversation history + Input interface{} `json:"input"` + + // Expected is the expected output for validation (optional) + // If set, the actual output will be compared against this + Expected interface{} `json:"expected,omitempty"` + + // Assert defines custom assertion rules (optional) + // If set, these rules will be used instead of simple expected comparison + // Can be a single assertion or an array of assertions + Assert interface{} `json:"assert,omitempty"` + + // Environment (per-test case, can be overridden by command line flags) + // =============================== + + // UserID is the user ID for this test case (overridden by -u flag) + UserID string `json:"user,omitempty"` + + // TeamID is the team ID for this test case (overridden by -t flag) + TeamID string `json:"team,omitempty"` + + // Metadata contains additional metadata for the test case + Metadata map[string]interface{} `json:"metadata,omitempty"` + + // Skip indicates whether to skip this test case + Skip bool `json:"skip,omitempty"` + + // Timeout overrides the default timeout for this test case + // Format: "30s", "1m", "2m30s" + Timeout string `json:"timeout,omitempty"` +} + +// Assertion represents a single assertion rule +type Assertion struct { + // Type is the assertion type: + // - "equals": exact match (default if expected is set) + // - "contains": output contains the expected string/value + // - "not_contains": output does not contain the string/value + // - "json_path": extract value using JSON path and compare + // - "regex": match output against regex pattern + // - "script": run a custom assertion script + // - "type": check output type (string, object, array, number, boolean) + // - "schema": validate against JSON schema + Type string `json:"type"` + + // Value is the expected value or pattern (depends on type) + Value interface{} `json:"value,omitempty"` + + // Path is the JSON path for json_path assertions (e.g., "$.need_search") + Path string `json:"path,omitempty"` + + // Script is the assertion script name for script assertions + // The script receives (output, input, expected) and returns {pass: bool, message: string} + Script string `json:"script,omitempty"` + + // Message is a custom failure message + Message string `json:"message,omitempty"` + + // Negate inverts the assertion result + Negate bool `json:"negate,omitempty"` +} + +// AssertionResult represents the result of an assertion +type AssertionResult struct { + // Passed indicates whether the assertion passed + Passed bool `json:"passed"` + + // Message describes the assertion result + Message string `json:"message,omitempty"` + + // Assertion is the original assertion that was evaluated + Assertion *Assertion `json:"assertion,omitempty"` + + // Actual is the actual value that was compared + Actual interface{} `json:"actual,omitempty"` + + // Expected is the expected value + Expected interface{} `json:"expected,omitempty"` +} + +// GetEnvironment returns the effective test environment for this test case +// Priority: command line flags > test case fields > defaults +func (tc *Case) GetEnvironment(opts *Options) *Environment { + env := NewEnvironment("", "") + + // Apply test case specific values + if tc.UserID != "" { + env.UserID = tc.UserID + } + if tc.TeamID != "" { + env.TeamID = tc.TeamID + } + + // Apply command line overrides (highest priority) + if opts != nil { + if opts.UserID != "" { + env.UserID = opts.UserID + } + if opts.TeamID != "" { + env.TeamID = opts.TeamID + } + if opts.Locale != "" { + env.Locale = opts.Locale + } + } + + return env +} + +// GetMessages converts the Input to a slice of context.Message +// This handles all input formats: string, Message, []Message +func (tc *Case) GetMessages() ([]context.Message, error) { + return ParseInput(tc.Input) +} + +// GetTimeout returns the timeout duration for this test case +// Returns the override timeout if set, otherwise returns the default +func (tc *Case) GetTimeout(defaultTimeout time.Duration) time.Duration { + if tc.Timeout == "" { + return defaultTimeout + } + d, err := time.ParseDuration(tc.Timeout) + if err != nil { + return defaultTimeout + } + return d +} + +// Result represents the result of running a single test case +type Result struct { + // ID is the test case identifier + ID string `json:"id"` + + // Status is the test execution status + Status Status `json:"status"` + + // Input is the original test input (for reference in reports) + Input interface{} `json:"input"` + + // Output is the actual output from the agent + Output interface{} `json:"output,omitempty"` + + // Expected is the expected output (if specified in test case) + Expected interface{} `json:"expected,omitempty"` + + // DurationMs is the execution duration in milliseconds + DurationMs int64 `json:"duration_ms"` + + // Error contains the error message if status is failed/error/timeout + Error string `json:"error,omitempty"` + + // Metadata contains additional result metadata + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// RunDetail represents the result of a single run in stability testing +type RunDetail struct { + // Run is the run number (1-based) + Run int `json:"run"` + + // Status is the execution status for this run + Status Status `json:"status"` + + // DurationMs is the execution duration in milliseconds + DurationMs int64 `json:"duration_ms"` + + // Output is the output from this run + Output interface{} `json:"output,omitempty"` + + // Error contains the error message if this run failed + Error string `json:"error,omitempty"` +} + +// StabilityResult represents the stability analysis result for a test case +type StabilityResult struct { + // ID is the test case identifier + ID string `json:"id"` + + // Input is the original test input + Input interface{} `json:"input"` + + // Expected is the expected output (if specified) + Expected interface{} `json:"expected,omitempty"` + + // Runs is the total number of runs + Runs int `json:"runs"` + + // Passed is the number of runs that passed + Passed int `json:"passed"` + + // Failed is the number of runs that failed + Failed int `json:"failed"` + + // PassRate is the pass rate percentage (0-100) + PassRate float64 `json:"pass_rate"` + + // Consistency is a measure of output consistency (0-1) + // 1.0 means all outputs are identical, lower values indicate variation + Consistency float64 `json:"consistency"` + + // Stable indicates whether the test is considered stable + Stable bool `json:"stable"` + + // StabilityClass is the stability classification + StabilityClass StabilityClass `json:"stability_class"` + + // Timing statistics + AvgDurationMs float64 `json:"avg_duration_ms"` + MinDurationMs int64 `json:"min_duration_ms"` + MaxDurationMs int64 `json:"max_duration_ms"` + StdDeviationMs float64 `json:"std_deviation_ms"` + + // RunDetails contains details for each run + RunDetails []*RunDetail `json:"run_details"` +} + +// CalculateStability calculates stability metrics from run details +func (sr *StabilityResult) CalculateStability() { + if len(sr.RunDetails) == 0 { + return + } + + sr.Runs = len(sr.RunDetails) + sr.Passed = 0 + sr.Failed = 0 + + var totalDuration int64 + sr.MinDurationMs = math.MaxInt64 + sr.MaxDurationMs = 0 + + for _, rd := range sr.RunDetails { + if rd.Status == StatusPassed { + sr.Passed++ + } else { + sr.Failed++ + } + + totalDuration += rd.DurationMs + if rd.DurationMs < sr.MinDurationMs { + sr.MinDurationMs = rd.DurationMs + } + if rd.DurationMs > sr.MaxDurationMs { + sr.MaxDurationMs = rd.DurationMs + } + } + + // Calculate pass rate + sr.PassRate = float64(sr.Passed) / float64(sr.Runs) * 100 + + // Calculate average duration + sr.AvgDurationMs = float64(totalDuration) / float64(sr.Runs) + + // Calculate standard deviation + var sumSquares float64 + for _, rd := range sr.RunDetails { + diff := float64(rd.DurationMs) - sr.AvgDurationMs + sumSquares += diff * diff + } + sr.StdDeviationMs = math.Sqrt(sumSquares / float64(sr.Runs)) + + // Determine stability classification + sr.StabilityClass = ClassifyStability(sr.PassRate) + sr.Stable = sr.PassRate == 100 + + // Calculate consistency (simplified: based on pass rate) + sr.Consistency = sr.PassRate / 100 +} + +// ClassifyStability returns the stability classification based on pass rate +func ClassifyStability(passRate float64) StabilityClass { + switch { + case passRate == 100: + return StabilityStable + case passRate >= 80: + return StabilityMostlyStable + case passRate >= 50: + return StabilityUnstable + default: + return StabilityHighlyUnstable + } +} + +// Summary contains aggregated statistics for the test run +type Summary struct { + // Total number of test cases + Total int `json:"total"` + + // Passed number of test cases that passed + Passed int `json:"passed"` + + // Failed number of test cases that failed + Failed int `json:"failed"` + + // Skipped number of test cases that were skipped + Skipped int `json:"skipped"` + + // Errors number of test cases with runtime errors + Errors int `json:"errors"` + + // Timeouts number of test cases that timed out + Timeouts int `json:"timeouts"` + + // DurationMs is the total execution duration in milliseconds + DurationMs int64 `json:"duration_ms"` + + // AgentID is the ID of the agent being tested + AgentID string `json:"agent_id"` + + // AgentPath is the file path of the agent (for path-based resolution) + AgentPath string `json:"agent_path,omitempty"` + + // Connector is the connector used for the test + Connector string `json:"connector"` + + // Stability metrics (when Runs > 1) + // =============================== + + // RunsPerCase is the number of runs per test case + RunsPerCase int `json:"runs_per_case,omitempty"` + + // TotalRuns is the total number of runs (Total * RunsPerCase) + TotalRuns int `json:"total_runs,omitempty"` + + // OverallPassRate is the overall pass rate percentage + OverallPassRate float64 `json:"overall_pass_rate,omitempty"` + + // StableCases is the number of cases with 100% pass rate + StableCases int `json:"stable_cases,omitempty"` + + // UnstableCases is the number of cases with < 100% pass rate + UnstableCases int `json:"unstable_cases,omitempty"` +} + +// Report represents the complete test report +type Report struct { + // Summary contains aggregated statistics + Summary *Summary `json:"summary"` + + // Environment contains the test environment configuration + Environment *Environment `json:"environment,omitempty"` + + // Results contains individual test results (for single run) + Results []*Result `json:"results,omitempty"` + + // StabilityResults contains stability analysis results (for multiple runs) + StabilityResults []*StabilityResult `json:"stability_results,omitempty"` + + // Metadata contains additional report metadata + Metadata *ReportMetadata `json:"metadata"` +} + +// ReportMetadata contains metadata about the test report +type ReportMetadata struct { + // StartedAt is when the test run started + StartedAt time.Time `json:"started_at"` + + // CompletedAt is when the test run completed + CompletedAt time.Time `json:"completed_at"` + + // Version is the Yao version + Version string `json:"version"` + + // InputFile is the path to the input file + InputFile string `json:"input_file"` + + // OutputFile is the path to the output file + OutputFile string `json:"output_file"` + + // Options contains the test options used + Options *Options `json:"options,omitempty"` +} + +// HasFailures returns true if there are any failed, error, or timeout tests +func (r *Report) HasFailures() bool { + return r.Summary.Failed > 0 || r.Summary.Errors > 0 || r.Summary.Timeouts > 0 +} + +// PassRate returns the pass rate as a percentage (0-100) +func (r *Report) PassRate() float64 { + if r.Summary.Total == 0 { + return 0 + } + return float64(r.Summary.Passed) / float64(r.Summary.Total) * 100 +} + +// IsStabilityTest returns true if this is a stability test (multiple runs) +func (r *Report) IsStabilityTest() bool { + return r.Summary.RunsPerCase > 1 +} + +// AgentInfo contains information about the agent being tested +type AgentInfo struct { + // ID is the agent identifier + ID string `json:"id"` + + // Name is the human-readable name + Name string `json:"name"` + + // Description is the agent description + Description string `json:"description,omitempty"` + + // Path is the file system path to the agent + Path string `json:"path"` + + // Connector is the default connector + Connector string `json:"connector"` + + // Type is the agent type (e.g., "worker", "assistant") + Type string `json:"type,omitempty"` +} + +// ReporterInput is the input passed to a custom reporter agent +type ReporterInput struct { + // Report is the test report to format + Report *Report `json:"report"` + + // Format is the desired output format + Format string `json:"format"` + + // Options contains additional formatting options + Options *ReporterOptions `json:"options,omitempty"` +} + +// ReporterOptions contains options for custom reporter agents +type ReporterOptions struct { + // Verbose includes detailed output in the report + Verbose bool `json:"verbose,omitempty"` + + // IncludeOutputs includes full outputs in the report + IncludeOutputs bool `json:"include_outputs,omitempty"` + + // IncludeInputs includes full inputs in the report + IncludeInputs bool `json:"include_inputs,omitempty"` + + // MaxOutputLength limits the output length in the report + MaxOutputLength int `json:"max_output_length,omitempty"` + + // Theme is the report theme (for HTML reports) + Theme string `json:"theme,omitempty"` + + // Title is the report title + Title string `json:"title,omitempty"` +} diff --git a/cmd/README.md b/cmd/README.md new file mode 100644 index 00000000..544a96c5 --- /dev/null +++ b/cmd/README.md @@ -0,0 +1,373 @@ +# Yao CLI Commands + +The Yao CLI provides a set of commands for managing, running, and testing Yao applications. + +## Installation + +```bash +# Build from source +go build -o yao . + +# Or install via go install +go install github.com/yaoapp/yao@latest +``` + +## Global Flags + +| Flag | Short | Description | +|------|-------|-------------| +| `--app` | `-a` | Application directory path | +| `--file` | `-f` | Application package file (.yaz) | +| `--key` | `-k` | Application license key | + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `YAO_ROOT` | Application root directory | +| `YAO_LANG` | Language setting (e.g., `zh-CN` for Chinese) | + +## Commands + +### `yao start` + +Start the Yao application engine. + +```bash +# Start in current directory +yao start + +# Start with specific app directory +yao start -a /path/to/app + +# Start in debug mode +yao start --debug +``` + +**Flags:** + +| Flag | Description | +|------|-------------| +| `--debug` | Enable development/debug mode | +| `--disable-watching` | Disable file watching | + +--- + +### `yao run` + +Execute a Yao process. + +```bash +# Run a process +yao run models.user.Find 1 + +# Run with JSON arguments +yao run models.user.Create '::[{"name":"John","age":30}]' + +# Run in silent mode (JSON output only) +yao run -s models.user.Find 1 +``` + +**Flags:** + +| Flag | Short | Description | +|------|-------|-------------| +| `--silent` | `-s` | Silent mode - output result as JSON only | + +**Argument Syntax:** + +- Regular arguments: `arg1 arg2` +- JSON arguments: `'::[{"key":"value"}]'` (prefix with `::`) +- Escaped `::`: `'\::literal'` + +--- + +### `yao migrate` + +Update database schema based on model definitions. + +```bash +# Migrate all models +yao migrate + +# Migrate specific model +yao migrate -n user + +# Force migrate in production mode +yao migrate --force + +# Reset (drop and recreate) tables +yao migrate --reset +``` + +**Flags:** + +| Flag | Short | Description | +|------|-------|-------------| +| `--name` | `-n` | Specific model name to migrate | +| `--force` | | Force migrate in production mode | +| `--reset` | | Drop tables before migration | + +--- + +### `yao inspect` + +Display application configuration. + +```bash +yao inspect +``` + +--- + +### `yao version` + +Show Yao version information. + +```bash +# Show version +yao version + +# Show all version details +yao version --all +``` + +**Flags:** + +| Flag | Description | +|------|-------------| +| `--all` | Print all version information (Go version, commit, build time, etc.) | + +--- + +## Agent Commands + +Commands for testing and managing AI agents. + +### `yao agent test` + +Test an agent with input cases from a JSONL file or direct message. + +```bash +# Test with direct message (development mode) +yao agent test -i "Extract keywords from: AI and machine learning" -n workers.system.keyword + +# Test with JSONL file +yao agent test -i tests/inputs.jsonl + +# Test with custom output file +yao agent test -i tests/inputs.jsonl -o report.html + +# Test with specific connector +yao agent test -i tests/inputs.jsonl -c openai.gpt4 + +# Stability testing (multiple runs) +yao agent test -i tests/inputs.jsonl --runs 5 + +# Parallel execution +yao agent test -i tests/inputs.jsonl --parallel 4 + +# Verbose output +yao agent test -i tests/inputs.jsonl -v +``` + +**Flags:** + +| Flag | Short | Description | +|------|-------|-------------| +| `--input` | `-i` | Input: JSONL file path or direct message (required) | +| `--output` | `-o` | Output file path (default: `output-{timestamp}.jsonl`) | +| `--name` | `-n` | Agent ID (default: auto-detect from path) | +| `--connector` | `-c` | Override default connector | +| `--user` | `-u` | Test user ID (default: `test-user`) | +| `--team` | `-t` | Test team ID (default: `test-team`) | +| `--reporter` | `-r` | Reporter agent ID for custom report generation | +| `--runs` | | Number of runs per test case for stability analysis (default: 1) | +| `--timeout` | | Timeout per test case (default: `5m`) | +| `--parallel` | | Number of parallel test cases (default: 1) | +| `--verbose` | `-v` | Enable verbose output | +| `--fail-fast` | | Stop on first failure | +| `--app` | `-a` | Application directory | +| `--env` | `-e` | Environment file | + +**Input Modes:** + +1. **Direct Message Mode**: For quick development/debugging + ```bash + yao agent test -i "Hello world" -n my.agent + ``` + - Outputs result directly to stdout + - No report file generated + - Ideal for iterative development + +2. **File Mode**: For comprehensive testing + ```bash + yao agent test -i tests/inputs.jsonl + ``` + - Reads test cases from JSONL file + - Generates detailed report + - Supports stability analysis + +**JSONL Input Format:** + +```jsonl +{"id": "T001", "input": "Simple text input"} +{"id": "T002", "input": {"role": "user", "content": "Message with role"}} +{"id": "T003", "input": [{"role": "system", "content": "System prompt"}, {"role": "user", "content": "User message"}]} +{"id": "T004", "input": "Test with timeout", "timeout": "30s"} +{"id": "T005", "input": "Skip this test", "skip": true} +{"id": "T006", "input": "Test with specific user", "user": "alice", "team": "engineering"} +``` + +**Output Formats:** + +| Extension | Format | Description | +|-----------|--------|-------------| +| `.jsonl` | JSONL | Streaming format (default) | +| `.json` | JSON | Complete structured report | +| `.md` | Markdown | Human-readable with tables | +| `.html` | HTML | Interactive web report | + +**Agent Resolution:** + +The agent is resolved in the following priority order: + +1. Explicit `-n` flag: `yao agent test -i msg -n my.agent` +2. `YAO_ROOT` environment variable +3. Auto-detect from input file path (traverses up to find `package.yao`) +4. Auto-detect from current working directory + +--- + +## SUI Commands + +SUI (Serverless UI) template engine commands. + +### `yao sui watch` + +Auto-build templates when files change. + +```bash +yao sui watch [data] + +# Example +yao sui watch default index '::{}' +``` + +### `yao sui build` + +Build a template. + +```bash +yao sui build [data] + +# Example +yao sui build default index '::{}' + +# Debug mode +yao sui build default index '::{}' --debug +``` + +### `yao sui trans` + +Translate template content. + +```bash +yao sui trans + +# With specific locales +yao sui trans default index -l "en-US,zh-CN,ja-JP" +``` + +**SUI Flags:** + +| Flag | Short | Description | +|------|-------|-------------| +| `--data` | `-d` | Session data as JSON (prefix with `::`) | +| `--debug` | `-D` | Enable debug mode | +| `--locales` | `-l` | Locales for translation (comma-separated) | + +--- + +## Examples + +### Development Workflow + +```bash +# Start development server +yao start --debug + +# Run a process +yao run scripts.test.Hello "World" + +# Test an agent interactively +yao agent test -i "What is the weather today?" -n assistant.weather + +# Watch and auto-build templates +yao sui watch default home +``` + +### Testing Workflow + +```bash +# Run comprehensive agent tests +yao agent test -i tests/inputs.jsonl -o report.html -v + +# Stability analysis (run each test 10 times) +yao agent test -i tests/inputs.jsonl --runs 10 -o stability-report.json + +# Parallel testing with timeout +yao agent test -i tests/inputs.jsonl --parallel 4 --timeout 2m + +# CI/CD integration +yao agent test -i tests/inputs.jsonl -o results.jsonl && echo "Tests passed" +``` + +### Database Migration + +```bash +# Migrate all models +yao migrate + +# Migrate specific model with reset +yao migrate -n user --reset --force +``` + +--- + +## Exit Codes + +| Code | Description | +|------|-------------| +| 0 | Success | +| 1 | Error or test failure | + +--- + +## Directory Structure + +``` +myapp/ +├── app.yao # Application configuration +├── .env # Environment variables +├── models/ # Data models +├── apis/ # API definitions +├── flows/ # Business flows +├── scripts/ # JavaScript/TypeScript scripts +├── assistants/ # AI agents +│ └── my-agent/ +│ ├── package.yao # Agent configuration +│ ├── prompts.yml # Agent prompts +│ └── tests/ +│ └── inputs.jsonl # Test cases +└── public/ # Static files +``` + +--- + +## See Also + +- [Yao Documentation](https://yaoapps.com/docs) +- [Agent Test Design](../agent/test/DESIGN.md) +- [SUI Documentation](https://yaoapps.com/docs/sui) + diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go new file mode 100644 index 00000000..d5fcffaf --- /dev/null +++ b/cmd/agent/agent.go @@ -0,0 +1,72 @@ +package agent + +import ( + "os" + "path/filepath" + + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/yao/config" +) + +var appPath string +var envFile string + +var langs = map[string]string{ + "Test an agent with input cases": "使用测试用例测试智能体", + "Test an agent with input cases from JSONL file or direct message": "使用 JSONL 文件或直接消息测试智能体", + "Application directory": "应用目录", + "Environment file": "环境变量文件", + "Input: JSONL file path or message (required)": "输入: JSONL 文件路径或消息 (必需)", + "Path to output file (default: output-{timestamp}.jsonl)": "输出文件路径 (默认: output-{timestamp}.jsonl)", + "Explicit agent ID (default: auto-detect)": "指定智能体 ID (默认: 自动检测)", + "Override connector": "覆盖连接器", + "Test user ID (default: test-user)": "测试用户 ID (默认: test-user)", + "Test team ID (default: test-team)": "测试团队 ID (默认: test-team)", + "Reporter agent ID for custom report": "自定义报告生成器智能体 ID", + "Number of runs for stability analysis": "稳定性分析的运行次数", + "Default timeout per test case": "每个测试用例的默认超时时间", + "Number of parallel test cases": "并行测试用例数", + "Verbose output": "详细输出", + "Stop on first failure": "遇到第一个失败时停止", + "Error: input is required (-i flag)": "错误: 需要输入 (-i 参数)", + "Error: failed to get current directory": "错误: 获取当前目录失败", + "Error: agent (-n) is required when using direct message input and not in an agent directory": "错误: 使用直接消息输入且不在智能体目录时需要指定 -n 参数", + "Hint: Make sure you're in a Yao application directory or specify --app flag": "提示: 确保在 Yao 应用目录中或使用 --app 参数指定", + "Error: invalid timeout format": "错误: 无效的超时格式", +} + +// L Language switch +func L(words string) string { + var lang = os.Getenv("YAO_LANG") + if lang == "" { + return words + } + + if trans, has := langs[words]; has { + return trans + } + return words +} + +// Boot sets the configuration +func Boot() { + root := config.Conf.Root + if appPath != "" { + r, err := filepath.Abs(appPath) + if err != nil { + exception.New("Root error %s", 500, err.Error()).Throw() + } + root = r + } + if envFile != "" { + config.Conf = config.LoadFrom(envFile) + } else { + config.Conf = config.LoadFrom(filepath.Join(root, ".env")) + } + + if config.Conf.Mode == "production" { + config.Production() + } else if config.Conf.Mode == "development" { + config.Development() + } +} diff --git a/cmd/agent/test.go b/cmd/agent/test.go new file mode 100644 index 00000000..3098a5e3 --- /dev/null +++ b/cmd/agent/test.go @@ -0,0 +1,244 @@ +package agent + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/yaoapp/gou/plugin" + "github.com/yaoapp/yao/agent" + "github.com/yaoapp/yao/agent/test" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/engine" + "github.com/yaoapp/yao/kb" + "github.com/yaoapp/yao/share" +) + +// Test command flags +var ( + testInput string + testOutput string + testAgent string + testConnector string + testUser string + testTeam string + testReporter string + testRuns int + testTimeout string + testParallel int + testVerbose bool + testFailFast bool +) + +// TestCmd is the agent test command +var TestCmd = &cobra.Command{ + Use: "test", + Short: L("Test an agent with input cases"), + Long: L("Test an agent with input cases from JSONL file or direct message"), + Run: func(cmd *cobra.Command, args []string) { + defer share.SessionStop() + defer plugin.KillAll() + + // Validate input + if testInput == "" { + color.Red(L("Error: input is required (-i flag)") + "\n") + os.Exit(1) + } + + // Detect input mode + inputMode := test.DetectInputMode(testInput) + + // For message mode, agent must be specified or resolvable from cwd + if inputMode == test.InputModeMessage && testAgent == "" { + // Try to find app root from current directory + cwd, err := os.Getwd() + if err != nil { + color.Red(L("Error: failed to get current directory")+": %s\n", err.Error()) + os.Exit(1) + } + + // Try to find package.yao from cwd + resolver := test.NewResolver() + _, err = resolver.ResolveFromPath(cwd) + if err != nil { + color.Red(L("Error: agent (-n) is required when using direct message input and not in an agent directory") + "\n") + os.Exit(1) + } + } + + // Find app root directory + // Priority: -a flag > YAO_ROOT env > auto-detect from path + var err error + + if appPath == "" { + // Check YAO_ROOT environment variable + if yaoRoot := os.Getenv("YAO_ROOT"); yaoRoot != "" { + appPath = yaoRoot + } + } + + if appPath == "" { + // Auto-detect from path + if inputMode == test.InputModeFile { + // For file mode, find app root from input file path + appPath, err = findAppRoot(testInput) + } else { + // For message mode, find app root from current directory + cwd, _ := os.Getwd() + appPath, err = findAppRoot(cwd) + } + + if err != nil { + color.Red("Error: %s\n", err.Error()) + color.Yellow(L("Hint: Make sure you're in a Yao application directory or specify --app flag") + "\n") + os.Exit(1) + } + } + + // Boot the application + Boot() + + // Set Runtime Mode + config.Conf.Runtime.Mode = "standard" + cfg := config.Conf + cfg.Session.IsCLI = true + + // Load engine + _, err = engine.Load(cfg, engine.LoadOption{Action: "agent-test"}) + if err != nil { + color.Red("Engine: %s\n", err.Error()) + os.Exit(1) + } + + // Load KB (required for agent KB features) + _, err = kb.Load(cfg) + if err != nil { + color.Red("KB: %s\n", err.Error()) + os.Exit(1) + } + + // Load agent + err = agent.Load(cfg) + if err != nil { + color.Red("Agent: %s\n", err.Error()) + os.Exit(1) + } + + // Parse timeout + timeout := 5 * time.Minute + if testTimeout != "" { + d, err := time.ParseDuration(testTimeout) + if err != nil { + color.Red(L("Error: invalid timeout format")+": %s\n", testTimeout) + os.Exit(1) + } + timeout = d + } + + // Build test options + opts := &test.Options{ + Input: testInput, + InputMode: inputMode, + OutputFile: testOutput, + AgentID: testAgent, + Connector: testConnector, + UserID: testUser, + TeamID: testTeam, + ReporterID: testReporter, + Runs: testRuns, + Timeout: timeout, + Parallel: testParallel, + Verbose: testVerbose, + FailFast: testFailFast, + } + + // Merge with defaults + opts = test.MergeOptions(opts, test.DefaultOptions()) + + // Resolve output path (only for file mode, direct message mode outputs to stdout) + if inputMode == test.InputModeFile { + opts.OutputFile = test.ResolveOutputPath(opts) + } + + // Run tests + runner := test.NewRunner(opts) + report, err := runner.Run() + if err != nil { + color.Red("Error: %s\n", err.Error()) + os.Exit(1) + } + + // Exit with appropriate code + if report.HasFailures() { + os.Exit(1) + } + }, +} + +// findAppRoot finds the Yao application root directory by looking for app.yao +// It traverses up from the given path until it finds app.yao or reaches the filesystem root +func findAppRoot(startPath string) (string, error) { + // Get absolute path + absPath, err := filepath.Abs(startPath) + if err != nil { + return "", fmt.Errorf("failed to get absolute path: %w", err) + } + + // If it's a file, start from its directory + info, err := os.Stat(absPath) + if err != nil { + return "", fmt.Errorf("path not found: %s", absPath) + } + + var dir string + if info.IsDir() { + dir = absPath + } else { + dir = filepath.Dir(absPath) + } + + // Traverse up to find app.yao + for { + // Check for app.yao, app.json, or app.jsonc + for _, appFile := range []string{"app.yao", "app.json", "app.jsonc"} { + appFilePath := filepath.Join(dir, appFile) + if _, err := os.Stat(appFilePath); err == nil { + return dir, nil + } + } + + // Move to parent directory + parent := filepath.Dir(dir) + if parent == dir { + // Reached root, no app.yao found + break + } + dir = parent + } + + return "", fmt.Errorf("no app.yao found in path hierarchy of %s", startPath) +} + +func init() { + // Test command flags + TestCmd.Flags().StringVarP(&appPath, "app", "a", "", L("Application directory")) + TestCmd.Flags().StringVarP(&envFile, "env", "e", "", L("Environment file")) + TestCmd.Flags().StringVarP(&testInput, "input", "i", "", L("Input: JSONL file path or message (required)")) + TestCmd.Flags().StringVarP(&testOutput, "output", "o", "", L("Path to output file (default: output-{timestamp}.jsonl)")) + TestCmd.Flags().StringVarP(&testAgent, "name", "n", "", L("Explicit agent ID (default: auto-detect)")) + TestCmd.Flags().StringVarP(&testConnector, "connector", "c", "", L("Override connector")) + TestCmd.Flags().StringVarP(&testUser, "user", "u", "", L("Test user ID (default: test-user)")) + TestCmd.Flags().StringVarP(&testTeam, "team", "t", "", L("Test team ID (default: test-team)")) + TestCmd.Flags().StringVarP(&testReporter, "reporter", "r", "", L("Reporter agent ID for custom report")) + TestCmd.Flags().IntVar(&testRuns, "runs", 1, L("Number of runs for stability analysis")) + TestCmd.Flags().StringVar(&testTimeout, "timeout", "5m", L("Default timeout per test case")) + TestCmd.Flags().IntVar(&testParallel, "parallel", 1, L("Number of parallel test cases")) + TestCmd.Flags().BoolVarP(&testVerbose, "verbose", "v", false, L("Verbose output")) + TestCmd.Flags().BoolVar(&testFailFast, "fail-fast", false, L("Stop on first failure")) + + // Mark input as required + TestCmd.MarkFlagRequired("input") +} diff --git a/cmd/root.go b/cmd/root.go index f5a62e13..b89f4c96 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/yaoapp/kun/exception" + "github.com/yaoapp/yao/cmd/agent" "github.com/yaoapp/yao/cmd/sui" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/pack" @@ -84,7 +85,6 @@ var rootCmd = &cobra.Command{ Use: share.BUILDNAME, Short: "Yao App Engine", Long: `Yao App Engine`, - Args: cobra.MinimumNArgs(1), CompletionOptions: cobra.CompletionOptions{ DisableDefaultCmd: true, }, @@ -93,10 +93,14 @@ var rootCmd = &cobra.Command{ switch args[0] { case "fuxi": fuxi() + return } + fmt.Fprintln(os.Stderr, L("One or more arguments are not correct"), args) + os.Exit(1) + return } - fmt.Fprintln(os.Stderr, L("One or more arguments are not correct"), args) - os.Exit(1) + // No arguments - show help + cmd.Help() }, } @@ -104,13 +108,11 @@ var studioCmd = &cobra.Command{ Use: "studio", Short: "Yao Studio CLI", Long: `Yao Studio CLI`, - Args: cobra.MinimumNArgs(1), CompletionOptions: cobra.CompletionOptions{ DisableDefaultCmd: true, }, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintln(os.Stderr, L("One or more arguments are not correct"), args) - os.Exit(1) + cmd.Help() }, } @@ -118,13 +120,23 @@ var suiCmd = &cobra.Command{ Use: "sui", Short: L("SUI Template Engine"), Long: L("SUI Template Engine"), - Args: cobra.MinimumNArgs(1), CompletionOptions: cobra.CompletionOptions{ DisableDefaultCmd: true, }, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintln(os.Stderr, L("One or more arguments are not correct"), args) - os.Exit(1) + cmd.Help() + }, +} + +var agentCmd = &cobra.Command{ + Use: "agent", + Short: L("Agent commands"), + Long: L("Agent commands for testing and management"), + CompletionOptions: cobra.CompletionOptions{ + DisableDefaultCmd: true, + }, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() }, } @@ -138,6 +150,9 @@ func init() { suiCmd.AddCommand(sui.BuildCmd) suiCmd.AddCommand(sui.TransCmd) + // Agent + agentCmd.AddCommand(agent.TestCmd) + rootCmd.AddCommand( versionCmd, migrateCmd, @@ -152,6 +167,7 @@ func init() { // packCmd, // studioCmd, suiCmd, + agentCmd, // upgradeCmd, ) // rootCmd.SetHelpCommand(helpCmd) diff --git a/cmd/run.go b/cmd/run.go index d1ce8c30..aa840ae8 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -3,6 +3,8 @@ package cmd import ( "context" "fmt" + "os" + "path/filepath" "strings" "github.com/fatih/color" @@ -40,6 +42,16 @@ var runCmd = &cobra.Command{ } }() + // Auto-detect app root if not specified + if appPath == "" { + cwd, err := os.Getwd() + if err == nil { + if root, err := findAppRootFromPath(cwd); err == nil { + appPath = root + } + } + } + Boot() // Set Runtime Mode @@ -174,3 +186,47 @@ var runCmd = &cobra.Command{ func init() { runCmd.PersistentFlags().BoolVarP(&runSilent, "silent", "s", false, L("Silent mode")) } + +// findAppRootFromPath finds the Yao application root directory by looking for app.yao +// It traverses up from the given path until it finds app.yao or reaches the filesystem root +func findAppRootFromPath(startPath string) (string, error) { + // Get absolute path + absPath, err := filepath.Abs(startPath) + if err != nil { + return "", fmt.Errorf("failed to get absolute path: %w", err) + } + + // If it's a file, start from its directory + info, err := os.Stat(absPath) + if err != nil { + return "", fmt.Errorf("path not found: %s", absPath) + } + + var dir string + if info.IsDir() { + dir = absPath + } else { + dir = filepath.Dir(absPath) + } + + // Traverse up to find app.yao + for { + // Check for app.yao, app.json, or app.jsonc + for _, appFile := range []string{"app.yao", "app.json", "app.jsonc"} { + appFilePath := filepath.Join(dir, appFile) + if _, err := os.Stat(appFilePath); err == nil { + return dir, nil + } + } + + // Move to parent directory + parent := filepath.Dir(dir) + if parent == dir { + // Reached root, no app.yao found + break + } + dir = parent + } + + return "", fmt.Errorf("no app.yao found in path hierarchy of %s", startPath) +} diff --git a/data/bindata.go b/data/bindata.go index 2932a0f1..e9cf4f75 100644 --- a/data/bindata.go +++ b/data/bindata.go @@ -335,7 +335,7 @@ func cuiSetupIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -355,7 +355,7 @@ func cuiV09IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -375,7 +375,7 @@ func cuiV10IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -395,7 +395,7 @@ func cuiV10Layouts__indexAsyncJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -415,7 +415,7 @@ func cuiV10UmiJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -435,7 +435,7 @@ func initEnv() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.env", size: 219, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.env", size: 219, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -455,7 +455,7 @@ func initVscodeSettingsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -475,7 +475,7 @@ func initVscodeTypesRuntimeConsoleDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -495,7 +495,7 @@ func initVscodeTypesRuntimeExceptionDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -515,7 +515,7 @@ func initVscodeTypesRuntimeFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -535,7 +535,7 @@ func initVscodeTypesRuntimeGlobalDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -555,7 +555,7 @@ func initVscodeTypesRuntimeHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -575,7 +575,7 @@ func initVscodeTypesRuntimeIoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -595,7 +595,7 @@ func initVscodeTypesRuntimeLogDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -615,7 +615,7 @@ func initVscodeTypesRuntimeNeoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -635,7 +635,7 @@ func initVscodeTypesRuntimeProcessFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -655,7 +655,7 @@ func initVscodeTypesRuntimeProcessHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -675,7 +675,7 @@ func initVscodeTypesRuntimeProcessModelDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -695,7 +695,7 @@ func initVscodeTypesRuntimeProcessDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -715,7 +715,7 @@ func initVscodeTypesRuntimeQueryDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -735,7 +735,7 @@ func initVscodeTypesRuntimeStoreDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -755,7 +755,7 @@ func initVscodeTypesRuntimeSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -775,7 +775,7 @@ func initVscodeTypesRuntimeTimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -795,7 +795,7 @@ func initVscodeTypesRuntimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -815,7 +815,7 @@ func initVscodeTypesSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -835,7 +835,7 @@ func initAppYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/app.yao", size: 3115, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/app.yao", size: 3115, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -855,7 +855,7 @@ func initDataReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -875,7 +875,7 @@ func initDataTemplatesDefault__assetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -895,7 +895,7 @@ func initDataTemplatesDefault__assetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -915,7 +915,7 @@ func initDataTemplatesDefault__assetsImagesLogosLogo_colorSvg() (*asset, error) return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -935,7 +935,7 @@ func initDataTemplatesDefault__assetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -955,7 +955,7 @@ func initDataTemplatesDefault__dataJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -975,7 +975,7 @@ func initDataTemplatesDefault__documentHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -995,7 +995,7 @@ func initDataTemplatesDefaultIndexIndexCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1015,7 +1015,7 @@ func initDataTemplatesDefaultIndexIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1035,7 +1035,7 @@ func initDataTemplatesDefaultIndexIndexJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1055,7 +1055,7 @@ func initDbReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1075,7 +1075,7 @@ func initFlowsMenuFlowYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/flows/menu.flow.yao", size: 813, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/flows/menu.flow.yao", size: 813, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1095,7 +1095,7 @@ func initFormsAccountFormYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/forms/account.form.yao", size: 1194, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/forms/account.form.yao", size: 1194, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1115,7 +1115,7 @@ func initIconsAppIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1135,7 +1135,7 @@ func initIconsAppIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1155,7 +1155,7 @@ func initIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1175,7 +1175,7 @@ func initLoginsAdminLoginYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/logins/admin.login.yao", size: 302, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/logins/admin.login.yao", size: 302, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1195,7 +1195,7 @@ func initLogsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1215,7 +1215,7 @@ func initModelsAdminUserModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/models/admin/user.mod.yao", size: 6416, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/models/admin/user.mod.yao", size: 6416, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1235,7 +1235,7 @@ func initModelsTestsPetModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/models/tests/pet.mod.yao", size: 525, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/models/tests/pet.mod.yao", size: 525, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1255,7 +1255,7 @@ func initNeoNeoYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/neo/neo.yml", size: 724, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/neo/neo.yml", size: 724, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1275,7 +1275,7 @@ func initPublicReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/README.md", size: 108, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/public/README.md", size: 108, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1295,7 +1295,7 @@ func initPublicAssetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/public/assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1315,7 +1315,7 @@ func initPublicAssetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/public/assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1335,7 +1335,7 @@ func initPublicAssetsImagesLogosLogo_colorSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/public/assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1355,7 +1355,7 @@ func initPublicAssetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/public/assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1375,7 +1375,7 @@ func initPublicAssetsLibsuiMinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/libsui.min.js", size: 12569, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/public/assets/libsui.min.js", size: 12569, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1395,7 +1395,7 @@ func initPublicAssetsLibsuiMinJsMap() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/libsui.min.js.map", size: 38553, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/public/assets/libsui.min.js.map", size: 38553, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1415,7 +1415,7 @@ func initPublicIndexCfg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/index.cfg", size: 85, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/public/index.cfg", size: 85, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1435,7 +1435,7 @@ func initPublicIndexSui() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/index.sui", size: 5682, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/public/index.sui", size: 5682, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1455,7 +1455,7 @@ func initScriptsAccountTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/account.ts", size: 2521, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/scripts/account.ts", size: 2521, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1475,7 +1475,7 @@ func initScriptsAiNeoTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/ai/neo.ts", size: 375, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/scripts/ai/neo.ts", size: 375, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1495,7 +1495,7 @@ func initScriptsTestsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/tests.ts", size: 1044, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/scripts/tests.ts", size: 1044, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1515,7 +1515,7 @@ func initScriptsUtilsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/utils.ts", size: 1230, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/scripts/utils.ts", size: 1230, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1535,7 +1535,7 @@ func initSuisWebSuiYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1555,7 +1555,7 @@ func initTablesAccountTabYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/tables/account.tab.yao", size: 5597, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/tables/account.tab.yao", size: 5597, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1575,7 +1575,7 @@ func initTsconfigJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1595,7 +1595,7 @@ func libsuiAgentTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/agent.ts", size: 15267, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "libsui/agent.ts", size: 15267, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1615,7 +1615,7 @@ func libsuiIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/index.ts", size: 13049, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "libsui/index.ts", size: 13049, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1635,7 +1635,7 @@ func libsuiUtilsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1655,7 +1655,7 @@ func libsuiYaoTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1675,7 +1675,7 @@ func publicIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1695,7 +1695,7 @@ func uiIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1715,7 +1715,7 @@ func yaoAssistantsEntityPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/entity/package.yao", size: 212, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/entity/package.yao", size: 212, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1735,7 +1735,7 @@ func yaoAssistantsEntityPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/entity/prompts.yml", size: 930, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/entity/prompts.yml", size: 930, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1755,7 +1755,7 @@ func yaoAssistantsKeywordPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/keyword/package.yao", size: 200, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/keyword/package.yao", size: 200, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1775,12 +1775,12 @@ func yaoAssistantsKeywordPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/keyword/prompts.yml", size: 545, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/keyword/prompts.yml", size: 545, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _yaoAssistantsKeywordSrcIndexTs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x9c\x56\xdf\x92\xdb\xb4\x17\xbe\xf7\x53\x9c\x9f\xa7\xd3\x9f\x93\x3a\xf6\xb6\xbd\x60\x26\x4b\x5a\x4a\x29\xd3\x42\x59\x3a\xdd\x32\x5c\xc4\x61\x56\x6b\x1f\x6f\x44\x64\xc9\x48\x0a\xbb\x19\x36\x33\x3c\x07\x97\x3c\x07\x4f\xc3\x0b\xf0\x0a\x8c\xfe\xd9\x4a\xb6\xb4\x03\x7b\xb1\x56\xe4\x73\xbe\xef\xfc\xd3\x27\x97\xd3\x69\x02\x53\xf8\x1a\x77\xd7\x42\x36\xf0\xe2\x46\x4b\x52\x6b\x2a\x38\x3c\xbb\x42\xae\x61\x06\x67\x78\xa3\xe1\xa5\x10\x1b\x63\xf7\x86\x48\x85\x0a\x5e\xbf\xfe\x06\x24\xaa\x5e\x70\x85\x40\x78\x03\xe8\xfc\x14\x6c\x1c\x90\x82\x6b\xaa\xd7\x80\x52\x0a\x09\x5a\x30\x94\x84\xd7\x98\xc0\xb4\x4c\x92\xb2\x84\xcf\xb4\x9a\x71\x51\xaf\xb1\xde\x24\x89\x0f\xc1\xf2\xac\x85\xd8\xc0\x0c\x7a\x29\x6a\x54\x86\xc9\xe3\x05\x02\x13\x58\x20\x36\x4e\xdf\x19\x9b\x1f\x95\xe0\x85\x8d\x0c\x5a\x21\xa1\x25\x5b\xa6\x67\x9e\x54\xc3\x57\xe7\xdf\x9e\x41\x4f\xa4\xa2\xfc\xca\x06\xd0\x6e\xb9\x03\x32\x8c\x59\x02\x50\xeb\x9b\x39\x10\x93\x6e\xf1\x5c\x70\x8d\x37\x3a\x4f\x00\x7a\xb2\x63\x82\x34\xe1\x8d\x31\x36\x55\x78\xe3\xb6\x93\xc9\xf1\x8b\xb7\xa1\x1e\xb7\xc0\xb7\x8c\xc1\x2f\x06\x59\x70\xa5\xa1\x16\x5d\xcf\xd0\x52\x2e\x02\x6c\x31\x6e\x9e\x26\x09\x40\x59\xc2\x99\x88\x2c\x73\x90\xa8\xb7\x92\x3b\x2c\x93\x96\xd2\x84\x37\x44\x36\xb0\x26\xbc\x61\x36\x19\xa0\x2d\x64\xff\x8b\xe0\x6f\x6f\x21\xfa\x59\xd4\x26\x1b\xae\x27\x36\x16\x88\x11\x4f\x13\x80\xbd\xe7\x7d\x8b\x9d\xf8\x19\xa1\x23\x72\xd3\x88\x6b\x0e\xb5\x68\x10\x2e\x99\xa8\x37\x06\xbf\x97\xa8\x90\xeb\x04\x80\xa1\x49\xc5\x22\xc2\x02\xee\xd2\x14\x5a\xd2\x2e\x9b\x9c\xfa\xb8\xc2\xae\xd2\x44\x6a\xf5\x3d\xd5\xeb\x2c\xbd\xb8\xb8\x30\xcd\x4a\x27\x21\xa4\x18\xcf\x9b\x33\x5a\x63\xf6\x89\x85\xd9\x03\x32\x85\x1f\x42\xfb\x38\xd2\x63\x87\x74\x14\x14\xf2\xe6\xdf\x80\x9c\xe4\x30\x1b\x81\xee\x5a\x85\xcc\x5d\x41\xdf\xc9\x1d\x68\x61\x67\x0e\xdd\xf8\xb5\x52\x74\xf1\x18\x78\x3f\x5f\xd4\x70\x66\xe6\xa0\xb4\xa4\xfc\x6a\xb9\x82\x05\x2c\x57\x16\x4e\xcb\x9d\x8f\xad\x2c\xcd\xb4\x7f\x64\xd8\xfd\x9c\x43\x66\x87\x04\x15\x5c\x4a\xb1\x41\x6e\xa3\xc8\xed\xff\xe7\x39\xa0\xae\x8b\x49\xc8\x57\x39\x27\x6c\x60\x01\x6f\xdc\xb1\xcb\xd2\x91\x24\xcd\x61\x98\x22\xa2\x7c\x28\x30\x84\xfc\x74\x8c\xf9\xd4\xbe\xda\xfb\xf1\xb7\xc1\xbb\x9a\x7b\xf8\xfb\xf7\xe1\x99\x94\x64\x57\x50\x65\x9f\x7e\xbf\x08\x50\x43\x13\x46\x74\x7b\x5c\x0e\x8c\x8a\x96\x32\x8d\x32\xf3\x86\x00\xd9\x66\x02\x8b\x27\xa0\x77\x3d\x8a\x16\x36\xb0\x58\x2c\x20\x75\x21\xa5\x86\x72\xe3\x7b\x53\x30\xe4\x57\x7a\x0d\x4f\xe0\xc4\xfb\x4e\x7c\xc0\x76\xce\x6a\xa2\xeb\x35\x64\x38\x19\x8b\xfd\xaa\x3d\xa8\x35\xa1\x4c\xe5\xb6\x1d\x5a\x04\x35\x1a\x03\xb5\x0d\x36\xca\x91\x1c\xc5\xef\x2d\xbd\xbe\xaa\x2f\xa5\xe8\xde\x19\xd9\x09\x45\x8d\x0f\xe2\xab\x16\x94\xa6\x8c\x01\x17\x03\x84\xa3\x0c\xea\xc7\xaf\x1c\x93\x24\xd7\x81\xcd\x54\x78\xa8\x8e\x4f\xd2\x14\xe1\x24\xa4\xf2\x1f\x83\x79\xeb\xc4\xc2\x37\x2f\x80\x24\x83\x8a\x38\xf0\x86\x68\x32\xbf\xd3\xb7\xf9\x18\xbe\xab\xb1\x79\xec\x4f\x93\xfd\x20\xf6\x2f\xde\x5b\xc0\x9e\x11\xca\x6d\x62\x70\xbd\xf6\x53\x3b\x8c\xb4\xed\x80\xf1\x7d\xe9\x47\xbb\x15\xb2\x23\x5a\x01\xa3\x1b\x9c\x9b\x17\x33\x78\x2e\xba\x8e\xcc\x14\xf6\x44\x12\x8d\xcd\x1c\x52\x4f\xf0\x30\x0f\x54\x8f\x86\xd5\xe3\xd4\x39\xbd\xa6\x1c\xdf\xeb\x53\xf1\xe0\x33\xac\x82\xcf\xe7\x5b\x66\x4e\x6e\x2f\x28\xd7\x6a\x0e\xe9\x0c\x46\xa7\x61\xfd\xc8\x1b\x9f\x6d\xbb\x4b\x94\x16\xfa\x61\x11\x19\x3e\x2a\x0e\x2c\xa3\x9b\xe9\x9f\x1a\x65\x4a\x13\x4e\xdc\x24\x92\x8b\xf1\xae\xf9\x90\x96\x8c\x5a\x5f\x8b\xae\x13\xdc\x68\x7b\x4b\x6f\x50\x95\x6a\xdb\xda\x45\x50\x79\x86\x84\x5b\x45\x18\x46\xba\x90\xd8\x33\x52\x63\x56\xfe\xb0\xac\x54\x75\xbe\x9a\x3e\x1d\x34\x60\x59\xa9\xf9\x5f\x7f\xfc\xb6\x9a\x56\xcb\xa7\x25\xcd\x21\x4d\x27\x11\x57\x3a\x84\x94\x7a\xc2\x23\xc0\x6a\xe5\x11\xef\x95\xc7\xbe\x5a\x12\x6a\xae\x3a\x58\x39\x9f\xbb\x3a\xcb\x4c\xf3\x2e\x77\x33\xf3\x8c\x3e\x12\x86\x7a\x98\x7d\x33\xfa\x3e\xa5\x42\xf5\x8c\xea\xac\x5c\x56\xbc\x92\xab\x07\xa5\xc3\x32\x42\x9a\x8d\xf6\x20\x5a\xe7\x17\xa9\x81\x0f\xe8\x32\xee\x7b\x0e\xdc\x76\x56\xe5\xf0\xd3\x56\x68\x5b\xbe\x03\x45\x87\x85\xc5\xf1\xa7\xe3\xa8\x84\xb3\x6a\x5a\xfd\xf9\xeb\xef\x55\x53\x15\xab\x07\x77\x32\x77\x44\xaa\xf4\x0c\xef\x81\x48\xff\x7f\xb1\x7a\x70\xeb\x1e\xf7\xca\xab\x63\x80\x28\xa2\xd8\x2f\xaf\xd4\x87\xea\x6c\x06\x83\x04\xa7\xa8\xda\xb6\x06\xe7\x1b\xda\x03\x76\xbd\xde\x81\xfd\xaa\x13\xc0\x84\xfd\x0c\x39\x50\xa1\x48\x69\xad\x00\x1f\xee\x7e\x0a\x0f\x4f\x4e\x46\xa5\x37\xa8\xa6\x23\x70\xb9\x73\xdc\x06\xc9\xc8\x11\xa1\x5c\x41\xb7\x65\x9a\xf6\x2c\x14\x30\x26\xa1\xbc\x66\xdb\x06\x55\x96\xe6\x69\x74\x73\x44\x57\x9a\x36\x6d\x0f\xe6\xae\xed\xc6\xb6\xe8\x48\x9f\x65\xbd\xbd\x35\x7a\x9f\xa2\xbf\x0a\xcc\x5f\x34\x0a\x06\xc3\x8c\x82\xc5\x8a\x29\x86\x6b\x4d\x1f\xe5\x1a\x6f\x1d\x25\x7a\x28\x90\x45\xbf\x55\x6b\x8b\x10\x51\xbb\xbb\xe8\x70\xe5\xbf\x80\x46\x94\x43\x04\xff\x6b\x00\xd9\x8f\x57\x9a\x3f\x23\x5f\x60\xb3\xed\x19\xad\x89\xc6\x51\xbb\x97\x45\x51\x70\xbc\x86\x73\xd4\xc3\xdd\x31\x59\x19\x7d\xfe\x3b\x00\x00\xff\xff\x21\xb8\xdb\xfa\x0f\x0c\x00\x00") +var _yaoAssistantsKeywordSrcIndexTs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x9c\x56\xdf\x72\xe3\xb4\x17\xbe\xf7\x53\x9c\x9f\x67\x67\x7f\x4e\xea\xd8\x6d\xb9\x4b\xc9\x96\x65\x59\x66\x81\x52\x3a\xdb\xe5\x2a\x0e\x83\x62\xcb\x89\xc7\xb2\x64\x24\x99\x34\x43\x33\xc3\x73\x70\xc9\x73\xf0\x34\xbc\x00\xaf\xc0\x1c\x49\xb6\x95\xb4\xec\x30\xf4\xc6\x8a\x74\xce\xf7\x9d\x7f\xfa\xd4\x74\x3a\x0d\x60\x0a\xdf\xd0\xfd\x4e\xc8\x02\xde\x3e\x68\x49\x72\x5d\x09\x0e\xaf\x37\x94\x6b\x98\xc1\x2d\x7d\xd0\xf0\x4e\x88\x1a\xed\xee\x88\x54\x54\xc1\xcd\xcd\xb7\x20\xa9\x6a\x05\x57\x14\x08\x2f\x80\x5a\x3f\x05\xb5\x05\x52\xb0\xab\xf4\x16\xa8\x94\x42\x82\x16\x8c\x4a\xc2\x73\x1a\xc0\x34\x0d\x82\x34\x85\xcf\xb4\x9a\x71\x91\x6f\x69\x5e\x07\x81\x0b\xc1\xf0\x6c\x85\xa8\x61\x06\xad\x14\x39\x55\xc8\xe4\xf0\x7a\x02\x0c\xac\x27\x46\xa7\xef\xd1\x46\xd3\x07\x9d\xb8\xc8\xbf\xbe\xff\xee\x16\x4a\x21\xa1\x24\x1d\xd3\x33\x47\xad\xc1\xec\x7b\x20\xa5\x14\x8d\x49\x43\x74\xba\xed\xb4\x89\xac\xec\xb8\x3d\xc4\x50\xa2\x00\x20\xd7\x0f\x73\x20\x58\x87\xe4\x8d\xe0\x48\x13\x07\x00\x2d\xd9\x33\x41\x8a\xfe\x04\x8d\xb1\x3c\x77\x76\x3b\x98\x9c\x1e\xbc\xef\x0b\xf5\x08\xbc\x63\x0c\x7e\x41\x64\xc1\x95\x86\x5c\x34\x2d\xa3\x86\x72\xd1\xc3\x26\xe3\xe6\x55\x10\x00\xa4\x29\xdc\x0a\xcf\x32\x06\x49\x75\x27\xb9\xc5\xc2\x4c\x95\x26\xbc\x20\xb2\x80\x2d\xe1\x05\xab\xf8\x26\x00\xa8\x4a\x88\xfe\xe7\xc1\x3f\x3e\x82\xf7\x33\xc9\x31\x1b\xae\x27\x26\x16\xf0\x11\xaf\x02\x80\x43\xe0\x45\x68\x0c\x61\x01\x4f\xbd\xd1\x94\x51\x3d\xb4\x7c\x0e\x4a\xcb\x8a\x6f\x96\x2b\x58\xc0\x72\x65\xa2\xd7\x72\xef\x38\xd2\x14\x9b\xf5\xaf\x7a\x35\xb6\xa9\xf7\x7c\x87\x99\x51\x05\x0d\x91\x75\x21\x76\x1c\x72\x51\x50\x58\x33\x91\xd7\x2a\x86\xb5\x14\x35\xe5\xa6\xc5\x31\x50\x9d\x27\xc6\xcd\x26\xd0\xe2\xc0\x16\xb0\x80\x3b\x3b\x52\x51\x78\x1a\x41\x18\xc3\x50\x0e\xa2\x5c\xb4\x30\x64\x75\x3d\xa6\x75\x65\x8e\x0e\xae\x8f\x26\x3f\x5b\x69\x47\xf2\xf2\x25\xbc\x96\x92\xec\x93\x4a\x99\xaf\xdb\x4f\x7a\xa8\xc9\xe4\x09\xba\xe9\xfb\x91\x51\x52\x56\x4c\x53\x19\x39\x43\x80\xa8\x9e\xc0\xe2\x15\xe8\x7d\x4b\x45\x09\x35\x2c\x16\x0b\x08\x6d\x48\x21\x52\xd6\x89\x96\x55\x13\x4d\x12\x46\xf9\x46\x6f\xe1\x15\x9c\x3b\xdf\x89\x0b\x18\x5b\x0a\x39\xd1\xf9\x16\x22\x3a\x19\xfb\xf1\x55\x79\x74\x23\x48\xc5\x54\x6c\x3a\xa6\x45\x7f\x30\x06\x6a\x2e\x0c\xd6\x2e\x38\x89\xdf\x59\x3a\x05\x51\x5f\x4a\xd1\x7c\xc0\xfb\xd3\x17\x75\x98\x28\xcb\xa8\x74\xc5\x18\x70\x31\x40\x58\xca\x3e\x10\xbe\xb1\x4c\x92\xec\x7a\x36\xac\xf0\x50\x1d\x97\x24\x16\xe1\xbc\x4f\xe5\x3f\x06\xf3\xde\x4e\xbd\x6b\x5e\x0f\x12\x0c\xd7\xc1\x82\x17\x44\x93\xf9\x93\xbe\xcd\xc7\xf0\x6d\x8d\xf1\x73\xb8\x0a\x0e\x83\x9c\xbd\x7d\xb6\x80\x2d\x23\x15\x37\x89\xc1\x6e\xeb\x66\xd6\x44\x60\x12\xc7\x0e\xa0\x6f\x3f\xee\xa5\x90\x0d\xd1\x0a\x58\x55\xd3\x39\x1e\xcc\xe0\x8d\x68\x1a\x32\x53\xb4\x25\x92\x68\x5a\xcc\x21\x74\x04\x17\x71\x4f\x75\x39\xac\x3e\x09\xad\xd3\x4d\xc5\xe9\xb3\x3e\x19\xef\x7d\x86\x55\xef\xf3\x79\xc7\xf0\x72\xb7\xa2\xe2\x5a\xcd\x21\x9c\xc1\xe8\x34\xac\x2f\x9d\xf1\x6d\xd7\xac\xa9\x34\xd0\x17\x89\x67\x78\x99\x1c\x59\x7a\x12\xfb\x4f\x8d\xc2\xd2\xf4\x37\x6e\xe2\x29\xca\x28\x9a\x1f\x93\x1b\xd3\xd5\x46\xfc\x4c\x51\xae\x1a\xc1\xa1\x95\xb4\xac\x1e\xa8\x4a\x55\x57\x9a\x85\xd3\xac\x9c\x51\xc2\x8d\x2e\x0c\x23\x9d\x48\xda\x32\x92\xd3\x28\xfd\x61\x99\xa9\xec\x7e\x35\xbd\x1e\x34\x60\x99\xa9\xf9\x5f\x7f\xfc\xb6\x9a\x66\xcb\xeb\xb4\x8a\x21\x0c\x27\x1e\x57\x38\x84\x14\x3a\xc2\x13\xc0\x6c\xe5\x10\x5f\xa4\xa7\xbe\x5a\x92\x0a\x35\x1b\x56\xd6\xc7\xde\xe5\x3e\x99\x0f\x72\x0f\x0c\x9b\xb7\xde\xcf\xf0\x7b\x2c\x8d\xb6\x1e\xb8\x8f\xa3\xef\x52\x4a\x54\xcb\x2a\x1d\xa5\xcb\x8c\x67\x72\x75\x96\x5a\x2c\x54\xd9\x68\xb4\x07\x51\x5a\x3f\x4f\x0d\x5c\x40\x6b\xbf\xef\x31\x70\xd3\x59\x15\xc3\x4f\x9d\xd0\xa6\x7c\x47\xa2\x0f\x0b\x83\xe3\x6e\xc7\x49\x09\x67\xd9\x34\xfb\xf3\xd7\xdf\xb3\x22\x4b\x56\x67\x4f\x32\xb7\x44\x2a\x75\x0c\xcf\x40\x84\xff\xff\x71\x75\xf6\x68\x3f\x2f\xd2\xcd\x29\x80\x17\x91\xef\x17\x67\xea\x63\x75\xc6\xc1\x20\xbd\x93\x57\x6d\x53\x83\xfb\xba\x6a\x81\x36\xad\xde\x83\xf9\xbf\x45\x00\x13\xe6\x3d\x3d\x52\x21\x4f\x69\x8d\x00\x1f\xef\x7e\x0a\x17\xe7\xe7\xa3\xd2\x23\x2a\x76\x04\xd6\x7b\xcb\x8d\x48\x28\x47\xa4\xe2\x0a\x9a\x8e\xe9\xaa\x65\x7d\x01\x7d\x92\x8a\xe7\xac\x2b\xa8\x8a\xc2\x38\xf4\x5e\x0e\xef\x61\xd3\xd8\xf6\xde\xdc\xb6\x1d\x6d\x93\x86\xb4\x51\xd4\x9a\x57\xa3\x75\x29\xba\xa7\x00\xff\xbc\x51\x40\x0c\x1c\x05\x83\xe5\x53\x0c\xcf\x9a\x3e\xc9\xd5\xdf\x3a\x49\xf4\x58\x20\x93\xb6\x53\x5b\x83\xe0\x51\xdb\xb7\xe8\x78\x75\x00\xca\x14\xf5\x50\x8e\x11\xdc\xaf\x01\xe4\x30\x3e\x69\xee\x8e\x7c\x41\x8b\xae\x65\x55\x4e\x34\x1d\xb5\x7b\x99\x24\x09\xa7\x3b\xb8\xa7\x7a\x78\x3b\x26\x2b\xd4\xe7\xbf\x03\x00\x00\xff\xff\x39\x8d\xcc\xe8\xf1\x0a\x00\x00") func yaoAssistantsKeywordSrcIndexTsBytes() ([]byte, error) { return bindataRead( @@ -1795,7 +1795,7 @@ func yaoAssistantsKeywordSrcIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/keyword/src/index.ts", size: 3087, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/keyword/src/index.ts", size: 2801, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1815,7 +1815,7 @@ func yaoAssistantsNeedsearchPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/needsearch/package.yao", size: 207, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/needsearch/package.yao", size: 207, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1835,12 +1835,12 @@ func yaoAssistantsNeedsearchPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/needsearch/prompts.yml", size: 955, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/needsearch/prompts.yml", size: 955, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _yaoAssistantsNeedsearchSrcIndexTs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x56\x4d\x6f\x1b\x37\x10\xbd\xef\xaf\x78\xdd\x83\xb1\x6b\xc8\x2b\x17\x45\x50\x40\xc2\x46\x4d\xd3\x16\x6d\xe1\xb8\x41\x92\xa2\x87\x20\x88\xa9\xdd\x91\xc5\x8a\x22\x05\x92\xaa\x64\xd4\xfe\xef\x05\x3f\x56\x5a\x4a\x96\x8c\xe6\x10\x4b\x1c\xce\x9b\x37\xc3\x79\x33\x1a\x5e\x5e\x66\xb8\xc4\x2d\x51\x8b\x8f\xc4\x74\x33\xc7\x9b\x7b\x92\x16\x57\xb8\xa5\xad\xc5\xaf\x4a\x2d\xdc\x85\xf7\x4c\x1b\x32\xb8\xb9\x79\x07\x4d\x66\xa5\xa4\x21\x30\xd9\x82\xb6\x56\xb3\xc6\x1a\x98\xe0\xcc\xa5\x75\xde\x1b\x6e\xe7\x20\xad\x95\x86\x55\x82\x34\x93\x0d\x65\xb8\x1c\x66\xd9\x70\x88\x1f\xac\xb9\x92\xaa\x99\x53\xb3\xc8\x32\xe7\xa0\x67\xac\xa1\x18\xfe\x03\x99\xb5\xb0\xf8\x37\x03\x24\x51\xfb\x35\xe0\x8e\x30\x55\x4a\x10\x93\xe3\x0c\x31\xd4\x57\xfb\xb0\x22\x33\x82\xb1\x9a\xcb\xfb\xcf\x5f\x9c\xa5\x51\x72\xc6\x5b\x92\x0d\x8d\x20\xd7\xcb\x29\xe9\x71\xf6\x94\x65\xbb\x24\xb7\x16\x73\xa5\x16\xb8\xc2\x4a\xab\x86\x8c\x4b\x29\x25\xde\x25\xe7\xee\xff\xe9\xcc\x7f\x1b\x25\x2b\x9f\x3d\x66\x4a\x63\xc6\xd6\xc2\x5e\xc5\x9c\x2c\x7e\xff\xf8\xc7\x2d\x56\x4c\x1b\x2e\xef\x7d\x7e\xb3\xb5\x6c\x2c\x57\xd2\x07\x2b\x1c\x25\xbb\x1d\x81\xb9\x92\x56\x6f\x95\xb4\xb4\xb5\x83\x0c\x58\xb1\x07\xa1\x58\xdb\x59\xdc\x65\x57\xe9\xf7\xe1\x38\x2b\x0f\x0d\x1f\xba\x9a\x3f\x42\xae\x85\xf0\xe5\x69\x94\x34\x16\x8d\x5a\xae\x04\xf9\x90\x75\x07\x5b\xed\x0f\xc7\x59\x06\x0c\x87\xb8\x55\xbd\x9b\x03\x68\xb2\x6b\x2d\x03\x96\x4b\xcb\x58\x26\x5b\xa6\x5b\xcc\x99\x6c\x85\x4f\x06\x7c\x86\xe2\x9b\x1e\xfc\xe3\x23\x7a\x5f\xab\x46\xf9\x8a\x95\x9e\x0b\xfa\x88\xee\x25\x9e\x62\xdc\x0f\xb4\x54\xff\x10\x96\x4c\x2f\x5a\xb5\x91\x68\x54\x4b\x98\x0a\xd5\x2c\x1c\xfe\x4a\x93\x21\x69\x33\x40\x90\x4b\x25\xbc\x41\x8d\xe3\x30\x95\xd5\x7c\x59\x94\xe3\xc8\xab\x3b\x35\x96\x69\x6b\xfe\xe2\x76\x5e\xe4\x77\x77\x77\xee\xb1\xf2\xb2\xa3\xd4\xc7\x8b\xd7\x05\x6f\xa8\xf8\xbe\x1c\xf7\xa8\x45\x37\xc7\x19\x24\x0c\x9d\xc3\x7f\x19\xfb\xbb\x43\x6c\x5f\x8b\x94\x34\xc9\xf6\xff\x40\x5e\x0f\x70\x95\xc2\x5a\xcd\xb8\x7b\xa4\x1e\xfe\xb1\x73\x57\xb0\xf0\x0e\x3f\x91\xef\x5c\xd7\xdf\x6b\xd1\x15\x3c\x7c\x19\xa5\xca\xab\x23\x9d\x44\x7d\x33\x26\x0c\x0d\xfc\x79\xaa\xbe\xcf\x5f\x06\x1d\xf9\x9d\xf2\xae\xdd\xd1\x93\x8f\x6c\xf5\x43\x84\x1b\x0e\x9d\x9e\x5e\x90\xd3\x4e\x49\xe8\xda\xdb\x9d\x50\x8b\x1a\xef\x83\x60\x8b\x7c\x8f\x90\x0f\xb0\x6b\x42\x66\x62\x9c\x84\xf8\x24\x99\x1b\x87\xec\x27\xe9\xf0\x48\xd3\x98\xec\x27\x88\x33\x3c\x45\xe5\xf9\xac\xc2\x73\x06\x6a\xe5\x2e\x6e\xa8\x66\xd5\x0b\x8f\x1a\x3f\x86\xf0\xf1\x72\xdf\x58\x8e\x53\xbf\x3e\x33\xd4\x78\xa3\x35\x7b\xa8\xb8\xf1\x7f\x3b\xf7\xfe\x9d\x32\xba\x03\x13\x3c\x63\xae\x66\x5c\x58\xd2\xc5\xee\x96\xfb\x57\xd8\x12\xf5\xeb\xe4\x08\x70\xd7\xd5\x0c\x16\x75\x5d\x23\x0f\x15\xc9\x71\x71\x71\x70\xed\x73\xbe\xa1\x69\x3e\x40\xbe\xf0\xff\xb7\xd3\xfc\x4b\xc5\x65\x23\xd6\x2d\x99\xc2\x56\x56\xdd\xa8\x0d\xe9\xb7\xcc\x50\x51\x96\x3d\xdf\xfd\x67\xd7\x2d\x07\x49\xef\xeb\x8d\x3a\x3b\x20\x14\x93\xea\x5f\x71\x04\xc3\xab\xe4\xbd\x08\x13\xbc\x63\x76\x5e\x2d\xb9\x2c\xbe\x1d\xc4\xcf\x6c\xeb\x74\x73\x04\x91\x30\x1b\xe1\xba\x7a\x15\x9f\xd7\xcb\xbf\x61\xb6\x99\xa3\xa0\x72\xdf\xb3\xbf\xcd\x92\x96\x65\x5c\x98\x81\xef\x6a\xab\xba\xdd\x87\x99\x56\x4b\xb8\xc9\x9e\xed\x33\x43\xdd\x99\x7f\xd1\x6a\xf9\xc9\x2d\x83\xae\x57\xd3\xf1\xe8\xa7\x66\xec\xf2\x9d\x38\xe3\x30\x0d\x2c\x5a\x66\xd9\x28\xda\xa2\xb2\xf6\x3b\xed\xe7\xc8\x21\xdd\x62\x9e\xd1\x4a\x30\x2e\x3d\x2f\x6c\xe6\x24\x93\x5d\x15\x32\x49\x37\xd6\x21\x5f\xe7\xd9\x29\xa4\x1c\x1d\xaf\xe7\x20\x50\xe1\x5e\x1d\xb5\x8f\x93\x36\x41\x37\x7a\xde\xba\x45\xef\xd5\x4e\xdb\x95\xe0\x0d\xb7\xe0\xb2\xe5\x0d\xb3\x4a\x9b\x1d\x8e\x53\x46\xfc\x01\x12\x3a\xc1\x03\xef\x3b\x2c\xb7\x7a\x4d\x79\x89\xc7\xc7\x67\xad\xce\xfd\xb4\x35\x54\xe7\xb4\xdd\x75\xf6\x49\xe3\xe2\x8c\xad\x9d\xe6\x21\xcf\x98\x84\x3a\x9b\x82\x9f\xa2\x67\x72\x50\x78\x89\xa8\x54\xa1\x52\xf9\xae\xba\x07\x0d\xe0\xb5\xbf\xe3\x13\x0e\x3f\xa5\x3f\x94\x50\x47\x1d\xba\x21\xf6\x6c\x25\xca\xbe\x63\xb5\x5a\x9b\x79\x34\x9c\xf2\x8a\x25\x3a\x2e\x9d\x54\x1b\x41\xed\x3d\xe5\x51\x78\xc7\xb8\x8b\x33\xb0\xed\x09\x58\xa7\x88\x29\x33\x67\x50\x77\xef\xe2\x57\x9f\x25\xbd\xe4\x92\xfa\x9b\x21\xe9\x3b\xd4\xfb\x97\x9b\x84\x5d\x87\x51\xbf\x23\x2f\x2e\x92\x18\x82\xe4\xbd\x9d\xe3\x35\xae\x7d\x8c\x44\xad\xc9\xda\x74\x5f\x9e\xdb\x9a\x3e\xe8\xa4\x8f\x89\x13\xab\xb4\x7a\x35\x70\x39\xdc\xa8\x4d\xef\xdc\x8b\xc9\x0b\x3b\x6a\x96\x87\xdf\x2f\x6e\x30\xfc\x17\x00\x00\xff\xff\x71\xd9\x90\x2f\xd1\x0b\x00\x00") +var _yaoAssistantsNeedsearchSrcIndexTs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x56\x4d\x6f\xe3\x36\x10\xbd\xeb\x57\xbc\xea\x10\x58\x81\x22\xa7\x87\xbd\x38\xd0\xba\xdb\xb4\xc5\xb6\xc8\xa6\x41\x76\x7b\x0a\x82\x05\x2d\x8d\x6c\xc1\x34\x69\x90\x14\xec\xa0\xc9\x7f\x2f\xf8\x21\x4b\x74\xe2\x74\x73\x48\x6c\x0e\x67\xe6\xcd\xe3\x9b\x99\x4c\xcf\xcf\x13\x9c\xe3\x96\xa8\xc6\x57\x62\xaa\x5a\xe1\xd3\x92\x84\xc1\x05\x6e\x69\x6f\xf0\x59\xca\xb5\xbd\x70\xc7\x94\x26\x8d\x9b\x9b\x2f\x50\xa4\xb7\x52\x68\x02\x13\x35\x68\x6f\x14\xab\x8c\x86\xf6\xce\xad\x30\xd6\x7b\xd7\x9a\x15\x48\x29\xa9\x60\x24\x27\xc5\x44\x45\x09\xce\xa7\x49\x32\x9d\xe2\x17\xa3\x2f\x84\xac\x56\x54\xad\x93\xc4\x3a\xa8\x86\x55\x14\xd2\xdf\x93\xee\xb8\xc1\xbf\x09\x20\x88\xea\xef\x3e\xee\x0c\x0b\x29\x39\x31\x71\x95\x20\xa4\xfa\x6e\x9e\xb6\xa4\x67\xd0\x46\xb5\x62\xf9\xf0\x68\x2d\x95\x14\x4d\x5b\x93\xa8\x68\x06\xd1\x6d\x16\xa4\xae\x92\x97\x24\x39\x14\xb9\x37\x58\x49\xb9\xc6\x05\xb6\x4a\x56\xa4\x6d\x49\x31\xf0\xbe\x38\x7b\xff\x1f\x6b\x36\xb4\x37\xc5\xef\xbe\xca\xbf\xbe\xfe\x7d\x8b\x46\x2a\x34\xac\xe3\xe6\x22\x54\x66\xe0\xce\x03\x13\xad\x14\x68\x94\xdc\x38\xaa\x64\x67\xb6\x9d\x71\x85\x37\x9d\xf0\x46\x8b\x62\x62\xb1\x9a\xfd\x0c\xcc\x72\x5d\x5c\x4b\x61\xd3\xe4\x09\xb0\x65\x4f\x5c\xb2\xba\xb7\xd8\xcb\xf6\x09\xee\xfc\x71\x92\x1d\x1b\xee\xfb\xc7\x78\x86\xe8\x38\x77\xbc\x55\x52\x68\x83\x4a\x6e\xb6\x9c\x5c\xca\xb2\x0f\x5b\x0c\x87\x57\x49\x02\x4c\xa7\xb8\x95\xa3\x9b\x39\x14\x99\x4e\x09\x1f\xcb\x56\xaa\x0d\x13\x35\x53\x35\x56\x4c\xd4\xbc\x15\xcb\x04\x68\x1b\x4c\x7e\x1a\x85\x7f\x7e\xc6\xe8\x6b\x51\x49\x47\x65\xe6\xb0\x60\x1c\xd1\x3e\xd1\x4b\x32\x42\xe8\x39\x2f\xf1\xda\xbb\x87\xf7\x1b\x39\xae\xed\xbb\x74\xdc\x24\x00\xa7\xfe\xcb\x2c\x56\x4c\x19\xf2\x45\xaa\x69\x18\xd7\x94\xbb\xf3\x58\x35\x0f\x8f\xfe\x74\xac\x98\x4b\x7b\xf4\xe2\x32\x1b\xf5\x14\xc2\x4d\xa7\x56\x07\x3f\x24\x83\x41\x01\xbd\xe7\x67\x4b\x1a\x69\x6c\x98\x5a\xd7\x72\x27\x50\xc9\x9a\xb0\xe0\xb2\x5a\xeb\x1c\x0b\x25\xd7\x24\x9c\x7a\x72\x90\xa9\x8a\x1e\x91\x36\xd8\xda\x7e\xab\x51\xe2\xce\x0b\x75\x92\x1e\x23\x48\x73\x1c\x98\x66\x3a\xa0\x8d\xca\x9f\x47\x5d\x73\xcc\xc1\x3c\x6e\x9d\x98\x8c\xf9\xd0\x3f\xd6\xf0\x12\xe4\xe5\xb8\xf1\x02\xf0\x00\xb3\x43\x5e\xff\x26\xc5\x28\x3d\x4a\xfc\xea\xd3\x87\xcb\x63\x63\x76\x15\xfb\x8d\x91\xa1\xc4\x27\xa5\xd8\x53\xd1\x6a\xf7\xb7\x77\x1f\xdf\xc9\x82\x3b\x30\xc7\x1b\xe6\xa2\x69\xb9\x21\x35\x39\xdc\xb2\x3f\x13\x93\xa1\xfc\x18\x1d\x01\xf6\xba\x6c\x60\x50\x96\x25\x52\xcf\x48\x8a\xb3\xb3\xa3\x6b\x0f\xe9\x8e\x16\x69\x8e\x74\xed\x7e\xd7\x8b\xf4\xb1\x68\x45\xc5\xbb\x9a\xf4\xc4\x14\x46\xde\xc8\x1d\xa9\x6b\xa6\x69\x92\x65\x23\xdf\xe1\xb3\xd5\xdc\x51\xd1\x03\xdf\x28\x93\x23\x40\xa1\xa8\xf1\x15\x0b\xd0\xbf\x4a\x3a\xca\x30\xc7\x17\x66\x56\xc5\xa6\x15\x93\x9f\xf3\xf0\x99\xed\x27\x97\xf9\xeb\x10\x11\xb2\x19\x2e\x8b\x0f\xe1\x79\xad\xee\x51\x31\x53\xad\x30\xa1\x6c\x50\xfe\x9f\x4d\x34\xd6\x58\xcb\x75\xee\x7a\xc3\xc8\xde\xe0\x87\x9d\x15\x67\x32\x54\x86\xb2\x37\xff\xa1\xe4\xe6\x9b\x9d\x78\xbd\x56\x0f\x33\x60\x3a\xc5\xbd\x1f\x0d\x41\xeb\x87\x16\x0f\x13\xc3\xa3\xa8\x99\x61\xb3\x60\x0b\xfd\x39\x4c\xf4\xd0\x0e\x47\x33\xdc\x21\xda\x72\xd6\x0a\x87\x0b\xbb\x55\xe8\x32\x97\xa9\x15\x4b\x5f\x49\x3c\x96\x8f\xf1\x5a\xcf\xbe\x43\xb2\xd9\xeb\xe5\xe4\xdb\x94\xdb\x57\x47\xe9\xc7\x43\x24\x82\x7e\x80\x5d\xdb\x35\xe7\xa6\x05\xed\xb7\xbc\xad\x5a\x83\x56\xd4\x6d\xc5\x8c\x54\xfa\x10\xc7\x76\x46\x58\xbf\x5e\x09\x2e\xf0\xa0\xb0\xd4\xa8\x8e\xd2\x0c\xcf\xcf\x6f\x5a\xad\xfb\x69\xab\x67\xe7\xb4\xdd\x2a\xfb\xa4\x71\xfd\x8e\xad\x5e\xa4\xbe\xce\x50\x84\x7c\xb7\x04\x37\x8b\xdf\xa9\x41\xe2\xff\x80\x0a\xe9\x99\x4a\x0f\xec\x1e\x09\xc0\xf5\xfe\x01\x8f\x3f\xfc\x16\xff\x9b\x80\x32\xf4\xa1\x1d\x62\x6f\x32\x91\x8d\x1d\x8b\x6d\xa7\x57\xc1\x70\xca\x2b\x50\xf4\x9a\x3a\x21\x77\x9c\xea\x25\xa5\xa1\xf1\x5e\xc7\x5d\xbf\x13\xb6\x3e\x11\xd6\x76\xc4\x82\xe9\x77\xa2\x1e\xde\xc5\x2d\x50\x43\x6a\xd3\x0a\x1a\x6f\x86\x48\x77\x28\x87\x97\x9b\xfb\x8d\x89\xd9\x58\x91\x67\x67\x51\x0e\x4e\x62\x69\x56\xf8\x88\x4b\x97\x23\xea\xd6\x68\xf9\xda\x2f\x6f\xed\x5e\x97\x74\x3e\x8e\x89\x13\x0b\xb9\xf8\x90\xdb\x1a\x6e\xe4\x6e\x74\xee\x9a\xc9\x35\x76\xb4\x70\xdd\x60\xf8\x2f\x00\x00\xff\xff\x2f\x91\x05\xd4\xcf\x0a\x00\x00") func yaoAssistantsNeedsearchSrcIndexTsBytes() ([]byte, error) { return bindataRead( @@ -1855,7 +1855,7 @@ func yaoAssistantsNeedsearchSrcIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/needsearch/src/index.ts", size: 3025, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/needsearch/src/index.ts", size: 2767, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1875,7 +1875,7 @@ func yaoAssistantsPromptPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/prompt/package.yao", size: 186, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/prompt/package.yao", size: 186, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1895,7 +1895,7 @@ func yaoAssistantsPromptPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/prompt/prompts.yml", size: 621, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/prompt/prompts.yml", size: 621, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1915,7 +1915,7 @@ func yaoAssistantsQuerydslPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/package.yao", size: 196, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/package.yao", size: 196, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1935,7 +1935,7 @@ func yaoAssistantsQuerydslPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/prompts.yml", size: 1155, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/prompts.yml", size: 1155, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1955,7 +1955,7 @@ func yaoAssistantsTitlePackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/title/package.yao", size: 178, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/title/package.yao", size: 178, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1975,7 +1975,7 @@ func yaoAssistantsTitlePromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/title/prompts.yml", size: 958, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/assistants/title/prompts.yml", size: 958, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1995,7 +1995,7 @@ func yaoDataIcons404Png() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2015,7 +2015,7 @@ func yaoDataIconsIconIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2035,7 +2035,7 @@ func yaoDataIconsIconIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2055,7 +2055,7 @@ func yaoDataIconsIconPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2075,7 +2075,7 @@ func yaoDataIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2095,7 +2095,7 @@ func yaoDataKbProvidersChunkingSemanticEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/en.json", size: 5543, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/en.json", size: 5543, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2115,7 +2115,7 @@ func yaoDataKbProvidersChunkingSemanticZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/zh-cn.json", size: 5446, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/zh-cn.json", size: 5446, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2135,7 +2135,7 @@ func yaoDataKbProvidersChunkingStructuredEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/en.json", size: 2423, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/en.json", size: 2423, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2155,7 +2155,7 @@ func yaoDataKbProvidersChunkingStructuredZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/zh-cn.json", size: 2321, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/zh-cn.json", size: 2321, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2175,7 +2175,7 @@ func yaoDataKbProvidersConverterMcpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/en.json", size: 4235, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/en.json", size: 4235, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2195,7 +2195,7 @@ func yaoDataKbProvidersConverterMcpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/zh-cn.json", size: 4060, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/zh-cn.json", size: 4060, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2215,7 +2215,7 @@ func yaoDataKbProvidersConverterOcrEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/en.json", size: 6631, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/en.json", size: 6631, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2235,7 +2235,7 @@ func yaoDataKbProvidersConverterOcrZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/zh-cn.json", size: 6501, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/zh-cn.json", size: 6501, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2255,7 +2255,7 @@ func yaoDataKbProvidersConverterOfficeEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/en.json", size: 5476, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/en.json", size: 5476, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2275,7 +2275,7 @@ func yaoDataKbProvidersConverterOfficeZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/zh-cn.json", size: 5356, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/zh-cn.json", size: 5356, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2295,7 +2295,7 @@ func yaoDataKbProvidersConverterUtf8EnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/en.json", size: 292, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/en.json", size: 292, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2315,7 +2315,7 @@ func yaoDataKbProvidersConverterUtf8ZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/zh-cn.json", size: 281, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/zh-cn.json", size: 281, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2335,7 +2335,7 @@ func yaoDataKbProvidersConverterVideoEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/en.json", size: 6411, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/en.json", size: 6411, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2355,7 +2355,7 @@ func yaoDataKbProvidersConverterVideoZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/zh-cn.json", size: 6297, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/zh-cn.json", size: 6297, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2375,7 +2375,7 @@ func yaoDataKbProvidersConverterVisionEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/en.json", size: 4085, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/en.json", size: 4085, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2395,7 +2395,7 @@ func yaoDataKbProvidersConverterVisionZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/zh-cn.json", size: 3949, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/zh-cn.json", size: 3949, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2415,7 +2415,7 @@ func yaoDataKbProvidersConverterWhisperEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/en.json", size: 4449, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/en.json", size: 4449, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2435,7 +2435,7 @@ func yaoDataKbProvidersConverterWhisperZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/zh-cn.json", size: 4312, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/zh-cn.json", size: 4312, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2455,7 +2455,7 @@ func yaoDataKbProvidersEmbeddingFastembedEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/en.json", size: 6865, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/en.json", size: 6865, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2475,7 +2475,7 @@ func yaoDataKbProvidersEmbeddingFastembedZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/zh-cn.json", size: 6685, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/zh-cn.json", size: 6685, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2495,7 +2495,7 @@ func yaoDataKbProvidersEmbeddingOpenaiEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/en.json", size: 5636, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/en.json", size: 5636, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2515,7 +2515,7 @@ func yaoDataKbProvidersEmbeddingOpenaiZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/zh-cn.json", size: 5463, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/zh-cn.json", size: 5463, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2535,7 +2535,7 @@ func yaoDataKbProvidersExtractionOpenaiEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/en.json", size: 9110, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/en.json", size: 9110, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2555,7 +2555,7 @@ func yaoDataKbProvidersExtractionOpenaiZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/zh-cn.json", size: 8827, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/zh-cn.json", size: 8827, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2575,7 +2575,7 @@ func yaoDataKbProvidersFetcherHttpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/en.json", size: 5885, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/en.json", size: 5885, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2595,7 +2595,7 @@ func yaoDataKbProvidersFetcherHttpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/zh-cn.json", size: 5925, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/zh-cn.json", size: 5925, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2615,7 +2615,7 @@ func yaoDataKbProvidersFetcherMcpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/en.json", size: 6819, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/en.json", size: 6819, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2635,7 +2635,7 @@ func yaoDataKbProvidersFetcherMcpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/zh-cn.json", size: 6611, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/zh-cn.json", size: 6611, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2655,7 +2655,7 @@ func yaoFieldsModelTransJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2675,7 +2675,7 @@ func yaoLangsEnUsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2695,7 +2695,7 @@ func yaoLangsZhCnGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2715,7 +2715,7 @@ func yaoLangsZhCnLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2735,7 +2735,7 @@ func yaoLangsZhCnLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2755,7 +2755,7 @@ func yaoLangsZhHkGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2775,7 +2775,7 @@ func yaoLangsZhHkLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2795,7 +2795,7 @@ func yaoLangsZhHkLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2815,7 +2815,7 @@ func yaoModelsAgentAssistantModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/assistant.mod.yao", size: 6945, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/agent/assistant.mod.yao", size: 6945, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2835,7 +2835,7 @@ func yaoModelsAgentChatModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/chat.mod.yao", size: 3093, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/agent/chat.mod.yao", size: 3093, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2855,7 +2855,7 @@ func yaoModelsAgentMessageModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/message.mod.yao", size: 3712, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/agent/message.mod.yao", size: 3712, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2875,7 +2875,7 @@ func yaoModelsAgentResumeModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/resume.mod.yao", size: 3896, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/agent/resume.mod.yao", size: 3896, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2895,7 +2895,7 @@ func yaoModelsAgentSearchModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/search.mod.yao", size: 3103, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/agent/search.mod.yao", size: 3103, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2915,7 +2915,7 @@ func yaoModelsAttachmentModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 4687, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 4687, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2935,7 +2935,7 @@ func yaoModelsAuditModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2955,7 +2955,7 @@ func yaoModelsConfigModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2975,7 +2975,7 @@ func yaoModelsDslModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3826, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3826, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2995,7 +2995,7 @@ func yaoModelsInvitationModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/invitation.mod.yao", size: 6693, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/invitation.mod.yao", size: 6693, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3015,7 +3015,7 @@ func yaoModelsJobCategoryModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/category.mod.yao", size: 2041, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/job/category.mod.yao", size: 2041, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3035,7 +3035,7 @@ func yaoModelsJobExecutionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/execution.mod.yao", size: 7201, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/job/execution.mod.yao", size: 7201, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3055,7 +3055,7 @@ func yaoModelsJobJobModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/job.mod.yao", size: 6330, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/job/job.mod.yao", size: 6330, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3075,7 +3075,7 @@ func yaoModelsJobLogModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/log.mod.yao", size: 4711, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/job/log.mod.yao", size: 4711, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3095,7 +3095,7 @@ func yaoModelsKbCollectionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/kb/collection.mod.yao", size: 5390, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/kb/collection.mod.yao", size: 5390, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3115,7 +3115,7 @@ func yaoModelsKbDocumentModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/kb/document.mod.yao", size: 9906, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/kb/document.mod.yao", size: 9906, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3135,7 +3135,7 @@ func yaoModelsMemberModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/member.mod.yao", size: 14798, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/member.mod.yao", size: 14798, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3155,7 +3155,7 @@ func yaoModelsRoleModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/role.mod.yao", size: 6434, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/role.mod.yao", size: 6434, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3175,7 +3175,7 @@ func yaoModelsTeamModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/team.mod.yao", size: 15823, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/team.mod.yao", size: 15823, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3195,7 +3195,7 @@ func yaoModelsUserOauth_accountModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user/oauth_account.mod.yao", size: 6928, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/user/oauth_account.mod.yao", size: 6928, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3215,7 +3215,7 @@ func yaoModelsUserTypeModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user/type.mod.yao", size: 7502, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/user/type.mod.yao", size: 7502, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3235,7 +3235,7 @@ func yaoModelsUserModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user.mod.yao", size: 12335, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/models/user.mod.yao", size: 12335, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3255,7 +3255,7 @@ func yaoReleaseAppYaz() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3275,7 +3275,7 @@ func yaoStoresAgentCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/stores/agent/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3295,7 +3295,7 @@ func yaoStoresAgentMemoryBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/memory.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/stores/agent/memory.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3315,7 +3315,7 @@ func yaoStoresCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/cache.lru.yao", size: 285, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/stores/cache.lru.yao", size: 285, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3335,7 +3335,7 @@ func yaoStoresKbCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/kb/cache.lru.yao", size: 304, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/stores/kb/cache.lru.yao", size: 304, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3355,7 +3355,7 @@ func yaoStoresKbStoreBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/kb/store.badger.yao", size: 349, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/stores/kb/store.badger.yao", size: 349, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3375,7 +3375,7 @@ func yaoStoresOauthCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3395,7 +3395,7 @@ func yaoStoresOauthClientBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/client.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/client.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3415,7 +3415,7 @@ func yaoStoresOauthStoreBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/store.badger.yao", size: 376, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/store.badger.yao", size: 376, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3435,7 +3435,7 @@ func yaoStoresStoreBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/store.badger.yao", size: 341, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/stores/store.badger.yao", size: 341, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3455,7 +3455,7 @@ func yaoUploadersAttachmentLocalYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/uploaders/attachment.local.yao", size: 1163, mode: os.FileMode(420), modTime: time.Unix(1765877832, 0)} + info := bindataFileInfo{name: "yao/uploaders/attachment.local.yao", size: 1163, mode: os.FileMode(420), modTime: time.Unix(1765935236, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/main.go b/main.go index 356fb79e..9ada21db 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( _ "github.com/yaoapp/gou/diff" _ "github.com/yaoapp/gou/encoding" + _ "github.com/yaoapp/gou/text" _ "github.com/yaoapp/yao/aigc" _ "github.com/yaoapp/yao/crypto" _ "github.com/yaoapp/yao/excel" diff --git a/openapi/config.go b/openapi/config.go index 0deb31e3..9925ef37 100644 --- a/openapi/config.go +++ b/openapi/config.go @@ -2,7 +2,7 @@ package openapi import ( "errors" - "fmt" + // "fmt" "path/filepath" "strings" "time" @@ -135,10 +135,10 @@ func (config *Config) UnmarshalJSON(data []byte) error { Features: tempConfig.OAuth.Features, } - fmt.Println("----debug----") - fmt.Println("tempConfig.OAuth.IssuerURL", tempConfig.OAuth.IssuerURL) - fmt.Println("config.OAuth.IssuerURL", config.OAuth.IssuerURL) - fmt.Println("----debug----") + // fmt.Println("----debug----") + // fmt.Println("tempConfig.OAuth.IssuerURL", tempConfig.OAuth.IssuerURL) + // fmt.Println("config.OAuth.IssuerURL", config.OAuth.IssuerURL) + // fmt.Println("----debug----") // Convert signing config with duration parsing config.OAuth.Signing = types.SigningConfig{ diff --git a/yao/assistants/keyword/src/index.ts b/yao/assistants/keyword/src/index.ts index bc546de5..26d37ae6 100644 --- a/yao/assistants/keyword/src/index.ts +++ b/yao/assistants/keyword/src/index.ts @@ -7,7 +7,7 @@ /** * Next hook - processes keyword extraction response - * Uses json.Parse for fault-tolerant JSON parsing + * Uses text.ExtractJSON for fault-tolerant JSON extraction from LLM output */ function Next( ctx: agent.Context, @@ -20,24 +20,13 @@ function Next( return null; } - // Remove markdown code block if present - let content = completion.content.trim(); - if (content.startsWith("```json")) { - content = content.slice(7); - } else if (content.startsWith("```")) { - content = content.slice(3); - } - if (content.endsWith("```")) { - content = content.slice(0, -3); - } - content = content.trim(); - - // Try to parse JSON from completion content + const content = completion.content; let keywords: string[] = []; try { - // Use json.Parse for fault-tolerant parsing (handles broken JSON, JSONC, etc.) - const parsed = Process("json.Parse", content) as { + // Use text.ExtractJSON for fault-tolerant extraction + // Handles markdown code blocks, broken JSON, etc. + const parsed = Process("text.ExtractJSON", content) as { keywords?: string[]; } | null; @@ -47,7 +36,7 @@ function Next( ); } } catch (e) { - // If json.Parse fails, try to extract keywords from text + // If extraction fails, try to extract keywords from text keywords = extractKeywordsFromText(content); } diff --git a/yao/assistants/needsearch/src/index.ts b/yao/assistants/needsearch/src/index.ts index 2ccc92cb..9394f807 100644 --- a/yao/assistants/needsearch/src/index.ts +++ b/yao/assistants/needsearch/src/index.ts @@ -13,7 +13,7 @@ interface SearchResult { /** * Next hook - processes search intent response - * Uses json.Parse for fault-tolerant JSON parsing + * Uses text.ExtractJSON for fault-tolerant JSON extraction from LLM output */ function Next( ctx: agent.Context, @@ -26,17 +26,7 @@ function Next( return null; } - // Remove markdown code block if present - let content = completion.content.trim(); - if (content.startsWith("```json")) { - content = content.slice(7); // Remove ```json - } else if (content.startsWith("```")) { - content = content.slice(3); // Remove ``` - } - if (content.endsWith("```")) { - content = content.slice(0, -3); // Remove trailing ``` - } - content = content.trim(); + const content = completion.content; // Default result let result: SearchResult = { @@ -46,8 +36,9 @@ function Next( }; try { - // Use json.Parse for fault-tolerant parsing - const parsed = Process("json.Parse", content) as { + // Use text.ExtractJSON for fault-tolerant extraction + // Handles markdown code blocks, broken JSON, etc. + const parsed = Process("text.ExtractJSON", content) as { need_search?: boolean; search_types?: string[]; confidence?: number; @@ -68,7 +59,7 @@ function Next( : 0.5; } } catch (e) { - // If json.Parse fails, try to extract from text + // If extraction fails, try to extract from text result = extractFromText(content); }