stageflow 0.40.0 → 0.42.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 +3 -1
- package/dist/agents/authorChat.d.ts +4 -0
- package/dist/agents/authorChat.js +113 -12
- package/dist/agents/authorSessionStore.d.ts +35 -0
- package/dist/agents/authorSessionStore.js +152 -0
- package/dist/agents/chatTurn.d.ts +2 -0
- package/dist/agents/chatTurn.js +17 -1
- package/dist/agents/errors.d.ts +1 -1
- package/dist/agents/errors.js +1 -0
- package/dist/agents/evalGate.d.ts +61 -0
- package/dist/agents/evalGate.js +103 -0
- package/dist/agents/evalSession.d.ts +26 -0
- package/dist/agents/evalSession.js +29 -0
- package/dist/agents/hostContext.d.ts +3 -0
- package/dist/agents/hostContext.js +4 -1
- package/dist/agents/runtime/piRuntime.d.ts +1 -1
- package/dist/agents/runtime/piRuntime.js +4 -4
- package/dist/agents/runtime/types.d.ts +3 -1
- package/dist/agents/service.d.ts +27 -2
- package/dist/agents/service.js +88 -13
- package/dist/agents/sessions.d.ts +8 -0
- package/dist/agents/sessions.js +50 -10
- package/dist/agents/store.d.ts +14 -3
- package/dist/agents/store.js +45 -7
- package/dist/agents/toolRisk.d.ts +21 -0
- package/dist/agents/toolRisk.js +50 -0
- package/dist/cli/doctorCommand.js +26 -0
- package/dist/cli/evalCommand.d.ts +20 -0
- package/dist/cli/evalCommand.js +720 -0
- package/dist/cli/exportAllCommand.js +1 -1
- package/dist/cli/hostClient.d.ts +60 -0
- package/dist/cli/hostClient.js +71 -0
- package/dist/cli/validateOutput.d.ts +2 -2
- package/dist/cli/validateOutput.js +2 -2
- package/dist/cli.js +10 -0
- package/dist/config/atomicWrite.d.ts +15 -0
- package/dist/config/atomicWrite.js +43 -0
- package/dist/config/browseCatalog.d.ts +2 -0
- package/dist/config/browseCatalog.js +4 -2
- package/dist/config/csv.d.ts +9 -0
- package/dist/config/csv.js +81 -0
- package/dist/config/datasetAgentCheck.d.ts +18 -0
- package/dist/config/datasetAgentCheck.js +109 -0
- package/dist/config/datasetCandidates.d.ts +22 -0
- package/dist/config/datasetCandidates.js +86 -0
- package/dist/config/datasetFromSessions.d.ts +34 -0
- package/dist/config/datasetFromSessions.js +105 -0
- package/dist/config/datasetHash.d.ts +1 -0
- package/dist/config/datasetHash.js +4 -0
- package/dist/config/datasetImport.d.ts +21 -0
- package/dist/config/datasetImport.js +120 -0
- package/dist/config/datasetStageCheck.d.ts +9 -0
- package/dist/config/datasetStageCheck.js +17 -0
- package/dist/config/datasetTypes.d.ts +27 -0
- package/dist/config/datasetTypes.js +1 -0
- package/dist/config/ensureCatalogScanRoot.d.ts +2 -1
- package/dist/config/ensureCatalogScanRoot.js +2 -6
- package/dist/config/evalCatalog.d.ts +22 -0
- package/dist/config/evalCatalog.js +67 -0
- package/dist/config/evalWriteRoots.d.ts +17 -0
- package/dist/config/evalWriteRoots.js +109 -0
- package/dist/config/hostConfig.js +1 -0
- package/dist/config/installEvalSample.d.ts +34 -0
- package/dist/config/installEvalSample.js +182 -0
- package/dist/config/loadDataset.d.ts +16 -0
- package/dist/config/loadDataset.js +318 -0
- package/dist/config/loadEval.d.ts +32 -0
- package/dist/config/loadEval.js +1085 -0
- package/dist/config/loadOutcome.d.ts +5 -0
- package/dist/config/loadStageflowManifest.js +64 -0
- package/dist/config/resolveAgentTarget.d.ts +32 -0
- package/dist/config/resolveAgentTarget.js +118 -0
- package/dist/config/resolveEvalTarget.d.ts +21 -0
- package/dist/config/resolveEvalTarget.js +89 -0
- package/dist/config/scanCatalogPaths.d.ts +1 -1
- package/dist/config/scanCatalogPaths.js +17 -5
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +9 -1
- package/dist/config/validateEval.d.ts +36 -0
- package/dist/config/validateEval.js +405 -0
- package/dist/config/writeDataset.d.ts +54 -0
- package/dist/config/writeDataset.js +186 -0
- package/dist/config/writeEval.d.ts +25 -0
- package/dist/config/writeEval.js +398 -0
- package/dist/diagnostics/checks.js +1 -1
- package/dist/evals/agentDiff.d.ts +13 -0
- package/dist/evals/agentDiff.js +11 -0
- package/dist/evals/canonical.d.ts +3 -0
- package/dist/evals/canonical.js +24 -0
- package/dist/evals/caseTask.d.ts +3 -0
- package/dist/evals/caseTask.js +9 -0
- package/dist/evals/compare.d.ts +94 -0
- package/dist/evals/compare.js +201 -0
- package/dist/evals/constants.d.ts +60 -0
- package/dist/evals/constants.js +95 -0
- package/dist/evals/engine/FakeEngine.d.ts +33 -0
- package/dist/evals/engine/FakeEngine.js +176 -0
- package/dist/evals/engine/index.d.ts +9 -0
- package/dist/evals/engine/index.js +19 -0
- package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +52 -0
- package/dist/evals/engine/promptfoo/PromptfooEngine.js +311 -0
- package/dist/evals/engine/promptfoo/errors.d.ts +5 -0
- package/dist/evals/engine/promptfoo/errors.js +8 -0
- package/dist/evals/engine/promptfoo/generate.d.ts +30 -0
- package/dist/evals/engine/promptfoo/generate.js +342 -0
- package/dist/evals/engine/promptfoo/guard.d.ts +10 -0
- package/dist/evals/engine/promptfoo/guard.js +116 -0
- package/dist/evals/engine/promptfoo/install.d.ts +17 -0
- package/dist/evals/engine/promptfoo/install.js +73 -0
- package/dist/evals/engine/promptfoo/judgeProviders.d.ts +2 -0
- package/dist/evals/engine/promptfoo/judgeProviders.js +9 -0
- package/dist/evals/engine/promptfoo/judgeServices.d.ts +6 -0
- package/dist/evals/engine/promptfoo/judgeServices.js +41 -0
- package/dist/evals/engine/promptfoo/locate.d.ts +21 -0
- package/dist/evals/engine/promptfoo/locate.js +68 -0
- package/dist/evals/engine/promptfoo/nodeVersion.d.ts +7 -0
- package/dist/evals/engine/promptfoo/nodeVersion.js +14 -0
- package/dist/evals/engine/promptfoo/normalize.d.ts +66 -0
- package/dist/evals/engine/promptfoo/normalize.js +261 -0
- package/dist/evals/engine/promptfoo/pinned.d.ts +5 -0
- package/dist/evals/engine/promptfoo/pinned.js +5 -0
- package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +14 -0
- package/dist/evals/engine/promptfoo/rubricPrompt.js +103 -0
- package/dist/evals/engine/promptfoo/runTokens.d.ts +18 -0
- package/dist/evals/engine/promptfoo/runTokens.js +31 -0
- package/dist/evals/engine/promptfoo/status.d.ts +13 -0
- package/dist/evals/engine/promptfoo/status.js +16 -0
- package/dist/evals/estimate.d.ts +56 -0
- package/dist/evals/estimate.js +66 -0
- package/dist/evals/graders/codeGrader.d.ts +6 -0
- package/dist/evals/graders/codeGrader.js +85 -0
- package/dist/evals/graders/conversationCheckGrader.d.ts +6 -0
- package/dist/evals/graders/conversationCheckGrader.js +62 -0
- package/dist/evals/graders/fieldGrader.d.ts +11 -0
- package/dist/evals/graders/fieldGrader.js +117 -0
- package/dist/evals/graders/goal.d.ts +3 -0
- package/dist/evals/graders/goal.js +14 -0
- package/dist/evals/graders/index.d.ts +18 -0
- package/dist/evals/graders/index.js +34 -0
- package/dist/evals/graders/notApplicable.d.ts +5 -0
- package/dist/evals/graders/notApplicable.js +9 -0
- package/dist/evals/graders/path.d.ts +26 -0
- package/dist/evals/graders/path.js +75 -0
- package/dist/evals/graders/referenceGrader.d.ts +5 -0
- package/dist/evals/graders/referenceGrader.js +59 -0
- package/dist/evals/graders/schemaGrader.d.ts +5 -0
- package/dist/evals/graders/schemaGrader.js +29 -0
- package/dist/evals/graders/subject.d.ts +6 -0
- package/dist/evals/graders/subject.js +33 -0
- package/dist/evals/graders/toolCallsGrader.d.ts +6 -0
- package/dist/evals/graders/toolCallsGrader.js +100 -0
- package/dist/evals/graders/transcript.d.ts +12 -0
- package/dist/evals/graders/transcript.js +57 -0
- package/dist/evals/graders/types.d.ts +20 -0
- package/dist/evals/graders/types.js +1 -0
- package/dist/evals/judgeProviders.d.ts +16 -0
- package/dist/evals/judgeProviders.js +27 -0
- package/dist/evals/lineDiff.d.ts +5 -0
- package/dist/evals/lineDiff.js +34 -0
- package/dist/evals/listStatus.d.ts +31 -0
- package/dist/evals/listStatus.js +48 -0
- package/dist/evals/manager.d.ts +174 -0
- package/dist/evals/manager.js +787 -0
- package/dist/evals/plan.d.ts +58 -0
- package/dist/evals/plan.js +602 -0
- package/dist/evals/providerKeys.d.ts +1 -0
- package/dist/evals/providerKeys.js +21 -0
- package/dist/evals/read/agentTargets.d.ts +37 -0
- package/dist/evals/read/agentTargets.js +101 -0
- package/dist/evals/read/api.d.ts +196 -0
- package/dist/evals/read/api.js +358 -0
- package/dist/evals/read/compare.d.ts +4 -0
- package/dist/evals/read/compare.js +39 -0
- package/dist/evals/read/datasets.d.ts +105 -0
- package/dist/evals/read/datasets.js +318 -0
- package/dist/evals/read/list.d.ts +66 -0
- package/dist/evals/read/list.js +173 -0
- package/dist/evals/read/results.d.ts +46 -0
- package/dist/evals/read/results.js +107 -0
- package/dist/evals/read/runs.d.ts +149 -0
- package/dist/evals/read/runs.js +289 -0
- package/dist/evals/read/suggestions.d.ts +43 -0
- package/dist/evals/read/suggestions.js +73 -0
- package/dist/evals/runner/FakeAgentBackend.d.ts +40 -0
- package/dist/evals/runner/FakeAgentBackend.js +82 -0
- package/dist/evals/runner/agentCaseRunner.d.ts +24 -0
- package/dist/evals/runner/agentCaseRunner.js +388 -0
- package/dist/evals/runner/caseRunner.d.ts +7 -0
- package/dist/evals/runner/caseRunner.js +17 -0
- package/dist/evals/runner/conversationSubject.d.ts +18 -0
- package/dist/evals/runner/conversationSubject.js +123 -0
- package/dist/evals/runner/simulatorModel.d.ts +51 -0
- package/dist/evals/runner/simulatorModel.js +58 -0
- package/dist/evals/runner/simulatorPrompt.d.ts +29 -0
- package/dist/evals/runner/simulatorPrompt.js +59 -0
- package/dist/evals/runner/stageCaseRunner.d.ts +30 -0
- package/dist/evals/runner/stageCaseRunner.js +279 -0
- package/dist/evals/runner/userDriver.d.ts +71 -0
- package/dist/evals/runner/userDriver.js +104 -0
- package/dist/evals/scoring.d.ts +34 -0
- package/dist/evals/scoring.js +67 -0
- package/dist/evals/snapshot.d.ts +183 -0
- package/dist/evals/snapshot.js +65 -0
- package/dist/evals/stageDiff.d.ts +8 -0
- package/dist/evals/stageDiff.js +48 -0
- package/dist/evals/stats.d.ts +95 -0
- package/dist/evals/stats.js +220 -0
- package/dist/evals/statsAdapter.d.ts +4 -0
- package/dist/evals/statsAdapter.js +30 -0
- package/dist/evals/statsTypes.d.ts +51 -0
- package/dist/evals/statsTypes.js +18 -0
- package/dist/evals/storeError.d.ts +5 -0
- package/dist/evals/storeError.js +8 -0
- package/dist/evals/storeTypes.d.ts +207 -0
- package/dist/evals/storeTypes.js +1 -0
- package/dist/evals/tTable.d.ts +3 -0
- package/dist/evals/tTable.js +15 -0
- package/dist/evals/triage/constants.d.ts +26 -0
- package/dist/evals/triage/constants.js +33 -0
- package/dist/evals/triage/credentials.d.ts +13 -0
- package/dist/evals/triage/credentials.js +20 -0
- package/dist/evals/triage/jevClassifier.d.ts +14 -0
- package/dist/evals/triage/jevClassifier.js +124 -0
- package/dist/evals/triage/judgeClassifier.d.ts +10 -0
- package/dist/evals/triage/judgeClassifier.js +76 -0
- package/dist/evals/triage/prompts.d.ts +16 -0
- package/dist/evals/triage/prompts.js +64 -0
- package/dist/evals/triage/service.d.ts +79 -0
- package/dist/evals/triage/service.js +217 -0
- package/dist/evals/triage/stateBuilder.d.ts +19 -0
- package/dist/evals/triage/stateBuilder.js +125 -0
- package/dist/evals/triage/types.d.ts +60 -0
- package/dist/evals/triage/types.js +8 -0
- package/dist/evals/types.d.ts +347 -0
- package/dist/evals/types.js +1 -0
- package/dist/mcp/deps.d.ts +7 -0
- package/dist/mcp/evalTools.d.ts +3 -0
- package/dist/mcp/evalTools.js +280 -0
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/tools.js +2 -0
- package/dist/models/response.d.ts +7 -2
- package/dist/models/response.js +6 -1
- package/dist/operatorAgent/agentAuthorContext.d.ts +1 -1
- package/dist/operatorAgent/agentAuthorContext.js +1 -1
- package/dist/operatorAgent/piModel.js +17 -4
- package/dist/operatorAgent/profiles/agentAuthor.d.ts +3 -1
- package/dist/operatorAgent/profiles/agentAuthor.js +145 -20
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +48 -15
- package/dist/operatorAgent/profiles/authorReadable.d.ts +2 -0
- package/dist/operatorAgent/profiles/authorReadable.js +26 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/port.d.ts +43 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +28 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +99 -5
- package/dist/runstore/sqlite/evalStore.d.ts +20 -0
- package/dist/runstore/sqlite/evalStore.js +541 -0
- package/dist/runstore/sqlite/migrations/015-evals.d.ts +12 -0
- package/dist/runstore/sqlite/migrations/015-evals.js +157 -0
- package/dist/runstore/sqlite/migrations/016-eval-subject-ref.d.ts +12 -0
- package/dist/runstore/sqlite/migrations/016-eval-subject-ref.js +20 -0
- package/dist/runstore/sqlite/migrations/017-eval-result-suggestions.d.ts +11 -0
- package/dist/runstore/sqlite/migrations/017-eval-result-suggestions.js +36 -0
- package/dist/runstore/sqlite/migrations/index.js +6 -0
- package/dist/runstore/sqlite/suggestionStore.d.ts +14 -0
- package/dist/runstore/sqlite/suggestionStore.js +132 -0
- package/dist/runtime/pipelineRunner.d.ts +2 -1
- package/dist/runtime/pipelineRunner.js +2 -0
- package/dist/runtime/runManager.d.ts +2 -1
- package/dist/runtime/runManager.js +8 -6
- package/dist/runtime/runRetentionSweep.js +2 -2
- package/dist/server/agentAuthorRoutes.js +50 -1
- package/dist/server/bootstrap.d.ts +8 -0
- package/dist/server/bootstrap.js +56 -0
- package/dist/server/controlToken.js +3 -0
- package/dist/server/evalInternalRoutes.d.ts +14 -0
- package/dist/server/evalInternalRoutes.js +87 -0
- package/dist/server/evalRouteContext.d.ts +46 -0
- package/dist/server/evalRouteContext.js +43 -0
- package/dist/server/evalRoutes.d.ts +5 -0
- package/dist/server/evalRoutes.js +1427 -0
- package/dist/server/evalTriageRoutes.d.ts +2 -0
- package/dist/server/evalTriageRoutes.js +95 -0
- package/dist/server/http.d.ts +5 -0
- package/dist/server/http.js +32 -2
- package/dist/server/mcpHost.js +3 -0
- package/dist/toolkit/meta.d.ts +1 -1
- package/dist/toolkit/meta.js +15 -0
- package/dist/toolkit/profiles.js +1 -1
- package/dist/types/stageflowManifest.d.ts +6 -0
- package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +9 -0
- package/dist/ui/assets/{MarkdownText-D7tYaIFO.js → MarkdownText-BTrOqu5J.js} +1 -1
- package/dist/ui/assets/chat-Df_wCpak.js +5 -0
- package/dist/ui/assets/chat-P7KQznHm.css +2 -0
- package/dist/ui/assets/{main-BFo79krC.css → main-CHlmTNJe.css} +1 -1
- package/dist/ui/assets/main-CvVfxmMd.js +271 -0
- package/dist/ui/chat.html +3 -3
- package/dist/ui/index.html +3 -3
- package/examples/README.md +2 -0
- package/examples/document-policy-review/README.md +35 -0
- package/examples/document-policy-review/agents/compliance.agent.yaml +42 -0
- package/examples/document-policy-review/documents/acme-vendor-sow.md +15 -0
- package/examples/document-policy-review/documents/support-runbook.md +13 -0
- package/examples/document-policy-review/policies/acceptable-use.md +13 -0
- package/examples/document-policy-review/policies/data-handling.md +17 -0
- package/examples/document-policy-review/policies/vendor-security.md +17 -0
- package/examples/evals/README.md +123 -0
- package/examples/evals/critique-sample.dataset.jsonl +8 -0
- package/examples/evals/critique-sample.eval.yaml +34 -0
- package/examples/evals/critique-sample.task.yaml +4 -0
- package/examples/evals/eval-sample.pipeline.yaml +35 -0
- package/examples/evals/support-sample.agent.yaml +20 -0
- package/examples/evals/support-sample.dataset.jsonl +6 -0
- package/examples/evals/support-sample.eval.yaml +38 -0
- package/examples/stageflow.yaml +6 -0
- package/package.json +1 -1
- package/skills/stageflow/SKILL.md +5 -2
- package/skills/stageflow/references/control-surface.md +4 -2
- package/skills/stageflow/references/evals.md +68 -0
- package/skills/stageflow-author/SKILL.md +4 -0
- package/skills/stageflow-run/SKILL.md +4 -0
- package/dist/ui/assets/AssistantRuntimeProvider-CsiPFhCT.js +0 -9
- package/dist/ui/assets/chat-BbLukq9W.js +0 -5
- package/dist/ui/assets/chat-zIZU_bP1.css +0 -2
- package/dist/ui/assets/main-CijYMeM0.js +0 -259
|
@@ -0,0 +1,1427 @@
|
|
|
1
|
+
import { formatId, nextIdNumber } from "../config/datasetImport.js";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { CatalogPathError, catalogPathErrorBody, resolveCatalogStartInput, resolveWritableCatalogRoot, } from "../config/catalogRelativePath.js";
|
|
5
|
+
import { EvalsWriteError, FileChangedError, contentHash } from "../config/atomicWrite.js";
|
|
6
|
+
import { casesFromRuns, listDatasetCandidates } from "../config/datasetCandidates.js";
|
|
7
|
+
import { casesFromSessions, listSessionCandidates, SessionCaseError, } from "../config/datasetFromSessions.js";
|
|
8
|
+
import { checkDatasetAgainstStage } from "../config/datasetStageCheck.js";
|
|
9
|
+
import { listDatasetFiles, listEvalFiles } from "../config/evalCatalog.js";
|
|
10
|
+
import { assertInsideProject, datasetIdsFor, datasetStem, findEvalFile, manifestForWrite, resolveWriteDir, } from "../config/evalWriteRoots.js";
|
|
11
|
+
import { installEvalSample, EVAL_SAMPLE_FILES } from "../config/installEvalSample.js";
|
|
12
|
+
import { loadDatasetFile, parseDatasetText } from "../config/loadDataset.js";
|
|
13
|
+
import { loadEvalOutcome, resolveConversation, resolveEvalPaths } from "../config/loadEval.js";
|
|
14
|
+
import { resolveEvalTarget } from "../config/resolveEvalTarget.js";
|
|
15
|
+
import { resolveSeededExamplesPath } from "../config/seededCatalog.js";
|
|
16
|
+
import { applyCaseOps, appendOrReplaceCases, convertDatasetToJsonl, createDatasetFile, } from "../config/writeDataset.js";
|
|
17
|
+
import { createEvalFile, previewEvalUpdate, renderEvalCreateYaml, updateEvalFile, } from "../config/writeEval.js";
|
|
18
|
+
import { CompareMismatchError } from "../evals/compare.js";
|
|
19
|
+
import { EVAL_LIMITS, EVAL_REVIEW_LABELS, EVAL_STARTED_VIA } from "../evals/constants.js";
|
|
20
|
+
import { PINNED_PROMPTFOO_VERSION } from "../evals/engine/promptfoo/pinned.js";
|
|
21
|
+
import { AGENT_ESTIMATE_ASSUMPTIONS, estimateRun, } from "../evals/estimate.js";
|
|
22
|
+
import { EvalRunError, } from "../evals/manager.js";
|
|
23
|
+
import { buildEvalPlan } from "../evals/plan.js";
|
|
24
|
+
import { buildEvalDetail, buildResultDetail, buildResultsPage, DEFAULT_LIMIT, ENGINE_NAME, engineCheck as engineCheckStatus, EvalApiError, evalScopeRoots, listEvalRunsPage, loadAggregatedEvalList, locateDataset, locateEval, locateEvalRoot, wireRootOf, MAX_LIMIT, planFindings as planFindingsShared, rel, validateEvalText, } from "../evals/read/api.js";
|
|
25
|
+
import { listAgentTargets } from "../evals/read/agentTargets.js";
|
|
26
|
+
import { buildCompare } from "../evals/read/compare.js";
|
|
27
|
+
import { buildDatasetDetail, previewUpload, resolveDatasetTarget, } from "../evals/read/datasets.js";
|
|
28
|
+
import { filterEvalListItems } from "../evals/read/list.js";
|
|
29
|
+
import { buildExport, buildRunDetail, completedRunsNewestFirst, statsOfRun, } from "../evals/read/runs.js";
|
|
30
|
+
import { EvalStoreError } from "../evals/storeError.js";
|
|
31
|
+
import { audit, bad, isRecord, optStringArray, readOptionalBody, runByIdOr404, } from "./evalRouteContext.js";
|
|
32
|
+
import { matchEvalTriageRoute } from "./evalTriageRoutes.js";
|
|
33
|
+
import { callerIdFromRequestAuth } from "./requestAuthContext.js";
|
|
34
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
35
|
+
const CANCEL_WAIT_MS = 5000;
|
|
36
|
+
const WRITE_ERROR_STATUS = {
|
|
37
|
+
EVAL_EXISTS: 409,
|
|
38
|
+
EVAL_NOT_FOUND: 404,
|
|
39
|
+
EVAL_INVALID: 422,
|
|
40
|
+
EVAL_DEFINITION_ID_MISMATCH: 422,
|
|
41
|
+
DATASET_EXISTS: 409,
|
|
42
|
+
DATASET_NOT_FOUND: 404,
|
|
43
|
+
DATASET_INVALID: 422,
|
|
44
|
+
DATASET_READ_ONLY: 409,
|
|
45
|
+
DATASET_CASE_OP_INVALID: 422,
|
|
46
|
+
DATASET_NAME_INVALID: 400,
|
|
47
|
+
SAMPLE_EXISTS: 409,
|
|
48
|
+
SAMPLE_SOURCE_MISSING: 500,
|
|
49
|
+
SAMPLE_NO_DEFAULT_MODEL: 422,
|
|
50
|
+
SAMPLE_AGENTS_UNAVAILABLE: 503,
|
|
51
|
+
};
|
|
52
|
+
const RUN_ERROR_STATUS = {
|
|
53
|
+
EVAL_INVALID: 422,
|
|
54
|
+
EVAL_RUN_ACTIVE: 409,
|
|
55
|
+
EVAL_RUN_NOT_FOUND: 404,
|
|
56
|
+
EVAL_REAL_TOOLS_NOT_CONFIRMED: 422,
|
|
57
|
+
EVAL_ENGINE_UNAVAILABLE: 503,
|
|
58
|
+
EVAL_NODE_TOO_OLD: 503,
|
|
59
|
+
EVAL_TAGS_ON_PASSED_RESULT: 400,
|
|
60
|
+
};
|
|
61
|
+
function sendError(c, status, code, message, extra = {}) {
|
|
62
|
+
c.json(c.res, status, { error: message, code, ...extra });
|
|
63
|
+
}
|
|
64
|
+
async function readBody(c) {
|
|
65
|
+
let body;
|
|
66
|
+
try {
|
|
67
|
+
body = await c.readJsonBody(c.req);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw bad("Invalid JSON body");
|
|
71
|
+
}
|
|
72
|
+
if (!isRecord(body))
|
|
73
|
+
throw bad("Body must be a JSON object");
|
|
74
|
+
return body;
|
|
75
|
+
}
|
|
76
|
+
function optString(body, key) {
|
|
77
|
+
const value = body[key];
|
|
78
|
+
if (value === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (typeof value !== "string" || value === "")
|
|
81
|
+
throw bad(`"${key}" must be a non-empty string`);
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function intParam(url, name, fallback, min, max) {
|
|
85
|
+
const raw = url.searchParams.get(name);
|
|
86
|
+
if (raw === null || raw === "")
|
|
87
|
+
return fallback;
|
|
88
|
+
const n = Number(raw);
|
|
89
|
+
if (!Number.isInteger(n) || n < min)
|
|
90
|
+
throw bad(`"${name}" must be an integer >= ${min}`);
|
|
91
|
+
return Math.min(n, max);
|
|
92
|
+
}
|
|
93
|
+
function listParam(url, name) {
|
|
94
|
+
const values = url.searchParams
|
|
95
|
+
.getAll(name)
|
|
96
|
+
.flatMap((v) => v.split(","))
|
|
97
|
+
.map((v) => v.trim())
|
|
98
|
+
.filter((v) => v !== "");
|
|
99
|
+
return values.length > 0 ? values : undefined;
|
|
100
|
+
}
|
|
101
|
+
async function projectRootFor(c, wire, write = false) {
|
|
102
|
+
const options = { store: c.store, bootCwd: c.cwd };
|
|
103
|
+
if (write)
|
|
104
|
+
return (await resolveWritableCatalogRoot(options, wire)).wireRoot.path;
|
|
105
|
+
if (wire === undefined) {
|
|
106
|
+
try {
|
|
107
|
+
return (await resolveCatalogStartInput(options, wire)).wireRoot.path;
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (!(err instanceof CatalogPathError))
|
|
111
|
+
throw err;
|
|
112
|
+
return (await resolveWritableCatalogRoot(options)).wireRoot.path;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return (await resolveCatalogStartInput(options, wire)).wireRoot.path;
|
|
116
|
+
}
|
|
117
|
+
function rootOptions(c) {
|
|
118
|
+
return { store: c.store, bootCwd: c.cwd };
|
|
119
|
+
}
|
|
120
|
+
async function evalRootFor(c, wire, id, write = false) {
|
|
121
|
+
return locateEval(rootOptions(c), wire, id, write);
|
|
122
|
+
}
|
|
123
|
+
function wireFromQuery(c) {
|
|
124
|
+
return c.url.searchParams.get("project_root") ?? undefined;
|
|
125
|
+
}
|
|
126
|
+
function wireFromBody(body) {
|
|
127
|
+
const value = body.project_root;
|
|
128
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
129
|
+
}
|
|
130
|
+
async function exists(abs) {
|
|
131
|
+
try {
|
|
132
|
+
await stat(abs);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function agentsOf(c) {
|
|
140
|
+
return c.agents?.();
|
|
141
|
+
}
|
|
142
|
+
function agentsInput(c) {
|
|
143
|
+
const agents = agentsOf(c);
|
|
144
|
+
return agents ? { agents } : {};
|
|
145
|
+
}
|
|
146
|
+
function engineCheck(c) {
|
|
147
|
+
engineCheckStatus(c.engineStatus());
|
|
148
|
+
}
|
|
149
|
+
function catalogOptions(c) {
|
|
150
|
+
return c.isKnownModel ? { isKnownModel: c.isKnownModel } : {};
|
|
151
|
+
}
|
|
152
|
+
async function evalDetail(c, projectRoot, absPath) {
|
|
153
|
+
return buildEvalDetail({
|
|
154
|
+
store: c.store,
|
|
155
|
+
projectRoot,
|
|
156
|
+
absPath,
|
|
157
|
+
...(c.isKnownModel ? { isKnownModel: c.isKnownModel } : {}),
|
|
158
|
+
...agentsInput(c),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async function planFindings(c, projectRoot, evalPath, evalText) {
|
|
162
|
+
return planFindingsShared({
|
|
163
|
+
projectRoot,
|
|
164
|
+
evalPath,
|
|
165
|
+
evalText,
|
|
166
|
+
...(c.isKnownModel ? { isKnownModel: c.isKnownModel } : {}),
|
|
167
|
+
...agentsInput(c),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function targetLabelFilter(items, projectRoot, url) {
|
|
171
|
+
const pipeline = url.searchParams.get("target_pipeline");
|
|
172
|
+
const stage = url.searchParams.get("target_stage");
|
|
173
|
+
const file = url.searchParams.get("target_file");
|
|
174
|
+
const same = (a, b) => a !== undefined && path.resolve(projectRoot, a) === path.resolve(projectRoot, b);
|
|
175
|
+
const kinds = filterEvalListItems(items, {
|
|
176
|
+
target_kind: url.searchParams.get("target_kind"),
|
|
177
|
+
agent: url.searchParams.get("agent"),
|
|
178
|
+
});
|
|
179
|
+
return kinds.filter((item) => {
|
|
180
|
+
const t = item.target.kind === "stage" ? item.target : undefined;
|
|
181
|
+
if (pipeline !== null && !same(t?.pipeline_path, pipeline))
|
|
182
|
+
return false;
|
|
183
|
+
if (stage !== null && t?.stage_id !== stage)
|
|
184
|
+
return false;
|
|
185
|
+
if (file !== null && !same(t?.file, file))
|
|
186
|
+
return false;
|
|
187
|
+
return true;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async function writeRoots(c) {
|
|
191
|
+
const projectRoot = await projectRootFor(c, wireFromQuery(c), true);
|
|
192
|
+
const manifest = await manifestForWrite(projectRoot);
|
|
193
|
+
const [evals, datasets] = await Promise.all([
|
|
194
|
+
resolveWriteDir(projectRoot, manifest, "evals"),
|
|
195
|
+
resolveWriteDir(projectRoot, manifest, "datasets"),
|
|
196
|
+
]);
|
|
197
|
+
return { project_root: projectRoot, evals_dir: evals.relDir, datasets_dir: datasets.relDir };
|
|
198
|
+
}
|
|
199
|
+
async function handleEvalList(c) {
|
|
200
|
+
const aggregated = await loadAggregatedEvalList({
|
|
201
|
+
options: rootOptions(c),
|
|
202
|
+
wire: wireFromQuery(c),
|
|
203
|
+
now: c.now?.() ?? new Date(),
|
|
204
|
+
...(c.isKnownModel ? { isKnownModel: c.isKnownModel } : {}),
|
|
205
|
+
...agentsInput(c),
|
|
206
|
+
});
|
|
207
|
+
const q = c.url.searchParams.get("q")?.trim().toLowerCase();
|
|
208
|
+
const filtered = aggregated.groups
|
|
209
|
+
.flatMap((g) => targetLabelFilter(g.items, g.root.path, c.url))
|
|
210
|
+
.filter((item) => {
|
|
211
|
+
if (q === undefined || q === "")
|
|
212
|
+
return true;
|
|
213
|
+
const dataset = path.basename(item.dataset_path).replace(/\.dataset\.(jsonl|json|csv)$/, "");
|
|
214
|
+
return [item.id, item.name, item.target.kind === "agent" ? item.target.agent_id : (item.target.stage_id ?? ""), dataset].some((v) => v.toLowerCase().includes(q));
|
|
215
|
+
});
|
|
216
|
+
c.json(c.res, 200, {
|
|
217
|
+
items: filtered,
|
|
218
|
+
next_cursor: null,
|
|
219
|
+
banner: aggregated.banner,
|
|
220
|
+
...(await writeRoots(c).then((roots) => ({ write_roots: roots }), () => ({}))),
|
|
221
|
+
footer: {
|
|
222
|
+
evals: aggregated.items.length,
|
|
223
|
+
datasets: aggregated.datasets,
|
|
224
|
+
cost_last_7_days: aggregated.cost,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function optDatasetCases(body) {
|
|
229
|
+
const value = body.dataset_cases;
|
|
230
|
+
if (value === undefined)
|
|
231
|
+
return undefined;
|
|
232
|
+
if (!Array.isArray(value))
|
|
233
|
+
throw bad(`"dataset_cases" must be an array`);
|
|
234
|
+
return withAssignedIds(value);
|
|
235
|
+
}
|
|
236
|
+
function withAssignedIds(cases) {
|
|
237
|
+
const isObject = (c) => c !== null && typeof c === "object" && !Array.isArray(c);
|
|
238
|
+
const hasId = (c) => isObject(c) && typeof c.id === "string" && c.id !== "";
|
|
239
|
+
const existing = cases.filter(hasId).map((c) => c.id);
|
|
240
|
+
let next = nextIdNumber(existing);
|
|
241
|
+
return cases.map((c) => (isObject(c) && !hasId(c) ? { ...c, id: formatId(next++) } : c));
|
|
242
|
+
}
|
|
243
|
+
function parseDefinitionOrYaml(body) {
|
|
244
|
+
const hasDefinition = body.definition !== undefined;
|
|
245
|
+
const hasYaml = body.yaml !== undefined;
|
|
246
|
+
if (hasDefinition === hasYaml)
|
|
247
|
+
throw bad("Provide exactly one of definition or yaml");
|
|
248
|
+
if (hasYaml) {
|
|
249
|
+
if (typeof body.yaml !== "string")
|
|
250
|
+
throw bad(`"yaml" must be a string`);
|
|
251
|
+
return { yaml: body.yaml };
|
|
252
|
+
}
|
|
253
|
+
if (!isRecord(body.definition))
|
|
254
|
+
throw bad(`"definition" must be an object`);
|
|
255
|
+
return { definition: body.definition };
|
|
256
|
+
}
|
|
257
|
+
async function handleEvalValidate(c) {
|
|
258
|
+
const body = await readBody(c);
|
|
259
|
+
const input = parseDefinitionOrYaml(body);
|
|
260
|
+
const projectRoot = await projectRootFor(c, wireFromBody(body));
|
|
261
|
+
const manifest = await manifestForWrite(projectRoot);
|
|
262
|
+
const suppliedPath = optString(body, "path");
|
|
263
|
+
const datasetCases = optDatasetCases(body);
|
|
264
|
+
let evalAbs;
|
|
265
|
+
if (suppliedPath !== undefined) {
|
|
266
|
+
evalAbs = path.resolve(projectRoot, suppliedPath);
|
|
267
|
+
assertInsideProject(projectRoot, evalAbs, "eval path");
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const rawId = isRecord(input.definition) ? input.definition.id : undefined;
|
|
271
|
+
const dir = await resolveWriteDir(projectRoot, manifest, "evals");
|
|
272
|
+
const fileName = typeof rawId === "string" && ID_RE.test(rawId) ? `${rawId}.eval.yaml` : "new.eval.yaml";
|
|
273
|
+
evalAbs = path.join(dir.absDir, fileName);
|
|
274
|
+
}
|
|
275
|
+
let derived;
|
|
276
|
+
if (input.yaml !== undefined) {
|
|
277
|
+
derived = input.yaml.endsWith("\n") ? input.yaml : `${input.yaml}\n`;
|
|
278
|
+
}
|
|
279
|
+
else if (await exists(evalAbs)) {
|
|
280
|
+
derived = previewEvalUpdate(await readFile(evalAbs, "utf8"), input.definition);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
derived = renderEvalCreateYaml(input.definition);
|
|
284
|
+
}
|
|
285
|
+
const { findings, parsed } = await validateEvalText({
|
|
286
|
+
projectRoot,
|
|
287
|
+
evalAbs,
|
|
288
|
+
derived,
|
|
289
|
+
...(datasetCases !== undefined ? { datasetCases } : {}),
|
|
290
|
+
...(c.isKnownModel ? { isKnownModel: c.isKnownModel } : {}),
|
|
291
|
+
...agentsInput(c),
|
|
292
|
+
});
|
|
293
|
+
c.json(c.res, 200, {
|
|
294
|
+
findings,
|
|
295
|
+
derived_yaml: derived,
|
|
296
|
+
definition: parsed.ok ? parsed.value.file : null,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async function handleEvalCreate(c) {
|
|
300
|
+
const body = await readBody(c);
|
|
301
|
+
if (!isRecord(body.definition))
|
|
302
|
+
throw bad(`"definition" must be an object`);
|
|
303
|
+
const definition = body.definition;
|
|
304
|
+
const directory = optString(body, "directory");
|
|
305
|
+
const projectRoot = await projectRootFor(c, wireFromBody(body), true);
|
|
306
|
+
const manifest = await manifestForWrite(projectRoot);
|
|
307
|
+
const dir = await resolveWriteDir(projectRoot, manifest, "evals", directory);
|
|
308
|
+
const rawId = definition.id;
|
|
309
|
+
const fileName = typeof rawId === "string" && ID_RE.test(rawId) ? `${rawId}.eval.yaml` : "new.eval.yaml";
|
|
310
|
+
const absPath = path.join(dir.absDir, fileName);
|
|
311
|
+
assertInsideProject(projectRoot, absPath, "eval path");
|
|
312
|
+
if (typeof rawId === "string" && (await findEvalFile(projectRoot, rawId, manifest)) !== undefined) {
|
|
313
|
+
throw new EvalsWriteError("EVAL_EXISTS", `Eval "${rawId}" already exists`);
|
|
314
|
+
}
|
|
315
|
+
const text = renderEvalCreateYaml(definition);
|
|
316
|
+
const checked = await planFindings(c, projectRoot, absPath, text);
|
|
317
|
+
if (!checked.ok) {
|
|
318
|
+
audit(c, "eval_create", "error", undefined, "EVAL_INVALID");
|
|
319
|
+
throw new EvalApiError(422, "EVAL_INVALID", "Eval definition is invalid", { findings: checked.findings });
|
|
320
|
+
}
|
|
321
|
+
const written = await createEvalFile({ projectRoot, definition, ...(directory !== undefined ? { directory } : {}) });
|
|
322
|
+
audit(c, "eval_create", "ok");
|
|
323
|
+
c.json(c.res, 201, await evalDetail(c, projectRoot, written.path));
|
|
324
|
+
}
|
|
325
|
+
async function handleEvalGet(c, id) {
|
|
326
|
+
const { projectRoot, ref } = await evalRootFor(c, wireFromQuery(c), id);
|
|
327
|
+
c.json(c.res, 200, await evalDetail(c, projectRoot, ref.absPath));
|
|
328
|
+
}
|
|
329
|
+
async function handleEvalUpdate(c, id) {
|
|
330
|
+
const body = await readBody(c);
|
|
331
|
+
const input = parseDefinitionOrYaml(body);
|
|
332
|
+
const baseHash = body.base_hash;
|
|
333
|
+
if (typeof baseHash !== "string" || baseHash === "")
|
|
334
|
+
throw bad(`"base_hash" is required`);
|
|
335
|
+
const { projectRoot, ref } = await evalRootFor(c, wireFromBody(body), id, true);
|
|
336
|
+
const current = await readFile(ref.absPath);
|
|
337
|
+
const currentHash = contentHash(current);
|
|
338
|
+
if (currentHash !== baseHash)
|
|
339
|
+
throw new FileChangedError(currentHash);
|
|
340
|
+
const text = input.yaml !== undefined
|
|
341
|
+
? input.yaml.endsWith("\n")
|
|
342
|
+
? input.yaml
|
|
343
|
+
: `${input.yaml}\n`
|
|
344
|
+
: previewEvalUpdate(current.toString("utf8"), input.definition);
|
|
345
|
+
const checked = await planFindings(c, projectRoot, ref.absPath, text);
|
|
346
|
+
if (!checked.ok) {
|
|
347
|
+
audit(c, "eval_update", "error", undefined, "EVAL_INVALID");
|
|
348
|
+
throw new EvalApiError(422, "EVAL_INVALID", "Eval definition is invalid", { findings: checked.findings });
|
|
349
|
+
}
|
|
350
|
+
await updateEvalFile({
|
|
351
|
+
projectRoot,
|
|
352
|
+
id,
|
|
353
|
+
baseHash,
|
|
354
|
+
...(input.yaml !== undefined ? { yaml: input.yaml } : { definition: input.definition }),
|
|
355
|
+
});
|
|
356
|
+
audit(c, "eval_update", "ok");
|
|
357
|
+
c.json(c.res, 200, await evalDetail(c, projectRoot, ref.absPath));
|
|
358
|
+
}
|
|
359
|
+
async function handleSampleInstall(c) {
|
|
360
|
+
const body = await readOptionalBody(c);
|
|
361
|
+
const projectRoot = await projectRootFor(c, wireFromBody(body), true);
|
|
362
|
+
try {
|
|
363
|
+
const kind = body.kind;
|
|
364
|
+
if (kind !== undefined && kind !== "stage" && kind !== "agent")
|
|
365
|
+
throw bad(`"kind" must be "stage" or "agent"`);
|
|
366
|
+
const agents = agentsOf(c);
|
|
367
|
+
const samplePort = agents?.create !== undefined && agents.setEnabled !== undefined
|
|
368
|
+
? {
|
|
369
|
+
get: agents.get.bind(agents),
|
|
370
|
+
create: agents.create.bind(agents),
|
|
371
|
+
setEnabled: agents.setEnabled.bind(agents),
|
|
372
|
+
}
|
|
373
|
+
: undefined;
|
|
374
|
+
const result = await installEvalSample({
|
|
375
|
+
projectRoot,
|
|
376
|
+
...(kind === "agent" ? { kind } : {}),
|
|
377
|
+
...(samplePort ? { agents: samplePort } : {}),
|
|
378
|
+
...(c.sampleSourceDir !== undefined ? { sourceDir: c.sampleSourceDir } : {}),
|
|
379
|
+
});
|
|
380
|
+
audit(c, "eval_sample_install", "ok");
|
|
381
|
+
c.json(c.res, 201, { eval_id: result.evalId });
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
if (err instanceof EvalsWriteError)
|
|
385
|
+
audit(c, "eval_sample_install", "error", undefined, err.code);
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function tryCaseFrom(value) {
|
|
390
|
+
if (!isRecord(value) || !isRecord(value.input))
|
|
391
|
+
throw bad(`"case" must be an object with an input object`);
|
|
392
|
+
return value;
|
|
393
|
+
}
|
|
394
|
+
function tryResultToWire(result) {
|
|
395
|
+
const r = result.result;
|
|
396
|
+
return {
|
|
397
|
+
result: {
|
|
398
|
+
case_id: r.caseId,
|
|
399
|
+
repeat_index: 0,
|
|
400
|
+
status: r.status,
|
|
401
|
+
error_code: r.errorCode,
|
|
402
|
+
error_message: r.errorMessage,
|
|
403
|
+
stage_run_id: r.stageRunId,
|
|
404
|
+
subject_ref: null,
|
|
405
|
+
turns: null,
|
|
406
|
+
tool_call_count: null,
|
|
407
|
+
output: r.output,
|
|
408
|
+
latency_ms: r.latencyMs,
|
|
409
|
+
cost_usd: r.costUsd,
|
|
410
|
+
tokens_in: r.tokensIn,
|
|
411
|
+
tokens_out: r.tokensOut,
|
|
412
|
+
score: r.score,
|
|
413
|
+
passed: r.passed,
|
|
414
|
+
grades: r.grades.map((g) => ({
|
|
415
|
+
grader_id: g.graderId,
|
|
416
|
+
grader_type: g.graderType,
|
|
417
|
+
status: g.status,
|
|
418
|
+
passed: g.passed,
|
|
419
|
+
score: g.score,
|
|
420
|
+
raw_value: g.rawValue,
|
|
421
|
+
reason: g.reason,
|
|
422
|
+
details: g.details,
|
|
423
|
+
error_code: g.errorCode,
|
|
424
|
+
judge_model: null,
|
|
425
|
+
judge_cost_usd: g.judgeCostUsd,
|
|
426
|
+
judge_tokens_in: null,
|
|
427
|
+
judge_tokens_out: null,
|
|
428
|
+
})),
|
|
429
|
+
budget_warnings: r.budgetWarnings,
|
|
430
|
+
review: null,
|
|
431
|
+
tags: [],
|
|
432
|
+
},
|
|
433
|
+
stage_run_id: result.stageRunId,
|
|
434
|
+
cost_usd: result.costUsd,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async function handleTry(c) {
|
|
438
|
+
const body = await readBody(c);
|
|
439
|
+
const evalId = optString(body, "eval_id");
|
|
440
|
+
const hasDefinition = body.definition !== undefined;
|
|
441
|
+
if ((evalId === undefined) === !hasDefinition)
|
|
442
|
+
throw bad("Provide exactly one of eval_id or definition");
|
|
443
|
+
const caseId = optString(body, "case_id");
|
|
444
|
+
const datasetCases = optDatasetCases(body);
|
|
445
|
+
const hasCase = body.case !== undefined;
|
|
446
|
+
if ((caseId === undefined) === !hasCase)
|
|
447
|
+
throw bad("Provide exactly one of case_id or case");
|
|
448
|
+
const located = evalId !== undefined ? await evalRootFor(c, wireFromBody(body), evalId) : undefined;
|
|
449
|
+
const projectRoot = located?.projectRoot ?? (await projectRootFor(c, wireFromBody(body)));
|
|
450
|
+
const manifest = await manifestForWrite(projectRoot);
|
|
451
|
+
let evalPath;
|
|
452
|
+
let definition;
|
|
453
|
+
if (located !== undefined) {
|
|
454
|
+
evalPath = located.ref.absPath;
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
if (!isRecord(body.definition))
|
|
458
|
+
throw bad(`"definition" must be an object`);
|
|
459
|
+
definition = body.definition;
|
|
460
|
+
const rawId = definition.id;
|
|
461
|
+
const dir = await resolveWriteDir(projectRoot, manifest, "evals");
|
|
462
|
+
evalPath = path.join(dir.absDir, typeof rawId === "string" && ID_RE.test(rawId) ? `${rawId}.eval.yaml` : "new.eval.yaml");
|
|
463
|
+
}
|
|
464
|
+
if (body.allow_real_tools !== undefined && typeof body.allow_real_tools !== "boolean") {
|
|
465
|
+
throw bad(`"allow_real_tools" must be a boolean`);
|
|
466
|
+
}
|
|
467
|
+
engineCheck(c);
|
|
468
|
+
const tried = await c.evalRunManager.tryOne({
|
|
469
|
+
projectRoot,
|
|
470
|
+
evalPath,
|
|
471
|
+
...(definition !== undefined ? { definition } : {}),
|
|
472
|
+
...(datasetCases !== undefined ? { datasetCases } : {}),
|
|
473
|
+
...(caseId !== undefined ? { caseId } : { case: tryCaseFrom(body.case) }),
|
|
474
|
+
...(body.allow_real_tools === true ? { allowRealTools: true } : {}),
|
|
475
|
+
});
|
|
476
|
+
audit(c, "eval_try", "ok");
|
|
477
|
+
c.json(c.res, 200, tryResultToWire(tried));
|
|
478
|
+
}
|
|
479
|
+
function startOverrides(body) {
|
|
480
|
+
const overrides = {};
|
|
481
|
+
if (body.repeats !== undefined) {
|
|
482
|
+
if (typeof body.repeats !== "number")
|
|
483
|
+
throw bad(`"repeats" must be a number`);
|
|
484
|
+
overrides.repeats = body.repeats;
|
|
485
|
+
}
|
|
486
|
+
const judge = optString(body, "judge");
|
|
487
|
+
if (judge !== undefined)
|
|
488
|
+
overrides.judge = judge;
|
|
489
|
+
const model = optString(body, "model");
|
|
490
|
+
if (model !== undefined)
|
|
491
|
+
overrides.model = model;
|
|
492
|
+
if (body.max_cost_usd !== undefined) {
|
|
493
|
+
if (typeof body.max_cost_usd !== "number")
|
|
494
|
+
throw bad(`"max_cost_usd" must be a number`);
|
|
495
|
+
overrides.max_cost_usd = body.max_cost_usd;
|
|
496
|
+
}
|
|
497
|
+
const caseIds = optStringArray(body, "case_ids");
|
|
498
|
+
if (caseIds !== undefined)
|
|
499
|
+
overrides.case_ids = caseIds;
|
|
500
|
+
const tags = optStringArray(body, "tags");
|
|
501
|
+
if (tags !== undefined)
|
|
502
|
+
overrides.tags = tags;
|
|
503
|
+
const failedFrom = optString(body, "only_failed_from");
|
|
504
|
+
if (failedFrom !== undefined)
|
|
505
|
+
overrides.only_failed_from = failedFrom;
|
|
506
|
+
const userModel = optString(body, "user_model");
|
|
507
|
+
if (userModel !== undefined)
|
|
508
|
+
overrides.user_model = userModel;
|
|
509
|
+
const tools = optString(body, "tools");
|
|
510
|
+
if (tools !== undefined)
|
|
511
|
+
overrides.tools = tools;
|
|
512
|
+
const approvals = optString(body, "approvals");
|
|
513
|
+
if (approvals !== undefined)
|
|
514
|
+
overrides.approvals = approvals;
|
|
515
|
+
if (body.allow_real_tools !== undefined) {
|
|
516
|
+
if (typeof body.allow_real_tools !== "boolean")
|
|
517
|
+
throw bad(`"allow_real_tools" must be a boolean`);
|
|
518
|
+
if (body.allow_real_tools)
|
|
519
|
+
overrides.allow_real_tools = true;
|
|
520
|
+
}
|
|
521
|
+
return overrides;
|
|
522
|
+
}
|
|
523
|
+
function queryOverrides(url) {
|
|
524
|
+
const body = {};
|
|
525
|
+
const num = (name) => {
|
|
526
|
+
const raw = url.searchParams.get(name);
|
|
527
|
+
if (raw === null || raw === "")
|
|
528
|
+
return undefined;
|
|
529
|
+
const n = Number(raw);
|
|
530
|
+
if (!Number.isFinite(n))
|
|
531
|
+
throw bad(`"${name}" must be a number`);
|
|
532
|
+
return n;
|
|
533
|
+
};
|
|
534
|
+
const repeats = num("repeats");
|
|
535
|
+
if (repeats !== undefined)
|
|
536
|
+
body.repeats = repeats;
|
|
537
|
+
const maxCost = num("max_cost_usd");
|
|
538
|
+
if (maxCost !== undefined)
|
|
539
|
+
body.max_cost_usd = maxCost;
|
|
540
|
+
for (const key of ["judge", "model", "only_failed_from", "user_model", "tools", "approvals"]) {
|
|
541
|
+
const value = url.searchParams.get(key);
|
|
542
|
+
if (value !== null && value !== "")
|
|
543
|
+
body[key] = value;
|
|
544
|
+
}
|
|
545
|
+
const caseIds = listParam(url, "case_ids");
|
|
546
|
+
if (caseIds)
|
|
547
|
+
body.case_ids = caseIds;
|
|
548
|
+
const tags = listParam(url, "tags");
|
|
549
|
+
if (tags)
|
|
550
|
+
body.tags = tags;
|
|
551
|
+
return startOverrides(body);
|
|
552
|
+
}
|
|
553
|
+
async function failedCaseIdsOf(c, projectRoot, evalPath, evalRunId) {
|
|
554
|
+
const run = await c.store.getEvalRun(evalRunId);
|
|
555
|
+
if (run === null || run.projectRoot !== projectRoot)
|
|
556
|
+
return undefined;
|
|
557
|
+
if (run.evalPath !== rel(projectRoot, path.resolve(projectRoot, evalPath)))
|
|
558
|
+
return undefined;
|
|
559
|
+
const results = await c.store.listEvalResults(evalRunId);
|
|
560
|
+
const failed = results.filter((r) => ["completed", "stage_failed", "needs_input", "error"].includes(r.status) && !r.passed);
|
|
561
|
+
return [...new Set(failed.map((r) => r.caseId))];
|
|
562
|
+
}
|
|
563
|
+
async function handleRunStart(c, id) {
|
|
564
|
+
const body = await readBody(c);
|
|
565
|
+
const overrides = startOverrides(body);
|
|
566
|
+
const label = optString(body, "label");
|
|
567
|
+
if (label !== undefined && label.length > EVAL_LIMITS.labelMaxChars) {
|
|
568
|
+
throw bad(`"label" must be at most ${EVAL_LIMITS.labelMaxChars} characters`);
|
|
569
|
+
}
|
|
570
|
+
const startedVia = body.started_via === undefined ? "console" : body.started_via;
|
|
571
|
+
if (typeof startedVia !== "string" || !EVAL_STARTED_VIA.includes(startedVia)) {
|
|
572
|
+
throw bad(`"started_via" must be one of ${EVAL_STARTED_VIA.join(", ")}`);
|
|
573
|
+
}
|
|
574
|
+
const { projectRoot, ref } = await evalRootFor(c, wireFromBody(body), id);
|
|
575
|
+
const startInput = {
|
|
576
|
+
projectRoot,
|
|
577
|
+
evalPath: ref.absPath,
|
|
578
|
+
overrides,
|
|
579
|
+
startedVia: startedVia,
|
|
580
|
+
...(label !== undefined ? { label } : {}),
|
|
581
|
+
...(callerIdFromRequestAuth() !== null ? { callerId: callerIdFromRequestAuth() } : {}),
|
|
582
|
+
};
|
|
583
|
+
try {
|
|
584
|
+
await c.evalRunManager.preflightAgentStart(startInput);
|
|
585
|
+
engineCheck(c);
|
|
586
|
+
const started = await c.evalRunManager.start(startInput);
|
|
587
|
+
audit(c, "eval_run_start", "ok", started.evalRunId);
|
|
588
|
+
c.json(c.res, 202, { eval_run_id: started.evalRunId, status: "queued", run_number: started.runNumber });
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
audit(c, "eval_run_start", "error", undefined, err instanceof EvalRunError ? err.code : "internal_error");
|
|
592
|
+
throw err;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async function recentStageCosts(c, target) {
|
|
596
|
+
if (target.stageId === undefined)
|
|
597
|
+
return [];
|
|
598
|
+
const pipelines = [`standalone-${target.stageId}`];
|
|
599
|
+
if (target.pipelinePath !== undefined)
|
|
600
|
+
pipelines.push(target.pipelinePath);
|
|
601
|
+
const summaries = (await Promise.all(pipelines.map((pipeline) => c.store.listRuns({ pipeline })))).flat();
|
|
602
|
+
summaries.sort((a, b) => (a.created_at < b.created_at ? 1 : a.created_at > b.created_at ? -1 : 0));
|
|
603
|
+
const costs = [];
|
|
604
|
+
for (const summary of summaries) {
|
|
605
|
+
for (const stage of summary.stages) {
|
|
606
|
+
if (stage.id === target.stageId && stage.cost_usd !== undefined)
|
|
607
|
+
costs.push(stage.cost_usd);
|
|
608
|
+
}
|
|
609
|
+
if (costs.length >= 20)
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
return costs.slice(0, 20);
|
|
613
|
+
}
|
|
614
|
+
async function handleRunEstimate(c, id) {
|
|
615
|
+
const { projectRoot, ref } = await evalRootFor(c, wireFromQuery(c), id);
|
|
616
|
+
const overrides = queryOverrides(c.url);
|
|
617
|
+
const built = await buildEvalPlan({
|
|
618
|
+
projectRoot,
|
|
619
|
+
evalPath: ref.absPath,
|
|
620
|
+
overrides,
|
|
621
|
+
evalRunId: "er_estimate",
|
|
622
|
+
engine: { name: ENGINE_NAME, version: PINNED_PROMPTFOO_VERSION, nodeVersion: process.versions.node },
|
|
623
|
+
failedCaseIdsOf: (runId) => failedCaseIdsOf(c, projectRoot, ref.absPath, runId),
|
|
624
|
+
...(c.isKnownModel ? { isKnownModel: c.isKnownModel } : {}),
|
|
625
|
+
...agentsInput(c),
|
|
626
|
+
});
|
|
627
|
+
if (!built.ok)
|
|
628
|
+
throw new EvalApiError(422, "EVAL_INVALID", "Eval is invalid", { findings: built.findings });
|
|
629
|
+
const { plan } = built;
|
|
630
|
+
const last = (await completedRunsNewestFirst(c.store, projectRoot, plan.evalId, 1))[0];
|
|
631
|
+
let previous = null;
|
|
632
|
+
if (last) {
|
|
633
|
+
const results = await c.store.listEvalResults(last.evalRunId);
|
|
634
|
+
const stats = statsOfRun(last, results);
|
|
635
|
+
previous = {
|
|
636
|
+
runNumber: last.runNumber,
|
|
637
|
+
costStage: stats.cost_stage,
|
|
638
|
+
costJudge: stats.cost_judge,
|
|
639
|
+
resultsExecuted: stats.results_executed,
|
|
640
|
+
rubricGradesGraded: results
|
|
641
|
+
.flatMap((r) => r.grades)
|
|
642
|
+
.filter((g) => (g.graderType === "rubric" || g.graderType === "goal_reached") && g.status === "graded").length,
|
|
643
|
+
wallSeconds: last.startedAt !== null && last.finishedAt !== null
|
|
644
|
+
? Math.max(0, (Date.parse(last.finishedAt) - Date.parse(last.startedAt)) / 1000)
|
|
645
|
+
: null,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
await sendEstimate(c, plan, previous);
|
|
649
|
+
}
|
|
650
|
+
async function agentEstimateBlock(c, plan) {
|
|
651
|
+
const conversation = plan.conversation ?? resolveConversation(undefined);
|
|
652
|
+
let turns = 0;
|
|
653
|
+
let simulatorCalls = 0;
|
|
654
|
+
for (const planCase of plan.cases) {
|
|
655
|
+
const mode = planCase.input.mode === "script" || planCase.input.mode === "simulated" ? planCase.input.mode : conversation.mode;
|
|
656
|
+
if (mode === "simulated") {
|
|
657
|
+
const t = Math.min(conversation.maxTurns, AGENT_ESTIMATE_ASSUMPTIONS.maxTurnsCap);
|
|
658
|
+
turns += t;
|
|
659
|
+
simulatorCalls += Math.max(0, t - 1);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
const script = Array.isArray(planCase.input.script) ? planCase.input.script.length : 1;
|
|
663
|
+
turns += Math.min(script, conversation.maxTurns);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const count = Math.max(1, plan.cases.length);
|
|
667
|
+
const judge = plan.graders.find((g) => g.type === "rubric" || g.type === "goal_reached");
|
|
668
|
+
const judgeModel = judge !== undefined && "judge" in judge && judge.judge !== "" ? judge.judge : undefined;
|
|
669
|
+
const wanted = [plan.stageModel, plan.userModel ?? undefined, judgeModel].filter((id) => typeof id === "string" && id !== "");
|
|
670
|
+
const prices = (await c.loadPrices?.([...new Set(wanted)], plan.projectRoot).catch(() => undefined)) ?? new Map();
|
|
671
|
+
return {
|
|
672
|
+
turnsPerCase: turns / count,
|
|
673
|
+
simulatorCallsPerCase: simulatorCalls / count,
|
|
674
|
+
judgeTranscriptTokens: AGENT_ESTIMATE_ASSUMPTIONS.judgeCall.tokensIn,
|
|
675
|
+
agentPrice: prices.get(plan.stageModel) ?? null,
|
|
676
|
+
simulatorPrice: plan.userModel != null ? (prices.get(plan.userModel) ?? null) : null,
|
|
677
|
+
judgePrice: judgeModel !== undefined ? (prices.get(judgeModel) ?? null) : null,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
async function sendEstimate(c, plan, previous) {
|
|
681
|
+
const agent = plan.target.kind === "agent" ? await agentEstimateBlock(c, plan) : undefined;
|
|
682
|
+
const estimate = estimateRun({
|
|
683
|
+
resultsTotal: plan.cases.length * plan.repeats,
|
|
684
|
+
rubricGraderCount: plan.graders.filter((g) => g.type === "rubric" || g.type === "goal_reached").length,
|
|
685
|
+
previous,
|
|
686
|
+
recentStageCosts: await recentStageCosts(c, {
|
|
687
|
+
...(plan.target.kind === "stage" && plan.target.pipelinePath !== undefined ? { pipelinePath: plan.target.pipelinePath } : {}),
|
|
688
|
+
...(plan.target.kind === "stage" && plan.target.stageId !== undefined ? { stageId: plan.target.stageId } : {}),
|
|
689
|
+
}),
|
|
690
|
+
...(agent !== undefined ? { agent } : {}),
|
|
691
|
+
});
|
|
692
|
+
c.json(c.res, 200, {
|
|
693
|
+
results_total: plan.cases.length * plan.repeats,
|
|
694
|
+
stage_calls: estimate.stage_calls,
|
|
695
|
+
judge_calls: estimate.judge_calls,
|
|
696
|
+
estimate_usd: estimate.estimate_usd,
|
|
697
|
+
estimate_seconds: estimate.estimate_seconds,
|
|
698
|
+
judge_included: !estimate.judge_excluded,
|
|
699
|
+
based_on_run_number: estimate.based_on_run_number,
|
|
700
|
+
...(agent !== undefined
|
|
701
|
+
? {
|
|
702
|
+
target_kind: "agent",
|
|
703
|
+
simulator_calls: estimate.simulator_calls ?? 0,
|
|
704
|
+
estimate_basis: estimate.estimate_basis ?? null,
|
|
705
|
+
}
|
|
706
|
+
: {}),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
async function handleDraftEstimate(c) {
|
|
710
|
+
const body = await readBody(c);
|
|
711
|
+
if (!isRecord(body.definition))
|
|
712
|
+
throw bad(`"definition" must be an object`);
|
|
713
|
+
const definition = body.definition;
|
|
714
|
+
const datasetCases = optDatasetCases(body);
|
|
715
|
+
const overrides = startOverrides({
|
|
716
|
+
...(body.repeats !== undefined ? { repeats: body.repeats } : {}),
|
|
717
|
+
...(body.user_model !== undefined ? { user_model: body.user_model } : {}),
|
|
718
|
+
...(body.tools !== undefined ? { tools: body.tools } : {}),
|
|
719
|
+
...(body.approvals !== undefined ? { approvals: body.approvals } : {}),
|
|
720
|
+
});
|
|
721
|
+
const projectRoot = await projectRootFor(c, wireFromBody(body));
|
|
722
|
+
const manifest = await manifestForWrite(projectRoot);
|
|
723
|
+
const dir = await resolveWriteDir(projectRoot, manifest, "evals");
|
|
724
|
+
const rawId = definition.id;
|
|
725
|
+
const evalPath = path.join(dir.absDir, typeof rawId === "string" && ID_RE.test(rawId) ? `${rawId}.eval.yaml` : "new.eval.yaml");
|
|
726
|
+
assertInsideProject(projectRoot, evalPath, "eval path");
|
|
727
|
+
const built = await buildEvalPlan({
|
|
728
|
+
projectRoot,
|
|
729
|
+
evalPath,
|
|
730
|
+
evalText: renderEvalCreateYaml(definition),
|
|
731
|
+
...(datasetCases !== undefined ? { datasetCases } : {}),
|
|
732
|
+
overrides,
|
|
733
|
+
evalRunId: "er_estimate",
|
|
734
|
+
engine: { name: ENGINE_NAME, version: PINNED_PROMPTFOO_VERSION, nodeVersion: process.versions.node },
|
|
735
|
+
...(c.isKnownModel ? { isKnownModel: c.isKnownModel } : {}),
|
|
736
|
+
...agentsInput(c),
|
|
737
|
+
});
|
|
738
|
+
if (!built.ok)
|
|
739
|
+
throw new EvalApiError(422, "EVAL_INVALID", "Eval is invalid", { findings: built.findings });
|
|
740
|
+
await sendEstimate(c, built.plan, null);
|
|
741
|
+
}
|
|
742
|
+
async function runDetailResponse(c, row) {
|
|
743
|
+
return buildRunDetail({
|
|
744
|
+
store: c.store,
|
|
745
|
+
row,
|
|
746
|
+
live: c.evalRunManager.getLive(row.evalRunId),
|
|
747
|
+
...agentsInput(c),
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async function handleRunByNumber(c, id, runNumber) {
|
|
751
|
+
const notFound = () => new EvalApiError(404, "EVAL_RUN_NOT_FOUND", `Eval run not found: ${id} #${runNumber}`);
|
|
752
|
+
const wire = wireFromQuery(c);
|
|
753
|
+
const found = [];
|
|
754
|
+
for (const root of await evalScopeRoots(rootOptions(c), wire)) {
|
|
755
|
+
const row = await c.store.getEvalRunByNumber(root.path, id, runNumber);
|
|
756
|
+
if (row !== null)
|
|
757
|
+
found.push({ root, row });
|
|
758
|
+
}
|
|
759
|
+
if (found.length > 1) {
|
|
760
|
+
throw new EvalApiError(409, "EVAL_AMBIGUOUS", `Eval "${id}" exists in more than one project; pass project_root`, {
|
|
761
|
+
project_roots: found.map((f) => f.root.project_root),
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
if (found[0] === undefined)
|
|
765
|
+
throw notFound();
|
|
766
|
+
c.json(c.res, 200, await runDetailResponse(c, found[0].row));
|
|
767
|
+
}
|
|
768
|
+
async function handleRunList(c) {
|
|
769
|
+
const wire = wireFromQuery(c);
|
|
770
|
+
const projectRoot = wire !== undefined ? await projectRootFor(c, wire) : undefined;
|
|
771
|
+
const wireRootOfPath = await wireRootOf(rootOptions(c));
|
|
772
|
+
const evalId = c.url.searchParams.get("eval_id") ?? undefined;
|
|
773
|
+
const status = c.url.searchParams.get("status") ?? undefined;
|
|
774
|
+
const stageRunId = c.url.searchParams.get("stage_run_id") ?? undefined;
|
|
775
|
+
const subjectRef = c.url.searchParams.get("subject_ref") ?? undefined;
|
|
776
|
+
const limit = intParam(c.url, "limit", DEFAULT_LIMIT, 1, MAX_LIMIT);
|
|
777
|
+
const cursor = c.url.searchParams.get("cursor") ?? undefined;
|
|
778
|
+
c.json(c.res, 200, await listEvalRunsPage(c.store, {
|
|
779
|
+
...(projectRoot !== undefined ? { projectRoot } : {}),
|
|
780
|
+
wireRootOf: wireRootOfPath,
|
|
781
|
+
...(evalId !== undefined ? { evalId } : {}),
|
|
782
|
+
...(status !== undefined ? { status } : {}),
|
|
783
|
+
...(stageRunId !== undefined ? { stageRunId } : {}),
|
|
784
|
+
...(subjectRef !== undefined ? { subjectRef } : {}),
|
|
785
|
+
limit,
|
|
786
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
789
|
+
async function handleRunGet(c, id) {
|
|
790
|
+
c.json(c.res, 200, await runDetailResponse(c, await runByIdOr404(c, id)));
|
|
791
|
+
}
|
|
792
|
+
async function handleResultsList(c, id) {
|
|
793
|
+
const grader = c.url.searchParams.get("grader") ?? undefined;
|
|
794
|
+
const q = c.url.searchParams.get("q") ?? undefined;
|
|
795
|
+
const tag = c.url.searchParams.get("tag") ?? undefined;
|
|
796
|
+
const status = c.url.searchParams.get("status") ?? undefined;
|
|
797
|
+
c.json(c.res, 200, await buildResultsPage(c.store, id, {
|
|
798
|
+
...(status !== undefined ? { status } : {}),
|
|
799
|
+
...(grader !== undefined ? { grader } : {}),
|
|
800
|
+
...(q !== undefined ? { q } : {}),
|
|
801
|
+
...(tag !== undefined ? { tag } : {}),
|
|
802
|
+
limit: intParam(c.url, "limit", DEFAULT_LIMIT, 1, MAX_LIMIT),
|
|
803
|
+
cursor: c.url.searchParams.get("cursor"),
|
|
804
|
+
}));
|
|
805
|
+
}
|
|
806
|
+
async function handleResultDetail(c, id, caseId) {
|
|
807
|
+
c.json(c.res, 200, await buildResultDetail(c.store, id, caseId));
|
|
808
|
+
}
|
|
809
|
+
async function handleReviewPut(c, id, caseId, repeat) {
|
|
810
|
+
const body = await readBody(c);
|
|
811
|
+
await runByIdOr404(c, id);
|
|
812
|
+
const patch = {};
|
|
813
|
+
if (body.label !== undefined) {
|
|
814
|
+
if (typeof body.label !== "string" || !EVAL_REVIEW_LABELS.includes(body.label)) {
|
|
815
|
+
throw bad(`"label" must be pass or fail`);
|
|
816
|
+
}
|
|
817
|
+
patch.label = body.label;
|
|
818
|
+
}
|
|
819
|
+
if (body.note !== undefined) {
|
|
820
|
+
if (body.note !== null && (typeof body.note !== "string" || body.note.length > EVAL_LIMITS.reviewNoteMaxChars)) {
|
|
821
|
+
throw bad(`"note" must be a string of at most ${EVAL_LIMITS.reviewNoteMaxChars} characters or null`);
|
|
822
|
+
}
|
|
823
|
+
patch.note = body.note;
|
|
824
|
+
}
|
|
825
|
+
if (body.tags !== undefined) {
|
|
826
|
+
const tags = optStringArray(body, "tags") ?? [];
|
|
827
|
+
if (tags.some((t) => t.length > EVAL_LIMITS.failureTypeNameMaxChars)) {
|
|
828
|
+
throw bad(`Each tag must be at most ${EVAL_LIMITS.failureTypeNameMaxChars} characters`);
|
|
829
|
+
}
|
|
830
|
+
patch.tags = tags;
|
|
831
|
+
}
|
|
832
|
+
try {
|
|
833
|
+
const review = await c.store.putEvalReview(id, caseId, repeat, patch);
|
|
834
|
+
audit(c, "eval_review", "ok", id);
|
|
835
|
+
c.json(c.res, 200, { label: review.label, note: review.note, tags: review.tags });
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
if (err instanceof EvalStoreError)
|
|
839
|
+
throw err;
|
|
840
|
+
if (err instanceof Error)
|
|
841
|
+
throw bad(err.message);
|
|
842
|
+
throw err;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
async function handleFailureTypes(c, id) {
|
|
846
|
+
const root = await locateEvalRoot(rootOptions(c), wireFromQuery(c), id);
|
|
847
|
+
c.json(c.res, 200, { items: await c.store.listFailureTypes(root.path, id), next_cursor: null });
|
|
848
|
+
}
|
|
849
|
+
async function handleCancel(c, id) {
|
|
850
|
+
await runByIdOr404(c, id);
|
|
851
|
+
await c.evalRunManager.cancel(id);
|
|
852
|
+
const { run } = await c.evalRunManager.awaitFinished(id, CANCEL_WAIT_MS);
|
|
853
|
+
audit(c, "eval_run_cancel", "ok", id);
|
|
854
|
+
c.json(c.res, 200, await runDetailResponse(c, run));
|
|
855
|
+
}
|
|
856
|
+
async function handleDelete(c, id) {
|
|
857
|
+
const row = await runByIdOr404(c, id);
|
|
858
|
+
if (row.status === "queued" || row.status === "running") {
|
|
859
|
+
throw new EvalApiError(409, "EVAL_RUN_ACTIVE", "Eval run is still running; cancel it first");
|
|
860
|
+
}
|
|
861
|
+
const sessionIds = row.targetKind === "agent" ? await c.store.listEvalSubjectRefs(id, "agent") : [];
|
|
862
|
+
const stageRunIds = await c.store.deleteEvalRun(id);
|
|
863
|
+
for (const runId of stageRunIds) {
|
|
864
|
+
await c.manager.deleteRun(runId, { channel: "rest" }).catch(() => undefined);
|
|
865
|
+
}
|
|
866
|
+
if (sessionIds.length > 0) {
|
|
867
|
+
await agentsOf(c)?.deleteEvalSessions(row.projectRoot, sessionIds).catch(() => undefined);
|
|
868
|
+
}
|
|
869
|
+
audit(c, "eval_run_delete", "ok", id);
|
|
870
|
+
c.res.writeHead(204);
|
|
871
|
+
c.res.end();
|
|
872
|
+
}
|
|
873
|
+
async function handleCompare(c) {
|
|
874
|
+
const a = c.url.searchParams.get("a");
|
|
875
|
+
const b = c.url.searchParams.get("b");
|
|
876
|
+
if (!a || !b)
|
|
877
|
+
throw bad(`"a" and "b" eval run ids are required`);
|
|
878
|
+
const [left, right] = [await runByIdOr404(c, a), await runByIdOr404(c, b)];
|
|
879
|
+
c.json(c.res, 200, await buildCompare(c.store, left, right));
|
|
880
|
+
}
|
|
881
|
+
async function handleExport(c, id) {
|
|
882
|
+
const row = await runByIdOr404(c, id);
|
|
883
|
+
const what = c.url.searchParams.get("what") ?? "results";
|
|
884
|
+
if (what !== "results" && what !== "config")
|
|
885
|
+
throw bad(`"what" must be results or config`);
|
|
886
|
+
const { filename, body } = await buildExport({ store: c.store, row, what });
|
|
887
|
+
c.res.writeHead(200, {
|
|
888
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
889
|
+
"Content-Disposition": `attachment; filename=${filename}`,
|
|
890
|
+
});
|
|
891
|
+
c.res.end(JSON.stringify(body));
|
|
892
|
+
}
|
|
893
|
+
function handleEngine(c) {
|
|
894
|
+
const status = c.engineStatus();
|
|
895
|
+
c.json(c.res, 200, {
|
|
896
|
+
installed: status.installed,
|
|
897
|
+
version: status.version,
|
|
898
|
+
pinned: status.pinned,
|
|
899
|
+
node: status.node,
|
|
900
|
+
node_ok: status.nodeOk,
|
|
901
|
+
install_command: status.installCommand,
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
async function loadEvalUses(projectRoot) {
|
|
905
|
+
const manifest = await manifestForWrite(projectRoot);
|
|
906
|
+
const refs = await listEvalFiles(manifest);
|
|
907
|
+
const uses = new Map();
|
|
908
|
+
const byId = new Map();
|
|
909
|
+
for (const ref of refs) {
|
|
910
|
+
const outcome = await loadEvalOutcome(ref.absPath, projectRoot);
|
|
911
|
+
if (!outcome.ok)
|
|
912
|
+
continue;
|
|
913
|
+
const use = { id: outcome.value.id, loaded: outcome.value };
|
|
914
|
+
const { datasetPath } = resolveEvalPaths(outcome.value.file, outcome.value.path);
|
|
915
|
+
const key = path.resolve(datasetPath);
|
|
916
|
+
uses.set(key, [...(uses.get(key) ?? []), use]);
|
|
917
|
+
if (!byId.has(use.id))
|
|
918
|
+
byId.set(use.id, use);
|
|
919
|
+
}
|
|
920
|
+
return { uses, byId };
|
|
921
|
+
}
|
|
922
|
+
async function handleDatasetList(c) {
|
|
923
|
+
const items = [];
|
|
924
|
+
for (const root of await evalScopeRoots(rootOptions(c), wireFromQuery(c))) {
|
|
925
|
+
const projectRoot = root.path;
|
|
926
|
+
const manifest = await manifestForWrite(projectRoot);
|
|
927
|
+
const { uses } = await loadEvalUses(projectRoot);
|
|
928
|
+
const files = await listDatasetFiles(manifest);
|
|
929
|
+
const ids = datasetIdsFor(files);
|
|
930
|
+
for (const file of files) {
|
|
931
|
+
const loaded = await loadDatasetFile(file.absPath, { root: projectRoot });
|
|
932
|
+
items.push({
|
|
933
|
+
project_root: root.project_root,
|
|
934
|
+
id: ids.get(file.absPath) ?? datasetStem(path.basename(file.absPath)),
|
|
935
|
+
path: file.relPath,
|
|
936
|
+
format: loaded.format ?? null,
|
|
937
|
+
cases: loaded.cases.length,
|
|
938
|
+
hash: loaded.contentHash,
|
|
939
|
+
editable: loaded.format === "jsonl",
|
|
940
|
+
used_by: (uses.get(path.resolve(file.absPath)) ?? []).map((u) => u.id),
|
|
941
|
+
findings_count: loaded.findings.length,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
c.json(c.res, 200, { items, next_cursor: null });
|
|
946
|
+
}
|
|
947
|
+
async function datasetDetailFor(c, projectRoot, id, absPath, evalId, query) {
|
|
948
|
+
const loaded = await loadDatasetFile(absPath, { root: projectRoot });
|
|
949
|
+
const { uses, byId } = await loadEvalUses(projectRoot);
|
|
950
|
+
const usedBy = (uses.get(path.resolve(absPath)) ?? []).map((u) => u.id);
|
|
951
|
+
const useEval = evalId !== undefined ? byId.get(evalId) : byId.get(usedBy[0] ?? "");
|
|
952
|
+
if (evalId !== undefined && useEval === undefined) {
|
|
953
|
+
throw new EvalApiError(404, "EVAL_NOT_FOUND", `Eval not found: ${evalId}`);
|
|
954
|
+
}
|
|
955
|
+
let inputSchema;
|
|
956
|
+
let agent;
|
|
957
|
+
if (useEval !== undefined) {
|
|
958
|
+
if (useEval.loaded.file.target.kind === "agent") {
|
|
959
|
+
agent = { mode: resolveConversation(useEval.loaded.file.conversation).mode };
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
const target = await resolveEvalTarget(useEval.loaded.file, useEval.loaded.path, projectRoot);
|
|
963
|
+
if (target.ok)
|
|
964
|
+
inputSchema = target.value.inputSchema;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return buildDatasetDetail({
|
|
968
|
+
id,
|
|
969
|
+
loaded,
|
|
970
|
+
inputSchema,
|
|
971
|
+
...(agent !== undefined ? { agent } : {}),
|
|
972
|
+
hasEval: useEval !== undefined,
|
|
973
|
+
usedBy,
|
|
974
|
+
query,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
function datasetQuery(url) {
|
|
978
|
+
const filter = url.searchParams.get("filter") ?? "all";
|
|
979
|
+
if (filter !== "all" && filter !== "invalid" && filter !== "tagged") {
|
|
980
|
+
throw bad(`"filter" must be all, invalid or tagged`);
|
|
981
|
+
}
|
|
982
|
+
const tag = url.searchParams.get("tag") ?? undefined;
|
|
983
|
+
const q = url.searchParams.get("q") ?? undefined;
|
|
984
|
+
return {
|
|
985
|
+
offset: intParam(url, "offset", 0, 0, Number.MAX_SAFE_INTEGER),
|
|
986
|
+
limit: intParam(url, "limit", DEFAULT_LIMIT, 1, MAX_LIMIT),
|
|
987
|
+
filter,
|
|
988
|
+
...(tag !== undefined ? { tag } : {}),
|
|
989
|
+
...(q !== undefined ? { q } : {}),
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
async function handleDatasetGet(c, id) {
|
|
993
|
+
const { projectRoot, ref } = await locateDataset(rootOptions(c), wireFromQuery(c), id, { write: false, missing: "throw" });
|
|
994
|
+
const evalId = c.url.searchParams.get("eval_id") ?? undefined;
|
|
995
|
+
c.json(c.res, 200, await datasetDetailFor(c, projectRoot, id, ref.absPath, evalId, datasetQuery(c.url)));
|
|
996
|
+
}
|
|
997
|
+
function targetFrom(value) {
|
|
998
|
+
if (!isRecord(value))
|
|
999
|
+
throw bad(`"target" must be an object`);
|
|
1000
|
+
const target = {};
|
|
1001
|
+
for (const key of ["pipeline_path", "stage_id", "file", "agent_id"]) {
|
|
1002
|
+
const v = value[key];
|
|
1003
|
+
if (v !== undefined) {
|
|
1004
|
+
if (typeof v !== "string" || v === "")
|
|
1005
|
+
throw bad(`target.${key} must be a non-empty string`);
|
|
1006
|
+
target[key] = v;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return target;
|
|
1010
|
+
}
|
|
1011
|
+
async function conversationModeOf(c, body) {
|
|
1012
|
+
const evalId = optString(body, "eval_id");
|
|
1013
|
+
if (evalId === undefined)
|
|
1014
|
+
return undefined;
|
|
1015
|
+
const { projectRoot, ref } = await evalRootFor(c, wireFromBody(body), evalId);
|
|
1016
|
+
const outcome = await loadEvalOutcome(ref.absPath, projectRoot);
|
|
1017
|
+
return outcome.ok && outcome.value.file.target.kind === "agent"
|
|
1018
|
+
? resolveConversation(outcome.value.file.conversation).mode
|
|
1019
|
+
: undefined;
|
|
1020
|
+
}
|
|
1021
|
+
async function handleDatasetPreview(c) {
|
|
1022
|
+
const body = await readBody(c);
|
|
1023
|
+
const format = body.format;
|
|
1024
|
+
if (format !== "jsonl" && format !== "json" && format !== "csv")
|
|
1025
|
+
throw bad(`"format" must be jsonl, json or csv`);
|
|
1026
|
+
if (typeof body.content !== "string")
|
|
1027
|
+
throw bad(`"content" must be a string`);
|
|
1028
|
+
const delimiter = body.delimiter;
|
|
1029
|
+
if (delimiter !== undefined && (typeof delimiter !== "string" || delimiter.length !== 1)) {
|
|
1030
|
+
throw bad(`"delimiter" must be a single character`);
|
|
1031
|
+
}
|
|
1032
|
+
let mapping;
|
|
1033
|
+
if (body.mapping !== undefined) {
|
|
1034
|
+
if (!isRecord(body.mapping))
|
|
1035
|
+
throw bad(`"mapping" must be an object`);
|
|
1036
|
+
mapping = body.mapping;
|
|
1037
|
+
}
|
|
1038
|
+
const projectRoot = await projectRootFor(c, wireFromBody(body));
|
|
1039
|
+
let inputSchema;
|
|
1040
|
+
let agent;
|
|
1041
|
+
if (body.target !== undefined) {
|
|
1042
|
+
const resolved = await resolveDatasetTarget(projectRoot, targetFrom(body.target), agentsOf(c));
|
|
1043
|
+
if (!resolved.ok)
|
|
1044
|
+
throw new EvalApiError(422, "EVAL_INVALID", resolved.message);
|
|
1045
|
+
inputSchema = resolved.value.inputSchema;
|
|
1046
|
+
if (resolved.value.agentId !== undefined) {
|
|
1047
|
+
agent = { mode: (await conversationModeOf(c, body)) ?? "simulated" };
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const preview = previewUpload({
|
|
1051
|
+
format,
|
|
1052
|
+
content: body.content,
|
|
1053
|
+
...(delimiter !== undefined ? { delimiter: delimiter } : {}),
|
|
1054
|
+
...(mapping !== undefined ? { mapping } : {}),
|
|
1055
|
+
...(inputSchema !== undefined ? { inputSchema } : {}),
|
|
1056
|
+
...(agent !== undefined ? { agent } : {}),
|
|
1057
|
+
});
|
|
1058
|
+
const unsupported = preview.findings.find((f) => f.code === "dataset.mapping_unsupported_field");
|
|
1059
|
+
if (unsupported !== undefined) {
|
|
1060
|
+
throw new EvalApiError(400, "EVAL_INVALID", unsupported.message, { findings: preview.findings });
|
|
1061
|
+
}
|
|
1062
|
+
c.json(c.res, 200, preview);
|
|
1063
|
+
}
|
|
1064
|
+
async function sampleCases(c) {
|
|
1065
|
+
const dir = c.sampleSourceDir ?? (() => {
|
|
1066
|
+
const examples = resolveSeededExamplesPath();
|
|
1067
|
+
return examples === undefined ? undefined : path.join(examples, "evals");
|
|
1068
|
+
})();
|
|
1069
|
+
if (dir === undefined)
|
|
1070
|
+
throw new EvalsWriteError("SAMPLE_SOURCE_MISSING", "The packaged sample files were not found");
|
|
1071
|
+
let text;
|
|
1072
|
+
try {
|
|
1073
|
+
text = await readFile(path.join(dir, EVAL_SAMPLE_FILES.dataset), "utf8");
|
|
1074
|
+
}
|
|
1075
|
+
catch {
|
|
1076
|
+
throw new EvalsWriteError("SAMPLE_SOURCE_MISSING", "The packaged sample dataset was not found");
|
|
1077
|
+
}
|
|
1078
|
+
return parseDatasetText(text, "jsonl").cases;
|
|
1079
|
+
}
|
|
1080
|
+
async function casesFromSessionsBody(c, projectRoot, body) {
|
|
1081
|
+
const resolved = await resolveDatasetTarget(projectRoot, targetFrom(body.target), agentsOf(c));
|
|
1082
|
+
if (!resolved.ok)
|
|
1083
|
+
throw new EvalApiError(422, "EVAL_INVALID", resolved.message);
|
|
1084
|
+
const agentId = resolved.value.agentId;
|
|
1085
|
+
if (agentId === undefined)
|
|
1086
|
+
throw bad(`source "sessions" needs target.agent_id`);
|
|
1087
|
+
const sessionIds = optStringArray(body, "session_ids");
|
|
1088
|
+
if (!sessionIds || sessionIds.length === 0)
|
|
1089
|
+
throw bad(`"session_ids" must be a non-empty array`);
|
|
1090
|
+
const agents = agentsOf(c);
|
|
1091
|
+
if (!agents)
|
|
1092
|
+
throw new EvalApiError(503, "EVAL_ENGINE_UNAVAILABLE", "Agents are not available on this Host");
|
|
1093
|
+
try {
|
|
1094
|
+
return await casesFromSessions({ sessions: agents, projectRoot, agentId, sessionIds });
|
|
1095
|
+
}
|
|
1096
|
+
catch (err) {
|
|
1097
|
+
if (err instanceof SessionCaseError)
|
|
1098
|
+
throw bad(err.message);
|
|
1099
|
+
throw err;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
async function casesFromRunsBody(c, projectRoot, body) {
|
|
1103
|
+
const resolved = await resolveDatasetTarget(projectRoot, targetFrom(body.target));
|
|
1104
|
+
if (!resolved.ok)
|
|
1105
|
+
throw new EvalApiError(422, "EVAL_INVALID", resolved.message);
|
|
1106
|
+
const runIds = optStringArray(body, "run_ids");
|
|
1107
|
+
if (!runIds || runIds.length === 0)
|
|
1108
|
+
throw bad(`"run_ids" must be a non-empty array`);
|
|
1109
|
+
if (resolved.value.stageId === undefined)
|
|
1110
|
+
throw bad("target does not name a stage");
|
|
1111
|
+
return casesFromRuns({
|
|
1112
|
+
store: c.store,
|
|
1113
|
+
runIds,
|
|
1114
|
+
target: {
|
|
1115
|
+
stageId: resolved.value.stageId,
|
|
1116
|
+
...(resolved.value.pipelineAbsPath !== undefined ? { pipelinePath: resolved.value.pipelineAbsPath } : {}),
|
|
1117
|
+
},
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
async function handleDatasetCreate(c) {
|
|
1121
|
+
const body = await readBody(c);
|
|
1122
|
+
const projectRoot = typeof body.dataset_id === "string" && body.dataset_id !== ""
|
|
1123
|
+
? (await locateDataset(rootOptions(c), wireFromBody(body), body.dataset_id, { write: true, missing: "default" })).projectRoot
|
|
1124
|
+
: await projectRootFor(c, wireFromBody(body), true);
|
|
1125
|
+
const directory = optString(body, "directory");
|
|
1126
|
+
let absPath;
|
|
1127
|
+
let id;
|
|
1128
|
+
if (body.dataset_id !== undefined) {
|
|
1129
|
+
id = optString(body, "dataset_id");
|
|
1130
|
+
const mode = body.mode;
|
|
1131
|
+
if (mode !== "append" && mode !== "replace")
|
|
1132
|
+
throw bad(`"mode" must be append or replace`);
|
|
1133
|
+
if (typeof body.base_hash !== "string" || body.base_hash === "")
|
|
1134
|
+
throw bad(`"base_hash" is required`);
|
|
1135
|
+
let newCases;
|
|
1136
|
+
if (body.source === "runs") {
|
|
1137
|
+
if (mode !== "append")
|
|
1138
|
+
throw bad(`source "runs" requires mode "append"`);
|
|
1139
|
+
newCases = await casesFromRunsBody(c, projectRoot, body);
|
|
1140
|
+
}
|
|
1141
|
+
else if (body.source === "sessions") {
|
|
1142
|
+
if (mode !== "append")
|
|
1143
|
+
throw bad(`source "sessions" requires mode "append"`);
|
|
1144
|
+
newCases = await casesFromSessionsBody(c, projectRoot, body);
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
if (!Array.isArray(body.cases))
|
|
1148
|
+
throw bad(`"cases" must be an array`);
|
|
1149
|
+
newCases = body.cases;
|
|
1150
|
+
}
|
|
1151
|
+
const written = await appendOrReplaceCases({
|
|
1152
|
+
projectRoot,
|
|
1153
|
+
id,
|
|
1154
|
+
baseHash: body.base_hash,
|
|
1155
|
+
mode,
|
|
1156
|
+
cases: newCases,
|
|
1157
|
+
});
|
|
1158
|
+
absPath = written.path;
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
const name = optString(body, "name");
|
|
1162
|
+
if (name === undefined)
|
|
1163
|
+
throw bad(`"name" is required`);
|
|
1164
|
+
id = name;
|
|
1165
|
+
let cases;
|
|
1166
|
+
if (body.source === "sessions") {
|
|
1167
|
+
cases = await casesFromSessionsBody(c, projectRoot, body);
|
|
1168
|
+
}
|
|
1169
|
+
else if (body.source === "sample" || body.source === "runs") {
|
|
1170
|
+
const target = targetFrom(body.target);
|
|
1171
|
+
const resolved = await resolveDatasetTarget(projectRoot, target, agentsOf(c));
|
|
1172
|
+
if (!resolved.ok)
|
|
1173
|
+
throw new EvalApiError(422, "EVAL_INVALID", resolved.message);
|
|
1174
|
+
if (resolved.value.agentId !== undefined)
|
|
1175
|
+
throw bad(`source "${body.source}" is not available for an agent target`);
|
|
1176
|
+
if (body.source === "sample") {
|
|
1177
|
+
const sample = await sampleCases(c);
|
|
1178
|
+
cases = sample;
|
|
1179
|
+
const failing = checkDatasetAgainstStage(sample, resolved.value.inputSchema).find((r) => !r.valid);
|
|
1180
|
+
if (failing) {
|
|
1181
|
+
throw new EvalsWriteError("DATASET_INVALID", "The sample cases do not match this stage's input");
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
cases = await casesFromRunsBody(c, projectRoot, body);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
if (!Array.isArray(body.cases))
|
|
1190
|
+
throw bad(`"cases" must be an array`);
|
|
1191
|
+
cases = body.cases;
|
|
1192
|
+
}
|
|
1193
|
+
const written = await createDatasetFile({
|
|
1194
|
+
projectRoot,
|
|
1195
|
+
name,
|
|
1196
|
+
cases,
|
|
1197
|
+
...(directory !== undefined ? { directory } : {}),
|
|
1198
|
+
});
|
|
1199
|
+
absPath = written.path;
|
|
1200
|
+
}
|
|
1201
|
+
audit(c, "dataset_write", "ok");
|
|
1202
|
+
c.json(c.res, 201, await datasetDetailFor(c, projectRoot, id, absPath, undefined, { offset: 0, limit: MAX_LIMIT, filter: "all" }));
|
|
1203
|
+
}
|
|
1204
|
+
const CASE_OPS = new Set(["add", "update", "delete", "set_tags"]);
|
|
1205
|
+
async function handleDatasetPatch(c, id) {
|
|
1206
|
+
const body = await readBody(c);
|
|
1207
|
+
if (typeof body.base_hash !== "string" || body.base_hash === "")
|
|
1208
|
+
throw bad(`"base_hash" is required`);
|
|
1209
|
+
if (!Array.isArray(body.ops) || body.ops.some((op) => !isRecord(op) || !CASE_OPS.has(String(op.op)))) {
|
|
1210
|
+
throw bad(`"ops" must be an array of add, update, delete or set_tags operations`);
|
|
1211
|
+
}
|
|
1212
|
+
const { projectRoot } = await locateDataset(rootOptions(c), wireFromBody(body), id, { write: true, missing: "default" });
|
|
1213
|
+
const written = await applyCaseOps({
|
|
1214
|
+
projectRoot,
|
|
1215
|
+
id,
|
|
1216
|
+
baseHash: body.base_hash,
|
|
1217
|
+
ops: body.ops,
|
|
1218
|
+
});
|
|
1219
|
+
audit(c, "dataset_write", "ok");
|
|
1220
|
+
c.json(c.res, 200, await datasetDetailFor(c, projectRoot, id, written.path, undefined, { offset: 0, limit: MAX_LIMIT, filter: "all" }));
|
|
1221
|
+
}
|
|
1222
|
+
async function handleDatasetConvert(c, id) {
|
|
1223
|
+
const body = await readBody(c);
|
|
1224
|
+
if (typeof body.base_hash !== "string" || body.base_hash === "")
|
|
1225
|
+
throw bad(`"base_hash" is required`);
|
|
1226
|
+
const { projectRoot } = await locateDataset(rootOptions(c), wireFromBody(body), id, { write: true, missing: "default" });
|
|
1227
|
+
const written = await convertDatasetToJsonl({ projectRoot, id, baseHash: body.base_hash });
|
|
1228
|
+
audit(c, "dataset_write", "ok");
|
|
1229
|
+
c.json(c.res, 201, await datasetDetailFor(c, projectRoot, written.id, written.path, undefined, { offset: 0, limit: MAX_LIMIT, filter: "all" }));
|
|
1230
|
+
}
|
|
1231
|
+
async function handleAgentTargets(c) {
|
|
1232
|
+
const agents = agentsOf(c);
|
|
1233
|
+
const projectRoot = await projectRootFor(c, wireFromQuery(c));
|
|
1234
|
+
c.json(c.res, 200, { items: agents ? await listAgentTargets(agents, projectRoot) : [] });
|
|
1235
|
+
}
|
|
1236
|
+
async function handleSessionCandidates(c, agentId) {
|
|
1237
|
+
const projectRoot = await projectRootFor(c, wireFromQuery(c));
|
|
1238
|
+
const agents = agentsOf(c);
|
|
1239
|
+
if (!agents)
|
|
1240
|
+
throw new EvalApiError(503, "EVAL_ENGINE_UNAVAILABLE", "Agents are not available on this Host");
|
|
1241
|
+
try {
|
|
1242
|
+
const items = await listSessionCandidates({
|
|
1243
|
+
sessions: agents,
|
|
1244
|
+
projectRoot,
|
|
1245
|
+
agentId,
|
|
1246
|
+
limit: intParam(c.url, "limit", DEFAULT_LIMIT, 1, MAX_LIMIT),
|
|
1247
|
+
});
|
|
1248
|
+
c.json(c.res, 200, { items, next_cursor: null });
|
|
1249
|
+
}
|
|
1250
|
+
catch (err) {
|
|
1251
|
+
if (err instanceof SessionCaseError)
|
|
1252
|
+
throw new EvalApiError(404, "AGENT_NOT_FOUND", err.message);
|
|
1253
|
+
throw err;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async function handleCandidates(c) {
|
|
1257
|
+
const agent = c.url.searchParams.get("agent");
|
|
1258
|
+
if (agent !== null && agent !== "")
|
|
1259
|
+
return handleSessionCandidates(c, agent);
|
|
1260
|
+
const projectRoot = await projectRootFor(c, wireFromQuery(c));
|
|
1261
|
+
const target = {};
|
|
1262
|
+
for (const key of ["pipeline_path", "stage_id", "file"]) {
|
|
1263
|
+
const v = c.url.searchParams.get(key);
|
|
1264
|
+
if (v !== null && v !== "")
|
|
1265
|
+
target[key] = v;
|
|
1266
|
+
}
|
|
1267
|
+
const resolved = await resolveDatasetTarget(projectRoot, target);
|
|
1268
|
+
if (!resolved.ok)
|
|
1269
|
+
throw new EvalApiError(422, "EVAL_INVALID", resolved.message);
|
|
1270
|
+
if (resolved.value.stageId === undefined)
|
|
1271
|
+
throw bad("target does not name a stage");
|
|
1272
|
+
const items = await listDatasetCandidates({
|
|
1273
|
+
store: c.store,
|
|
1274
|
+
target: {
|
|
1275
|
+
stageId: resolved.value.stageId,
|
|
1276
|
+
...(resolved.value.pipelineAbsPath !== undefined ? { pipelinePath: resolved.value.pipelineAbsPath } : {}),
|
|
1277
|
+
},
|
|
1278
|
+
limit: intParam(c.url, "limit", DEFAULT_LIMIT, 1, MAX_LIMIT),
|
|
1279
|
+
});
|
|
1280
|
+
c.json(c.res, 200, {
|
|
1281
|
+
items: items.map((i) => ({ run_id: i.runId, finished_at: i.finishedAt, preview: i.preview })),
|
|
1282
|
+
next_cursor: null,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
function mapError(c, err) {
|
|
1286
|
+
if (err instanceof EvalApiError) {
|
|
1287
|
+
sendError(c, err.status, err.code, err.message, err.extra);
|
|
1288
|
+
}
|
|
1289
|
+
else if (err instanceof CatalogPathError) {
|
|
1290
|
+
c.json(c.res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1291
|
+
}
|
|
1292
|
+
else if (err instanceof FileChangedError) {
|
|
1293
|
+
sendError(c, 409, "FILE_CHANGED", err.message, { current_hash: err.currentHash });
|
|
1294
|
+
}
|
|
1295
|
+
else if (err instanceof EvalsWriteError) {
|
|
1296
|
+
sendError(c, WRITE_ERROR_STATUS[err.code] ?? 500, err.code, err.message, err.findings.length > 0 ? { findings: err.findings } : {});
|
|
1297
|
+
}
|
|
1298
|
+
else if (err instanceof EvalRunError) {
|
|
1299
|
+
sendError(c, RUN_ERROR_STATUS[err.code] ?? 500, err.code, err.message, err.findings ? { findings: err.findings } : {});
|
|
1300
|
+
}
|
|
1301
|
+
else if (err instanceof EvalStoreError) {
|
|
1302
|
+
sendError(c, RUN_ERROR_STATUS[err.code] ?? 500, err.code, err.message);
|
|
1303
|
+
}
|
|
1304
|
+
else if (err instanceof CompareMismatchError) {
|
|
1305
|
+
sendError(c, 400, err.code, err.message);
|
|
1306
|
+
}
|
|
1307
|
+
else {
|
|
1308
|
+
sendError(c, 500, "internal_error", "Internal error");
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
export function isEvalRoutePath(pathname) {
|
|
1312
|
+
return (pathname === "/api/evals" ||
|
|
1313
|
+
pathname.startsWith("/api/evals/") ||
|
|
1314
|
+
pathname === "/api/datasets" ||
|
|
1315
|
+
pathname.startsWith("/api/datasets/") ||
|
|
1316
|
+
pathname === "/api/eval-runs" ||
|
|
1317
|
+
pathname.startsWith("/api/eval-runs/") ||
|
|
1318
|
+
pathname === "/api/eval-compare" ||
|
|
1319
|
+
pathname === "/api/eval-engine");
|
|
1320
|
+
}
|
|
1321
|
+
export async function handleEvalRoutes(req, res, deps) {
|
|
1322
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1323
|
+
const method = req.method ?? "GET";
|
|
1324
|
+
const pathname = url.pathname;
|
|
1325
|
+
if (!isEvalRoutePath(pathname))
|
|
1326
|
+
return false;
|
|
1327
|
+
if (pathname.startsWith("/api/evals/internal/"))
|
|
1328
|
+
return false;
|
|
1329
|
+
const seg = (value) => decodeURIComponent(value ?? "");
|
|
1330
|
+
const manager = deps.evalRunManager;
|
|
1331
|
+
const c = { ...deps, req, res, url, method, pathname };
|
|
1332
|
+
if (method === "GET" && pathname === "/api/eval-engine") {
|
|
1333
|
+
handleEngine(c);
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
const m = (re) => pathname.match(re);
|
|
1337
|
+
const run = async (fn) => {
|
|
1338
|
+
if (manager === undefined) {
|
|
1339
|
+
sendError(c, 503, "EVAL_ENGINE_UNAVAILABLE", "Evals are not available on this Host");
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
await fn();
|
|
1344
|
+
}
|
|
1345
|
+
catch (err) {
|
|
1346
|
+
mapError(c, err);
|
|
1347
|
+
}
|
|
1348
|
+
return true;
|
|
1349
|
+
};
|
|
1350
|
+
if (method === "GET" && pathname === "/api/evals")
|
|
1351
|
+
return run(() => handleEvalList(c));
|
|
1352
|
+
if (method === "POST" && pathname === "/api/evals")
|
|
1353
|
+
return run(() => handleEvalCreate(c));
|
|
1354
|
+
if (method === "POST" && pathname === "/api/evals/validate")
|
|
1355
|
+
return run(() => handleEvalValidate(c));
|
|
1356
|
+
if (method === "POST" && pathname === "/api/evals/sample")
|
|
1357
|
+
return run(() => handleSampleInstall(c));
|
|
1358
|
+
if (method === "POST" && pathname === "/api/evals/try")
|
|
1359
|
+
return run(() => handleTry(c));
|
|
1360
|
+
if (method === "GET" && pathname === "/api/evals/agent-targets")
|
|
1361
|
+
return run(() => handleAgentTargets(c));
|
|
1362
|
+
if (method === "POST" && pathname === "/api/evals/runs/estimate")
|
|
1363
|
+
return run(() => handleDraftEstimate(c));
|
|
1364
|
+
let match = m(/^\/api\/evals\/([^/]+)\/runs\/estimate$/);
|
|
1365
|
+
if (method === "GET" && match)
|
|
1366
|
+
return run(() => handleRunEstimate(c, seg(match[1])));
|
|
1367
|
+
match = m(/^\/api\/evals\/([^/]+)\/runs\/(\d+)$/);
|
|
1368
|
+
if (method === "GET" && match)
|
|
1369
|
+
return run(() => handleRunByNumber(c, seg(match[1]), Number(match[2])));
|
|
1370
|
+
match = m(/^\/api\/evals\/([^/]+)\/runs$/);
|
|
1371
|
+
if (method === "POST" && match)
|
|
1372
|
+
return run(() => handleRunStart(c, seg(match[1])));
|
|
1373
|
+
match = m(/^\/api\/evals\/([^/]+)\/failure-types$/);
|
|
1374
|
+
if (method === "GET" && match)
|
|
1375
|
+
return run(() => handleFailureTypes(c, seg(match[1])));
|
|
1376
|
+
match = m(/^\/api\/evals\/([^/]+)$/);
|
|
1377
|
+
if (match && method === "GET")
|
|
1378
|
+
return run(() => handleEvalGet(c, seg(match[1])));
|
|
1379
|
+
if (match && method === "PUT")
|
|
1380
|
+
return run(() => handleEvalUpdate(c, seg(match[1])));
|
|
1381
|
+
if (method === "GET" && pathname === "/api/datasets")
|
|
1382
|
+
return run(() => handleDatasetList(c));
|
|
1383
|
+
if (method === "POST" && pathname === "/api/datasets")
|
|
1384
|
+
return run(() => handleDatasetCreate(c));
|
|
1385
|
+
if (method === "POST" && pathname === "/api/datasets/preview")
|
|
1386
|
+
return run(() => handleDatasetPreview(c));
|
|
1387
|
+
if (method === "GET" && pathname === "/api/datasets/candidates")
|
|
1388
|
+
return run(() => handleCandidates(c));
|
|
1389
|
+
match = m(/^\/api\/datasets\/([^/]+)\/cases$/);
|
|
1390
|
+
if (method === "PATCH" && match)
|
|
1391
|
+
return run(() => handleDatasetPatch(c, seg(match[1])));
|
|
1392
|
+
match = m(/^\/api\/datasets\/([^/]+)\/convert$/);
|
|
1393
|
+
if (method === "POST" && match)
|
|
1394
|
+
return run(() => handleDatasetConvert(c, seg(match[1])));
|
|
1395
|
+
match = m(/^\/api\/datasets\/([^/]+)$/);
|
|
1396
|
+
if (method === "GET" && match)
|
|
1397
|
+
return run(() => handleDatasetGet(c, seg(match[1])));
|
|
1398
|
+
if (method === "GET" && pathname === "/api/eval-runs")
|
|
1399
|
+
return run(() => handleRunList(c));
|
|
1400
|
+
match = m(/^\/api\/eval-runs\/([^/]+)\/results\/([^/]+)\/(\d+)\/review$/);
|
|
1401
|
+
if (method === "PUT" && match) {
|
|
1402
|
+
return run(() => handleReviewPut(c, seg(match[1]), seg(match[2]), Number(match[3])));
|
|
1403
|
+
}
|
|
1404
|
+
const triageRoute = matchEvalTriageRoute(c, seg);
|
|
1405
|
+
if (triageRoute)
|
|
1406
|
+
return run(triageRoute);
|
|
1407
|
+
match = m(/^\/api\/eval-runs\/([^/]+)\/results\/([^/]+)$/);
|
|
1408
|
+
if (method === "GET" && match)
|
|
1409
|
+
return run(() => handleResultDetail(c, seg(match[1]), seg(match[2])));
|
|
1410
|
+
match = m(/^\/api\/eval-runs\/([^/]+)\/results$/);
|
|
1411
|
+
if (method === "GET" && match)
|
|
1412
|
+
return run(() => handleResultsList(c, seg(match[1])));
|
|
1413
|
+
match = m(/^\/api\/eval-runs\/([^/]+)\/cancel$/);
|
|
1414
|
+
if (method === "POST" && match)
|
|
1415
|
+
return run(() => handleCancel(c, seg(match[1])));
|
|
1416
|
+
match = m(/^\/api\/eval-runs\/([^/]+)\/export$/);
|
|
1417
|
+
if (method === "GET" && match)
|
|
1418
|
+
return run(() => handleExport(c, seg(match[1])));
|
|
1419
|
+
match = m(/^\/api\/eval-runs\/([^/]+)$/);
|
|
1420
|
+
if (method === "GET" && match)
|
|
1421
|
+
return run(() => handleRunGet(c, seg(match[1])));
|
|
1422
|
+
if (method === "DELETE" && match)
|
|
1423
|
+
return run(() => handleDelete(c, seg(match[1])));
|
|
1424
|
+
if (method === "GET" && pathname === "/api/eval-compare")
|
|
1425
|
+
return run(() => handleCompare(c));
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|