@hue-run/sdk 0.4.2 → 0.5.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/CLI.md +192 -4
- package/ENVIRONMENTS.md +29 -8
- package/EVALUATIONS.md +125 -4
- package/README.md +27 -2
- package/dist/cli/eval-direct.d.ts +47 -0
- package/dist/cli/eval-direct.js +171 -0
- package/dist/cli/eval.d.ts +28 -0
- package/dist/cli/eval.js +985 -0
- package/dist/cli/login.d.ts +32 -0
- package/dist/cli/login.js +712 -0
- package/dist/cli/mcp.d.ts +76 -0
- package/dist/cli/mcp.js +466 -0
- package/dist/evals/client.d.ts +30 -1
- package/dist/evals/client.js +138 -1
- package/dist/evals/files.d.ts +50 -0
- package/dist/evals/files.js +223 -0
- package/dist/evals/local-worker.d.ts +49 -4
- package/dist/evals/local-worker.js +63 -12
- package/dist/evals/runner.d.ts +30 -10
- package/dist/evals/runner.js +216 -68
- package/dist/evals/scenarios.d.ts +79 -0
- package/dist/evals/scenarios.js +169 -0
- package/dist/evals/scorers.d.ts +13 -0
- package/dist/evals/scorers.js +21 -3
- package/dist/evals/simulation.d.ts +12 -1
- package/dist/evals/simulation.js +36 -0
- package/dist/evals/types.d.ts +174 -1
- package/dist/evals/types.js +17 -1
- package/dist/evals/verdicts.d.ts +161 -0
- package/dist/evals/verdicts.js +192 -0
- package/dist/evals.d.ts +9 -3
- package/dist/evals.js +5 -1
- package/dist/setup/cli.js +13 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, extname, join } from "node:path";
|
|
3
|
+
import { outputContentTypes } from "../evals/files.js";
|
|
4
|
+
import { withFiles } from "../evals/types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Direct (file) cases for `hue eval`: a case is a task plus pinned input files, and the agent's
|
|
7
|
+
* answer is one or more generated documents. The agent command receives a private case directory
|
|
8
|
+
* instead of stdin/stdout JSON, the same layout Hue's document workers use:
|
|
9
|
+
*
|
|
10
|
+
* <case dir>/inputs.json the case inputs
|
|
11
|
+
* <case dir>/case.json id, external key, run config and the staged file list
|
|
12
|
+
* <case dir>/files/<role>/<name> verified copies of the agent-visible pinned files
|
|
13
|
+
* <case dir>/output/ where the command writes what it produced
|
|
14
|
+
*
|
|
15
|
+
* Everything under `output/` becomes the execution's generated files. Optional helpers there:
|
|
16
|
+
* `manifest.json` (`{ "primary": "<filename>", "output": <json> }`), `result.json` (the JSON
|
|
17
|
+
* output) and `summary.txt` / `summary.md` (recorded as `{ "summary": "…" }`).
|
|
18
|
+
*/
|
|
19
|
+
export const directCaseEnvironment = [
|
|
20
|
+
"HUE_CASE_DIR",
|
|
21
|
+
"HUE_CASE_INPUTS",
|
|
22
|
+
"HUE_CASE_OUTPUT_DIR",
|
|
23
|
+
"HUE_CASE_ID",
|
|
24
|
+
"HUE_CASE_KEY",
|
|
25
|
+
"HUE_EXECUTION_ID",
|
|
26
|
+
];
|
|
27
|
+
/** Generated-file extensions Hue accepts and the content type recorded for each. */
|
|
28
|
+
export const outputExtensions = {
|
|
29
|
+
".pdf": "application/pdf",
|
|
30
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
31
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
32
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
33
|
+
".json": "application/json",
|
|
34
|
+
".txt": "text/plain",
|
|
35
|
+
".csv": "text/csv",
|
|
36
|
+
".png": "image/png",
|
|
37
|
+
".jpg": "image/jpeg",
|
|
38
|
+
".jpeg": "image/jpeg",
|
|
39
|
+
".webp": "image/webp",
|
|
40
|
+
};
|
|
41
|
+
for (const type of Object.values(outputExtensions))
|
|
42
|
+
if (!outputContentTypes.includes(type))
|
|
43
|
+
throw new Error(`outputExtensions maps to a content type Hue does not accept: ${type}`);
|
|
44
|
+
const HELPER_FILES = new Set(["manifest.json", "result.json", "output.json"]);
|
|
45
|
+
const SUMMARY_FILES = ["summary.txt", "summary.md", "final.txt", "resumen.txt"];
|
|
46
|
+
/** Writes the case directory: inputs, descriptor, verified file copies and an empty output folder. */
|
|
47
|
+
export async function stageDirectCase(root, input) {
|
|
48
|
+
const caseDirectory = join(root, "case");
|
|
49
|
+
const outputDirectory = join(caseDirectory, "output");
|
|
50
|
+
await mkdir(outputDirectory, { recursive: true, mode: 0o700 });
|
|
51
|
+
const inputsPath = join(caseDirectory, "inputs.json");
|
|
52
|
+
await writeFile(inputsPath, JSON.stringify(input.inputs, null, 2), { mode: 0o600 });
|
|
53
|
+
const used = new Set();
|
|
54
|
+
const files = [];
|
|
55
|
+
for (const file of input.files) {
|
|
56
|
+
const folder = join(caseDirectory, "files", file.role);
|
|
57
|
+
await mkdir(folder, { recursive: true, mode: 0o700 });
|
|
58
|
+
// Two inputs may share a filename (two incident reports, say); keep both.
|
|
59
|
+
const name = basename(file.filename);
|
|
60
|
+
const extension = extname(name);
|
|
61
|
+
const stem = name.slice(0, name.length - extension.length);
|
|
62
|
+
let target = join(folder, name);
|
|
63
|
+
for (let copy = 2; used.has(target); copy++)
|
|
64
|
+
target = join(folder, `${stem} (${copy})${extension}`);
|
|
65
|
+
used.add(target);
|
|
66
|
+
await copyFile(file.path, target);
|
|
67
|
+
files.push({ role: file.role, filename: basename(target), path: target });
|
|
68
|
+
}
|
|
69
|
+
await writeFile(join(caseDirectory, "case.json"), JSON.stringify({
|
|
70
|
+
id: input.item.id,
|
|
71
|
+
externalKey: input.item.externalKey,
|
|
72
|
+
executionId: input.executionId,
|
|
73
|
+
config: input.config,
|
|
74
|
+
files,
|
|
75
|
+
outputDirectory,
|
|
76
|
+
}, null, 2), { mode: 0o600 });
|
|
77
|
+
return { caseDirectory, inputsPath, outputDirectory, files };
|
|
78
|
+
}
|
|
79
|
+
async function readJsonFile(path) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code === "ENOENT")
|
|
85
|
+
return undefined;
|
|
86
|
+
throw new Error(`${basename(path)} in the output directory is not valid JSON`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Regular files anywhere under the output directory, named by their `/`-joined relative path.
|
|
90
|
+
* Hidden and lock entries (dot names, `~$…`) and symlinks are skipped at every level. */
|
|
91
|
+
async function listOutputFiles(directory, prefix = "") {
|
|
92
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
93
|
+
const found = [];
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("~$"))
|
|
96
|
+
continue;
|
|
97
|
+
const path = join(directory, entry.name);
|
|
98
|
+
const name = `${prefix}${entry.name}`;
|
|
99
|
+
if (entry.isDirectory())
|
|
100
|
+
found.push(...(await listOutputFiles(path, `${name}/`)));
|
|
101
|
+
else if (entry.isFile())
|
|
102
|
+
found.push({ name, path });
|
|
103
|
+
}
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
/** Artifact filenames cannot contain path separators or controls. Encode only those forbidden
|
|
107
|
+
* characters plus `%`, then represent `/` as `%2F`; distinct output paths stay distinct. */
|
|
108
|
+
function artifactFilename(relativePath) {
|
|
109
|
+
return relativePath
|
|
110
|
+
.split("/")
|
|
111
|
+
.map((segment) => segment
|
|
112
|
+
.replaceAll("%", "%25")
|
|
113
|
+
.replaceAll("\\", "%5C")
|
|
114
|
+
// eslint-disable-next-line no-control-regex -- artifact filenames forbid controls
|
|
115
|
+
.replace(/[\x00-\x1f\x7f]/gu, (character) => encodeURIComponent(character)))
|
|
116
|
+
.join("%2F");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Turns the output folder into the target's result. Every regular file, in subdirectories too,
|
|
120
|
+
* becomes a generated file; an unsupported extension is the agent's error rather than a silently
|
|
121
|
+
* dropped document. `fallbackOutput` (for example the command's stdout) is used when no JSON
|
|
122
|
+
* output or summary file was written.
|
|
123
|
+
*/
|
|
124
|
+
export async function collectDirectOutputs(outputDirectory, fallbackOutput) {
|
|
125
|
+
const manifest = await readJsonFile(join(outputDirectory, "manifest.json"));
|
|
126
|
+
const declaredPrimary = manifest && typeof manifest === "object" && !Array.isArray(manifest)
|
|
127
|
+
? manifest.primary
|
|
128
|
+
: undefined;
|
|
129
|
+
const declaredOutput = manifest && typeof manifest === "object" && !Array.isArray(manifest)
|
|
130
|
+
? manifest.output
|
|
131
|
+
: undefined;
|
|
132
|
+
let output = declaredOutput ??
|
|
133
|
+
(await readJsonFile(join(outputDirectory, "result.json"))) ??
|
|
134
|
+
(await readJsonFile(join(outputDirectory, "output.json")));
|
|
135
|
+
if (output === undefined)
|
|
136
|
+
for (const name of SUMMARY_FILES) {
|
|
137
|
+
const text = await readFile(join(outputDirectory, name), "utf8").catch(() => undefined);
|
|
138
|
+
if (text !== undefined) {
|
|
139
|
+
output = { summary: text };
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (output === undefined)
|
|
144
|
+
output = fallbackOutput;
|
|
145
|
+
// Helper and summary names are reserved at the top level only; a nested one is a document.
|
|
146
|
+
const entries = (await listOutputFiles(outputDirectory)).filter((entry) => !HELPER_FILES.has(entry.name) && !SUMMARY_FILES.includes(entry.name));
|
|
147
|
+
// Directory order differs between filesystems; keep uploads stable.
|
|
148
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
149
|
+
const unsupported = entries.filter((entry) => !outputExtensions[extname(entry.name).toLowerCase()]);
|
|
150
|
+
if (unsupported.length)
|
|
151
|
+
throw new Error(`The agent wrote files Hue does not accept as generated documents: ${unsupported.map((entry) => entry.name).join(", ")} (accepted: ${Object.keys(outputExtensions).join(" ")})`);
|
|
152
|
+
const files = [];
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if ((await stat(entry.path)).size === 0)
|
|
155
|
+
throw new Error(`The agent wrote an empty file: ${entry.name}`);
|
|
156
|
+
files.push({
|
|
157
|
+
path: entry.path,
|
|
158
|
+
filename: artifactFilename(entry.name),
|
|
159
|
+
contentType: outputExtensions[extname(entry.name).toLowerCase()],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (typeof declaredPrimary === "string") {
|
|
163
|
+
const primaryIndex = entries.findIndex((entry) => entry.name === declaredPrimary);
|
|
164
|
+
if (primaryIndex < 0)
|
|
165
|
+
throw new Error(`manifest.json names a primary file that was not written: ${declaredPrimary}`);
|
|
166
|
+
files[primaryIndex].primary = true;
|
|
167
|
+
}
|
|
168
|
+
else if (files.length === 1)
|
|
169
|
+
files[0].primary = true;
|
|
170
|
+
return withFiles(output, files);
|
|
171
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ExperimentCase, LocalFile } from "../evals/types.js";
|
|
2
|
+
import { TargetResult } from "../evals/types.js";
|
|
3
|
+
import { type SimulationTargetContext } from "../evals/simulation.js";
|
|
4
|
+
import type { JsonValue } from "../types.js";
|
|
5
|
+
/** Adapter contract: the module's `default` or `runMyAgent` export. */
|
|
6
|
+
export type EvalAdapter = (inputs: JsonValue, context: SimulationTargetContext) => JsonValue | undefined | Promise<JsonValue | undefined>;
|
|
7
|
+
/**
|
|
8
|
+
* Context of a direct (file) case: pinned input files in, generated documents out. The same
|
|
9
|
+
* adapter module serves both modes; `mode` tells it which context it received.
|
|
10
|
+
*/
|
|
11
|
+
export interface DirectTargetContext {
|
|
12
|
+
mode: "direct";
|
|
13
|
+
config: JsonValue;
|
|
14
|
+
item: Pick<ExperimentCase, "id" | "externalKey">;
|
|
15
|
+
executionId: string;
|
|
16
|
+
/** Verified copies of the agent-visible pinned files (`source`, templates, originals). */
|
|
17
|
+
files: LocalFile[];
|
|
18
|
+
/** Private scratch directory for this case; generated files may be written here. */
|
|
19
|
+
outputDirectory: string;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
export type DirectEvalAdapter = (inputs: JsonValue, context: DirectTargetContext) => JsonValue | TargetResult | undefined | Promise<JsonValue | TargetResult | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* `hue eval`: runs a local adapter or command against a Scenario or eval set through
|
|
25
|
+
* `runSimulation`, or registers it as an outbound worker through `runLocalAgent`.
|
|
26
|
+
* Returns the process exit code.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runEvalCommand(argv: string[]): Promise<number>;
|