@outputai/cli 0.14.1-next.46e7530.0 → 0.14.1-next.4e817d8.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/dist/api/generated/api.d.ts +0 -16
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/workflow/run.d.ts +0 -1
- package/dist/commands/workflow/run.js +2 -12
- package/dist/commands/workflow/run.spec.js +0 -23
- package/dist/commands/workflow/start.d.ts +0 -1
- package/dist/commands/workflow/start.js +1 -11
- package/dist/commands/workflow/start.spec.js +0 -27
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/cost_calculator.js +2 -2
- package/dist/services/cost_calculator.spec.js +1 -1
- package/dist/services/datasets.js +4 -4
- package/dist/services/datasets.test.js +8 -8
- package/oclif.manifest.json +2 -22
- package/package.json +7 -7
- package/dist/utils/search_attributes_flag.d.ts +0 -2
- package/dist/utils/search_attributes_flag.js +0 -22
- package/dist/utils/search_attributes_flag.spec.d.ts +0 -1
- package/dist/utils/search_attributes_flag.spec.js +0 -38
|
@@ -390,12 +390,6 @@ export type ServiceUnavailableResponse = ErrorResponse;
|
|
|
390
390
|
* Internal server error (e.g. Temporal connection failure)
|
|
391
391
|
*/
|
|
392
392
|
export type InternalServerErrorResponse = ErrorResponse;
|
|
393
|
-
/**
|
|
394
|
-
* (Optional) Temporal search attributes to set on the execution. Each attribute must be registered on the namespace. Values are scalars (string, number, boolean) or string arrays (KeywordList). Unregistered names or values that do not match the registered type are rejected with 400.
|
|
395
|
-
*/
|
|
396
|
-
export type PostWorkflowRunBodySearchAttributes = {
|
|
397
|
-
[key: string]: string | number | boolean | string[];
|
|
398
|
-
};
|
|
399
393
|
export type PostWorkflowRunBody = {
|
|
400
394
|
/** The name of the workflow to execute */
|
|
401
395
|
workflowName: string;
|
|
@@ -410,17 +404,9 @@ export type PostWorkflowRunBody = {
|
|
|
410
404
|
* @deprecated
|
|
411
405
|
*/
|
|
412
406
|
taskQueue?: string;
|
|
413
|
-
/** (Optional) Temporal search attributes to set on the execution. Each attribute must be registered on the namespace. Values are scalars (string, number, boolean) or string arrays (KeywordList). Unregistered names or values that do not match the registered type are rejected with 400. */
|
|
414
|
-
searchAttributes?: PostWorkflowRunBodySearchAttributes;
|
|
415
407
|
/** (Optional) The max time to wait for the execution, defaults to 30s */
|
|
416
408
|
timeout?: number;
|
|
417
409
|
};
|
|
418
|
-
/**
|
|
419
|
-
* (Optional) Temporal search attributes to set on the execution. Each attribute must be registered on the namespace. Values are scalars (string, number, boolean) or string arrays (KeywordList). Unregistered names or values that do not match the registered type are rejected with 400.
|
|
420
|
-
*/
|
|
421
|
-
export type PostWorkflowStartBodySearchAttributes = {
|
|
422
|
-
[key: string]: string | number | boolean | string[];
|
|
423
|
-
};
|
|
424
410
|
export type PostWorkflowStartBody = {
|
|
425
411
|
/** The name of the workflow to execute */
|
|
426
412
|
workflowName: string;
|
|
@@ -435,8 +421,6 @@ export type PostWorkflowStartBody = {
|
|
|
435
421
|
* @deprecated
|
|
436
422
|
*/
|
|
437
423
|
taskQueue?: string;
|
|
438
|
-
/** (Optional) Temporal search attributes to set on the execution. Each attribute must be registered on the namespace. Values are scalars (string, number, boolean) or string arrays (KeywordList). Unregistered names or values that do not match the registered type are rejected with 400. */
|
|
439
|
-
searchAttributes?: PostWorkflowStartBodySearchAttributes;
|
|
440
424
|
};
|
|
441
425
|
export type PostWorkflowStart200 = {
|
|
442
426
|
/** The id of the started workflow */
|
|
@@ -11,7 +11,6 @@ export default class WorkflowRun extends Command {
|
|
|
11
11
|
static flags: {
|
|
12
12
|
input: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
catalog: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
-
'search-attributes': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
14
|
};
|
|
16
15
|
run(): Promise<WorkflowResultResponse>;
|
|
17
16
|
catch(error: Error): Promise<void>;
|
|
@@ -3,7 +3,6 @@ import { postWorkflowRun } from '#api/generated/api.js';
|
|
|
3
3
|
import { formatWorkflowResult, isErrorStatus } from '#utils/format_workflow_result.js';
|
|
4
4
|
import { handleApiError } from '#utils/error_handler.js';
|
|
5
5
|
import { resolveInput } from '#utils/resolve_input.js';
|
|
6
|
-
import { parseSearchAttributesFlag } from '#utils/search_attributes_flag.js';
|
|
7
6
|
import { getRetryDelayFromResponse } from '#utils/header_utils.js';
|
|
8
7
|
import { sleep } from '#utils/sleep.js';
|
|
9
8
|
import { HttpError } from '#api/http_client.js';
|
|
@@ -35,8 +34,7 @@ export default class WorkflowRun extends Command {
|
|
|
35
34
|
'<%= config.bin %> <%= command.id %> simple my_scenario --json',
|
|
36
35
|
'<%= config.bin %> <%= command.id %> simple --input \'{"values":[1,2,3]}\'',
|
|
37
36
|
'<%= config.bin %> <%= command.id %> simple --input input.json',
|
|
38
|
-
'<%= config.bin %> <%= command.id %> simple --input \'{"key":"value"}\' --catalog my-catalog'
|
|
39
|
-
'<%= config.bin %> <%= command.id %> simple basic_input --search-attributes \'{"ClientId":"acme"}\''
|
|
37
|
+
'<%= config.bin %> <%= command.id %> simple --input \'{"key":"value"}\' --catalog my-catalog'
|
|
40
38
|
];
|
|
41
39
|
static args = {
|
|
42
40
|
workflowName: Args.string({
|
|
@@ -61,17 +59,10 @@ export default class WorkflowRun extends Command {
|
|
|
61
59
|
deprecateAliases: true,
|
|
62
60
|
description: 'Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
|
|
63
61
|
env: 'OUTPUT_CATALOG_ID'
|
|
64
|
-
}),
|
|
65
|
-
'search-attributes': Flags.string({
|
|
66
|
-
char: 'a',
|
|
67
|
-
description: 'Temporal search attributes as a JSON object or file path, e.g. {"ClientId":"acme"}. ' +
|
|
68
|
-
'Each attribute must be registered on the namespace',
|
|
69
|
-
required: false
|
|
70
62
|
})
|
|
71
63
|
};
|
|
72
64
|
async run() {
|
|
73
65
|
const { args, flags } = await this.parse(WorkflowRun);
|
|
74
|
-
const searchAttributes = parseSearchAttributesFlag(flags['search-attributes']);
|
|
75
66
|
const input = await resolveInput({
|
|
76
67
|
workflowName: args.workflowName,
|
|
77
68
|
scenario: args.scenario,
|
|
@@ -85,8 +76,7 @@ export default class WorkflowRun extends Command {
|
|
|
85
76
|
body: {
|
|
86
77
|
workflowName: args.workflowName,
|
|
87
78
|
input,
|
|
88
|
-
catalog: flags.catalog
|
|
89
|
-
searchAttributes
|
|
79
|
+
catalog: flags.catalog
|
|
90
80
|
},
|
|
91
81
|
options: { config: { timeout: 600000 } },
|
|
92
82
|
log: msg => this.log(msg)
|
|
@@ -114,29 +114,6 @@ describe('workflow run command', () => {
|
|
|
114
114
|
}));
|
|
115
115
|
expect(postWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ catalog: 'my-catalog' }), expect.anything());
|
|
116
116
|
});
|
|
117
|
-
it('passes --search-attributes to postWorkflowRun', async () => {
|
|
118
|
-
const { cmd, postWorkflowRun } = await createCommand();
|
|
119
|
-
cmd.parse = vi.fn().mockResolvedValue({
|
|
120
|
-
args: { workflowName: 'my_workflow', scenario: 'basic' },
|
|
121
|
-
flags: { input: undefined, catalog: undefined, 'search-attributes': '{"ClientId":"acme"}', format: 'text' }
|
|
122
|
-
});
|
|
123
|
-
postWorkflowRun.mockResolvedValue({
|
|
124
|
-
data: { v: '2', workflowId: 'wf-1', runId: 'run-1', status: 'completed', input: {}, output: {}, trace: null, error: null },
|
|
125
|
-
status: 200,
|
|
126
|
-
headers: new Headers()
|
|
127
|
-
});
|
|
128
|
-
await cmd.run();
|
|
129
|
-
expect(postWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ searchAttributes: { ClientId: 'acme' } }), expect.anything());
|
|
130
|
-
});
|
|
131
|
-
it('rejects invalid --search-attributes before calling the API', async () => {
|
|
132
|
-
const { cmd, postWorkflowRun } = await createCommand();
|
|
133
|
-
cmd.parse = vi.fn().mockResolvedValue({
|
|
134
|
-
args: { workflowName: 'my_workflow', scenario: 'basic' },
|
|
135
|
-
flags: { input: undefined, catalog: undefined, 'search-attributes': '["acme"]', format: 'text' }
|
|
136
|
-
});
|
|
137
|
-
await expect(cmd.run()).rejects.toThrow('--search-attributes must be a JSON object');
|
|
138
|
-
expect(postWorkflowRun).not.toHaveBeenCalled();
|
|
139
|
-
});
|
|
140
117
|
it('retries when response has Retry-After and succeeds on second attempt', async () => {
|
|
141
118
|
const { cmd, postWorkflowRun, resolveInput } = await createCommand();
|
|
142
119
|
resolveInput.mockResolvedValue({});
|
|
@@ -14,7 +14,6 @@ export default class WorkflowStart extends Command {
|
|
|
14
14
|
color: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
input: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
16
|
catalog: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
-
'search-attributes': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
17
|
monitor: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
18
|
};
|
|
20
19
|
run(): Promise<PostWorkflowStart200>;
|
|
@@ -5,7 +5,6 @@ import { handleApiError, handleCommandError } from '#utils/error_handler.js';
|
|
|
5
5
|
import { isErrorStatus } from '#utils/format_workflow_result.js';
|
|
6
6
|
import { gatedMonitorStreamFlags, MONITOR_DEFAULTS } from '#utils/monitor_flags.js';
|
|
7
7
|
import { resolveInput } from '#utils/resolve_input.js';
|
|
8
|
-
import { parseSearchAttributesFlag } from '#utils/search_attributes_flag.js';
|
|
9
8
|
/**
|
|
10
9
|
* Distinct from 1 (the workflow itself failed) and 2 (usage): the workflow was
|
|
11
10
|
* started and is still running, only the attached stream gave up. A caller that
|
|
@@ -21,7 +20,6 @@ export default class WorkflowStart extends Command {
|
|
|
21
20
|
'<%= config.bin %> <%= command.id %> simple --input input.json',
|
|
22
21
|
'<%= config.bin %> <%= command.id %> simple --input input.json --monitor',
|
|
23
22
|
'<%= config.bin %> <%= command.id %> simple --input \'{"key":"value"}\' --catalog my-catalog',
|
|
24
|
-
'<%= config.bin %> <%= command.id %> simple basic_input --search-attributes \'{"ClientId":"acme"}\'',
|
|
25
23
|
'<%= config.bin %> <%= command.id %> simple --json'
|
|
26
24
|
];
|
|
27
25
|
static args = {
|
|
@@ -48,12 +46,6 @@ export default class WorkflowStart extends Command {
|
|
|
48
46
|
description: 'Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
|
|
49
47
|
env: 'OUTPUT_CATALOG_ID'
|
|
50
48
|
}),
|
|
51
|
-
'search-attributes': Flags.string({
|
|
52
|
-
char: 'a',
|
|
53
|
-
description: 'Temporal search attributes as a JSON object or file path, e.g. {"ClientId":"acme"}. ' +
|
|
54
|
-
'Each attribute must be registered on the namespace',
|
|
55
|
-
required: false
|
|
56
|
-
}),
|
|
57
49
|
// No `default: false`: a defaulted flag counts as present, so it would
|
|
58
50
|
// satisfy the `dependsOn` guard the three flags in `gatedMonitorStreamFlags`
|
|
59
51
|
// point at, letting `--interval` and friends be accepted (and then ignored)
|
|
@@ -84,7 +76,6 @@ export default class WorkflowStart extends Command {
|
|
|
84
76
|
this.error('Cannot combine --monitor with --json. Use "workflow run --json" to wait for the result, ' +
|
|
85
77
|
'or "workflow monitor <id> --format json" to stream newline-delimited JSON.', { exit: 2 });
|
|
86
78
|
}
|
|
87
|
-
const searchAttributes = parseSearchAttributesFlag(flags['search-attributes']);
|
|
88
79
|
const input = await resolveInput({
|
|
89
80
|
workflowName: args.workflowName,
|
|
90
81
|
scenario: args.scenario,
|
|
@@ -97,8 +88,7 @@ export default class WorkflowStart extends Command {
|
|
|
97
88
|
const response = await postWorkflowStart({
|
|
98
89
|
workflowName: args.workflowName,
|
|
99
90
|
input,
|
|
100
|
-
catalog: flags.catalog
|
|
101
|
-
searchAttributes
|
|
91
|
+
catalog: flags.catalog
|
|
102
92
|
});
|
|
103
93
|
if (!response || !response.data) {
|
|
104
94
|
this.error('API returned invalid response', { exit: 1 });
|
|
@@ -140,33 +140,6 @@ describe('workflow start command', () => {
|
|
|
140
140
|
expect(postWorkflowStart).toHaveBeenCalledWith(expect.objectContaining({ workflowName: 'my_workflow', catalog: 'my-catalog' }));
|
|
141
141
|
expect(result).toEqual({ workflowId: 'wf-123' });
|
|
142
142
|
});
|
|
143
|
-
it('passes --search-attributes to postWorkflowStart', async () => {
|
|
144
|
-
const { cmd, postWorkflowStart, resolveInput } = await createCommand({ 'search-attributes': '{"ClientId":"acme","Tags":["a"]}' });
|
|
145
|
-
resolveInput.mockResolvedValue({});
|
|
146
|
-
postWorkflowStart.mockResolvedValue({
|
|
147
|
-
data: { workflowId: 'wf-123' },
|
|
148
|
-
status: 200,
|
|
149
|
-
headers: new Headers()
|
|
150
|
-
});
|
|
151
|
-
await cmd.run();
|
|
152
|
-
expect(postWorkflowStart).toHaveBeenCalledWith(expect.objectContaining({ searchAttributes: { ClientId: 'acme', Tags: ['a'] } }));
|
|
153
|
-
});
|
|
154
|
-
it('omits searchAttributes when --search-attributes is not set', async () => {
|
|
155
|
-
const { cmd, postWorkflowStart, resolveInput } = await createCommand();
|
|
156
|
-
resolveInput.mockResolvedValue({});
|
|
157
|
-
postWorkflowStart.mockResolvedValue({
|
|
158
|
-
data: { workflowId: 'wf-123' },
|
|
159
|
-
status: 200,
|
|
160
|
-
headers: new Headers()
|
|
161
|
-
});
|
|
162
|
-
await cmd.run();
|
|
163
|
-
expect(postWorkflowStart.mock.calls[0][0].searchAttributes).toBeUndefined();
|
|
164
|
-
});
|
|
165
|
-
it('rejects invalid --search-attributes before calling the API', async () => {
|
|
166
|
-
const { cmd, postWorkflowStart } = await createCommand({ 'search-attributes': '{"ClientId":{"id":1}}' });
|
|
167
|
-
await expect(cmd.run()).rejects.toThrow('must be a string, number, boolean, or array of strings');
|
|
168
|
-
expect(postWorkflowStart).not.toHaveBeenCalled();
|
|
169
|
-
});
|
|
170
143
|
it('passes undefined catalog through when none is set', async () => {
|
|
171
144
|
const { cmd, postWorkflowStart, resolveInput } = await createCommand();
|
|
172
145
|
resolveInput.mockResolvedValue({});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
4
|
const ARRAY_ACCESS_PATTERN = /^(\w+)\[(\d+)\]$/;
|
|
5
5
|
function tokenCost(tokens, pricePerMillion) {
|
|
6
6
|
return (tokens / 1_000_000) * pricePerMillion;
|
|
@@ -144,7 +144,7 @@ export function extractValue(obj, path) {
|
|
|
144
144
|
}, obj);
|
|
145
145
|
}
|
|
146
146
|
function loadYaml(filePath) {
|
|
147
|
-
return
|
|
147
|
+
return yaml.load(readFileSync(filePath, 'utf-8'));
|
|
148
148
|
}
|
|
149
149
|
export function loadPricingConfig(configPath) {
|
|
150
150
|
const bundledPath = new URL('../assets/config/costs.yml', import.meta.url).pathname;
|
|
@@ -7,7 +7,7 @@ vi.mock('node:fs', () => ({
|
|
|
7
7
|
}));
|
|
8
8
|
const mockLoad = vi.fn();
|
|
9
9
|
vi.mock('js-yaml', () => ({
|
|
10
|
-
load: (...args) => mockLoad(...args)
|
|
10
|
+
default: { load: (...args) => mockLoad(...args) }
|
|
11
11
|
}));
|
|
12
12
|
import { extractValue, findLLMCalls, findHTTPCalls, identifyService, calculateServiceCost, calculateCost, loadPricingConfig } from '#services/cost_calculator.js';
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { resolve, join } from 'node:path';
|
|
4
|
-
import
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
5
|
import { DatasetSchema } from '@outputai/evals';
|
|
6
6
|
import { getTrace } from '#services/trace_reader.js';
|
|
7
7
|
import { sanitizeSecrets } from '#utils/secret_sanitizer.js';
|
|
@@ -27,7 +27,7 @@ export async function resolveDefaultDatasetsDir(workflowName, basePath, workflow
|
|
|
27
27
|
return resolve(basePath ?? getWorkflowsBasePath(), WORKFLOWS_PATHS[0], workflowName, DATASETS_DIR);
|
|
28
28
|
}
|
|
29
29
|
export async function readDatasetFile(filePath) {
|
|
30
|
-
const raw =
|
|
30
|
+
const raw = yaml.load(await readFile(filePath, 'utf-8'));
|
|
31
31
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
32
32
|
throw new Error(`Invalid dataset file: ${filePath}`);
|
|
33
33
|
}
|
|
@@ -69,13 +69,13 @@ export async function writeDataset(dataset, filePath) {
|
|
|
69
69
|
if (!existsSync(dir)) {
|
|
70
70
|
await mkdir(dir, { recursive: true });
|
|
71
71
|
}
|
|
72
|
-
const loaded = existsSync(filePath) ?
|
|
72
|
+
const loaded = existsSync(filePath) ? yaml.load(await readFile(filePath, 'utf-8')) : null;
|
|
73
73
|
const fileObj = (loaded && typeof loaded === 'object' && !Array.isArray(loaded)) ?
|
|
74
74
|
loaded :
|
|
75
75
|
{};
|
|
76
76
|
const { name, _source, ...caseBody } = dataset;
|
|
77
77
|
fileObj[name] = { ...fileObj[name], ...caseBody };
|
|
78
|
-
await writeFile(filePath,
|
|
78
|
+
await writeFile(filePath, yaml.dump(fileObj, { lineWidth: 120, noRefs: true, sortKeys: false }), 'utf-8');
|
|
79
79
|
}
|
|
80
80
|
export async function listDatasets(workflowName, basePath) {
|
|
81
81
|
const dir = await resolveDatasetsDir(workflowName, basePath);
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
|
2
2
|
import { mkdtemp, rm, writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
6
|
import { readDatasetFile, readAllDatasets, writeDataset, listDatasets, resolveDefaultDatasetsDir, datasetFilePath } from './datasets.js';
|
|
7
7
|
import * as catalog from '#api/workflow_catalog.js';
|
|
8
8
|
vi.mock('#api/workflow_catalog.js', () => ({
|
|
@@ -20,7 +20,7 @@ afterEach(async () => {
|
|
|
20
20
|
await rm(ctx.tmpDir, { recursive: true, force: true });
|
|
21
21
|
});
|
|
22
22
|
function writeYaml(filePath, obj) {
|
|
23
|
-
return writeFile(filePath,
|
|
23
|
+
return writeFile(filePath, yaml.dump(obj, { lineWidth: 120, noRefs: true, sortKeys: false }), 'utf-8');
|
|
24
24
|
}
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
// readDatasetFile
|
|
@@ -142,7 +142,7 @@ describe('writeDataset', () => {
|
|
|
142
142
|
const filePath = join(ctx.tmpDir, 'cases.yml');
|
|
143
143
|
const dataset = { name: 'new_case', input: { q: 'hello' } };
|
|
144
144
|
await writeDataset(dataset, filePath);
|
|
145
|
-
const raw =
|
|
145
|
+
const raw = yaml.load(await readFile(filePath, 'utf-8'));
|
|
146
146
|
expect(raw).toHaveProperty('new_case');
|
|
147
147
|
expect(raw.new_case.input).toEqual({ q: 'hello' });
|
|
148
148
|
expect(raw.new_case).not.toHaveProperty('name');
|
|
@@ -151,7 +151,7 @@ describe('writeDataset', () => {
|
|
|
151
151
|
const filePath = join(ctx.tmpDir, 'cases.yml');
|
|
152
152
|
const dataset = { name: 'my_case', input: { q: 'x' }, _source: '/some/path.yml' };
|
|
153
153
|
await writeDataset(dataset, filePath);
|
|
154
|
-
const raw =
|
|
154
|
+
const raw = yaml.load(await readFile(filePath, 'utf-8'));
|
|
155
155
|
expect(raw.my_case).not.toHaveProperty('_source');
|
|
156
156
|
});
|
|
157
157
|
it('updates only the target case, leaving other cases untouched', async () => {
|
|
@@ -161,7 +161,7 @@ describe('writeDataset', () => {
|
|
|
161
161
|
case_b: { input: { x: 2 }, ground_truth: { expected: 2 } }
|
|
162
162
|
});
|
|
163
163
|
await writeDataset({ name: 'case_a', input: { x: 1 }, last_output: { output: { result: 1 }, date: '2026-01-01T00:00:00.000Z' } }, filePath);
|
|
164
|
-
const raw =
|
|
164
|
+
const raw = yaml.load(await readFile(filePath, 'utf-8'));
|
|
165
165
|
expect(raw).toHaveProperty('case_b');
|
|
166
166
|
expect(raw.case_b.ground_truth).toEqual({ expected: 2 });
|
|
167
167
|
});
|
|
@@ -175,7 +175,7 @@ describe('writeDataset', () => {
|
|
|
175
175
|
name: 'my_case', input: { q: 'hello' },
|
|
176
176
|
last_eval: { output: { datasetName: 'my_case', verdict: 'pass', evaluators: [] }, date: '2026-01-01T00:01:00.000Z' }
|
|
177
177
|
}, filePath);
|
|
178
|
-
const raw =
|
|
178
|
+
const raw = yaml.load(await readFile(filePath, 'utf-8'));
|
|
179
179
|
const caseObj = raw.my_case;
|
|
180
180
|
expect(caseObj).toHaveProperty('last_output');
|
|
181
181
|
expect(caseObj).toHaveProperty('last_eval');
|
|
@@ -184,14 +184,14 @@ describe('writeDataset', () => {
|
|
|
184
184
|
it('creates parent directories if they do not exist', async () => {
|
|
185
185
|
const filePath = join(ctx.tmpDir, 'deep', 'nested', 'cases.yml');
|
|
186
186
|
await writeDataset({ name: 'my_case', input: { q: 'x' } }, filePath);
|
|
187
|
-
const raw =
|
|
187
|
+
const raw = yaml.load(await readFile(filePath, 'utf-8'));
|
|
188
188
|
expect(raw).toHaveProperty('my_case');
|
|
189
189
|
});
|
|
190
190
|
it('recovers gracefully when existing file contains non-object YAML', async () => {
|
|
191
191
|
const filePath = join(ctx.tmpDir, 'cases.yml');
|
|
192
192
|
await writeFile(filePath, 'just a string', 'utf-8');
|
|
193
193
|
await writeDataset({ name: 'my_case', input: { q: 'x' } }, filePath);
|
|
194
|
-
const raw =
|
|
194
|
+
const raw = yaml.load(await readFile(filePath, 'utf-8'));
|
|
195
195
|
expect(raw).toHaveProperty('my_case');
|
|
196
196
|
});
|
|
197
197
|
});
|
package/oclif.manifest.json
CHANGED
|
@@ -1165,8 +1165,7 @@
|
|
|
1165
1165
|
"<%= config.bin %> <%= command.id %> simple my_scenario --json",
|
|
1166
1166
|
"<%= config.bin %> <%= command.id %> simple --input '{\"values\":[1,2,3]}'",
|
|
1167
1167
|
"<%= config.bin %> <%= command.id %> simple --input input.json",
|
|
1168
|
-
"<%= config.bin %> <%= command.id %> simple --input '{\"key\":\"value\"}' --catalog my-catalog"
|
|
1169
|
-
"<%= config.bin %> <%= command.id %> simple basic_input --search-attributes '{\"ClientId\":\"acme\"}'"
|
|
1168
|
+
"<%= config.bin %> <%= command.id %> simple --input '{\"key\":\"value\"}' --catalog my-catalog"
|
|
1170
1169
|
],
|
|
1171
1170
|
"flags": {
|
|
1172
1171
|
"json": {
|
|
@@ -1200,15 +1199,6 @@
|
|
|
1200
1199
|
"hasDynamicHelp": false,
|
|
1201
1200
|
"multiple": false,
|
|
1202
1201
|
"type": "option"
|
|
1203
|
-
},
|
|
1204
|
-
"search-attributes": {
|
|
1205
|
-
"char": "a",
|
|
1206
|
-
"description": "Temporal search attributes as a JSON object or file path, e.g. {\"ClientId\":\"acme\"}. Each attribute must be registered on the namespace",
|
|
1207
|
-
"name": "search-attributes",
|
|
1208
|
-
"required": false,
|
|
1209
|
-
"hasDynamicHelp": false,
|
|
1210
|
-
"multiple": false,
|
|
1211
|
-
"type": "option"
|
|
1212
1202
|
}
|
|
1213
1203
|
},
|
|
1214
1204
|
"hasDynamicHelp": false,
|
|
@@ -1248,7 +1238,6 @@
|
|
|
1248
1238
|
"<%= config.bin %> <%= command.id %> simple --input input.json",
|
|
1249
1239
|
"<%= config.bin %> <%= command.id %> simple --input input.json --monitor",
|
|
1250
1240
|
"<%= config.bin %> <%= command.id %> simple --input '{\"key\":\"value\"}' --catalog my-catalog",
|
|
1251
|
-
"<%= config.bin %> <%= command.id %> simple basic_input --search-attributes '{\"ClientId\":\"acme\"}'",
|
|
1252
1241
|
"<%= config.bin %> <%= command.id %> simple --json"
|
|
1253
1242
|
],
|
|
1254
1243
|
"flags": {
|
|
@@ -1284,15 +1273,6 @@
|
|
|
1284
1273
|
"multiple": false,
|
|
1285
1274
|
"type": "option"
|
|
1286
1275
|
},
|
|
1287
|
-
"search-attributes": {
|
|
1288
|
-
"char": "a",
|
|
1289
|
-
"description": "Temporal search attributes as a JSON object or file path, e.g. {\"ClientId\":\"acme\"}. Each attribute must be registered on the namespace",
|
|
1290
|
-
"name": "search-attributes",
|
|
1291
|
-
"required": false,
|
|
1292
|
-
"hasDynamicHelp": false,
|
|
1293
|
-
"multiple": false,
|
|
1294
|
-
"type": "option"
|
|
1295
|
-
},
|
|
1296
1276
|
"monitor": {
|
|
1297
1277
|
"char": "m",
|
|
1298
1278
|
"description": "After starting, attach and stream status updates until the workflow ends (Ctrl+C detaches; the workflow keeps running). Cannot be combined with --json",
|
|
@@ -1772,5 +1752,5 @@
|
|
|
1772
1752
|
]
|
|
1773
1753
|
}
|
|
1774
1754
|
},
|
|
1775
|
-
"version": "0.14.1-next.
|
|
1755
|
+
"version": "0.14.1-next.4e817d8.0"
|
|
1776
1756
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.14.1-next.
|
|
3
|
+
"version": "0.14.1-next.4e817d8.0",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"output": "./bin/run.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
20
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.278",
|
|
21
21
|
"@hackylabs/deep-redact": "3.0.5",
|
|
22
22
|
"@inquirer/prompts": "8.7.2",
|
|
23
23
|
"@oclif/core": "4.10.6",
|
|
@@ -30,15 +30,15 @@
|
|
|
30
30
|
"handlebars": "4.7.9",
|
|
31
31
|
"ink": "7.0.1",
|
|
32
32
|
"ink-spinner": "5.0.0",
|
|
33
|
-
"js-yaml": "
|
|
33
|
+
"js-yaml": "4.3.2",
|
|
34
34
|
"json-schema-library": "11.6.2",
|
|
35
35
|
"ky": "2.0.2",
|
|
36
36
|
"react": "19.3.0",
|
|
37
37
|
"semver": "7.8.5",
|
|
38
38
|
"undici": "8.11.0",
|
|
39
|
-
"@outputai/core": "0.14.1-next.
|
|
40
|
-
"@outputai/
|
|
41
|
-
"@outputai/
|
|
39
|
+
"@outputai/core": "0.14.1-next.4e817d8.0",
|
|
40
|
+
"@outputai/llm": "0.14.1-next.4e817d8.0",
|
|
41
|
+
"@outputai/evals": "0.14.1-next.4e817d8.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/cli-progress": "3.11.6",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@types/react": "19.3.0",
|
|
48
48
|
"@types/semver": "7.8.0",
|
|
49
49
|
"oclif": "4.23.10",
|
|
50
|
-
"orval": "8.
|
|
50
|
+
"orval": "8.35.0"
|
|
51
51
|
},
|
|
52
52
|
"license": "Apache-2.0",
|
|
53
53
|
"publishConfig": {
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { parseInputFlag } from '#utils/input_parser.js';
|
|
2
|
-
const isScalar = (value) => ['string', 'number', 'boolean'].includes(typeof value);
|
|
3
|
-
const isValidValue = (value) => isScalar(value) || (Array.isArray(value) && value.every(v => typeof v === 'string'));
|
|
4
|
-
const validate = (parsed) => {
|
|
5
|
-
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
6
|
-
throw new Error('--search-attributes must be a JSON object, e.g. \'{"ClientId":"acme"}\'');
|
|
7
|
-
}
|
|
8
|
-
const invalid = Object.entries(parsed).find(([, value]) => !isValidValue(value));
|
|
9
|
-
if (invalid) {
|
|
10
|
-
throw new Error(`--search-attributes value for "${invalid[0]}" must be a string, number, boolean, or array of strings`);
|
|
11
|
-
}
|
|
12
|
-
return parsed;
|
|
13
|
-
};
|
|
14
|
-
const parse = (flag) => {
|
|
15
|
-
try {
|
|
16
|
-
return parseInputFlag(flag);
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
throw new Error(`--search-attributes: ${error.message}`);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
export const parseSearchAttributesFlag = (flag) => (flag === undefined ? undefined : validate(parse(flag)));
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseSearchAttributesFlag } from './search_attributes_flag.js';
|
|
3
|
-
describe('parseSearchAttributesFlag', () => {
|
|
4
|
-
it('returns undefined when the flag is not set', () => {
|
|
5
|
-
expect(parseSearchAttributesFlag(undefined)).toBeUndefined();
|
|
6
|
-
});
|
|
7
|
-
it('parses a JSON object of scalars and string arrays', () => {
|
|
8
|
-
const flag = '{"ClientId":"acme","Priority":5,"Active":true,"Tags":["sales","emea"]}';
|
|
9
|
-
expect(parseSearchAttributesFlag(flag)).toEqual({
|
|
10
|
-
ClientId: 'acme',
|
|
11
|
-
Priority: 5,
|
|
12
|
-
Active: true,
|
|
13
|
-
Tags: ['sales', 'emea']
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
it('accepts an empty object', () => {
|
|
17
|
-
expect(parseSearchAttributesFlag('{}')).toEqual({});
|
|
18
|
-
});
|
|
19
|
-
it.each([
|
|
20
|
-
['an array', '["acme"]'],
|
|
21
|
-
['a string', '"acme"'],
|
|
22
|
-
['null', 'null'],
|
|
23
|
-
['a number', '5']
|
|
24
|
-
])('rejects %s', (_, flag) => {
|
|
25
|
-
expect(() => parseSearchAttributesFlag(flag)).toThrow('--search-attributes must be a JSON object');
|
|
26
|
-
});
|
|
27
|
-
it.each([
|
|
28
|
-
['an object value', '{"ClientId":{"id":1}}'],
|
|
29
|
-
['a null value', '{"ClientId":null}'],
|
|
30
|
-
['a number array', '{"Tags":[1,2]}'],
|
|
31
|
-
['a mixed array', '{"Tags":["a",1]}']
|
|
32
|
-
])('rejects %s', (_, flag) => {
|
|
33
|
-
expect(() => parseSearchAttributesFlag(flag)).toThrow('must be a string, number, boolean, or array of strings');
|
|
34
|
-
});
|
|
35
|
-
it('reports invalid JSON', () => {
|
|
36
|
-
expect(() => parseSearchAttributesFlag('{not json')).toThrow(/^--search-attributes: (Invalid JSON input|Input file not found)/);
|
|
37
|
-
});
|
|
38
|
-
});
|