@opensearch-project/agent-health 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -16
- package/cli/dist/index.js +752 -226
- package/dist/assets/index-4BAkkFzo.js +267 -0
- package/dist/assets/index-C3K5cBQr.css +1 -0
- package/dist/index.html +2 -2
- package/dist/opensearch-logo-dark.svg +5 -0
- package/dist/opensearch-logo-light.svg +5 -0
- package/dist/test-first-run-improved.html +469 -0
- package/lib/dist/config/index.js +97 -56
- package/lib/dist/index.js +82 -6
- package/package.json +6 -4
- package/server/dist/app.js +3926 -2377
- package/server/dist/index.js +3926 -2377
- package/dist/assets/index-D5yuaEp4.js +0 -267
- package/dist/assets/index-D6wGwYUm.css +0 -1
package/cli/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// cli/index.ts
|
|
4
|
-
import { Command as
|
|
5
|
-
import
|
|
4
|
+
import { Command as Command10 } from "commander";
|
|
5
|
+
import chalk10 from "chalk";
|
|
6
6
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7
7
|
import { dirname as dirname3, join as join4, resolve as resolve4 } from "path";
|
|
8
8
|
import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
|
|
@@ -96,9 +96,9 @@ var ENV_CONFIG = {
|
|
|
96
96
|
mlcommonsHeaderAwsSessionToken: getEnvVar("MLCOMMONS_HEADER_AWS_SESSION_TOKEN", ""),
|
|
97
97
|
// Travel Planner multi-agent endpoint (OTel Demo in Docker)
|
|
98
98
|
travelPlannerEndpoint: getEnvVar("TRAVEL_PLANNER_ENDPOINT", "http://localhost:3000"),
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
// OpenAI-compatible (optional - for OpenAI-compatible judge/agent endpoints)
|
|
100
|
+
openaiCompatibleApiKey: getEnvVar("OPENAI_COMPATIBLE_API_KEY", ""),
|
|
101
|
+
openaiCompatibleEndpoint: getEnvVar("OPENAI_COMPATIBLE_ENDPOINT", "http://localhost:4000/v1/chat/completions"),
|
|
102
102
|
// Claude Code Telemetry (optional - for OTEL traces from Claude Code)
|
|
103
103
|
claudeCodeTelemetryEnabled: getEnvVar("CLAUDE_CODE_TELEMETRY_ENABLED", "false") === "true",
|
|
104
104
|
otelExporterEndpoint: getEnvVar("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
|
|
@@ -106,39 +106,47 @@ var ENV_CONFIG = {
|
|
|
106
106
|
otelExporterProtocol: getEnvVar("OTEL_EXPORTER_OTLP_PROTOCOL", ""),
|
|
107
107
|
otelExporterHeaders: getEnvVar("OTEL_EXPORTER_OTLP_HEADERS", "")
|
|
108
108
|
};
|
|
109
|
-
function buildMLCommonsHeaders() {
|
|
110
|
-
const headers = {};
|
|
111
|
-
if (ENV_CONFIG.mlcommonsHeaderOpenSearchUrl) {
|
|
112
|
-
headers["opensearch-url"] = ENV_CONFIG.mlcommonsHeaderOpenSearchUrl;
|
|
113
|
-
}
|
|
114
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsRegion) {
|
|
115
|
-
headers["aws-region"] = ENV_CONFIG.mlcommonsHeaderAwsRegion;
|
|
116
|
-
}
|
|
117
|
-
if (ENV_CONFIG.mlcommonsHeaderAuthorization) {
|
|
118
|
-
headers["Authorization"] = ENV_CONFIG.mlcommonsHeaderAuthorization;
|
|
119
|
-
} else {
|
|
120
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsServiceName) {
|
|
121
|
-
headers["aws-service-name"] = ENV_CONFIG.mlcommonsHeaderAwsServiceName;
|
|
122
|
-
}
|
|
123
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsAccessKeyId) {
|
|
124
|
-
headers["aws-access-key-id"] = ENV_CONFIG.mlcommonsHeaderAwsAccessKeyId;
|
|
125
|
-
}
|
|
126
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsSecretAccessKey) {
|
|
127
|
-
headers["aws-secret-access-key"] = ENV_CONFIG.mlcommonsHeaderAwsSecretAccessKey;
|
|
128
|
-
}
|
|
129
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsSessionToken) {
|
|
130
|
-
headers["aws-session-token"] = ENV_CONFIG.mlcommonsHeaderAwsSessionToken;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return headers;
|
|
134
|
-
}
|
|
135
109
|
|
|
136
110
|
// lib/constants.ts
|
|
111
|
+
var CONNECTOR_TYPE_INFO = {
|
|
112
|
+
"agui-streaming": {
|
|
113
|
+
label: "AG-UI Streaming",
|
|
114
|
+
description: "AG-UI protocol over SSE. Use for ML-Commons and AG-UI compatible agents.",
|
|
115
|
+
serverOnly: false
|
|
116
|
+
},
|
|
117
|
+
"rest": {
|
|
118
|
+
label: "REST",
|
|
119
|
+
description: "Standard HTTP POST. Agent receives JSON, returns JSON. No streaming.",
|
|
120
|
+
serverOnly: false
|
|
121
|
+
},
|
|
122
|
+
"openai-compatible": {
|
|
123
|
+
label: "OpenAI Compatible",
|
|
124
|
+
description: "OpenAI chat completions format (POST /v1/chat/completions). Works with LiteLLM, Ollama, vLLM.",
|
|
125
|
+
serverOnly: false
|
|
126
|
+
},
|
|
127
|
+
"subprocess": {
|
|
128
|
+
label: "Subprocess",
|
|
129
|
+
description: "Runs a CLI command as a child process. Server-only \u2014 use the CLI or benchmark runner.",
|
|
130
|
+
serverOnly: true
|
|
131
|
+
},
|
|
132
|
+
"claude-code": {
|
|
133
|
+
label: "Claude Code",
|
|
134
|
+
description: "Invokes the Claude Code CLI. Server-only \u2014 use the CLI or benchmark runner.",
|
|
135
|
+
serverOnly: true
|
|
136
|
+
},
|
|
137
|
+
"mock": {
|
|
138
|
+
label: "Mock",
|
|
139
|
+
description: "Built-in demo agent for testing. No real endpoint needed.",
|
|
140
|
+
serverOnly: false
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var VALID_CONNECTOR_TYPES = Object.keys(CONNECTOR_TYPE_INFO);
|
|
144
|
+
var BROWSER_SAFE_CONNECTORS = Object.entries(CONNECTOR_TYPE_INFO).filter(([, info]) => !info.serverOnly).map(([type]) => type);
|
|
137
145
|
function getClaudeCodeConnectorEnv() {
|
|
138
146
|
const env = {
|
|
139
|
-
AWS_PROFILE:
|
|
147
|
+
AWS_PROFILE: ENV_CONFIG.awsProfile || "Bedrock",
|
|
140
148
|
CLAUDE_CODE_USE_BEDROCK: "1",
|
|
141
|
-
AWS_REGION:
|
|
149
|
+
AWS_REGION: ENV_CONFIG.awsRegion || "us-west-2",
|
|
142
150
|
DISABLE_PROMPT_CACHING: "1",
|
|
143
151
|
DISABLE_ERROR_REPORTING: "1"
|
|
144
152
|
};
|
|
@@ -165,37 +173,15 @@ var DEFAULT_CONFIG = {
|
|
|
165
173
|
endpoint: "mock://demo",
|
|
166
174
|
description: "Mock agent for testing (simulated responses)",
|
|
167
175
|
connectorType: "mock",
|
|
168
|
-
models: ["demo-model"],
|
|
169
176
|
headers: {},
|
|
170
177
|
useTraces: false
|
|
171
178
|
},
|
|
172
|
-
{
|
|
173
|
-
key: "mlcommons-local",
|
|
174
|
-
name: "ML-Commons (Localhost)",
|
|
175
|
-
endpoint: ENV_CONFIG.mlcommonsEndpoint,
|
|
176
|
-
description: "Local OpenSearch ML-Commons conversational agent",
|
|
177
|
-
connectorType: "agui-streaming",
|
|
178
|
-
models: ["claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-3.5"],
|
|
179
|
-
headers: buildMLCommonsHeaders(),
|
|
180
|
-
useTraces: true
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
key: "travel-planner",
|
|
184
|
-
name: "Travel Planner",
|
|
185
|
-
endpoint: ENV_CONFIG.travelPlannerEndpoint,
|
|
186
|
-
description: "Multi-agent Travel Planner demo (requires OTel Demo running via Docker)",
|
|
187
|
-
connectorType: "agui-streaming",
|
|
188
|
-
models: ["claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-3.5"],
|
|
189
|
-
headers: {},
|
|
190
|
-
useTraces: true
|
|
191
|
-
},
|
|
192
179
|
{
|
|
193
180
|
key: "claude-code",
|
|
194
181
|
name: "Claude Code",
|
|
195
182
|
endpoint: "claude",
|
|
196
183
|
description: "Claude Code CLI agent (requires claude command installed)",
|
|
197
184
|
connectorType: "claude-code",
|
|
198
|
-
models: ["claude-sonnet-4"],
|
|
199
185
|
headers: {},
|
|
200
186
|
useTraces: ENV_CONFIG.claudeCodeTelemetryEnabled && !!ENV_CONFIG.otelExporterEndpoint,
|
|
201
187
|
connectorConfig: { env: getClaudeCodeConnectorEnv() }
|
|
@@ -209,6 +195,48 @@ var DEFAULT_CONFIG = {
|
|
|
209
195
|
context_window: 2e5,
|
|
210
196
|
max_output_tokens: 4096
|
|
211
197
|
},
|
|
198
|
+
"claude-opus-4.6": {
|
|
199
|
+
model_id: "us.anthropic.claude-opus-4-6-v1",
|
|
200
|
+
display_name: "Claude Opus 4.6",
|
|
201
|
+
provider: "bedrock",
|
|
202
|
+
context_window: 2e5,
|
|
203
|
+
max_output_tokens: 128e3
|
|
204
|
+
},
|
|
205
|
+
"claude-sonnet-4.6": {
|
|
206
|
+
model_id: "us.anthropic.claude-sonnet-4-6",
|
|
207
|
+
display_name: "Claude Sonnet 4.6",
|
|
208
|
+
provider: "bedrock",
|
|
209
|
+
context_window: 2e5,
|
|
210
|
+
max_output_tokens: 64e3
|
|
211
|
+
},
|
|
212
|
+
"claude-haiku-4.5": {
|
|
213
|
+
model_id: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
214
|
+
display_name: "Claude Haiku 4.5",
|
|
215
|
+
provider: "bedrock",
|
|
216
|
+
context_window: 2e5,
|
|
217
|
+
max_output_tokens: 64e3
|
|
218
|
+
},
|
|
219
|
+
"claude-opus-4.5": {
|
|
220
|
+
model_id: "us.anthropic.claude-opus-4-5-20251101-v1:0",
|
|
221
|
+
display_name: "Claude Opus 4.5",
|
|
222
|
+
provider: "bedrock",
|
|
223
|
+
context_window: 2e5,
|
|
224
|
+
max_output_tokens: 64e3
|
|
225
|
+
},
|
|
226
|
+
"claude-opus-4.1": {
|
|
227
|
+
model_id: "us.anthropic.claude-opus-4-1-20250805-v1:0",
|
|
228
|
+
display_name: "Claude Opus 4.1",
|
|
229
|
+
provider: "bedrock",
|
|
230
|
+
context_window: 2e5,
|
|
231
|
+
max_output_tokens: 32e3
|
|
232
|
+
},
|
|
233
|
+
"claude-opus-4": {
|
|
234
|
+
model_id: "us.anthropic.claude-opus-4-20250514-v1:0",
|
|
235
|
+
display_name: "Claude Opus 4",
|
|
236
|
+
provider: "bedrock",
|
|
237
|
+
context_window: 2e5,
|
|
238
|
+
max_output_tokens: 32e3
|
|
239
|
+
},
|
|
212
240
|
"claude-sonnet-4.5": {
|
|
213
241
|
model_id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
|
214
242
|
display_name: "Claude Sonnet 4.5",
|
|
@@ -232,10 +260,24 @@ var DEFAULT_CONFIG = {
|
|
|
232
260
|
},
|
|
233
261
|
"gpt-4o": {
|
|
234
262
|
model_id: "gpt-4o",
|
|
235
|
-
display_name: "GPT-4o
|
|
236
|
-
provider: "
|
|
263
|
+
display_name: "GPT-4o",
|
|
264
|
+
provider: "openai-compatible",
|
|
237
265
|
context_window: 128e3,
|
|
238
266
|
max_output_tokens: 4096
|
|
267
|
+
},
|
|
268
|
+
"deepseek-r1:8b": {
|
|
269
|
+
model_id: "deepseek-r1:8b",
|
|
270
|
+
display_name: "DeepSeek R1 8B (Ollama)",
|
|
271
|
+
provider: "openai-compatible",
|
|
272
|
+
context_window: 128e3,
|
|
273
|
+
max_output_tokens: 8192
|
|
274
|
+
},
|
|
275
|
+
"gemma3:12b": {
|
|
276
|
+
model_id: "gemma3:12b",
|
|
277
|
+
display_name: "Gemma 3 12B (Ollama)",
|
|
278
|
+
provider: "openai-compatible",
|
|
279
|
+
context_window: 128e3,
|
|
280
|
+
max_output_tokens: 8192
|
|
239
281
|
}
|
|
240
282
|
},
|
|
241
283
|
defaults: {
|
|
@@ -272,7 +314,6 @@ function toAgentConfig(userAgent) {
|
|
|
272
314
|
endpoint: userAgent.endpoint,
|
|
273
315
|
description: userAgent.description,
|
|
274
316
|
enabled: userAgent.enabled ?? true,
|
|
275
|
-
models: userAgent.models,
|
|
276
317
|
headers: userAgent.headers ?? {},
|
|
277
318
|
useTraces: userAgent.useTraces ?? false,
|
|
278
319
|
connectorType: userAgent.connectorType,
|
|
@@ -1560,12 +1601,12 @@ var RESTConnector = class extends BaseConnector {
|
|
|
1560
1601
|
};
|
|
1561
1602
|
var restConnector = new RESTConnector();
|
|
1562
1603
|
|
|
1563
|
-
// services/connectors/
|
|
1564
|
-
var
|
|
1604
|
+
// services/connectors/openai-compatible/OpenAICompatibleConnector.ts
|
|
1605
|
+
var OpenAICompatibleConnector = class extends BaseConnector {
|
|
1565
1606
|
constructor() {
|
|
1566
1607
|
super(...arguments);
|
|
1567
|
-
this.type = "
|
|
1568
|
-
this.name = "
|
|
1608
|
+
this.type = "openai-compatible";
|
|
1609
|
+
this.name = "OpenAI-compatible";
|
|
1569
1610
|
this.supportsStreaming = false;
|
|
1570
1611
|
}
|
|
1571
1612
|
/**
|
|
@@ -1606,7 +1647,7 @@ var LiteLLMConnector = class extends BaseConnector {
|
|
|
1606
1647
|
async execute(endpoint, request, auth, onProgress, onRawEvent) {
|
|
1607
1648
|
const payload = request.payload || this.buildPayload(request);
|
|
1608
1649
|
const headers = this.buildAuthHeaders(auth);
|
|
1609
|
-
this.debug("Executing
|
|
1650
|
+
this.debug("Executing OpenAI-compatible request");
|
|
1610
1651
|
this.debug("Endpoint:", endpoint);
|
|
1611
1652
|
this.debug("Model:", payload.model);
|
|
1612
1653
|
const response = await fetch(endpoint, {
|
|
@@ -1619,7 +1660,7 @@ var LiteLLMConnector = class extends BaseConnector {
|
|
|
1619
1660
|
});
|
|
1620
1661
|
if (!response.ok) {
|
|
1621
1662
|
const errorText = await response.text();
|
|
1622
|
-
throw new Error(`
|
|
1663
|
+
throw new Error(`OpenAI-compatible request failed: ${response.status} - ${errorText}`);
|
|
1623
1664
|
}
|
|
1624
1665
|
const data = await response.json();
|
|
1625
1666
|
onRawEvent?.(data);
|
|
@@ -1670,13 +1711,13 @@ var LiteLLMConnector = class extends BaseConnector {
|
|
|
1670
1711
|
return steps;
|
|
1671
1712
|
}
|
|
1672
1713
|
};
|
|
1673
|
-
var
|
|
1714
|
+
var openaiCompatibleConnector = new OpenAICompatibleConnector();
|
|
1674
1715
|
|
|
1675
1716
|
// services/connectors/index.ts
|
|
1676
1717
|
connectorRegistry.register(aguiStreamingConnector);
|
|
1677
1718
|
connectorRegistry.register(mockConnector);
|
|
1678
1719
|
connectorRegistry.register(restConnector);
|
|
1679
|
-
connectorRegistry.register(
|
|
1720
|
+
connectorRegistry.register(openaiCompatibleConnector);
|
|
1680
1721
|
console.log("[Connectors] Browser-safe connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
|
|
1681
1722
|
|
|
1682
1723
|
// services/connectors/subprocess/SubprocessConnector.ts
|
|
@@ -2043,7 +2084,39 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
|
|
|
2043
2084
|
this.isInThinking = false;
|
|
2044
2085
|
}
|
|
2045
2086
|
/**
|
|
2046
|
-
*
|
|
2087
|
+
* Build CLI args from ClaudeCodeConnectorConfig
|
|
2088
|
+
*/
|
|
2089
|
+
buildConfigArgs(config) {
|
|
2090
|
+
const args = [];
|
|
2091
|
+
if (config.dangerouslySkipPermissions) {
|
|
2092
|
+
args.push("--dangerously-skip-permissions");
|
|
2093
|
+
}
|
|
2094
|
+
if (config.systemPrompt) {
|
|
2095
|
+
args.push("--system-prompt", config.systemPrompt);
|
|
2096
|
+
} else if (config.appendSystemPrompt) {
|
|
2097
|
+
args.push("--append-system-prompt", config.appendSystemPrompt);
|
|
2098
|
+
}
|
|
2099
|
+
if (config.allowedTools?.length) {
|
|
2100
|
+
args.push("--allowed-tools", ...config.allowedTools);
|
|
2101
|
+
}
|
|
2102
|
+
if (config.disallowedTools?.length) {
|
|
2103
|
+
args.push("--disallowed-tools", ...config.disallowedTools);
|
|
2104
|
+
}
|
|
2105
|
+
if (config.mcpConfigPath) {
|
|
2106
|
+
args.push("--mcp-config", config.mcpConfigPath);
|
|
2107
|
+
} else if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
2108
|
+
args.push("--mcp-config", JSON.stringify({ mcpServers: config.mcpServers }));
|
|
2109
|
+
}
|
|
2110
|
+
if (config.strictMcpConfig) {
|
|
2111
|
+
args.push("--strict-mcp-config");
|
|
2112
|
+
}
|
|
2113
|
+
if (config.additionalArgs) {
|
|
2114
|
+
args.push(...config.additionalArgs);
|
|
2115
|
+
}
|
|
2116
|
+
return args;
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Override execute to reset state and apply connectorConfig
|
|
2047
2120
|
*/
|
|
2048
2121
|
async execute(endpoint, request, auth, onProgress, onRawEvent) {
|
|
2049
2122
|
this.debug("========== execute() STARTED ==========");
|
|
@@ -2051,11 +2124,55 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
|
|
|
2051
2124
|
this.debug("Test case:", request.testCase.name);
|
|
2052
2125
|
this.debug("Config:", this["config"]);
|
|
2053
2126
|
this.resetState();
|
|
2054
|
-
this.
|
|
2055
|
-
const
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2127
|
+
const originalEnv = this.config.env ? structuredClone(this.config.env) : {};
|
|
2128
|
+
const originalArgs = this.config.args ? [...this.config.args] : [];
|
|
2129
|
+
const originalInputMode = this.config.inputMode;
|
|
2130
|
+
const originalTimeout = this.config.timeout;
|
|
2131
|
+
const originalWorkingDir = this.config.workingDir;
|
|
2132
|
+
const ccConfig = request.connectorConfig;
|
|
2133
|
+
if (ccConfig) {
|
|
2134
|
+
this.debug("Applying connectorConfig:", Object.keys(ccConfig));
|
|
2135
|
+
if (ccConfig.env) {
|
|
2136
|
+
this.config.env = { ...this.config.env, ...ccConfig.env };
|
|
2137
|
+
}
|
|
2138
|
+
if (ccConfig.usePromptArg) {
|
|
2139
|
+
this.config.inputMode = "arg";
|
|
2140
|
+
}
|
|
2141
|
+
if (ccConfig.timeout !== void 0) {
|
|
2142
|
+
this.config.timeout = ccConfig.timeout;
|
|
2143
|
+
}
|
|
2144
|
+
if (ccConfig.workingDir) {
|
|
2145
|
+
this.config.workingDir = ccConfig.workingDir;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
if (this.config.env?.CLAUDE_CODE_USE_BEDROCK === "1") {
|
|
2149
|
+
this.config.env = { ...this.config.env, ANTHROPIC_API_KEY: "" };
|
|
2150
|
+
this.debug("Bedrock mode: cleared ANTHROPIC_API_KEY to bypass credit check");
|
|
2151
|
+
}
|
|
2152
|
+
if (request.modelId) {
|
|
2153
|
+
this.config.args = [...this.config.args || [], "--model", request.modelId];
|
|
2154
|
+
this.debug("Model flag added:", request.modelId);
|
|
2155
|
+
}
|
|
2156
|
+
if (ccConfig) {
|
|
2157
|
+
const configArgs = this.buildConfigArgs(ccConfig);
|
|
2158
|
+
if (configArgs.length > 0) {
|
|
2159
|
+
this.config.args = [...this.config.args || [], ...configArgs];
|
|
2160
|
+
this.debug("Config args added:", configArgs);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
try {
|
|
2164
|
+
this.debug("State reset, calling super.execute()...");
|
|
2165
|
+
const result = await super.execute(endpoint, request, auth, onProgress, onRawEvent);
|
|
2166
|
+
this.debug("super.execute() returned with", result.trajectory.length, "steps");
|
|
2167
|
+
this.debug("========== execute() COMPLETED ==========");
|
|
2168
|
+
return result;
|
|
2169
|
+
} finally {
|
|
2170
|
+
this.config.env = originalEnv;
|
|
2171
|
+
this.config.args = originalArgs;
|
|
2172
|
+
this.config.inputMode = originalInputMode;
|
|
2173
|
+
this.config.timeout = originalTimeout;
|
|
2174
|
+
this.config.workingDir = originalWorkingDir;
|
|
2175
|
+
}
|
|
2059
2176
|
}
|
|
2060
2177
|
/**
|
|
2061
2178
|
* Health check - verify claude command exists
|
|
@@ -2309,6 +2426,12 @@ function createServerCleanup(result, isCI) {
|
|
|
2309
2426
|
}
|
|
2310
2427
|
|
|
2311
2428
|
// cli/utils/apiClient.ts
|
|
2429
|
+
var ServerError = class extends Error {
|
|
2430
|
+
constructor(message) {
|
|
2431
|
+
super(message);
|
|
2432
|
+
this.name = "ServerError";
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2312
2435
|
var ApiClient = class {
|
|
2313
2436
|
constructor(baseUrl) {
|
|
2314
2437
|
this.baseUrl = baseUrl;
|
|
@@ -2424,7 +2547,7 @@ var ApiClient = class {
|
|
|
2424
2547
|
} else if (event.type === "completed" || event.type === "cancelled") {
|
|
2425
2548
|
finalRun = event.run;
|
|
2426
2549
|
} else if (event.type === "error") {
|
|
2427
|
-
throw new
|
|
2550
|
+
throw new ServerError(event.error);
|
|
2428
2551
|
}
|
|
2429
2552
|
} catch (e) {
|
|
2430
2553
|
if (e instanceof SyntaxError) continue;
|
|
@@ -2434,6 +2557,9 @@ var ApiClient = class {
|
|
|
2434
2557
|
}
|
|
2435
2558
|
}
|
|
2436
2559
|
} catch (streamError) {
|
|
2560
|
+
if (streamError instanceof ServerError) {
|
|
2561
|
+
throw streamError;
|
|
2562
|
+
}
|
|
2437
2563
|
if (runId) {
|
|
2438
2564
|
console.warn(`[ApiClient] SSE stream disconnected: ${streamError instanceof Error ? streamError.message : streamError}`);
|
|
2439
2565
|
console.warn(`[ApiClient] Falling back to polling for run ${runId}...`);
|
|
@@ -2671,7 +2797,7 @@ var ApiClient = class {
|
|
|
2671
2797
|
}
|
|
2672
2798
|
const response = await this.listTestCasesWithMeta();
|
|
2673
2799
|
return response.data.find(
|
|
2674
|
-
(tc) => tc.name
|
|
2800
|
+
(tc) => tc.name?.toLowerCase() === identifier.toLowerCase()
|
|
2675
2801
|
) || null;
|
|
2676
2802
|
}
|
|
2677
2803
|
/**
|
|
@@ -2719,7 +2845,7 @@ var ApiClient = class {
|
|
|
2719
2845
|
if (event.type === "completed") {
|
|
2720
2846
|
result = event.report;
|
|
2721
2847
|
} else if (event.type === "error") {
|
|
2722
|
-
throw new
|
|
2848
|
+
throw new ServerError(event.error);
|
|
2723
2849
|
}
|
|
2724
2850
|
} catch (e) {
|
|
2725
2851
|
if (e instanceof SyntaxError) continue;
|
|
@@ -2769,12 +2895,53 @@ var ApiClient = class {
|
|
|
2769
2895
|
}
|
|
2770
2896
|
return res.json();
|
|
2771
2897
|
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Fetch traces from OpenSearch with optional filters
|
|
2900
|
+
*/
|
|
2901
|
+
async fetchTraces(params) {
|
|
2902
|
+
const res = await fetch(`${this.baseUrl}/api/traces`, {
|
|
2903
|
+
method: "POST",
|
|
2904
|
+
headers: { "Content-Type": "application/json" },
|
|
2905
|
+
body: JSON.stringify(params)
|
|
2906
|
+
});
|
|
2907
|
+
if (!res.ok) {
|
|
2908
|
+
const errorBody = await res.text();
|
|
2909
|
+
let errorMessage;
|
|
2910
|
+
try {
|
|
2911
|
+
const parsed = JSON.parse(errorBody);
|
|
2912
|
+
errorMessage = parsed.error || errorBody;
|
|
2913
|
+
} catch {
|
|
2914
|
+
errorMessage = errorBody;
|
|
2915
|
+
}
|
|
2916
|
+
throw new Error(`Failed to fetch traces: ${errorMessage}`);
|
|
2917
|
+
}
|
|
2918
|
+
return res.json();
|
|
2919
|
+
}
|
|
2772
2920
|
};
|
|
2773
2921
|
|
|
2774
|
-
// cli/
|
|
2922
|
+
// cli/utils/formatOutput.ts
|
|
2923
|
+
var OUTPUT_FORMAT_DESCRIPTION = "Output format: table, json, markdown";
|
|
2924
|
+
function formatMarkdownTable(headers, rows) {
|
|
2925
|
+
const separator = headers.map(() => "---");
|
|
2926
|
+
const lines = [
|
|
2927
|
+
`| ${headers.join(" | ")} |`,
|
|
2928
|
+
`| ${separator.join(" | ")} |`,
|
|
2929
|
+
...rows.map((row) => `| ${row.join(" | ")} |`)
|
|
2930
|
+
];
|
|
2931
|
+
return lines.join("\n");
|
|
2932
|
+
}
|
|
2775
2933
|
function formatJson(data) {
|
|
2776
2934
|
return JSON.stringify(data, null, 2);
|
|
2777
2935
|
}
|
|
2936
|
+
function parseOutputFormat(format) {
|
|
2937
|
+
const normalized = format.toLowerCase();
|
|
2938
|
+
if (normalized === "table" || normalized === "json" || normalized === "markdown" || normalized === "md") {
|
|
2939
|
+
return normalized === "md" ? "markdown" : normalized;
|
|
2940
|
+
}
|
|
2941
|
+
return "table";
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// cli/commands/list.ts
|
|
2778
2945
|
function displayStorageWarnings(meta) {
|
|
2779
2946
|
if (!meta.storageConfigured) {
|
|
2780
2947
|
console.log(chalk.yellow("\n \u26A0 Storage not configured"));
|
|
@@ -2795,25 +2962,24 @@ async function listAgents(format, config) {
|
|
|
2795
2962
|
console.log(formatJson(agents));
|
|
2796
2963
|
return;
|
|
2797
2964
|
}
|
|
2965
|
+
const headers = ["Key", "Name", "Connector", "Endpoint"];
|
|
2966
|
+
const rows = agents.map((agent) => [
|
|
2967
|
+
agent.key,
|
|
2968
|
+
agent.name,
|
|
2969
|
+
agent.connectorType || "agui-streaming",
|
|
2970
|
+
agent.endpoint.substring(0, 47) + (agent.endpoint.length > 47 ? "..." : "")
|
|
2971
|
+
]);
|
|
2972
|
+
if (format === "markdown") {
|
|
2973
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2798
2976
|
const table = new Table({
|
|
2799
|
-
head:
|
|
2800
|
-
|
|
2801
|
-
chalk.cyan("Name"),
|
|
2802
|
-
chalk.cyan("Connector"),
|
|
2803
|
-
chalk.cyan("Models"),
|
|
2804
|
-
chalk.cyan("Endpoint")
|
|
2805
|
-
],
|
|
2806
|
-
colWidths: [15, 20, 15, 25, 40],
|
|
2977
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
2978
|
+
colWidths: [15, 20, 15, 50],
|
|
2807
2979
|
wordWrap: true
|
|
2808
2980
|
});
|
|
2809
|
-
for (const
|
|
2810
|
-
table.push(
|
|
2811
|
-
agent.key,
|
|
2812
|
-
agent.name,
|
|
2813
|
-
agent.connectorType || "agui-streaming",
|
|
2814
|
-
agent.models.slice(0, 3).join(", ") + (agent.models.length > 3 ? "..." : ""),
|
|
2815
|
-
agent.endpoint.substring(0, 37) + (agent.endpoint.length > 37 ? "..." : "")
|
|
2816
|
-
]);
|
|
2981
|
+
for (const row of rows) {
|
|
2982
|
+
table.push(row);
|
|
2817
2983
|
}
|
|
2818
2984
|
console.log(chalk.bold("\nAvailable Agents:\n"));
|
|
2819
2985
|
console.log(table.toString());
|
|
@@ -2835,31 +3001,35 @@ async function listTestCases(format, config) {
|
|
|
2835
3001
|
try {
|
|
2836
3002
|
const client = new ApiClient(serverResult.baseUrl);
|
|
2837
3003
|
const response = await client.listTestCasesWithMeta();
|
|
2838
|
-
|
|
3004
|
+
if (format !== "markdown") {
|
|
3005
|
+
displayStorageWarnings(response.meta);
|
|
3006
|
+
}
|
|
2839
3007
|
if (format === "json") {
|
|
2840
3008
|
console.log(formatJson(response));
|
|
2841
3009
|
return;
|
|
2842
3010
|
}
|
|
2843
|
-
const
|
|
2844
|
-
|
|
2845
|
-
chalk.cyan("ID"),
|
|
2846
|
-
chalk.cyan("Name"),
|
|
2847
|
-
chalk.cyan("Labels"),
|
|
2848
|
-
chalk.cyan("Version"),
|
|
2849
|
-
chalk.cyan("Source")
|
|
2850
|
-
],
|
|
2851
|
-
colWidths: [25, 28, 28, 10, 10],
|
|
2852
|
-
wordWrap: true
|
|
2853
|
-
});
|
|
2854
|
-
for (const tc of response.data) {
|
|
3011
|
+
const headers = ["ID", "Name", "Labels", "Version", "Source"];
|
|
3012
|
+
const rows = response.data.map((tc) => {
|
|
2855
3013
|
const isDemo = tc.id.startsWith("demo-");
|
|
2856
|
-
|
|
3014
|
+
return [
|
|
2857
3015
|
tc.id,
|
|
2858
3016
|
tc.name,
|
|
2859
3017
|
tc.labels?.slice(0, 3).join(", ") || "",
|
|
2860
3018
|
`v${tc.currentVersion || 1}`,
|
|
2861
|
-
isDemo ?
|
|
2862
|
-
]
|
|
3019
|
+
isDemo ? "Sample" : "Stored"
|
|
3020
|
+
];
|
|
3021
|
+
});
|
|
3022
|
+
if (format === "markdown") {
|
|
3023
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
const table = new Table({
|
|
3027
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
3028
|
+
colWidths: [25, 28, 28, 10, 10],
|
|
3029
|
+
wordWrap: true
|
|
3030
|
+
});
|
|
3031
|
+
for (const row of rows) {
|
|
3032
|
+
table.push(row);
|
|
2863
3033
|
}
|
|
2864
3034
|
console.log(chalk.bold("\nAvailable Test Cases:\n"));
|
|
2865
3035
|
console.log(table.toString());
|
|
@@ -2888,31 +3058,35 @@ async function listBenchmarks(format, config) {
|
|
|
2888
3058
|
try {
|
|
2889
3059
|
const client = new ApiClient(serverResult.baseUrl);
|
|
2890
3060
|
const response = await client.listBenchmarksWithMeta();
|
|
2891
|
-
|
|
3061
|
+
if (format !== "markdown") {
|
|
3062
|
+
displayStorageWarnings(response.meta);
|
|
3063
|
+
}
|
|
2892
3064
|
if (format === "json") {
|
|
2893
3065
|
console.log(formatJson(response));
|
|
2894
3066
|
return;
|
|
2895
3067
|
}
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
chalk.cyan("ID"),
|
|
2899
|
-
chalk.cyan("Name"),
|
|
2900
|
-
chalk.cyan("Test Cases"),
|
|
2901
|
-
chalk.cyan("Created"),
|
|
2902
|
-
chalk.cyan("Source")
|
|
2903
|
-
],
|
|
2904
|
-
colWidths: [28, 28, 12, 22, 10],
|
|
2905
|
-
wordWrap: true
|
|
2906
|
-
});
|
|
2907
|
-
for (const b of response.data) {
|
|
3068
|
+
const headers = ["ID", "Name", "Test Cases", "Created", "Source"];
|
|
3069
|
+
const rows = response.data.map((b) => {
|
|
2908
3070
|
const isDemo = b.id.startsWith("demo-");
|
|
2909
|
-
|
|
3071
|
+
return [
|
|
2910
3072
|
b.id,
|
|
2911
3073
|
b.name,
|
|
2912
3074
|
b.testCaseIds.length.toString(),
|
|
2913
3075
|
new Date(b.createdAt).toLocaleDateString(),
|
|
2914
|
-
isDemo ?
|
|
2915
|
-
]
|
|
3076
|
+
isDemo ? "Sample" : "Stored"
|
|
3077
|
+
];
|
|
3078
|
+
});
|
|
3079
|
+
if (format === "markdown") {
|
|
3080
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
const table = new Table({
|
|
3084
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
3085
|
+
colWidths: [28, 28, 12, 22, 10],
|
|
3086
|
+
wordWrap: true
|
|
3087
|
+
});
|
|
3088
|
+
for (const row of rows) {
|
|
3089
|
+
table.push(row);
|
|
2916
3090
|
}
|
|
2917
3091
|
console.log(chalk.bold("\nAvailable Benchmarks:\n"));
|
|
2918
3092
|
console.log(table.toString());
|
|
@@ -2949,19 +3123,25 @@ function listConnectors(format) {
|
|
|
2949
3123
|
console.log(formatJson(connectors));
|
|
2950
3124
|
return;
|
|
2951
3125
|
}
|
|
3126
|
+
const headers = ["Type", "Name", "Streaming"];
|
|
3127
|
+
const rows = connectors.map((c) => [
|
|
3128
|
+
c.type,
|
|
3129
|
+
c.name,
|
|
3130
|
+
c.streaming ? "Yes" : "No"
|
|
3131
|
+
]);
|
|
3132
|
+
if (format === "markdown") {
|
|
3133
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
2952
3136
|
const table = new Table({
|
|
2953
|
-
head:
|
|
2954
|
-
chalk.cyan("Type"),
|
|
2955
|
-
chalk.cyan("Name"),
|
|
2956
|
-
chalk.cyan("Streaming")
|
|
2957
|
-
],
|
|
3137
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
2958
3138
|
colWidths: [20, 25, 12]
|
|
2959
3139
|
});
|
|
2960
|
-
for (const
|
|
3140
|
+
for (const row of rows) {
|
|
2961
3141
|
table.push([
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3142
|
+
row[0],
|
|
3143
|
+
row[1],
|
|
3144
|
+
row[2] === "Yes" ? chalk.green("Yes") : chalk.gray("No")
|
|
2965
3145
|
]);
|
|
2966
3146
|
}
|
|
2967
3147
|
console.log(chalk.bold("\nRegistered Connectors:\n"));
|
|
@@ -2980,23 +3160,24 @@ async function listModels(format, config) {
|
|
|
2980
3160
|
console.log(formatJson(models));
|
|
2981
3161
|
return;
|
|
2982
3162
|
}
|
|
3163
|
+
const headers = ["Key", "Display Name", "Provider", "Context"];
|
|
3164
|
+
const rows = models.map((m) => [
|
|
3165
|
+
m.key,
|
|
3166
|
+
m.display_name || m.key,
|
|
3167
|
+
m.provider || "bedrock",
|
|
3168
|
+
m.context_window ? `${Math.round(m.context_window / 1e3)}k` : "-"
|
|
3169
|
+
]);
|
|
3170
|
+
if (format === "markdown") {
|
|
3171
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
2983
3174
|
const table = new Table({
|
|
2984
|
-
head:
|
|
2985
|
-
chalk.cyan("Key"),
|
|
2986
|
-
chalk.cyan("Display Name"),
|
|
2987
|
-
chalk.cyan("Provider"),
|
|
2988
|
-
chalk.cyan("Context")
|
|
2989
|
-
],
|
|
3175
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
2990
3176
|
colWidths: [25, 30, 12, 12],
|
|
2991
3177
|
wordWrap: true
|
|
2992
3178
|
});
|
|
2993
|
-
for (const
|
|
2994
|
-
table.push(
|
|
2995
|
-
m.key,
|
|
2996
|
-
m.display_name || m.key,
|
|
2997
|
-
m.provider || "bedrock",
|
|
2998
|
-
m.context_window ? `${Math.round(m.context_window / 1e3)}k` : "-"
|
|
2999
|
-
]);
|
|
3179
|
+
for (const row of rows) {
|
|
3180
|
+
table.push(row);
|
|
3000
3181
|
}
|
|
3001
3182
|
console.log(chalk.bold("\nAvailable Models:\n"));
|
|
3002
3183
|
console.log(table.toString());
|
|
@@ -3013,8 +3194,8 @@ async function listModels(format, config) {
|
|
|
3013
3194
|
}
|
|
3014
3195
|
}
|
|
3015
3196
|
function createListCommand() {
|
|
3016
|
-
const command = new Command("list").description("List available resources").argument("<resource>", "Resource type: agents, test-cases, benchmarks, connectors, models").option("-o, --output <format>",
|
|
3017
|
-
const format = options.output;
|
|
3197
|
+
const command = new Command("list").description("List available resources").argument("<resource>", "Resource type: agents, test-cases, benchmarks, connectors, models").option("-o, --output <format>", OUTPUT_FORMAT_DESCRIPTION, "table").action(async (resource, options) => {
|
|
3198
|
+
const format = parseOutputFormat(options.output);
|
|
3018
3199
|
const config = await loadConfig();
|
|
3019
3200
|
for (const connector of config.connectors) {
|
|
3020
3201
|
connectorRegistry.register(connector);
|
|
@@ -3058,8 +3239,8 @@ function findAgent(identifier, config) {
|
|
|
3058
3239
|
(a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
|
|
3059
3240
|
);
|
|
3060
3241
|
}
|
|
3061
|
-
function getDefaultModel(
|
|
3062
|
-
return
|
|
3242
|
+
function getDefaultModel(config) {
|
|
3243
|
+
return Object.keys(config.models)[0] || "claude-sonnet";
|
|
3063
3244
|
}
|
|
3064
3245
|
async function commandExists(command) {
|
|
3065
3246
|
const { execSync: execSync2 } = await import("child_process");
|
|
@@ -3113,26 +3294,36 @@ async function runForAgent(client, testCaseId, agent, modelId, verbose) {
|
|
|
3113
3294
|
throw error;
|
|
3114
3295
|
}
|
|
3115
3296
|
}
|
|
3116
|
-
function
|
|
3297
|
+
function buildResultRows(results) {
|
|
3298
|
+
return results.map((r) => {
|
|
3299
|
+
if (!r.report) {
|
|
3300
|
+
return [r.agent.name, "ERROR", "-", "-", "-"];
|
|
3301
|
+
}
|
|
3302
|
+
const status = r.report.passFailStatus === "passed" ? "PASSED" : r.report.passFailStatus === "failed" ? "FAILED" : r.report.status;
|
|
3303
|
+
return [
|
|
3304
|
+
r.agent.name,
|
|
3305
|
+
status,
|
|
3306
|
+
r.report.metrics?.accuracy ? `${Math.round(r.report.metrics.accuracy)}%` : "-",
|
|
3307
|
+
r.report.trajectorySteps.toString(),
|
|
3308
|
+
r.report.id?.substring(0, 27) + "..." || "-"
|
|
3309
|
+
];
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
function displayResults(results, format) {
|
|
3313
|
+
const headers = ["Agent", "Status", "Accuracy", "Steps", "Report ID"];
|
|
3314
|
+
const rows = buildResultRows(results);
|
|
3315
|
+
if (format === "markdown") {
|
|
3316
|
+
console.log("\n");
|
|
3317
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3117
3320
|
const table = new Table2({
|
|
3118
|
-
head:
|
|
3119
|
-
chalk2.cyan("Agent"),
|
|
3120
|
-
chalk2.cyan("Status"),
|
|
3121
|
-
chalk2.cyan("Accuracy"),
|
|
3122
|
-
chalk2.cyan("Steps"),
|
|
3123
|
-
chalk2.cyan("Report ID")
|
|
3124
|
-
],
|
|
3321
|
+
head: headers.map((h) => chalk2.cyan(h)),
|
|
3125
3322
|
colWidths: [20, 12, 12, 10, 30]
|
|
3126
3323
|
});
|
|
3127
3324
|
for (const r of results) {
|
|
3128
3325
|
if (!r.report) {
|
|
3129
|
-
table.push([
|
|
3130
|
-
r.agent.name,
|
|
3131
|
-
chalk2.red("ERROR"),
|
|
3132
|
-
"-",
|
|
3133
|
-
"-",
|
|
3134
|
-
"-"
|
|
3135
|
-
]);
|
|
3326
|
+
table.push([r.agent.name, chalk2.red("ERROR"), "-", "-", "-"]);
|
|
3136
3327
|
continue;
|
|
3137
3328
|
}
|
|
3138
3329
|
const statusStr = r.report.passFailStatus === "passed" ? chalk2.green("PASSED") : r.report.passFailStatus === "failed" ? chalk2.red("FAILED") : chalk2.yellow(r.report.status);
|
|
@@ -3148,7 +3339,7 @@ function displayTableResults(results) {
|
|
|
3148
3339
|
console.log(table.toString());
|
|
3149
3340
|
}
|
|
3150
3341
|
function createRunCommand() {
|
|
3151
|
-
const command = new Command2("run").description("Run a test case against agents").requiredOption("-t, --test-case <id>", "Test case ID or name").option("-a, --agent <key>", "Agent key (can be specified multiple times)", (val, arr) => [...arr, val], []).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>",
|
|
3342
|
+
const command = new Command2("run").description("Run a test case against agents").requiredOption("-t, --test-case <id>", "Test case ID or name").option("-a, --agent <key>", "Agent key (can be specified multiple times)", (val, arr) => [...arr, val], []).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>", OUTPUT_FORMAT_DESCRIPTION, "table").option("-v, --verbose", "Show detailed trajectory output").action(async (options) => {
|
|
3152
3343
|
console.log(chalk2.bold("\nAgent Health - Test Case Runner\n"));
|
|
3153
3344
|
const config = await loadConfig();
|
|
3154
3345
|
for (const connector of config.connectors) {
|
|
@@ -3186,7 +3377,7 @@ function createRunCommand() {
|
|
|
3186
3377
|
console.log("");
|
|
3187
3378
|
const results = [];
|
|
3188
3379
|
for (const agent of agents) {
|
|
3189
|
-
const modelId = options.model || getDefaultModel(
|
|
3380
|
+
const modelId = options.model || getDefaultModel(config);
|
|
3190
3381
|
const validationError = await validateAgentRequirements(agent);
|
|
3191
3382
|
if (validationError) {
|
|
3192
3383
|
console.error(chalk2.red(` Error: ${validationError}`));
|
|
@@ -3197,17 +3388,27 @@ function createRunCommand() {
|
|
|
3197
3388
|
const report = await runForAgent(client, testCase.id, agent, modelId, options.verbose || false);
|
|
3198
3389
|
results.push({ agent, report });
|
|
3199
3390
|
} catch (error) {
|
|
3200
|
-
|
|
3391
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3392
|
+
console.error(chalk2.red(` Error running ${agent.name}: ${errorMsg}`));
|
|
3393
|
+
const lowerError = errorMsg.toLowerCase();
|
|
3394
|
+
if (lowerError.includes("401") || lowerError.includes("403") || lowerError.includes("unauthorized") || lowerError.includes("forbidden") || lowerError.includes("token") || lowerError.includes("auth")) {
|
|
3395
|
+
console.log(chalk2.gray(` Hint: Authentication issue. Check agent-health.config.ts (headers, hooks.beforeRequest, or credentials).`));
|
|
3396
|
+
} else if (lowerError.includes("econnrefused") || lowerError.includes("enotfound") || lowerError.includes("connect")) {
|
|
3397
|
+
console.log(chalk2.gray(` Hint: Cannot reach agent endpoint. Run: npx @opensearch-project/agent-health doctor`));
|
|
3398
|
+
} else if (lowerError.includes("hook") || lowerError.includes("beforerequest")) {
|
|
3399
|
+
console.log(chalk2.gray(` Hint: The beforeRequest hook in agent-health.config.ts threw an error.`));
|
|
3400
|
+
}
|
|
3201
3401
|
results.push({ agent, report: null });
|
|
3202
3402
|
}
|
|
3203
3403
|
}
|
|
3204
|
-
|
|
3205
|
-
|
|
3404
|
+
const outputFormat = parseOutputFormat(options.output);
|
|
3405
|
+
if (outputFormat === "json") {
|
|
3406
|
+
console.log(formatJson(results.map((r) => ({
|
|
3206
3407
|
agent: { key: r.agent.key, name: r.agent.name },
|
|
3207
3408
|
report: r.report
|
|
3208
|
-
}))
|
|
3409
|
+
}))));
|
|
3209
3410
|
} else {
|
|
3210
|
-
|
|
3411
|
+
displayResults(results, outputFormat);
|
|
3211
3412
|
}
|
|
3212
3413
|
} catch (error) {
|
|
3213
3414
|
console.error(chalk2.red(`
|
|
@@ -3354,8 +3555,8 @@ function findAgent2(identifier, config) {
|
|
|
3354
3555
|
(a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
|
|
3355
3556
|
);
|
|
3356
3557
|
}
|
|
3357
|
-
function getDefaultModel2(
|
|
3358
|
-
return
|
|
3558
|
+
function getDefaultModel2(config) {
|
|
3559
|
+
return Object.keys(config.models)[0] || "claude-sonnet";
|
|
3359
3560
|
}
|
|
3360
3561
|
function isFilePath(value) {
|
|
3361
3562
|
return value.toLowerCase().endsWith(".json");
|
|
@@ -3391,7 +3592,7 @@ async function fetchReportsForRun(api, run) {
|
|
|
3391
3592
|
);
|
|
3392
3593
|
return reportsMap;
|
|
3393
3594
|
}
|
|
3394
|
-
async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
3595
|
+
async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose, concurrency) {
|
|
3395
3596
|
const results = {
|
|
3396
3597
|
agent,
|
|
3397
3598
|
passed: 0,
|
|
@@ -3406,7 +3607,8 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3406
3607
|
{
|
|
3407
3608
|
name: `CLI Run - ${agent.name}`,
|
|
3408
3609
|
agentKey: agent.key,
|
|
3409
|
-
modelId
|
|
3610
|
+
modelId,
|
|
3611
|
+
...concurrency && concurrency > 1 ? { concurrency } : {}
|
|
3410
3612
|
},
|
|
3411
3613
|
(event) => {
|
|
3412
3614
|
if (event.type === "started") {
|
|
@@ -3415,9 +3617,13 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3415
3617
|
const current = event.currentTestCaseIndex + 1;
|
|
3416
3618
|
const testCaseName = event.currentTestCase?.name || `Test ${current}`;
|
|
3417
3619
|
spinner.text = `${agent.name}: ${testCaseName} (${current}/${totalTestCases})`;
|
|
3418
|
-
if (
|
|
3620
|
+
if (event.result) {
|
|
3419
3621
|
const status = event.result.status === "completed" ? chalk3.green("\u2713") : chalk3.red("\u2717");
|
|
3420
3622
|
spinner.text = `${agent.name}: ${testCaseName} ${status} (${current}/${totalTestCases})`;
|
|
3623
|
+
if (verbose && event.result.status === "failed" && event.result.error) {
|
|
3624
|
+
spinner.info(`${agent.name}: ${testCaseName} ${chalk3.red("\u2717")} - ${event.result.error}`);
|
|
3625
|
+
spinner.start(`${agent.name}: (${current}/${totalTestCases})`);
|
|
3626
|
+
}
|
|
3421
3627
|
}
|
|
3422
3628
|
}
|
|
3423
3629
|
}
|
|
@@ -3444,6 +3650,7 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3444
3650
|
}
|
|
3445
3651
|
} catch (error) {
|
|
3446
3652
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3653
|
+
const isServerError = error instanceof ServerError;
|
|
3447
3654
|
if (startedRunId) {
|
|
3448
3655
|
results.runId = startedRunId;
|
|
3449
3656
|
try {
|
|
@@ -3472,29 +3679,71 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3472
3679
|
}
|
|
3473
3680
|
return results;
|
|
3474
3681
|
}
|
|
3682
|
+
if (run.status === "failed") {
|
|
3683
|
+
const runError = run.error || errorMessage;
|
|
3684
|
+
spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${runError}`);
|
|
3685
|
+
if (stats.passed > 0 || stats.failed > 0) {
|
|
3686
|
+
console.log(chalk3.gray(` Partial results: ${stats.passed} passed, ${stats.failed} failed out of ${stats.total}`));
|
|
3687
|
+
}
|
|
3688
|
+
return results;
|
|
3689
|
+
}
|
|
3475
3690
|
}
|
|
3476
3691
|
} catch {
|
|
3477
3692
|
}
|
|
3478
3693
|
}
|
|
3479
|
-
|
|
3480
|
-
if (isStreamError && startedRunId) {
|
|
3481
|
-
spinner.warn(`${agent.name}: ${chalk3.yellow("Stream disconnected")} - server may still be processing`);
|
|
3482
|
-
console.log(chalk3.gray(` Check status: Use the UI to monitor progress`));
|
|
3483
|
-
} else {
|
|
3694
|
+
if (isServerError) {
|
|
3484
3695
|
spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${errorMessage}`);
|
|
3696
|
+
} else {
|
|
3697
|
+
const isStreamError = errorMessage.includes("terminated") || errorMessage.includes("network") || errorMessage.includes("stream") || errorMessage.includes("aborted");
|
|
3698
|
+
if (isStreamError && startedRunId) {
|
|
3699
|
+
spinner.warn(`${agent.name}: ${chalk3.yellow("Stream disconnected")} - server may still be processing`);
|
|
3700
|
+
console.log(chalk3.gray(` Check status: Use the UI to monitor progress`));
|
|
3701
|
+
} else {
|
|
3702
|
+
spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${errorMessage}`);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
const lowerError = errorMessage.toLowerCase();
|
|
3706
|
+
if (lowerError.includes("401") || lowerError.includes("403") || lowerError.includes("unauthorized") || lowerError.includes("forbidden") || lowerError.includes("token") || lowerError.includes("auth")) {
|
|
3707
|
+
console.log(chalk3.gray(` Hint: This looks like an authentication issue. Check your agent-health.config.ts`));
|
|
3708
|
+
console.log(chalk3.gray(` (headers, hooks.beforeRequest, or credentials) and re-run.`));
|
|
3709
|
+
} else if (lowerError.includes("econnrefused") || lowerError.includes("enotfound") || lowerError.includes("connect")) {
|
|
3710
|
+
console.log(chalk3.gray(` Hint: Could not connect to the agent endpoint. Verify the endpoint in agent-health.config.ts`));
|
|
3711
|
+
console.log(chalk3.gray(` is reachable: npx @opensearch-project/agent-health doctor`));
|
|
3712
|
+
} else if (lowerError.includes("not found") || lowerError.includes("agent not found")) {
|
|
3713
|
+
console.log(chalk3.gray(` Hint: Agent key not found. List available agents: npx @opensearch-project/agent-health list agents`));
|
|
3714
|
+
} else if (lowerError.includes("hook") || lowerError.includes("beforerequest")) {
|
|
3715
|
+
console.log(chalk3.gray(` Hint: The beforeRequest hook in agent-health.config.ts threw an error.`));
|
|
3716
|
+
console.log(chalk3.gray(` Check the hook logic and any external services it calls.`));
|
|
3717
|
+
}
|
|
3718
|
+
if (errorMessage !== "terminated") {
|
|
3719
|
+
console.log(chalk3.gray(` Debug: Run with DEBUG=true for verbose server logs`));
|
|
3485
3720
|
}
|
|
3486
3721
|
}
|
|
3487
3722
|
return results;
|
|
3488
3723
|
}
|
|
3489
|
-
function
|
|
3724
|
+
function buildSummaryRows(allResults, totalTestCases) {
|
|
3725
|
+
return allResults.map((results) => {
|
|
3726
|
+
const passRate = totalTestCases > 0 ? results.passed / totalTestCases * 100 : 0;
|
|
3727
|
+
return [
|
|
3728
|
+
results.agent.name,
|
|
3729
|
+
results.passed.toString(),
|
|
3730
|
+
results.failed.toString(),
|
|
3731
|
+
`${passRate.toFixed(0)}%`,
|
|
3732
|
+
results.run?.id || results.runId || "N/A"
|
|
3733
|
+
];
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
function displaySummary(allResults, totalTestCases, format) {
|
|
3737
|
+
const headers = ["Agent", "Passed", "Failed", "Pass Rate", "Run ID"];
|
|
3738
|
+
const rows = buildSummaryRows(allResults, totalTestCases);
|
|
3739
|
+
if (format === "markdown") {
|
|
3740
|
+
console.log("\n");
|
|
3741
|
+
console.log("## Benchmark Summary\n");
|
|
3742
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3490
3745
|
const table = new Table3({
|
|
3491
|
-
head:
|
|
3492
|
-
chalk3.cyan("Agent"),
|
|
3493
|
-
chalk3.cyan("Passed"),
|
|
3494
|
-
chalk3.cyan("Failed"),
|
|
3495
|
-
chalk3.cyan("Pass Rate"),
|
|
3496
|
-
chalk3.cyan("Run ID")
|
|
3497
|
-
],
|
|
3746
|
+
head: headers.map((h) => chalk3.cyan(h)),
|
|
3498
3747
|
colWidths: [25, 10, 10, 12, 35]
|
|
3499
3748
|
});
|
|
3500
3749
|
for (const results of allResults) {
|
|
@@ -3565,7 +3814,7 @@ function createBenchmarkCommand() {
|
|
|
3565
3814
|
"Agent key (can be specified multiple times)",
|
|
3566
3815
|
(val, arr) => [...arr, val],
|
|
3567
3816
|
[]
|
|
3568
|
-
).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>",
|
|
3817
|
+
).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>", OUTPUT_FORMAT_DESCRIPTION, "table").option("--export <path>", "Export results to file").option("--format <type>", "Report format for --export: json (default), html, pdf", "json").option("-c, --concurrency <n>", "Number of test cases to run in parallel (default: 1)", "1").option("-v, --verbose", "Show detailed output").option("--stop-server", "Stop the server after benchmark completes (default: keep running)").action(async (options) => {
|
|
3569
3818
|
console.log(chalk3.bold("\nAgent Health - Benchmark Runner\n"));
|
|
3570
3819
|
const config = await loadConfig();
|
|
3571
3820
|
const serverConfig = { ...DEFAULT_SERVER_CONFIG, ...config.server };
|
|
@@ -3699,6 +3948,9 @@ function createBenchmarkCommand() {
|
|
|
3699
3948
|
console.log(chalk3.gray(` - ${a.name} (${a.key})`));
|
|
3700
3949
|
}
|
|
3701
3950
|
console.log("");
|
|
3951
|
+
console.log(chalk3.gray(" To add a custom agent, configure it in agent-health.config.ts"));
|
|
3952
|
+
console.log(chalk3.gray(" Generate one with: npx @opensearch-project/agent-health init"));
|
|
3953
|
+
console.log("");
|
|
3702
3954
|
process.exit(1);
|
|
3703
3955
|
}
|
|
3704
3956
|
agents.push(agent);
|
|
@@ -3706,19 +3958,25 @@ function createBenchmarkCommand() {
|
|
|
3706
3958
|
console.log(chalk3.gray(` Agents: ${agents.map((a) => a.name).join(", ")}`));
|
|
3707
3959
|
}
|
|
3708
3960
|
console.log("");
|
|
3961
|
+
const concurrency = Math.max(1, Math.min(20, parseInt(options.concurrency, 10) || 1));
|
|
3962
|
+
if (concurrency > 1) {
|
|
3963
|
+
console.log(chalk3.gray(` Concurrency: ${concurrency}`));
|
|
3964
|
+
}
|
|
3709
3965
|
const allResults = [];
|
|
3710
3966
|
for (const agent of agents) {
|
|
3711
|
-
const modelId = options.model || getDefaultModel2(
|
|
3967
|
+
const modelId = options.model || getDefaultModel2(config);
|
|
3712
3968
|
const results = await runBenchmarkForAgent(
|
|
3713
3969
|
api,
|
|
3714
3970
|
agent,
|
|
3715
3971
|
modelId,
|
|
3716
3972
|
benchmark,
|
|
3717
|
-
options.verbose || false
|
|
3973
|
+
options.verbose || false,
|
|
3974
|
+
concurrency
|
|
3718
3975
|
);
|
|
3719
3976
|
allResults.push(results);
|
|
3720
3977
|
}
|
|
3721
|
-
|
|
3978
|
+
const outputFormat = parseOutputFormat(options.output);
|
|
3979
|
+
if (outputFormat === "json") {
|
|
3722
3980
|
const jsonOutput = allResults.map((r) => ({
|
|
3723
3981
|
agent: { key: r.agent.key, name: r.agent.name },
|
|
3724
3982
|
runId: r.run?.id || r.runId,
|
|
@@ -3727,9 +3985,9 @@ function createBenchmarkCommand() {
|
|
|
3727
3985
|
passRate: benchmark.testCaseIds.length > 0 ? r.passed / benchmark.testCaseIds.length * 100 : 0,
|
|
3728
3986
|
results: r.run?.results
|
|
3729
3987
|
}));
|
|
3730
|
-
console.log(
|
|
3988
|
+
console.log(formatJson(jsonOutput));
|
|
3731
3989
|
} else {
|
|
3732
|
-
|
|
3990
|
+
displaySummary(allResults, benchmark.testCaseIds.length, outputFormat);
|
|
3733
3991
|
}
|
|
3734
3992
|
if (options.export) {
|
|
3735
3993
|
await exportResults(benchmark, allResults, options.export, options.format, serverResult.baseUrl);
|
|
@@ -4045,7 +4303,7 @@ function checkOpenSearchObservability() {
|
|
|
4045
4303
|
]
|
|
4046
4304
|
};
|
|
4047
4305
|
}
|
|
4048
|
-
function
|
|
4306
|
+
function displayResults2(results) {
|
|
4049
4307
|
console.log(chalk6.bold("\n Configuration Check\n"));
|
|
4050
4308
|
for (const result of results) {
|
|
4051
4309
|
const icon = {
|
|
@@ -4096,7 +4354,7 @@ function createDoctorCommand() {
|
|
|
4096
4354
|
if (options.output === "json") {
|
|
4097
4355
|
console.log(JSON.stringify(results, null, 2));
|
|
4098
4356
|
} else {
|
|
4099
|
-
|
|
4357
|
+
displayResults2(results);
|
|
4100
4358
|
}
|
|
4101
4359
|
});
|
|
4102
4360
|
return command;
|
|
@@ -4127,7 +4385,6 @@ export default defineConfig({
|
|
|
4127
4385
|
username: process.env.OPENSEARCH_USER || 'admin',
|
|
4128
4386
|
password: process.env.OPENSEARCH_PASS || 'admin',
|
|
4129
4387
|
},
|
|
4130
|
-
models: ['claude-sonnet'],
|
|
4131
4388
|
},
|
|
4132
4389
|
|
|
4133
4390
|
// Claude Code CLI agent (optional)
|
|
@@ -4144,7 +4401,6 @@ export default defineConfig({
|
|
|
4144
4401
|
},
|
|
4145
4402
|
}),
|
|
4146
4403
|
endpoint: 'claude', // Command name
|
|
4147
|
-
models: ['claude-sonnet-4'],
|
|
4148
4404
|
},
|
|
4149
4405
|
*/
|
|
4150
4406
|
],
|
|
@@ -4426,6 +4682,222 @@ function createMigrateCommand() {
|
|
|
4426
4682
|
return command;
|
|
4427
4683
|
}
|
|
4428
4684
|
|
|
4685
|
+
// cli/commands/compare-services.ts
|
|
4686
|
+
import { Command as Command9 } from "commander";
|
|
4687
|
+
import chalk9 from "chalk";
|
|
4688
|
+
function analyzeErrorPatterns(spans) {
|
|
4689
|
+
const errorSpans = spans.filter((s) => s.status === "ERROR");
|
|
4690
|
+
if (errorSpans.length === 0) {
|
|
4691
|
+
return { errorSpans, patterns: [], avgDurationMs: 0 };
|
|
4692
|
+
}
|
|
4693
|
+
const patternMap = /* @__PURE__ */ new Map();
|
|
4694
|
+
let totalDuration = 0;
|
|
4695
|
+
for (const span of errorSpans) {
|
|
4696
|
+
const errorType = extractErrorType(span);
|
|
4697
|
+
const spanName = span.name || "Unknown";
|
|
4698
|
+
const duration = span.duration || 0;
|
|
4699
|
+
const errorMsg = extractErrorMessage(span);
|
|
4700
|
+
totalDuration += duration;
|
|
4701
|
+
if (!patternMap.has(errorType)) {
|
|
4702
|
+
patternMap.set(errorType, {
|
|
4703
|
+
count: 0,
|
|
4704
|
+
spanNames: /* @__PURE__ */ new Set(),
|
|
4705
|
+
durations: [],
|
|
4706
|
+
messages: /* @__PURE__ */ new Set()
|
|
4707
|
+
});
|
|
4708
|
+
}
|
|
4709
|
+
const pattern = patternMap.get(errorType);
|
|
4710
|
+
pattern.count++;
|
|
4711
|
+
pattern.spanNames.add(spanName);
|
|
4712
|
+
pattern.durations.push(duration);
|
|
4713
|
+
if (errorMsg) pattern.messages.add(errorMsg);
|
|
4714
|
+
}
|
|
4715
|
+
const patterns = Array.from(patternMap.entries()).map(([errorType, data]) => ({
|
|
4716
|
+
errorType,
|
|
4717
|
+
count: data.count,
|
|
4718
|
+
spanNames: Array.from(data.spanNames),
|
|
4719
|
+
avgDurationMs: data.durations.reduce((a, b) => a + b, 0) / data.durations.length,
|
|
4720
|
+
exampleMessages: Array.from(data.messages).slice(0, 3)
|
|
4721
|
+
// Top 3 examples
|
|
4722
|
+
})).sort((a, b) => b.count - a.count);
|
|
4723
|
+
const avgDurationMs = totalDuration / errorSpans.length;
|
|
4724
|
+
return { errorSpans, patterns, avgDurationMs };
|
|
4725
|
+
}
|
|
4726
|
+
function extractErrorType(span) {
|
|
4727
|
+
const attrs = span.attributes || {};
|
|
4728
|
+
if (attrs["error.type"]) return attrs["error.type"];
|
|
4729
|
+
if (attrs["exception.type"]) return attrs["exception.type"];
|
|
4730
|
+
if (attrs["http.status_code"] >= 400) return `HTTP ${attrs["http.status_code"]}`;
|
|
4731
|
+
const name = span.name || "";
|
|
4732
|
+
if (name.includes("timeout")) return "Timeout";
|
|
4733
|
+
if (name.includes("connection")) return "Connection Error";
|
|
4734
|
+
if (name.includes("auth")) return "Authentication Error";
|
|
4735
|
+
return "Unknown Error";
|
|
4736
|
+
}
|
|
4737
|
+
function extractErrorMessage(span) {
|
|
4738
|
+
const attrs = span.attributes || {};
|
|
4739
|
+
if (attrs["error.message"]) return attrs["error.message"];
|
|
4740
|
+
if (attrs["exception.message"]) return attrs["exception.message"];
|
|
4741
|
+
if (span.events && Array.isArray(span.events)) {
|
|
4742
|
+
for (const event of span.events) {
|
|
4743
|
+
if (event.name === "exception" && event.attributes?.["exception.message"]) {
|
|
4744
|
+
return event.attributes["exception.message"];
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
return null;
|
|
4749
|
+
}
|
|
4750
|
+
async function analyzeServiceErrors(client, serviceName, startTime, endTime, limit = 1e3) {
|
|
4751
|
+
console.log(chalk9.gray(`
|
|
4752
|
+
Fetching traces for service: ${serviceName}...`));
|
|
4753
|
+
const response = await client.fetchTraces({
|
|
4754
|
+
serviceName,
|
|
4755
|
+
startTime,
|
|
4756
|
+
endTime,
|
|
4757
|
+
size: limit
|
|
4758
|
+
});
|
|
4759
|
+
const spans = response.spans || [];
|
|
4760
|
+
console.log(chalk9.gray(` Found ${spans.length} spans`));
|
|
4761
|
+
const traceMap = /* @__PURE__ */ new Map();
|
|
4762
|
+
for (const span of spans) {
|
|
4763
|
+
if (!traceMap.has(span.traceId)) {
|
|
4764
|
+
traceMap.set(span.traceId, []);
|
|
4765
|
+
}
|
|
4766
|
+
traceMap.get(span.traceId).push(span);
|
|
4767
|
+
}
|
|
4768
|
+
const totalTraces = traceMap.size;
|
|
4769
|
+
let tracesWithErrors = 0;
|
|
4770
|
+
for (const traceSpans of traceMap.values()) {
|
|
4771
|
+
if (traceSpans.some((s) => s.status === "ERROR")) {
|
|
4772
|
+
tracesWithErrors++;
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
const errorRate = totalTraces > 0 ? tracesWithErrors / totalTraces * 100 : 0;
|
|
4776
|
+
const { errorSpans, patterns, avgDurationMs } = analyzeErrorPatterns(spans);
|
|
4777
|
+
return {
|
|
4778
|
+
serviceName,
|
|
4779
|
+
totalTraces,
|
|
4780
|
+
tracesWithErrors,
|
|
4781
|
+
errorRate,
|
|
4782
|
+
totalErrorSpans: errorSpans.length,
|
|
4783
|
+
errorPatterns: patterns,
|
|
4784
|
+
avgErrorDurationMs: avgDurationMs
|
|
4785
|
+
};
|
|
4786
|
+
}
|
|
4787
|
+
function printServiceAnalysis(analysis) {
|
|
4788
|
+
console.log(chalk9.bold.cyan(`
|
|
4789
|
+
${"=".repeat(60)}`));
|
|
4790
|
+
console.log(chalk9.bold.cyan(`Service: ${analysis.serviceName}`));
|
|
4791
|
+
console.log(chalk9.bold.cyan("=".repeat(60)));
|
|
4792
|
+
console.log(chalk9.white(`Total Traces: ${analysis.totalTraces}`));
|
|
4793
|
+
console.log(chalk9.white(`Traces with Errors: ${analysis.tracesWithErrors}`));
|
|
4794
|
+
const errorRateColor = analysis.errorRate > 10 ? chalk9.red : analysis.errorRate > 5 ? chalk9.yellow : chalk9.green;
|
|
4795
|
+
console.log(errorRateColor(`Error Rate: ${analysis.errorRate.toFixed(2)}%`));
|
|
4796
|
+
console.log(chalk9.white(`Total Error Spans: ${analysis.totalErrorSpans}`));
|
|
4797
|
+
console.log(chalk9.white(`Avg Error Span Duration: ${analysis.avgErrorDurationMs.toFixed(2)}ms`));
|
|
4798
|
+
if (analysis.errorPatterns.length > 0) {
|
|
4799
|
+
console.log(chalk9.bold.white("\nError Patterns:"));
|
|
4800
|
+
for (const pattern of analysis.errorPatterns) {
|
|
4801
|
+
console.log(chalk9.yellow(`
|
|
4802
|
+
\u2022 ${pattern.errorType}`));
|
|
4803
|
+
console.log(chalk9.gray(` Count: ${pattern.count}`));
|
|
4804
|
+
console.log(chalk9.gray(` Avg Duration: ${pattern.avgDurationMs.toFixed(2)}ms`));
|
|
4805
|
+
console.log(chalk9.gray(` Affected Spans: ${pattern.spanNames.join(", ")}`));
|
|
4806
|
+
if (pattern.exampleMessages.length > 0) {
|
|
4807
|
+
console.log(chalk9.gray(` Example Messages:`));
|
|
4808
|
+
pattern.exampleMessages.forEach((msg) => {
|
|
4809
|
+
console.log(chalk9.gray(` - ${msg.substring(0, 80)}${msg.length > 80 ? "..." : ""}`));
|
|
4810
|
+
});
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
} else {
|
|
4814
|
+
console.log(chalk9.green("\n\u2713 No error patterns detected"));
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
function printComparison(service1, service2) {
|
|
4818
|
+
console.log(chalk9.bold.magenta(`
|
|
4819
|
+
${"=".repeat(60)}`));
|
|
4820
|
+
console.log(chalk9.bold.magenta("COMPARISON SUMMARY"));
|
|
4821
|
+
console.log(chalk9.bold.magenta("=".repeat(60)));
|
|
4822
|
+
const errorRateDiff = service1.errorRate - service2.errorRate;
|
|
4823
|
+
const diffColor = Math.abs(errorRateDiff) < 1 ? chalk9.white : errorRateDiff > 0 ? chalk9.red : chalk9.green;
|
|
4824
|
+
const diffSymbol = errorRateDiff > 0 ? "\u2191" : errorRateDiff < 0 ? "\u2193" : "=";
|
|
4825
|
+
console.log(chalk9.bold.white("\nError Rate:"));
|
|
4826
|
+
console.log(` ${service1.serviceName}: ${service1.errorRate.toFixed(2)}%`);
|
|
4827
|
+
console.log(` ${service2.serviceName}: ${service2.errorRate.toFixed(2)}%`);
|
|
4828
|
+
console.log(diffColor(` Difference: ${diffSymbol} ${Math.abs(errorRateDiff).toFixed(2)}%`));
|
|
4829
|
+
console.log(chalk9.bold.white("\nUnique Error Patterns:"));
|
|
4830
|
+
const patterns1 = new Set(service1.errorPatterns.map((p) => p.errorType));
|
|
4831
|
+
const patterns2 = new Set(service2.errorPatterns.map((p) => p.errorType));
|
|
4832
|
+
const onlyIn1 = Array.from(patterns1).filter((p) => !patterns2.has(p));
|
|
4833
|
+
const onlyIn2 = Array.from(patterns2).filter((p) => !patterns1.has(p));
|
|
4834
|
+
const inBoth = Array.from(patterns1).filter((p) => patterns2.has(p));
|
|
4835
|
+
if (onlyIn1.length > 0) {
|
|
4836
|
+
console.log(chalk9.cyan(`
|
|
4837
|
+
Only in ${service1.serviceName}:`));
|
|
4838
|
+
onlyIn1.forEach((p) => console.log(chalk9.gray(` \u2022 ${p}`)));
|
|
4839
|
+
}
|
|
4840
|
+
if (onlyIn2.length > 0) {
|
|
4841
|
+
console.log(chalk9.cyan(`
|
|
4842
|
+
Only in ${service2.serviceName}:`));
|
|
4843
|
+
onlyIn2.forEach((p) => console.log(chalk9.gray(` \u2022 ${p}`)));
|
|
4844
|
+
}
|
|
4845
|
+
if (inBoth.length > 0) {
|
|
4846
|
+
console.log(chalk9.cyan(`
|
|
4847
|
+
Common error patterns:`));
|
|
4848
|
+
inBoth.forEach((p) => {
|
|
4849
|
+
const count1 = service1.errorPatterns.find((x) => x.errorType === p)?.count || 0;
|
|
4850
|
+
const count2 = service2.errorPatterns.find((x) => x.errorType === p)?.count || 0;
|
|
4851
|
+
console.log(chalk9.gray(` \u2022 ${p}: ${count1} vs ${count2}`));
|
|
4852
|
+
});
|
|
4853
|
+
}
|
|
4854
|
+
console.log(chalk9.bold.yellow("\nRecommendations:"));
|
|
4855
|
+
if (service1.errorRate > service2.errorRate * 1.5) {
|
|
4856
|
+
console.log(chalk9.yellow(` \u26A0 ${service1.serviceName} has significantly higher error rate - investigate urgently`));
|
|
4857
|
+
} else if (service2.errorRate > service1.errorRate * 1.5) {
|
|
4858
|
+
console.log(chalk9.yellow(` \u26A0 ${service2.serviceName} has significantly higher error rate - investigate urgently`));
|
|
4859
|
+
} else {
|
|
4860
|
+
console.log(chalk9.green(` \u2713 Error rates are comparable`));
|
|
4861
|
+
}
|
|
4862
|
+
if (onlyIn1.length > 2) {
|
|
4863
|
+
console.log(chalk9.yellow(` \u26A0 ${service1.serviceName} has ${onlyIn1.length} unique error types - review configuration`));
|
|
4864
|
+
}
|
|
4865
|
+
if (onlyIn2.length > 2) {
|
|
4866
|
+
console.log(chalk9.yellow(` \u26A0 ${service2.serviceName} has ${onlyIn2.length} unique error types - review configuration`));
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
function createCompareServicesCommand() {
|
|
4870
|
+
const cmd = new Command9("compare-services");
|
|
4871
|
+
cmd.description("Compare error patterns between two services from trace data").requiredOption("-s, --services <service1,service2>", 'Comma-separated service names (e.g., "lambda-api,eks-api")').option("--start <time>", 'Start time (ISO 8601 format or relative like "1h", "24h")').option("--end <time>", "End time (ISO 8601 format)").option("--limit <number>", "Maximum number of spans to fetch per service", "1000").action(async (options) => {
|
|
4872
|
+
try {
|
|
4873
|
+
const config = await loadConfig();
|
|
4874
|
+
const serverResult = await ensureServer(config.server);
|
|
4875
|
+
const client = new ApiClient(serverResult.baseUrl);
|
|
4876
|
+
const serviceNames = options.services.split(",").map((s) => s.trim());
|
|
4877
|
+
if (serviceNames.length !== 2) {
|
|
4878
|
+
console.error(chalk9.red("Error: Please provide exactly two service names"));
|
|
4879
|
+
process.exit(1);
|
|
4880
|
+
}
|
|
4881
|
+
const [service1Name, service2Name] = serviceNames;
|
|
4882
|
+
const limit = parseInt(options.limit, 10);
|
|
4883
|
+
console.log(chalk9.bold.cyan("\nComparing Error Patterns Between Services"));
|
|
4884
|
+
console.log(chalk9.gray(`Service 1: ${service1Name}`));
|
|
4885
|
+
console.log(chalk9.gray(`Service 2: ${service2Name}`));
|
|
4886
|
+
if (options.start) console.log(chalk9.gray(`Time Range: ${options.start} to ${options.end || "now"}`));
|
|
4887
|
+
const analysis1 = await analyzeServiceErrors(client, service1Name, options.start, options.end, limit);
|
|
4888
|
+
const analysis2 = await analyzeServiceErrors(client, service2Name, options.start, options.end, limit);
|
|
4889
|
+
printServiceAnalysis(analysis1);
|
|
4890
|
+
printServiceAnalysis(analysis2);
|
|
4891
|
+
printComparison(analysis1, analysis2);
|
|
4892
|
+
console.log();
|
|
4893
|
+
} catch (error) {
|
|
4894
|
+
console.error(chalk9.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4895
|
+
process.exit(1);
|
|
4896
|
+
}
|
|
4897
|
+
});
|
|
4898
|
+
return cmd;
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4429
4901
|
// cli/index.ts
|
|
4430
4902
|
var __filename3 = fileURLToPath3(import.meta.url);
|
|
4431
4903
|
var __dirname3 = dirname3(__filename3);
|
|
@@ -4439,59 +4911,112 @@ try {
|
|
|
4439
4911
|
function loadEnvFile(envPath) {
|
|
4440
4912
|
const absolutePath = resolve4(process.cwd(), envPath);
|
|
4441
4913
|
if (!existsSync5(absolutePath)) {
|
|
4442
|
-
console.error(
|
|
4914
|
+
console.error(chalk10.red(`
|
|
4443
4915
|
Error: Environment file not found: ${absolutePath}
|
|
4444
4916
|
`));
|
|
4445
4917
|
process.exit(1);
|
|
4446
4918
|
}
|
|
4447
4919
|
const result = loadDotenv({ path: absolutePath });
|
|
4448
4920
|
if (result.error) {
|
|
4449
|
-
console.error(
|
|
4921
|
+
console.error(chalk10.red(`
|
|
4450
4922
|
Error loading environment file: ${result.error.message}
|
|
4451
4923
|
`));
|
|
4452
4924
|
process.exit(1);
|
|
4453
4925
|
}
|
|
4454
|
-
console.log(
|
|
4926
|
+
console.log(chalk10.gray(` Loaded environment from: ${envPath}`));
|
|
4455
4927
|
}
|
|
4456
4928
|
var defaultEnvPath = resolve4(process.cwd(), ".env");
|
|
4457
4929
|
if (existsSync5(defaultEnvPath)) {
|
|
4458
4930
|
loadDotenv({ path: defaultEnvPath });
|
|
4459
4931
|
}
|
|
4460
|
-
var program = new
|
|
4461
|
-
program.name("agent-health").description("Agent Health Evaluation Framework - Evaluate and monitor AI agent performance").version(version).enablePositionalOptions().passThroughOptions()
|
|
4932
|
+
var program = new Command10();
|
|
4933
|
+
program.name("agent-health").description("Agent Health Evaluation Framework - Evaluate and monitor AI agent performance").version(version).enablePositionalOptions().passThroughOptions().configureHelp({
|
|
4934
|
+
sortSubcommands: false,
|
|
4935
|
+
// Hide default command list — replaced by grouped custom help below
|
|
4936
|
+
subcommandTerm: () => "",
|
|
4937
|
+
formatHelp: (cmd, helper) => {
|
|
4938
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
4939
|
+
const helpWidth = helper.helpWidth || 80;
|
|
4940
|
+
const output = [];
|
|
4941
|
+
const desc = helper.commandDescription(cmd);
|
|
4942
|
+
if (desc) {
|
|
4943
|
+
output.push(desc, "");
|
|
4944
|
+
}
|
|
4945
|
+
output.push(`${chalk10.cyan.bold("Usage:")} ${helper.commandUsage(cmd)}`, "");
|
|
4946
|
+
const optionList = helper.visibleOptions(cmd).map((opt) => {
|
|
4947
|
+
const term = helper.optionTerm(opt);
|
|
4948
|
+
const desc2 = helper.optionDescription(opt);
|
|
4949
|
+
return ` ${term.padEnd(termWidth)} ${desc2}`;
|
|
4950
|
+
}).join("\n");
|
|
4951
|
+
if (optionList) {
|
|
4952
|
+
output.push(`${chalk10.cyan.bold("Options:")}`, optionList, "");
|
|
4953
|
+
}
|
|
4954
|
+
return output.join("\n");
|
|
4955
|
+
}
|
|
4956
|
+
});
|
|
4957
|
+
program.addHelpText("after", `
|
|
4958
|
+
${chalk10.cyan.bold("Getting Started:")}
|
|
4959
|
+
${chalk10.yellow("agent-health")} Launch the web UI and evaluation server
|
|
4960
|
+
${chalk10.yellow("agent-health init")} Generate an agent-health.config.ts file
|
|
4961
|
+
${chalk10.yellow("agent-health doctor")} Verify your setup (AWS creds, OpenSearch, agents)
|
|
4962
|
+
|
|
4963
|
+
${chalk10.cyan.bold("Running Evaluations:")}
|
|
4964
|
+
${chalk10.yellow("agent-health run")} ${chalk10.gray("-t <case> -a <agent>")} Run a single test case against an agent
|
|
4965
|
+
${chalk10.yellow("agent-health benchmark")} ${chalk10.gray("-f <file>")} Run a full benchmark from a test cases JSON file
|
|
4966
|
+
${chalk10.yellow("agent-health benchmark")} ${chalk10.gray("-b <id>")} Re-run an existing benchmark
|
|
4967
|
+
|
|
4968
|
+
${chalk10.cyan.bold("Viewing Results:")}
|
|
4969
|
+
${chalk10.yellow("agent-health list")} ${chalk10.gray("agents|benchmarks|...")} List agents, connectors, test cases, or benchmarks
|
|
4970
|
+
${chalk10.yellow("agent-health report")} ${chalk10.gray("-b <benchmark>")} Generate an HTML/PDF/JSON report
|
|
4971
|
+
${chalk10.yellow("agent-health export")} ${chalk10.gray("-b <benchmark>")} Export test cases as re-importable JSON
|
|
4972
|
+
${chalk10.yellow("agent-health compare-services")} ${chalk10.gray("-s A B")} Compare error patterns between services
|
|
4973
|
+
|
|
4974
|
+
${chalk10.cyan.bold("Maintenance:")}
|
|
4975
|
+
${chalk10.yellow("agent-health migrate")} Migrate legacy benchmark data to current format
|
|
4976
|
+
${chalk10.yellow("agent-health serve")} Start the server (same as default, explicit command)
|
|
4977
|
+
|
|
4978
|
+
${chalk10.cyan.bold("Examples:")}
|
|
4979
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health
|
|
4980
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health --port 8080 --no-browser
|
|
4981
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health run -t "RCA for 500 errors" -a langgraph
|
|
4982
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health benchmark -f ./test-cases.json -a my-agent
|
|
4983
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health list agents
|
|
4984
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health report -b bench-123 -f pdf -o report.pdf
|
|
4985
|
+
`);
|
|
4462
4986
|
program.option("-p, --port <number>", "Server port", "4001").option("-e, --env-file <path>", "Load environment variables from file (e.g., .env)").option("--no-browser", "Do not open browser automatically");
|
|
4463
4987
|
program.action(async (options) => {
|
|
4464
|
-
console.log(
|
|
4988
|
+
console.log(chalk10.cyan.bold(`
|
|
4465
4989
|
Agent Health v${version} - AI Agent Evaluation Framework
|
|
4466
4990
|
`));
|
|
4467
|
-
console.log(
|
|
4468
|
-
console.log(
|
|
4991
|
+
console.log(chalk10.gray(` Working directory: ${process.cwd()}`));
|
|
4992
|
+
console.log(chalk10.gray(` Package directory: ${__dirname3}`));
|
|
4469
4993
|
if (options.envFile) {
|
|
4470
4994
|
loadEnvFile(options.envFile);
|
|
4471
4995
|
} else if (existsSync5(defaultEnvPath)) {
|
|
4472
|
-
console.log(
|
|
4996
|
+
console.log(chalk10.gray(" Auto-loaded .env from current directory"));
|
|
4473
4997
|
}
|
|
4474
4998
|
const port = parseInt(options.port, 10);
|
|
4475
4999
|
const spinner = ora5("Starting server...").start();
|
|
4476
5000
|
try {
|
|
4477
5001
|
await startServer({ port });
|
|
4478
5002
|
spinner.succeed("Server started");
|
|
4479
|
-
console.log(
|
|
4480
|
-
console.log(
|
|
4481
|
-
console.log(
|
|
4482
|
-
console.log(
|
|
5003
|
+
console.log(chalk10.gray("\n Configuration:"));
|
|
5004
|
+
console.log(chalk10.gray(` Storage: Sample data (configure OpenSearch for persistence)`));
|
|
5005
|
+
console.log(chalk10.gray(` Agent: Select in UI (Demo Agent for mock, real agents require endpoints)`));
|
|
5006
|
+
console.log(chalk10.gray(` Judge: Select in UI (Demo Judge for mock, Bedrock requires AWS creds)
|
|
4483
5007
|
`));
|
|
4484
5008
|
const url = `http://localhost:${port}`;
|
|
4485
|
-
console.log(
|
|
5009
|
+
console.log(chalk10.green(` Server running at ${chalk10.bold(url)}
|
|
4486
5010
|
`));
|
|
5011
|
+
console.log(chalk10.green(` Demo data loaded`));
|
|
4487
5012
|
if (options.browser !== false) {
|
|
4488
|
-
console.log(
|
|
5013
|
+
console.log(chalk10.gray(" Opening browser..."));
|
|
4489
5014
|
await open(url);
|
|
4490
5015
|
}
|
|
4491
|
-
console.log(
|
|
5016
|
+
console.log(chalk10.gray(" Press Ctrl+C to stop\n"));
|
|
4492
5017
|
} catch (error) {
|
|
4493
5018
|
spinner.fail("Failed to start server");
|
|
4494
|
-
console.error(
|
|
5019
|
+
console.error(chalk10.red(`
|
|
4495
5020
|
Error: ${error instanceof Error ? error.message : error}
|
|
4496
5021
|
`));
|
|
4497
5022
|
process.exit(1);
|
|
@@ -4505,8 +5030,9 @@ program.addCommand(createReportCommand());
|
|
|
4505
5030
|
program.addCommand(createDoctorCommand());
|
|
4506
5031
|
program.addCommand(createInitCommand());
|
|
4507
5032
|
program.addCommand(createMigrateCommand());
|
|
5033
|
+
program.addCommand(createCompareServicesCommand());
|
|
4508
5034
|
program.command("serve").description("Start the Agent Health server (same as default action)").option("-p, --port <number>", "Server port", "4001").option("--no-browser", "Do not open browser automatically").action(async (options) => {
|
|
4509
|
-
console.log(
|
|
5035
|
+
console.log(chalk10.cyan.bold(`
|
|
4510
5036
|
Agent Health v${version} - AI Agent Evaluation Framework
|
|
4511
5037
|
`));
|
|
4512
5038
|
const port = parseInt(options.port, 10);
|
|
@@ -4515,16 +5041,16 @@ program.command("serve").description("Start the Agent Health server (same as def
|
|
|
4515
5041
|
await startServer({ port });
|
|
4516
5042
|
spinner.succeed("Server started");
|
|
4517
5043
|
const url = `http://localhost:${port}`;
|
|
4518
|
-
console.log(
|
|
5044
|
+
console.log(chalk10.green(` Server running at ${chalk10.bold(url)}
|
|
4519
5045
|
`));
|
|
4520
5046
|
if (options.browser !== false) {
|
|
4521
|
-
console.log(
|
|
5047
|
+
console.log(chalk10.gray(" Opening browser..."));
|
|
4522
5048
|
await open(url);
|
|
4523
5049
|
}
|
|
4524
|
-
console.log(
|
|
5050
|
+
console.log(chalk10.gray(" Press Ctrl+C to stop\n"));
|
|
4525
5051
|
} catch (error) {
|
|
4526
5052
|
spinner.fail("Failed to start server");
|
|
4527
|
-
console.error(
|
|
5053
|
+
console.error(chalk10.red(`
|
|
4528
5054
|
Error: ${error instanceof Error ? error.message : error}
|
|
4529
5055
|
`));
|
|
4530
5056
|
process.exit(1);
|
|
@@ -4533,15 +5059,15 @@ program.command("serve").description("Start the Agent Health server (same as def
|
|
|
4533
5059
|
program.on("command:*", (operands) => {
|
|
4534
5060
|
const unknownCommand = operands[0];
|
|
4535
5061
|
const availableCommands = program.commands.map((cmd) => cmd.name());
|
|
4536
|
-
console.error(
|
|
5062
|
+
console.error(chalk10.red(`
|
|
4537
5063
|
Error: Unknown command '${unknownCommand}'`));
|
|
4538
5064
|
console.log("");
|
|
4539
|
-
console.log(
|
|
5065
|
+
console.log(chalk10.cyan(" Available commands:"));
|
|
4540
5066
|
for (const cmd of availableCommands) {
|
|
4541
|
-
console.log(
|
|
5067
|
+
console.log(chalk10.gray(` - ${cmd}`));
|
|
4542
5068
|
}
|
|
4543
5069
|
console.log("");
|
|
4544
|
-
console.log(
|
|
5070
|
+
console.log(chalk10.gray(` Run ${chalk10.cyan("agent-health --help")} for usage information.
|
|
4545
5071
|
`));
|
|
4546
5072
|
process.exitCode = 1;
|
|
4547
5073
|
});
|