@marinoscar/kvox-eval 0.0.0-bootstrap → 0.1.0-next.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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/ask/index.d.ts +6 -0
- package/dist/ask/index.js +28 -0
- package/dist/ask/question-schema.d.ts +121 -0
- package/dist/ask/question-schema.js +167 -0
- package/dist/ask/report.d.ts +88 -0
- package/dist/ask/report.js +170 -0
- package/dist/ask/scorer.d.ts +104 -0
- package/dist/ask/scorer.js +164 -0
- package/dist/ask-run/ask-command.d.ts +62 -0
- package/dist/ask-run/ask-command.js +254 -0
- package/dist/ask-run/host/app.d.ts +65 -0
- package/dist/ask-run/host/app.js +146 -0
- package/dist/ask-run/host/db.d.ts +38 -0
- package/dist/ask-run/host/db.js +126 -0
- package/dist/ask-run/host/ports.d.ts +35 -0
- package/dist/ask-run/host/ports.js +113 -0
- package/dist/ask-run/index.d.ts +13 -0
- package/dist/ask-run/index.js +35 -0
- package/dist/ask-run/replay.d.ts +128 -0
- package/dist/ask-run/replay.js +212 -0
- package/dist/ask-run/run-ask-eval.d.ts +125 -0
- package/dist/ask-run/run-ask-eval.js +193 -0
- package/dist/ask-run/seed-golden-graph.d.ts +64 -0
- package/dist/ask-run/seed-golden-graph.js +495 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +12 -0
- package/dist/cli/kg-command.d.ts +66 -0
- package/dist/cli/kg-command.js +238 -0
- package/dist/cli/main.d.ts +18 -0
- package/dist/cli/main.js +69 -0
- package/dist/fixtures/domains.d.ts +16 -0
- package/dist/fixtures/domains.js +33 -0
- package/dist/fixtures/fixture-schema.d.ts +323 -0
- package/dist/fixtures/fixture-schema.js +199 -0
- package/dist/fixtures/index.d.ts +16 -0
- package/dist/fixtures/index.js +40 -0
- package/dist/fixtures/load.d.ts +73 -0
- package/dist/fixtures/load.js +137 -0
- package/dist/fixtures/questions.d.ts +15 -0
- package/dist/fixtures/questions.js +26 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +36 -0
- package/dist/kg/gold-runner.d.ts +13 -0
- package/dist/kg/gold-runner.js +57 -0
- package/dist/kg/index.d.ts +17 -0
- package/dist/kg/index.js +65 -0
- package/dist/kg/internals.d.ts +7 -0
- package/dist/kg/internals.js +17 -0
- package/dist/kg/prediction-schema.d.ts +245 -0
- package/dist/kg/prediction-schema.js +102 -0
- package/dist/kg/predictions.d.ts +11 -0
- package/dist/kg/predictions.js +49 -0
- package/dist/kg/report.d.ts +84 -0
- package/dist/kg/report.js +125 -0
- package/dist/kg/run-kg-eval.d.ts +77 -0
- package/dist/kg/run-kg-eval.js +137 -0
- package/dist/kg/runner.d.ts +72 -0
- package/dist/kg/runner.js +51 -0
- package/dist/kg/runners/extract-resolve-runner.d.ts +40 -0
- package/dist/kg/runners/extract-resolve-runner.js +199 -0
- package/dist/kg/runners/extract-runner.d.ts +65 -0
- package/dist/kg/runners/extract-runner.js +243 -0
- package/dist/kg/scorer.d.ts +226 -0
- package/dist/kg/scorer.js +463 -0
- package/dist/kg/targets.d.ts +17 -0
- package/dist/kg/targets.js +16 -0
- package/dist/kg/text.d.ts +45 -0
- package/dist/kg/text.js +100 -0
- package/dist/live/adapters.d.ts +16 -0
- package/dist/live/adapters.js +58 -0
- package/dist/live/env.d.ts +39 -0
- package/dist/live/env.js +56 -0
- package/dist/live/index.d.ts +6 -0
- package/dist/live/index.js +16 -0
- package/dist/live/live-ai.d.ts +104 -0
- package/dist/live/live-ai.js +157 -0
- package/dist/parity/constants.d.ts +79 -0
- package/dist/parity/constants.js +104 -0
- package/dist/parity/index.d.ts +20 -0
- package/dist/parity/index.js +38 -0
- package/dist/parity/internals.d.ts +8 -0
- package/dist/parity/internals.js +16 -0
- package/dist/parity/scenario.d.ts +45 -0
- package/dist/parity/scenario.js +245 -0
- package/dist/parity/script.d.ts +44 -0
- package/dist/parity/script.js +136 -0
- package/dist/parity/snapshot.d.ts +43 -0
- package/dist/parity/snapshot.js +71 -0
- package/dist/real-data/index.d.ts +2 -0
- package/dist/real-data/index.js +12 -0
- package/dist/real-data/real-data.d.ts +76 -0
- package/dist/real-data/real-data.js +156 -0
- package/fixtures/kg-golden/README.md +136 -0
- package/fixtures/kg-golden/ask-questions.json +458 -0
- package/fixtures/kg-golden/meetings/m01-northwind-pilot-kickoff.json +792 -0
- package/fixtures/kg-golden/meetings/m02-halden-migration-planning.json +814 -0
- package/fixtures/kg-golden/meetings/m03-larkspur-weekly-sync.json +601 -0
- package/fixtures/kg-golden/meetings/m04-corvid-intake-discovery.json +690 -0
- package/fixtures/kg-golden/meetings/m05-hosting-vendor-selection.json +631 -0
- package/fixtures/kg-golden/meetings/m06-northwind-floor-data-review.json +649 -0
- package/fixtures/kg-golden/meetings/m07-gullwing-intro-notes.json +428 -0
- package/fixtures/kg-golden/meetings/m08-halden-migration-checkpoint.json +672 -0
- package/fixtures/kg-golden/meetings/m09-northwind-platform-org-update.json +711 -0
- package/fixtures/kg-golden/meetings/m10-corvid-prototype-review.json +623 -0
- package/fixtures/kg-golden/meetings/m11-solvane-grid-dashboard-scoping.json +689 -0
- package/fixtures/kg-golden/meetings/m12-northwind-pilot-replan.json +681 -0
- package/fixtures/kg-golden/meetings/m13-halden-cutover-go-no-go.json +721 -0
- package/fixtures/kg-golden/meetings/m14-gullwing-discovery-readout.json +719 -0
- package/fixtures/kg-golden/meetings/m15-halden-handover-ravi-leaving.json +716 -0
- package/fixtures/kg-golden/meetings/m16-varden-claims-portal-intro.json +704 -0
- package/fixtures/kg-golden/meetings/m17-corvid-usability-debrief.json +572 -0
- package/fixtures/kg-golden/meetings/m18-hosting-decision-revisited.json +655 -0
- package/fixtures/kg-golden/meetings/m19-solvane-integration-kickoff.json +726 -0
- package/fixtures/kg-golden/meetings/m20-northwind-go-live-retro.json +673 -0
- package/fixtures/kg-golden/meetings/m21-quarterly-account-review.json +449 -0
- package/fixtures/kg-golden/meetings/m22-halden-intro-call-2020.json +558 -0
- package/fixtures/kg-golden/meetings/m23-gullwing-model-review.json +702 -0
- package/fixtures/kg-golden/meetings/m24-varden-discovery-midpoint.json +643 -0
- package/fixtures/kg-golden/meetings/m25-corvid-rollout-planning.json +642 -0
- package/fixtures/kg-golden/meetings/m26-larkspur-engineering-prep.json +695 -0
- package/fixtures/kg-golden/meetings/m27-solvane-phase-one-demo.json +649 -0
- package/fixtures/kg-golden/meetings/m28-northwind-building-5-scoping.json +715 -0
- package/fixtures/kg-golden/meetings/m29-halden-southern-cutover-review.json +752 -0
- package/fixtures/kg-golden/meetings/m30-varden-portal-build-kickoff.json +697 -0
- package/fixtures/kg-golden/meetings/m31-gullwing-rollout-renewal.json +681 -0
- package/fixtures/kg-golden/meetings/m32-brambleway-weekly-sync.json +472 -0
- package/fixtures/kg-golden/meetings/m33-nina-oliver-one-on-one.json +342 -0
- package/fixtures/kg-golden/meetings/m34-coffee-with-dana.json +249 -0
- package/fixtures/kg-golden/meetings/m35-brambleway-june-sync.json +553 -0
- package/fixtures/kg-golden/meetings/m36-copenhagen-conference-planning.json +296 -0
- package/host/generated/client.d.ts +1 -0
- package/host/generated/client.js +5 -0
- package/host/generated/default.d.ts +1 -0
- package/host/generated/default.js +5 -0
- package/host/generated/edge.d.ts +1 -0
- package/host/generated/edge.js +1658 -0
- package/host/generated/index-browser.js +1684 -0
- package/host/generated/index.d.ts +192335 -0
- package/host/generated/index.js +1658 -0
- package/host/generated/package.json +139 -0
- package/host/generated/query_compiler_fast_bg.js +2 -0
- package/host/generated/query_compiler_fast_bg.wasm +0 -0
- package/host/generated/query_compiler_fast_bg.wasm-base64.js +2 -0
- package/host/generated/runtime/client.d.ts +3433 -0
- package/host/generated/runtime/client.js +110 -0
- package/host/generated/runtime/index-browser.d.ts +90 -0
- package/host/generated/runtime/index-browser.js +6 -0
- package/host/generated/runtime/wasm-compiler-edge.js +100 -0
- package/host/generated/schema.prisma +6312 -0
- package/host/generated/wasm-edge-light-loader.mjs +5 -0
- package/host/generated/wasm-worker-loader.mjs +5 -0
- package/host/prisma/fragments/kvox.ask.prisma +126 -0
- package/host/prisma/fragments/kvox.graph.prisma +1073 -0
- package/host/prisma/fragments/kvox.notes.prisma +787 -0
- package/host/prisma/fragments/kvox.search.prisma +211 -0
- package/host/prisma/fragments/kvox.transcripts.prisma +790 -0
- package/host/prisma/kvox.lock +101 -0
- package/host/prisma/migrations/20240101000000_initial/migration.sql +297 -0
- package/host/prisma/migrations/20240101000001_add_personal_access_tokens/migration.sql +31 -0
- package/host/prisma/migrations/20240101000002_add_credentials/migration.sql +20 -0
- package/host/prisma/migrations/20240101000003_add_notification_deliveries/migration.sql +27 -0
- package/host/prisma/migrations/20240101000004_drop_stale_uuid_defaults/migration.sql +35 -0
- package/host/prisma/migrations/20240101000005_add_notifications/migration.sql +22 -0
- package/host/prisma/migrations/20240101000006_add_push_subscriptions/migration.sql +25 -0
- package/host/prisma/migrations/20240101000007_add_jobs/migration.sql +110 -0
- package/host/prisma/migrations/20240101000008_add_worker_nodes/migration.sql +90 -0
- package/host/prisma/migrations/20240101000009_add_database_backup_runs/migration.sql +104 -0
- package/host/prisma/migrations/20240101000010_add_notification_broadcasts/migration.sql +57 -0
- package/host/prisma/migrations/20240101000011_add_backup_run_job_link/migration.sql +87 -0
- package/host/prisma/migrations/20240101000012_add_job_node_secrets/migration.sql +61 -0
- package/host/prisma/migrations/20240101000013_add_backup_run_pg_dump_version/migration.sql +26 -0
- package/host/prisma/migrations/20240101000014_add_job_claim_token/migration.sql +92 -0
- package/host/prisma/migrations/20240101000015_add_ai_platform/migration.sql +108 -0
- package/host/prisma/migrations/20240101000016_revoke_viewer_ai_use/migration.sql +32 -0
- package/host/prisma/migrations/20240101000017_add_user_credentials/migration.sql +29 -0
- package/host/prisma/migrations/20240101000018_add_device_session_credential_link/migration.sql +52 -0
- package/host/prisma/migrations/20240101000019_add_worker_node_vitals/migration.sql +3 -0
- package/host/prisma/migrations/20240101000020_add_job_trace_context/migration.sql +6 -0
- package/host/prisma/migrations/20240101000021_add_retention_created_at_indexes/migration.sql +13 -0
- package/host/prisma/migrations/20240101000022_add_organizations/migration.sql +142 -0
- package/host/prisma/migrations/20240101000023_split_system_org_roles/migration.sql +131 -0
- package/host/prisma/migrations/20240101000024_org_scoped_rls/migration.sql +215 -0
- package/host/prisma/migrations/20240101000025_add_groups/migration.sql +165 -0
- package/host/prisma/migrations/20240101000026_add_grants/migration.sql +117 -0
- package/host/prisma/migrations/20240101000027_add_org_settings/migration.sql +49 -0
- package/host/prisma/migrations/20240101000028_add_org_credentials/migration.sql +54 -0
- package/host/prisma/migrations/20240101000029_add_jobs_org_id/migration.sql +18 -0
- package/host/prisma/migrations/20240101000030_add_jobs_org_id_status_index/migration.sql +6 -0
- package/host/prisma/migrations/20240101000031_add_broadcast_target_org/migration.sql +21 -0
- package/host/prisma/migrations/20240101000032_add_android_app/migration.sql +60 -0
- package/host/prisma/migrations/20240101000033_open_job_reason/migration.sql +16 -0
- package/host/prisma/migrations/20240101000034_ai_usage_feature/migration.sql +37 -0
- package/host/prisma/migrations/20240101000035_add_app_metadata/migration.sql +49 -0
- package/host/prisma/migrations/20240101000036_open_sharing_vocabularies/migration.sql +97 -0
- package/host/prisma/migrations/20260914130000_kvox_add_transcripts/migration.sql +322 -0
- package/host/prisma/migrations/20260914140000_kvox_add_notes/migration.sql +311 -0
- package/host/prisma/migrations/20260915120000_kvox_add_search_vectors/migration.sql +150 -0
- package/host/prisma/migrations/20260915130000_kvox_add_search_embeddings/migration.sql +362 -0
- package/host/prisma/migrations/20260915210000_kvox_add_note_title_source/migration.sql +30 -0
- package/host/prisma/migrations/20260923120000_kvox_add_note_generation_context_snapshot/migration.sql +24 -0
- package/host/prisma/migrations/20260923130000_kvox_add_user_hidden_note_templates/migration.sql +35 -0
- package/host/prisma/migrations/20260923140000_kvox_add_transcript_speaker_identities/migration.sql +6 -0
- package/host/prisma/migrations/20260923150000_kvox_transcript_name_checks/migration.sql +95 -0
- package/host/prisma/migrations/20260923160000_kvox_add_note_body_format/migration.sql +27 -0
- package/host/prisma/migrations/20260923170000_kvox_add_note_version_body_format/migration.sql +23 -0
- package/host/prisma/migrations/20260926012548_kvox_add_knowledge_graph/migration.sql +680 -0
- package/host/prisma/migrations/20260926030000_kvox_fix_kg_relations_valid_precision_chk/migration.sql +29 -0
- package/host/prisma/migrations/20260926030100_kvox_add_transcript_recorded_at/migration.sql +9 -0
- package/host/prisma/migrations/20260926030200_kvox_kg_evidence_invariant/migration.sql +60 -0
- package/host/prisma/migrations/20260926040000_kvox_add_kg_graph_layouts/migration.sql +30 -0
- package/host/prisma/migrations/20260926050000_kvox_add_ask_conversations/migration.sql +99 -0
- package/host/prisma/migrations/20260926231512_kvox_add_kg_exports/migration.sql +52 -0
- package/host/prisma/migrations/20261016120000_kvox_transcripts_org_id/migration.sql +49 -0
- package/host/prisma/platform.lock +319 -0
- package/host/prisma/schema/app.kvox.ask.prisma +110 -0
- package/host/prisma/schema/app.kvox.graph.prisma +1030 -0
- package/host/prisma/schema/app.kvox.notes.prisma +746 -0
- package/host/prisma/schema/app.kvox.search.prisma +208 -0
- package/host/prisma/schema/app.kvox.transcripts.prisma +726 -0
- package/host/prisma/schema/platform.ai.prisma +419 -0
- package/host/prisma/schema/platform.android-app.prisma +46 -0
- package/host/prisma/schema/platform.base.prisma +15 -0
- package/host/prisma/schema/platform.credentials.prisma +156 -0
- package/host/prisma/schema/platform.db-backup.prisma +359 -0
- package/host/prisma/schema/platform.identity.prisma +638 -0
- package/host/prisma/schema/platform.jobs.prisma +824 -0
- package/host/prisma/schema/platform.notifications.prisma +641 -0
- package/host/prisma/schema/platform.settings.prisma +53 -0
- package/host/prisma/schema/platform.sharing.prisma +208 -0
- package/host/prisma/schema/platform.storage.prisma +118 -0
- package/package.json +112 -1
- package/parity/requests.ask.json +2681 -0
- package/parity/requests.m01.json +1554 -0
- package/parity/requests.m02.json +1554 -0
- package/parity/requests.m03.json +1554 -0
- package/parity/requests.m04.json +1554 -0
- package/parity/requests.m05.json +1554 -0
- package/parity/requests.m06.json +1554 -0
- package/parity/requests.m07.json +1554 -0
- package/parity/requests.m08.json +1554 -0
- package/parity/requests.m09.json +1554 -0
- package/parity/requests.m10.json +1554 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// `kvox-eval ask` — the Ask evaluation, headless (issue #608; the web app's
|
|
4
|
+
// `npm run ask:eval`, #382)
|
|
5
|
+
// =============================================================================
|
|
6
|
+
//
|
|
7
|
+
// kvox-eval ask --model gpt-4o-mini
|
|
8
|
+
// kvox-eval ask --model <id> --only q01,q22
|
|
9
|
+
// kvox-eval ask --model <id> --category negative --min-match 0.7
|
|
10
|
+
// kvox-eval ask --provider openai-compatible --base-url <url> --model <id>
|
|
11
|
+
// kvox-eval ask --replay
|
|
12
|
+
//
|
|
13
|
+
// NO web app and NO external database: the run starts PGlite in memory, applies
|
|
14
|
+
// the platform and domain migrations, seeds the golden graph through the graph
|
|
15
|
+
// write service, and runs every selected question through the real
|
|
16
|
+
// `ask.respond` handler of `@marinoscar/kvox-api/ask` on the live model port
|
|
17
|
+
// (`createLiveKvoxAi`). Everything dies with the process. It prints the table
|
|
18
|
+
// and writes `report.json` and `answers.md` under `--out` (default: a stamped
|
|
19
|
+
// folder under the system temporary directory, or under the host's own
|
|
20
|
+
// git-ignored runs directory).
|
|
21
|
+
//
|
|
22
|
+
// `--replay` runs the harness's self-test instead: six scripted questions on a
|
|
23
|
+
// scripted runtime (no model, no key, no network), checked for citation
|
|
24
|
+
// validity, every turn complete and the sensitive-fact leak gate; it prints one
|
|
25
|
+
// line per question and writes `replay.json` (the same `--out` default).
|
|
26
|
+
//
|
|
27
|
+
// The API key comes from KVOX_EVAL_OPENAI_API_KEY only (ASK_EVAL_OPENAI_API_KEY
|
|
28
|
+
// for one more release), never a flag, and is never printed or written. The key
|
|
29
|
+
// gate runs FIRST, before the database starts. A failed provider round trip is
|
|
30
|
+
// reported by its error code and HTTP status only.
|
|
31
|
+
//
|
|
32
|
+
// Exit codes: 0 pass · 1 a gate failed (citation validity < 1, a sensitive
|
|
33
|
+
// leak) or answer match fell below `--min-match`, or a replay expectation
|
|
34
|
+
// failed · 2 configuration error (no key, a refused provider, a bad flag, an
|
|
35
|
+
// unknown question id, a malformed fixture, every model call failed, or a rate
|
|
36
|
+
// limit the run cannot wait out). A turn whose model call failed is otherwise
|
|
37
|
+
// scored as a failed answer and the run goes on.
|
|
38
|
+
// =============================================================================
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.ASK_COMMAND_USAGE = exports.ASK_EVAL_EXIT = void 0;
|
|
41
|
+
exports.runAskCommand = runAskCommand;
|
|
42
|
+
const fs_1 = require("fs");
|
|
43
|
+
const os_1 = require("os");
|
|
44
|
+
const path_1 = require("path");
|
|
45
|
+
const util_1 = require("util");
|
|
46
|
+
const ports_1 = require("@marinoscar/kvox-api/ports");
|
|
47
|
+
const index_js_1 = require("../ask/index.js");
|
|
48
|
+
const index_js_2 = require("../fixtures/index.js");
|
|
49
|
+
const index_js_3 = require("../live/index.js");
|
|
50
|
+
const index_js_4 = require("../live/index.js");
|
|
51
|
+
const replay_js_1 = require("./replay.js");
|
|
52
|
+
const run_ask_eval_js_1 = require("./run-ask-eval.js");
|
|
53
|
+
/**
|
|
54
|
+
* The exit codes of `kvox-eval ask` (and of the web app's `npm run ask:eval`).
|
|
55
|
+
*
|
|
56
|
+
* @stability experimental
|
|
57
|
+
*/
|
|
58
|
+
exports.ASK_EVAL_EXIT = Object.freeze({ ok: 0, failed: 1, config: 2 });
|
|
59
|
+
/**
|
|
60
|
+
* The usage text of `kvox-eval ask`.
|
|
61
|
+
*
|
|
62
|
+
* @stability experimental
|
|
63
|
+
*/
|
|
64
|
+
exports.ASK_COMMAND_USAGE = `usage: kvox-eval ask [--provider openai|openai-compatible] [--model <id>] [--base-url <url>]
|
|
65
|
+
[--only q01,q07] [--category <c>] [--out <dir>] [--min-match <0..1>]
|
|
66
|
+
kvox-eval ask --replay the scripted self-test (no model, no key)
|
|
67
|
+
reads the API key from ${index_js_3.KVOX_EVAL_API_KEY_ENV} (never a flag); default model ${run_ask_eval_js_1.DEFAULT_ASK_EVAL_MODEL}`;
|
|
68
|
+
/** A command line that cannot run; printed with the usage text. */
|
|
69
|
+
class UsageError extends Error {
|
|
70
|
+
}
|
|
71
|
+
/** Flags `--replay` does not take: the replay's questions, model and gate are fixed. */
|
|
72
|
+
const LIVE_ONLY_FLAGS = ['provider', 'model', 'base-url', 'only', 'category', 'min-match'];
|
|
73
|
+
/**
|
|
74
|
+
* Runs `kvox-eval ask` with the given arguments (everything after `ask`).
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const code = await runAskCommand(['--replay'], { out: console.log, err: console.error, env: process.env });
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @param argv - the arguments.
|
|
82
|
+
* @param io - output streams and environment.
|
|
83
|
+
* @param options - the host's name, usage text and runs directory.
|
|
84
|
+
* @returns the exit code ({@link ASK_EVAL_EXIT}).
|
|
85
|
+
* @stability experimental
|
|
86
|
+
*/
|
|
87
|
+
async function runAskCommand(argv, io, options = {}) {
|
|
88
|
+
const name = options.name ?? 'kvox-eval ask';
|
|
89
|
+
const usage = options.usage ?? exports.ASK_COMMAND_USAGE;
|
|
90
|
+
try {
|
|
91
|
+
return await run(argv, io, options, name, usage);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err instanceof UsageError) {
|
|
95
|
+
io.err(`${name}: ${err.message}`);
|
|
96
|
+
io.err(usage);
|
|
97
|
+
return exports.ASK_EVAL_EXIT.config;
|
|
98
|
+
}
|
|
99
|
+
if (err instanceof index_js_1.AskEvalQuestionLoadError || err instanceof index_js_2.KgEvalLoadError || err instanceof index_js_4.LiveProviderError) {
|
|
100
|
+
io.err(`${name}: ${err.message}`);
|
|
101
|
+
return exports.ASK_EVAL_EXIT.config;
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function run(argv, io, options, name, usage) {
|
|
107
|
+
let values;
|
|
108
|
+
try {
|
|
109
|
+
({ values } = (0, util_1.parseArgs)({
|
|
110
|
+
args: argv,
|
|
111
|
+
strict: true,
|
|
112
|
+
allowPositionals: false,
|
|
113
|
+
options: {
|
|
114
|
+
provider: { type: 'string' },
|
|
115
|
+
model: { type: 'string' },
|
|
116
|
+
'base-url': { type: 'string' },
|
|
117
|
+
only: { type: 'string' },
|
|
118
|
+
category: { type: 'string' },
|
|
119
|
+
out: { type: 'string' },
|
|
120
|
+
'min-match': { type: 'string' },
|
|
121
|
+
replay: { type: 'boolean' },
|
|
122
|
+
help: { type: 'boolean' },
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
throw new UsageError(err.message);
|
|
128
|
+
}
|
|
129
|
+
const str = (k) => (typeof values[k] === 'string' ? values[k] : undefined);
|
|
130
|
+
if (values.help) {
|
|
131
|
+
io.out(usage);
|
|
132
|
+
return exports.ASK_EVAL_EXIT.ok;
|
|
133
|
+
}
|
|
134
|
+
const log = (line) => io.err(`${name}: ${line}`);
|
|
135
|
+
if (values.replay) {
|
|
136
|
+
const extra = LIVE_ONLY_FLAGS.filter((flag) => values[flag] !== undefined);
|
|
137
|
+
if (extra.length > 0)
|
|
138
|
+
throw new UsageError(`--replay takes no ${extra.map((f) => `--${f}`).join(', ')}`);
|
|
139
|
+
return replay(io, log, outDirFor(str('out'), options));
|
|
140
|
+
}
|
|
141
|
+
const minMatchArg = str('min-match');
|
|
142
|
+
const minMatch = minMatchArg !== undefined ? Number(minMatchArg) : 0.8;
|
|
143
|
+
if (!Number.isFinite(minMatch) || minMatch < 0 || minMatch > 1)
|
|
144
|
+
throw new UsageError('--min-match must be a number between 0 and 1');
|
|
145
|
+
// The provider is refused (copilot, an unknown id) before the key is looked for,
|
|
146
|
+
// and the key gate runs before anything is loaded or started: a run with no key
|
|
147
|
+
// is a configuration error, not a run that seeds a graph and then fails.
|
|
148
|
+
const provider = str('provider') ?? 'openai';
|
|
149
|
+
(0, index_js_4.assertLiveProvider)(provider);
|
|
150
|
+
const model = str('model') ?? run_ask_eval_js_1.DEFAULT_ASK_EVAL_MODEL;
|
|
151
|
+
const key = (0, index_js_3.readEvalApiKey)(io.env, io.err);
|
|
152
|
+
if (key === null && provider === 'openai') {
|
|
153
|
+
io.err(`${name}: reads its API key from ${index_js_3.KVOX_EVAL_API_KEY_ENV}; it is not set`);
|
|
154
|
+
return exports.ASK_EVAL_EXIT.config;
|
|
155
|
+
}
|
|
156
|
+
const baseUrl = str('base-url');
|
|
157
|
+
const { ai, failures } = (0, index_js_4.createLiveKvoxAi)({
|
|
158
|
+
provider,
|
|
159
|
+
model,
|
|
160
|
+
apiKey: key?.apiKey ?? null,
|
|
161
|
+
...(baseUrl !== undefined ? { baseUrl } : {}),
|
|
162
|
+
});
|
|
163
|
+
const only = str('only')
|
|
164
|
+
?.split(',')
|
|
165
|
+
.map((s) => s.trim())
|
|
166
|
+
.filter(Boolean);
|
|
167
|
+
const questions = (0, index_js_1.selectAskEvalQuestions)((0, index_js_2.loadGoldenAskQuestions)(), only ?? null, str('category') ?? null);
|
|
168
|
+
if (questions.length === 0) {
|
|
169
|
+
io.err(`${name}: no questions selected`);
|
|
170
|
+
return exports.ASK_EVAL_EXIT.config;
|
|
171
|
+
}
|
|
172
|
+
// A malformed fixture is a configuration error (`KgEvalLoadError`), and must not seed a partial graph.
|
|
173
|
+
const fixtures = (0, index_js_2.loadGoldenSet)();
|
|
174
|
+
const outDir = outDirFor(str('out'), options);
|
|
175
|
+
let result;
|
|
176
|
+
try {
|
|
177
|
+
result = await (0, run_ask_eval_js_1.runAskEval)({ ai, model, questions, fixtures, minMatch, log });
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
// A failure the run cannot go on from (a rate limit before any text, which
|
|
181
|
+
// a queue would re-run later): reported by class, code and status only.
|
|
182
|
+
if (err instanceof ports_1.KvoxAiError || err instanceof ports_1.KvoxRateLimitError) {
|
|
183
|
+
reportFailures(failures, log);
|
|
184
|
+
io.err(`${name}: the live call failed (${err instanceof ports_1.KvoxAiError ? err.errorClass : 'rate_limit'}); the run stopped`);
|
|
185
|
+
return exports.ASK_EVAL_EXIT.config;
|
|
186
|
+
}
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
reportFailures(failures, log);
|
|
190
|
+
io.out((0, index_js_1.formatAskEvalReport)(result.report));
|
|
191
|
+
(0, fs_1.mkdirSync)(outDir, { recursive: true });
|
|
192
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(outDir, 'report.json'), `${JSON.stringify(result.report, null, 2)}\n`);
|
|
193
|
+
const answers = new Map(result.results.map((r) => [r.question.id, { question: r.question.question, content: r.message.content }]));
|
|
194
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(outDir, 'answers.md'), (0, index_js_1.formatAnswersMarkdown)(result.report, answers));
|
|
195
|
+
log(`report written to ${outDir}`);
|
|
196
|
+
// Not one turn reached the model: a configuration problem (a wrong key, model
|
|
197
|
+
// or base URL), not a measurement.
|
|
198
|
+
if (result.results.every((r) => r.modelError !== undefined)) {
|
|
199
|
+
io.err(`${name}: every model call failed (${[...new Set(result.results.map((r) => r.modelError?.errorClass))].join(', ')}); check the key, model and base URL`);
|
|
200
|
+
return exports.ASK_EVAL_EXIT.config;
|
|
201
|
+
}
|
|
202
|
+
if (!result.report.gatesPassed || !result.report.answerMatchPassed)
|
|
203
|
+
return exports.ASK_EVAL_EXIT.failed;
|
|
204
|
+
return exports.ASK_EVAL_EXIT.ok;
|
|
205
|
+
}
|
|
206
|
+
/** `--out`, else `<runs dir>/runs/<stamp>`, else the system temporary directory. */
|
|
207
|
+
function outDirFor(out, options) {
|
|
208
|
+
if (out)
|
|
209
|
+
return (0, path_1.resolve)(out);
|
|
210
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
211
|
+
const root = options.syntheticRunsDir ?? (0, path_1.join)((0, os_1.tmpdir)(), 'kvox-eval', 'ask');
|
|
212
|
+
return (0, path_1.join)(root, 'runs', stamp);
|
|
213
|
+
}
|
|
214
|
+
/** How many provider round trips failed, by code and status (never a message body). */
|
|
215
|
+
function reportFailures(failures, log) {
|
|
216
|
+
if (failures.length === 0)
|
|
217
|
+
return;
|
|
218
|
+
const counts = new Map();
|
|
219
|
+
for (const f of failures) {
|
|
220
|
+
const label = `${f.code}${f.status !== null ? `, HTTP ${f.status}` : ''}`;
|
|
221
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
222
|
+
}
|
|
223
|
+
log(`${failures.length} provider round trip(s) failed: ${[...counts].map(([l, n]) => `${n} x (${l})`).join('; ')}`);
|
|
224
|
+
}
|
|
225
|
+
/** `--replay`: the six scripted questions, one line each, then the gates; `replay.json` under `--out`. */
|
|
226
|
+
async function replay(io, log, outDir) {
|
|
227
|
+
const run = await (0, replay_js_1.runAskReplay)({ log });
|
|
228
|
+
io.out('Ask evaluation replay (scripted runtime, no model)');
|
|
229
|
+
for (const r of run.results) {
|
|
230
|
+
const tools = r.message.toolCalls.map((c) => c.name).join(' > ') || '-';
|
|
231
|
+
const cites = r.message.citations.map((c) => `[^${c.marker}] ${c.valid ? 'valid' : 'INVALID'}`).join(', ') || 'none';
|
|
232
|
+
const verdict = r.failures.length === 0 ? 'ok' : `FAILED: ${r.failures.join('; ')}`;
|
|
233
|
+
io.out(` ${r.scenario.id} ${r.message.status} tools: ${tools} citations: ${cites} validity ${r.citationValidity.toFixed(2)} ${verdict}`);
|
|
234
|
+
}
|
|
235
|
+
io.out(` leak gate: ${run.leakFree ? 'ok (the sensitive fact reached no request and no answer)' : 'FAILED'}`);
|
|
236
|
+
io.out(run.passed ? 'replay passed' : 'replay FAILED');
|
|
237
|
+
(0, fs_1.mkdirSync)(outDir, { recursive: true });
|
|
238
|
+
const summary = {
|
|
239
|
+
passed: run.passed,
|
|
240
|
+
leakFree: run.leakFree,
|
|
241
|
+
scenarios: run.results.map((r) => ({
|
|
242
|
+
id: r.scenario.id,
|
|
243
|
+
title: r.scenario.title,
|
|
244
|
+
status: r.message.status,
|
|
245
|
+
toolCalls: r.message.toolCalls.map((c) => c.name),
|
|
246
|
+
citations: r.message.citations.map((c) => ({ marker: c.marker, kind: c.kind, valid: c.valid })),
|
|
247
|
+
citationValidity: r.citationValidity,
|
|
248
|
+
failures: r.failures,
|
|
249
|
+
})),
|
|
250
|
+
};
|
|
251
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(outDir, 'replay.json'), `${JSON.stringify(summary, null, 2)}\n`);
|
|
252
|
+
log(`replay summary written to ${outDir}`);
|
|
253
|
+
return run.passed ? exports.ASK_EVAL_EXIT.ok : exports.ASK_EVAL_EXIT.failed;
|
|
254
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import type { INestApplicationContext, Type } from '@nestjs/common';
|
|
3
|
+
import type { KvoxAiPort, KvoxSettingsPort } from '@marinoscar/kvox-api/ports';
|
|
4
|
+
import { type AskEvalPrismaClient } from './db.js';
|
|
5
|
+
/**
|
|
6
|
+
* The evaluation user's email. A reserved domain (`.invalid`): the account exists
|
|
7
|
+
* only in the run's in-memory database.
|
|
8
|
+
*
|
|
9
|
+
* @stability experimental
|
|
10
|
+
*/
|
|
11
|
+
export declare const ASK_EVAL_USER_EMAIL = "ask-eval@kvox-eval.invalid";
|
|
12
|
+
/**
|
|
13
|
+
* What {@link startAskEvalHost} is built from.
|
|
14
|
+
*
|
|
15
|
+
* @stability experimental
|
|
16
|
+
*/
|
|
17
|
+
export interface StartAskEvalHostOptions {
|
|
18
|
+
/** The model port every turn runs on (the port under test). */
|
|
19
|
+
ai: KvoxAiPort;
|
|
20
|
+
/** Receives progress lines (steps and counts, never data). */
|
|
21
|
+
log?: (line: string) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A running evaluation host.
|
|
25
|
+
*
|
|
26
|
+
* @stability experimental
|
|
27
|
+
*/
|
|
28
|
+
export interface AskEvalHost {
|
|
29
|
+
/** The Nest application context the slices run in. */
|
|
30
|
+
readonly app: INestApplicationContext;
|
|
31
|
+
/** The client on the in-memory database. */
|
|
32
|
+
readonly prisma: AskEvalPrismaClient;
|
|
33
|
+
/** The model port the slices were given. */
|
|
34
|
+
readonly ai: KvoxAiPort;
|
|
35
|
+
/** The settings port the slices were given (in memory; the `ai` namespace declared). */
|
|
36
|
+
readonly settings: KvoxSettingsPort;
|
|
37
|
+
/** The evaluation user (owner of every seeded row). */
|
|
38
|
+
readonly userId: string;
|
|
39
|
+
/** The default organization (adopted from the platform migrations). */
|
|
40
|
+
readonly orgId: string;
|
|
41
|
+
/** A provider from anywhere in the container (`app.get(type, { strict: false })`). */
|
|
42
|
+
resolve<T>(type: Type<T>): T;
|
|
43
|
+
/** Closes the application context, the client and the database. Safe to call twice. */
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Starts the in-memory database, adopts its default organization, creates the
|
|
48
|
+
* evaluation user and boots the domain slices over the evaluation's ports.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* const host = await startAskEvalHost({ ai });
|
|
53
|
+
* try {
|
|
54
|
+
* const seeded = await seedGoldenGraph(host, loadGoldenSet());
|
|
55
|
+
* // ... post questions, run the handler
|
|
56
|
+
* } finally {
|
|
57
|
+
* await host.close();
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @param options - the model port and a progress sink.
|
|
62
|
+
* @returns the host; call `close()` when done.
|
|
63
|
+
* @stability experimental
|
|
64
|
+
*/
|
|
65
|
+
export declare function startAskEvalHost(options: StartAskEvalHostOptions): Promise<AskEvalHost>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// The headless Ask evaluation's host: PGlite + the domain slices on Nest (#608)
|
|
4
|
+
// =============================================================================
|
|
5
|
+
//
|
|
6
|
+
// What `ask.respond` needs and nothing more, as a Nest APPLICATION CONTEXT (no
|
|
7
|
+
// HTTP server): the ports module, then the transcription, search, transcripts,
|
|
8
|
+
// notes, graph and Ask slices, every optional feature switched off the way the
|
|
9
|
+
// bare host (examples/bare-host) switches them off:
|
|
10
|
+
//
|
|
11
|
+
// schedules: false no `@nestjs/schedule` timers; nothing housekeeps a
|
|
12
|
+
// database that lives for one run
|
|
13
|
+
// media.transcode: false no ffmpeg
|
|
14
|
+
// exporters / documentSources pdf, docx: false no pdfkit, docx, unpdf
|
|
15
|
+
// providers.assemblyai: false no transcription provider at all: the seeded
|
|
16
|
+
// transcripts are already `ready`
|
|
17
|
+
// semantic search off the seeded rows carry no embeddings, and a query
|
|
18
|
+
// embedding would spend the key under test
|
|
19
|
+
//
|
|
20
|
+
// NOT graph-extraction and NOT graph-rdf: the golden graph is seeded from the
|
|
21
|
+
// fixtures' HAND LABELS through `GraphWriteService`, so the evaluation isolates
|
|
22
|
+
// the agent (extraction quality is `kvox-eval kg`'s question).
|
|
23
|
+
//
|
|
24
|
+
// THE `peers` OPTION passes the very module objects this module imports, so the
|
|
25
|
+
// container holds one module per slice and each job handler registers once.
|
|
26
|
+
//
|
|
27
|
+
// The host module is a plain class: a dynamic module needs no decorator, and the
|
|
28
|
+
// package compiles without decorator metadata (the slices carry their own).
|
|
29
|
+
// =============================================================================
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.ASK_EVAL_USER_EMAIL = void 0;
|
|
32
|
+
exports.startAskEvalHost = startAskEvalHost;
|
|
33
|
+
require("reflect-metadata");
|
|
34
|
+
const core_1 = require("@nestjs/core");
|
|
35
|
+
const ask_1 = require("@marinoscar/kvox-api/ask");
|
|
36
|
+
const graph_1 = require("@marinoscar/kvox-api/graph");
|
|
37
|
+
const notes_1 = require("@marinoscar/kvox-api/notes");
|
|
38
|
+
const search_1 = require("@marinoscar/kvox-api/search");
|
|
39
|
+
const testing_1 = require("@marinoscar/kvox-api/testing");
|
|
40
|
+
const transcription_1 = require("@marinoscar/kvox-api/transcription");
|
|
41
|
+
const transcripts_1 = require("@marinoscar/kvox-api/transcripts");
|
|
42
|
+
const db_js_1 = require("./db.js");
|
|
43
|
+
const ports_js_1 = require("./ports.js");
|
|
44
|
+
/**
|
|
45
|
+
* The evaluation user's email. A reserved domain (`.invalid`): the account exists
|
|
46
|
+
* only in the run's in-memory database.
|
|
47
|
+
*
|
|
48
|
+
* @stability experimental
|
|
49
|
+
*/
|
|
50
|
+
exports.ASK_EVAL_USER_EMAIL = 'ask-eval@kvox-eval.invalid';
|
|
51
|
+
/** The domain slices `ask.respond` needs, wired like the bare host's, minus extraction and RDF. */
|
|
52
|
+
function createAskEvalModule(ports) {
|
|
53
|
+
const transcription = transcription_1.TranscriptionModule.forRoot({ providers: { assemblyai: false } });
|
|
54
|
+
const searchIndexing = search_1.SearchIndexingModule.forRoot();
|
|
55
|
+
const transcripts = transcripts_1.TranscriptsModule.forRoot({
|
|
56
|
+
schedules: false,
|
|
57
|
+
media: { transcode: false },
|
|
58
|
+
exporters: { pdf: false },
|
|
59
|
+
peers: { transcription, searchIndexing },
|
|
60
|
+
});
|
|
61
|
+
const notes = notes_1.NotesModule.forRoot({
|
|
62
|
+
schedules: false,
|
|
63
|
+
exporters: { pdf: false, docx: false },
|
|
64
|
+
documentSources: { pdf: false },
|
|
65
|
+
peers: { transcripts, searchIndexing },
|
|
66
|
+
});
|
|
67
|
+
const search = search_1.SearchModule.forRoot({ semantic: { enabled: false } });
|
|
68
|
+
const graph = graph_1.GraphModule.forRoot({ schedules: false, peers: { transcripts, notes, search } });
|
|
69
|
+
const ask = ask_1.AskModule.forRoot({ peers: { graph, notes, search } });
|
|
70
|
+
class AskEvalHostModule {
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
module: AskEvalHostModule,
|
|
74
|
+
imports: [(0, testing_1.createKvoxTestingModule)(ports), transcription, search, searchIndexing, transcripts, notes, graph, ask],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Starts the in-memory database, adopts its default organization, creates the
|
|
79
|
+
* evaluation user and boots the domain slices over the evaluation's ports.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* const host = await startAskEvalHost({ ai });
|
|
84
|
+
* try {
|
|
85
|
+
* const seeded = await seedGoldenGraph(host, loadGoldenSet());
|
|
86
|
+
* // ... post questions, run the handler
|
|
87
|
+
* } finally {
|
|
88
|
+
* await host.close();
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @param options - the model port and a progress sink.
|
|
93
|
+
* @returns the host; call `close()` when done.
|
|
94
|
+
* @stability experimental
|
|
95
|
+
*/
|
|
96
|
+
async function startAskEvalHost(options) {
|
|
97
|
+
const log = options.log ?? (() => { });
|
|
98
|
+
const database = await (0, db_js_1.startAskEvalDatabase)(log);
|
|
99
|
+
const { prisma } = database;
|
|
100
|
+
let app = null;
|
|
101
|
+
try {
|
|
102
|
+
// The platform's own migrations create the default organization (its partial
|
|
103
|
+
// unique index allows exactly one): adopt it, as the bare host does.
|
|
104
|
+
const org = await prisma.organization.findFirst({ where: { isDefault: true }, select: { id: true } });
|
|
105
|
+
if (!org)
|
|
106
|
+
throw new Error('The platform migrations created no default organization.');
|
|
107
|
+
const user = await prisma.user.create({
|
|
108
|
+
data: { email: exports.ASK_EVAL_USER_EMAIL, displayName: 'Ask evaluation', isActive: true },
|
|
109
|
+
select: { id: true },
|
|
110
|
+
});
|
|
111
|
+
const settings = (0, ports_js_1.createAskEvalSettings)();
|
|
112
|
+
app = await core_1.NestFactory.createApplicationContext(createAskEvalModule({
|
|
113
|
+
overrides: {
|
|
114
|
+
ai: options.ai,
|
|
115
|
+
prisma: prisma,
|
|
116
|
+
settings,
|
|
117
|
+
jobs: (0, ports_js_1.createAskEvalJobs)(org.id),
|
|
118
|
+
identity: (0, ports_js_1.createAskEvalIdentity)(user.id, org.id),
|
|
119
|
+
},
|
|
120
|
+
}), { logger: ['error', 'warn'] });
|
|
121
|
+
log('host: slices started');
|
|
122
|
+
const context = app;
|
|
123
|
+
let closed = false;
|
|
124
|
+
return {
|
|
125
|
+
app: context,
|
|
126
|
+
prisma,
|
|
127
|
+
ai: options.ai,
|
|
128
|
+
settings,
|
|
129
|
+
userId: user.id,
|
|
130
|
+
orgId: org.id,
|
|
131
|
+
resolve: (type) => context.get(type, { strict: false }),
|
|
132
|
+
async close() {
|
|
133
|
+
if (closed)
|
|
134
|
+
return;
|
|
135
|
+
closed = true;
|
|
136
|
+
await context.close().catch(() => undefined);
|
|
137
|
+
await database.close();
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
await app?.close().catch(() => undefined);
|
|
143
|
+
await database.close();
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PrismaClient } from '../../../host/generated/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* The package's `host/` folder: `prisma/migrations/` and `generated/` (built by
|
|
4
|
+
* `scripts/prepare-host.mjs`). `dist/ask-run/host/db.js` and `src/ask-run/host/db.ts`
|
|
5
|
+
* are both three levels below the package root.
|
|
6
|
+
*
|
|
7
|
+
* @stability experimental
|
|
8
|
+
*/
|
|
9
|
+
export declare const ASK_EVAL_HOST_DIR: string;
|
|
10
|
+
/**
|
|
11
|
+
* The Prisma client of the evaluation's database: the platform and domain schema,
|
|
12
|
+
* generated into `host/generated/` at build time.
|
|
13
|
+
*
|
|
14
|
+
* @stability experimental
|
|
15
|
+
*/
|
|
16
|
+
export type AskEvalPrismaClient = PrismaClient;
|
|
17
|
+
/**
|
|
18
|
+
* A started, fully migrated in-memory database and the client on it.
|
|
19
|
+
*
|
|
20
|
+
* @stability experimental
|
|
21
|
+
*/
|
|
22
|
+
export interface AskEvalDatabase {
|
|
23
|
+
/** The client (one pooled connection). */
|
|
24
|
+
readonly prisma: AskEvalPrismaClient;
|
|
25
|
+
/** How many migration folders were applied. */
|
|
26
|
+
readonly migrations: number;
|
|
27
|
+
/** Disconnects the client, stops the socket and closes PGlite. Safe to call twice. */
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Starts PGlite in memory, applies every shipped migration, serves it on a
|
|
32
|
+
* loopback socket and connects a Prisma client to it.
|
|
33
|
+
*
|
|
34
|
+
* @param log - receives one line per step (never data).
|
|
35
|
+
* @returns the database; call `close()` when done.
|
|
36
|
+
* @stability experimental
|
|
37
|
+
*/
|
|
38
|
+
export declare function startAskEvalDatabase(log?: (line: string) => void): Promise<AskEvalDatabase>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// The in-memory database of the headless Ask evaluation (issue #608)
|
|
4
|
+
// =============================================================================
|
|
5
|
+
//
|
|
6
|
+
// PGlite (PostgreSQL compiled to WebAssembly) in THIS process, with `vector`,
|
|
7
|
+
// `pg_trgm` and `uuid-ossp`, served on a loopback socket so the Prisma client's
|
|
8
|
+
// `pg` driver adapter can reach it. The data lives in memory and dies with the
|
|
9
|
+
// run: nothing an evaluation does ever touches a real database, so there is no
|
|
10
|
+
// safe-database guard to pass any more.
|
|
11
|
+
//
|
|
12
|
+
// THE SCHEMA. Extensions first (the bootstrap superuser's job, never a
|
|
13
|
+
// migration's), then every migration file the package ships under
|
|
14
|
+
// `host/prisma/migrations/` (the platform history stamped 2024-01-01, then the
|
|
15
|
+
// domain's under stable names, so the folder names sort in apply order), each
|
|
16
|
+
// executed as one simple-protocol script, exactly what `prisma migrate deploy`
|
|
17
|
+
// sends. No Prisma CLI runs at evaluation time; `scripts/prepare-host.mjs`
|
|
18
|
+
// composed the schema and generated the client at build time.
|
|
19
|
+
//
|
|
20
|
+
// ONE SESSION. PGlite serialises every query onto one backend, so the client
|
|
21
|
+
// uses a pool of ONE connection: an interactive transaction holds the only
|
|
22
|
+
// connection and anything outside it waits. A port implementation must never
|
|
23
|
+
// await a query on the pool while holding a transaction open (on PGlite that is
|
|
24
|
+
// a deadlock); the fake jobs port of `ports.ts` writes through the caller's tx.
|
|
25
|
+
// =============================================================================
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.ASK_EVAL_HOST_DIR = void 0;
|
|
28
|
+
exports.startAskEvalDatabase = startAskEvalDatabase;
|
|
29
|
+
const node_fs_1 = require("node:fs");
|
|
30
|
+
const node_net_1 = require("node:net");
|
|
31
|
+
const node_path_1 = require("node:path");
|
|
32
|
+
const pglite_1 = require("@electric-sql/pglite");
|
|
33
|
+
const pg_trgm_1 = require("@electric-sql/pglite/contrib/pg_trgm");
|
|
34
|
+
const uuid_ossp_1 = require("@electric-sql/pglite/contrib/uuid_ossp");
|
|
35
|
+
const pglite_pgvector_1 = require("@electric-sql/pglite-pgvector");
|
|
36
|
+
const pglite_socket_1 = require("@electric-sql/pglite-socket");
|
|
37
|
+
const adapter_pg_1 = require("@prisma/adapter-pg");
|
|
38
|
+
const index_js_1 = require("../../../host/generated/index.js");
|
|
39
|
+
/**
|
|
40
|
+
* The package's `host/` folder: `prisma/migrations/` and `generated/` (built by
|
|
41
|
+
* `scripts/prepare-host.mjs`). `dist/ask-run/host/db.js` and `src/ask-run/host/db.ts`
|
|
42
|
+
* are both three levels below the package root.
|
|
43
|
+
*
|
|
44
|
+
* @stability experimental
|
|
45
|
+
*/
|
|
46
|
+
exports.ASK_EVAL_HOST_DIR = (0, node_path_1.join)(__dirname, '..', '..', '..', 'host');
|
|
47
|
+
/** A free loopback port (bound and released). */
|
|
48
|
+
function freePort() {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const probe = (0, node_net_1.createServer)();
|
|
51
|
+
probe.on('error', reject);
|
|
52
|
+
probe.listen(0, '127.0.0.1', () => {
|
|
53
|
+
const address = probe.address();
|
|
54
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
55
|
+
probe.close(() => resolve(port));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** The shipped migration folders, in apply order. */
|
|
60
|
+
function migrationFolders() {
|
|
61
|
+
const dir = (0, node_path_1.join)(exports.ASK_EVAL_HOST_DIR, 'prisma', 'migrations');
|
|
62
|
+
let names;
|
|
63
|
+
try {
|
|
64
|
+
names = (0, node_fs_1.readdirSync)(dir);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error(`The evaluation's database host is missing (${dir}); build the package first (node scripts/prepare-host.mjs).`);
|
|
68
|
+
}
|
|
69
|
+
return names
|
|
70
|
+
.filter((name) => (0, node_fs_1.statSync)((0, node_path_1.join)(dir, name)).isDirectory())
|
|
71
|
+
.sort()
|
|
72
|
+
.map((name) => (0, node_path_1.join)(dir, name, 'migration.sql'));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Starts PGlite in memory, applies every shipped migration, serves it on a
|
|
76
|
+
* loopback socket and connects a Prisma client to it.
|
|
77
|
+
*
|
|
78
|
+
* @param log - receives one line per step (never data).
|
|
79
|
+
* @returns the database; call `close()` when done.
|
|
80
|
+
* @stability experimental
|
|
81
|
+
*/
|
|
82
|
+
async function startAskEvalDatabase(log = () => { }) {
|
|
83
|
+
const files = migrationFolders();
|
|
84
|
+
const db = await pglite_1.PGlite.create({ extensions: { vector: pglite_pgvector_1.vector, pg_trgm: pg_trgm_1.pg_trgm, uuid_ossp: uuid_ossp_1.uuid_ossp } });
|
|
85
|
+
let server = null;
|
|
86
|
+
let prisma = null;
|
|
87
|
+
const close = async () => {
|
|
88
|
+
const [p, s] = [prisma, server];
|
|
89
|
+
prisma = null;
|
|
90
|
+
server = null;
|
|
91
|
+
await p?.$disconnect().catch(() => undefined);
|
|
92
|
+
await s?.stop().catch(() => undefined);
|
|
93
|
+
if (!db.closed)
|
|
94
|
+
await db.close().catch(() => undefined);
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
// What the application's bootstrap superuser does before the first migration.
|
|
98
|
+
await db.exec('CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm;');
|
|
99
|
+
log(`database: applying ${files.length} migrations in memory`);
|
|
100
|
+
for (const file of files)
|
|
101
|
+
await db.exec((0, node_fs_1.readFileSync)(file, 'utf8'));
|
|
102
|
+
const port = await freePort();
|
|
103
|
+
server = new pglite_socket_1.PGLiteSocketServer({ db, port, host: '127.0.0.1', maxConnections: 1 });
|
|
104
|
+
await server.start();
|
|
105
|
+
const url = `postgresql://postgres@127.0.0.1:${port}/postgres?sslmode=disable`;
|
|
106
|
+
prisma = new index_js_1.PrismaClient({ adapter: new adapter_pg_1.PrismaPg({ connectionString: url, max: 1 }) });
|
|
107
|
+
await prisma.$connect();
|
|
108
|
+
log('database: ready');
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
await close();
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
const client = prisma;
|
|
115
|
+
let closed = false;
|
|
116
|
+
return {
|
|
117
|
+
prisma: client,
|
|
118
|
+
migrations: files.length,
|
|
119
|
+
async close() {
|
|
120
|
+
if (closed)
|
|
121
|
+
return;
|
|
122
|
+
closed = true;
|
|
123
|
+
await close();
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { KvoxJobsPort, KvoxSettingsPort } from '@marinoscar/kvox-api/ports';
|
|
2
|
+
import { type FakeKvoxIdentity } from '@marinoscar/kvox-api/testing';
|
|
3
|
+
/**
|
|
4
|
+
* The permissions the evaluation user holds: what `ask.respond` probes for its
|
|
5
|
+
* actor. Read-only on purpose; the agent writes nothing.
|
|
6
|
+
*
|
|
7
|
+
* @stability experimental
|
|
8
|
+
*/
|
|
9
|
+
export declare const ASK_EVAL_PERMISSIONS: readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* In-memory settings with the system `ai` namespace declared and the graph switch on.
|
|
12
|
+
*
|
|
13
|
+
* @returns the settings port.
|
|
14
|
+
* @stability experimental
|
|
15
|
+
*/
|
|
16
|
+
export declare function createAskEvalSettings(): KvoxSettingsPort;
|
|
17
|
+
/**
|
|
18
|
+
* The fake queue, with `enqueueWithin` writing a real `jobs` row in the caller's
|
|
19
|
+
* transaction (the turn's `ask_messages.job_id` references it). Nothing claims
|
|
20
|
+
* the row: the runner hands it to the handler directly.
|
|
21
|
+
*
|
|
22
|
+
* @param orgId - the organization every job row is stamped with (the default one).
|
|
23
|
+
* @returns the jobs port.
|
|
24
|
+
* @stability experimental
|
|
25
|
+
*/
|
|
26
|
+
export declare function createAskEvalJobs(orgId: string): KvoxJobsPort;
|
|
27
|
+
/**
|
|
28
|
+
* One principal: the evaluation user with {@link ASK_EVAL_PERMISSIONS}.
|
|
29
|
+
*
|
|
30
|
+
* @param userId - the evaluation user's id.
|
|
31
|
+
* @param orgId - the default organization's id.
|
|
32
|
+
* @returns the identity port.
|
|
33
|
+
* @stability experimental
|
|
34
|
+
*/
|
|
35
|
+
export declare function createAskEvalIdentity(userId: string, orgId: string): FakeKvoxIdentity;
|