@rubytech/create-maxy-code 0.1.169 → 0.1.171
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/premium-mcp-discover.test.js +42 -0
- package/dist/lib/premium-mcp-discover.js +9 -2
- package/package.json +1 -1
- package/payload/platform/plugins/.claude-plugin/marketplace.json +0 -15
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/premium-plugins/writer-craft/lib/mcp-stderr-tee/dist/index.d.ts +51 -0
- package/payload/premium-plugins/writer-craft/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -0
- package/payload/premium-plugins/writer-craft/lib/mcp-stderr-tee/dist/index.js +196 -0
- package/payload/premium-plugins/writer-craft/lib/mcp-stderr-tee/dist/index.js.map +1 -0
- package/payload/premium-plugins/writer-craft/lib/mcp-stderr-tee/package.json +7 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts +3 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/index.js +257 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/index.js.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/neo4j.d.ts +14 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/neo4j.d.ts.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/neo4j.js +47 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/neo4j.js.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts +42 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts.map +1 -0
- package/payload/{platform/plugins/writer-craft/mcp/src/lib/voice-corpus.ts → premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js} +5 -5
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts +36 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js +225 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts +26 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js +74 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts +22 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js +102 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts +14 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js +84 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/package-lock.json +1327 -0
- package/payload/server/{chunk-3TRIXQSJ.js → chunk-L2YK2VK3.js} +17 -29
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/server.js +1 -1
- package/payload/platform/plugins/teaching/.claude-plugin/plugin.json +0 -8
- package/payload/platform/plugins/teaching/PLUGIN.md +0 -58
- package/payload/platform/plugins/teaching/skills/interactive-tutor/SKILL.md +0 -59
- package/payload/platform/plugins/teaching/skills/interactive-tutor/references/assessment.md +0 -70
- package/payload/platform/plugins/teaching/skills/interactive-tutor/references/classroom-conduct.md +0 -43
- package/payload/platform/plugins/teaching/skills/interactive-tutor/references/teaching-modes.md +0 -83
- package/payload/platform/plugins/teaching/skills/lesson-planner/SKILL.md +0 -48
- package/payload/platform/plugins/teaching/skills/lesson-planner/references/context-gathering.md +0 -41
- package/payload/platform/plugins/teaching/skills/lesson-planner/references/plan-structure.md +0 -94
- package/payload/platform/plugins/teaching/skills/study-pack-builder/SKILL.md +0 -52
- package/payload/platform/plugins/teaching/skills/study-pack-builder/references/disaggregation.md +0 -49
- package/payload/platform/plugins/teaching/skills/study-pack-builder/references/materials.md +0 -116
- package/payload/platform/plugins/venture-studio/.claude-plugin/plugin.json +0 -8
- package/payload/platform/plugins/venture-studio/PLUGIN.md +0 -119
- package/payload/platform/plugins/venture-studio/bin/scaffold.sh +0 -116
- package/payload/platform/plugins/venture-studio/skills/brand-pack/SKILL.md +0 -256
- package/payload/platform/plugins/venture-studio/skills/brand-pack/references/color-psychology.md +0 -118
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/SKILL.md +0 -376
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/business-plan-template.md +0 -64
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/compliance-research-checklist.md +0 -53
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +0 -88
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/deck-blueprint-template.md +0 -39
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/design-tokens-application.md +0 -79
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/html-pdf-pipeline.md +0 -236
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/internal-workings-scrub.md +0 -33
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/termsheet-template.md +0 -88
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/templates/prospectus/index.html +0 -1565
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/templates/prospectus/render-pdf.mjs +0 -91
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/templates/prospectus/term_sheet.html +0 -715
- package/payload/platform/plugins/venture-studio/skills/office-hours/SKILL.md +0 -587
- package/payload/platform/plugins/venture-studio/skills/prototype-host/SKILL.md +0 -179
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/cloudflared-ingress-edit.md +0 -81
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/scaffold-frameworks.md +0 -60
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/systemd-user-service.md +0 -104
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/SKILL.md +0 -336
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/aarrr-metrics.md +0 -275
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/assumption-testing.md +0 -93
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/boolean-search.md +0 -308
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/build-measure-learn.md +0 -262
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/business-model-canvas.md +0 -171
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/commitment-signals.md +0 -246
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/design-thinking.md +0 -183
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/earlyvangelist.md +0 -190
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/first-principles.md +0 -58
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/fishbone.md +0 -114
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/five-whys.md +0 -43
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/ice-scoring.md +0 -237
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/innovation-accounting.md +0 -290
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/jtbd.md +0 -105
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/landing-page.md +0 -361
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/market-type.md +0 -167
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/mom-test.md +0 -193
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/mvp-types.md +0 -200
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/og-images.md +0 -239
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/pareto.md +0 -103
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/persona-development.md +0 -291
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/pivot-types.md +0 -225
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/positioning-statement.md +0 -179
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/prd.md +0 -363
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/pre-mortem.md +0 -74
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/problem-validation.md +0 -253
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/product-market-fit.md +0 -256
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/research-synthesis.md +0 -276
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/three-engines-of-growth.md +0 -248
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/validation-tests.md +0 -89
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/value-proposition-canvas.md +0 -121
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/win-loss-analysis.md +0 -242
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/workflow-mapping.md +0 -271
- package/payload/platform/plugins/writer-craft/.claude-plugin/plugin.json +0 -17
- package/payload/platform/plugins/writer-craft/PLUGIN.md +0 -130
- package/payload/platform/plugins/writer-craft/agents/writer-craft--manuscript-reviewer.md +0 -96
- package/payload/platform/plugins/writer-craft/mcp/package.json +0 -19
- package/payload/platform/plugins/writer-craft/mcp/scripts/smoke.mjs +0 -152
- package/payload/platform/plugins/writer-craft/mcp/src/index.ts +0 -289
- package/payload/platform/plugins/writer-craft/mcp/src/lib/neo4j.ts +0 -56
- package/payload/platform/plugins/writer-craft/mcp/src/tools/voice-distil-profile.ts +0 -303
- package/payload/platform/plugins/writer-craft/mcp/src/tools/voice-record-feedback.ts +0 -114
- package/payload/platform/plugins/writer-craft/mcp/src/tools/voice-retrieve-conditioning.ts +0 -145
- package/payload/platform/plugins/writer-craft/mcp/src/tools/voice-tag-content.ts +0 -117
- package/payload/platform/plugins/writer-craft/mcp/tsconfig.json +0 -8
- package/payload/platform/plugins/writer-craft/skills/citation-style/SKILL.md +0 -94
- package/payload/platform/plugins/writer-craft/skills/citation-style/references/book-and-chapter-models.md +0 -77
- package/payload/platform/plugins/writer-craft/skills/citation-style/references/citation-rules.md +0 -103
- package/payload/platform/plugins/writer-craft/skills/citation-style/references/journal-article-models.md +0 -74
- package/payload/platform/plugins/writer-craft/skills/citation-style/references/other-source-models.md +0 -146
- package/payload/platform/plugins/writer-craft/skills/citation-style/references/reference-list-rules.md +0 -70
- package/payload/platform/plugins/writer-craft/skills/editorial-practice/SKILL.md +0 -108
- package/payload/platform/plugins/writer-craft/skills/editorial-practice/references/copyediting.md +0 -73
- package/payload/platform/plugins/writer-craft/skills/editorial-practice/references/developmental-editing.md +0 -85
- package/payload/platform/plugins/writer-craft/skills/editorial-practice/references/genre-specific-editing.md +0 -78
- package/payload/platform/plugins/writer-craft/skills/editorial-practice/references/line-editing.md +0 -55
- package/payload/platform/plugins/writer-craft/skills/editorial-practice/references/self-editing.md +0 -89
- package/payload/platform/plugins/writer-craft/skills/persuasive-storytelling/SKILL.md +0 -114
- package/payload/platform/plugins/writer-craft/skills/persuasive-storytelling/references/audience-analysis.md +0 -73
- package/payload/platform/plugins/writer-craft/skills/persuasive-storytelling/references/crafting-persuasive-story.md +0 -76
- package/payload/platform/plugins/writer-craft/skills/persuasive-storytelling/references/persuasion-case-studies.md +0 -67
- package/payload/platform/plugins/writer-craft/skills/persuasive-storytelling/references/transformation-framework.md +0 -86
- package/payload/platform/plugins/writer-craft/skills/point-of-view/SKILL.md +0 -97
- package/payload/platform/plugins/writer-craft/skills/point-of-view/references/indirect-narration.md +0 -72
- package/payload/platform/plugins/writer-craft/skills/point-of-view/references/pov-types-and-voice.md +0 -91
- package/payload/platform/plugins/writer-craft/skills/point-of-view/references/protagonist-filter.md +0 -71
- package/payload/platform/plugins/writer-craft/skills/point-of-view/references/tense-and-person.md +0 -85
- package/payload/platform/plugins/writer-craft/skills/prose-craft/SKILL.md +0 -100
- package/payload/platform/plugins/writer-craft/skills/prose-craft/references/punctuation-and-grammar.md +0 -72
- package/payload/platform/plugins/writer-craft/skills/prose-craft/references/repetition.md +0 -71
- package/payload/platform/plugins/writer-craft/skills/prose-craft/references/sound-and-rhythm.md +0 -64
- package/payload/platform/plugins/writer-craft/skills/prose-craft/references/word-economy.md +0 -93
- package/payload/platform/plugins/writer-craft/skills/reader-engagement/SKILL.md +0 -100
- package/payload/platform/plugins/writer-craft/skills/reader-engagement/references/cause-effect-setup-payoff.md +0 -79
- package/payload/platform/plugins/writer-craft/skills/reader-engagement/references/conflict-escalation.md +0 -81
- package/payload/platform/plugins/writer-craft/skills/reader-engagement/references/hooking-readers.md +0 -67
- package/payload/platform/plugins/writer-craft/skills/reader-engagement/references/neurochemistry-of-engagement.md +0 -94
- package/payload/platform/plugins/writer-craft/skills/review-manuscript/SKILL.md +0 -111
- package/payload/platform/plugins/writer-craft/skills/review-manuscript/references/review-manuscript-checklist.md +0 -119
- package/payload/platform/plugins/writer-craft/skills/review-prose/SKILL.md +0 -99
- package/payload/platform/plugins/writer-craft/skills/review-prose/references/prose-review-checklist.md +0 -112
- package/payload/platform/plugins/writer-craft/skills/review-scene/SKILL.md +0 -99
- package/payload/platform/plugins/writer-craft/skills/review-scene/references/scene-analysis-framework.md +0 -95
- package/payload/platform/plugins/writer-craft/skills/story-architecture/SKILL.md +0 -106
- package/payload/platform/plugins/writer-craft/skills/story-architecture/references/blueprinting-and-scene-cards.md +0 -118
- package/payload/platform/plugins/writer-craft/skills/story-architecture/references/inner-issue-and-protagonist-goal.md +0 -66
- package/payload/platform/plugins/writer-craft/skills/story-architecture/references/misbelief-desire-worldview.md +0 -87
- package/payload/platform/plugins/writer-craft/skills/story-architecture/references/origin-scenes-and-escalation.md +0 -82
- package/payload/platform/plugins/writer-craft/skills/story-blueprint/SKILL.md +0 -133
- package/payload/platform/plugins/writer-craft/skills/story-blueprint/references/blueprinting-exercises.md +0 -118
- package/payload/platform/plugins/writer-craft/skills/story-blueprint/references/blueprinting-process.md +0 -128
- package/payload/platform/plugins/writer-craft/skills/voice-mirror/SKILL.md +0 -166
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal neo4j driver wrapper for the writer-craft MCP server.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors the public surface of `platform/plugins/memory/mcp/src/lib/neo4j.ts`:
|
|
5
|
-
* `getSession()` returns a session, `closeDriver()` releases the singleton.
|
|
6
|
-
* Password resolution follows the same `PLATFORM_ROOT/config/.neo4j-password`
|
|
7
|
-
* convention, with an explicit `NEO4J_URI` requirement to avoid silent
|
|
8
|
-
* cross-account orphaning.
|
|
9
|
-
*/
|
|
10
|
-
import neo4j, { Driver, Session } from "neo4j-driver";
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
|
-
import { resolve } from "node:path";
|
|
13
|
-
|
|
14
|
-
let driver: Driver | null = null;
|
|
15
|
-
|
|
16
|
-
function readPassword(): string {
|
|
17
|
-
if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
|
|
18
|
-
const passwordFile = resolve(
|
|
19
|
-
process.env.PLATFORM_ROOT ?? resolve(import.meta.dirname, "../../../../../platform"),
|
|
20
|
-
"config/.neo4j-password",
|
|
21
|
-
);
|
|
22
|
-
try {
|
|
23
|
-
return readFileSync(passwordFile, "utf-8").trim();
|
|
24
|
-
} catch {
|
|
25
|
-
throw new Error(
|
|
26
|
-
`Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`,
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function getDriver(): Driver {
|
|
32
|
-
if (!driver) {
|
|
33
|
-
const uri = process.env.NEO4J_URI;
|
|
34
|
-
if (!uri) {
|
|
35
|
-
throw new Error(
|
|
36
|
-
"[writer-craft] NEO4J_URI unset — refusing to default to bolt://localhost:7687",
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
const user = process.env.NEO4J_USER ?? "neo4j";
|
|
40
|
-
const password = readPassword();
|
|
41
|
-
process.stderr.write(`[writer-craft] resolved neo4j_uri=${uri}\n`);
|
|
42
|
-
driver = neo4j.driver(uri, neo4j.auth.basic(user, password));
|
|
43
|
-
}
|
|
44
|
-
return driver;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function getSession(): Session {
|
|
48
|
-
return getDriver().session();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function closeDriver(): Promise<void> {
|
|
52
|
-
if (driver) {
|
|
53
|
-
await driver.close();
|
|
54
|
-
driver = null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* voice-distil-profile — walk the operator's `human-only` corpus and
|
|
3
|
-
* persist a `:VoiceProfile` style card.
|
|
4
|
-
*
|
|
5
|
-
* Cadence guard: re-runs on ≥20% corpus growth since last profile, OR
|
|
6
|
-
* ≥30 days since `generatedAt`. `force: true` overrides both.
|
|
7
|
-
*
|
|
8
|
-
* Body-text sampling caps at ~120k chars (rough proxy for tokens at
|
|
9
|
-
* 4 chars/token = 30k tokens) — well inside Haiku's 200k context window
|
|
10
|
-
* with headroom for the system prompt + tool schema.
|
|
11
|
-
*/
|
|
12
|
-
import neo4j from "neo4j-driver";
|
|
13
|
-
import { getSession } from "../lib/neo4j.js";
|
|
14
|
-
import { VOICE_CORPUS_WHERE } from "../lib/voice-corpus.js";
|
|
15
|
-
|
|
16
|
-
// Task 424 — the Haiku style-card composition moved into the calling agent's
|
|
17
|
-
// turn. The agent reads the corpus exemplars and feedback intents (this tool
|
|
18
|
-
// returns them when called with `mode='sample'`), composes the style-card YAML,
|
|
19
|
-
// then calls this tool again with `mode='write'` + `styleCardYaml` set.
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Coerce a neo4j cypher RETURN value to a plain JS number.
|
|
23
|
-
*
|
|
24
|
-
* Neo4j-driver represents INTs as `Integer` objects (high/low 32-bit
|
|
25
|
-
* pair to safely carry int64). `Number()` *usually* works via valueOf()
|
|
26
|
-
* but the safe path is `Integer#toNumber()`. Returns null when the
|
|
27
|
-
* value is null/undefined so cadence-guard arithmetic stays explicit
|
|
28
|
-
* about the "no prior profile" case.
|
|
29
|
-
*/
|
|
30
|
-
function toJsNumber(value: unknown): number | null {
|
|
31
|
-
if (value === null || value === undefined) return null;
|
|
32
|
-
if (typeof value === "number") return value;
|
|
33
|
-
if (neo4j.isInt(value)) return (value as { toNumber: () => number }).toNumber();
|
|
34
|
-
if (typeof value === "string") {
|
|
35
|
-
const parsed = Number(value);
|
|
36
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const SAMPLE_CHAR_CAP = 120_000;
|
|
42
|
-
const RE_RUN_GROWTH_FRACTION = 0.2;
|
|
43
|
-
const RE_RUN_AGE_DAYS = 30;
|
|
44
|
-
|
|
45
|
-
export interface VoiceDistilProfileParams {
|
|
46
|
-
accountId: string;
|
|
47
|
-
adminUserId: string;
|
|
48
|
-
force?: boolean;
|
|
49
|
-
/**
|
|
50
|
-
* Operation mode (Task 424):
|
|
51
|
-
* - `'sample'` (default) — read the corpus + feedback intents and return
|
|
52
|
-
* them. No write. The calling agent composes the style-card YAML
|
|
53
|
-
* in-turn from the returned exemplars.
|
|
54
|
-
* - `'write'` — persist the style card the agent produced. Requires
|
|
55
|
-
* `styleCardYaml`.
|
|
56
|
-
*/
|
|
57
|
-
mode?: "sample" | "write";
|
|
58
|
-
/** Required when `mode='write'`. The style-card YAML produced by the agent. */
|
|
59
|
-
styleCardYaml?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface VoiceCorpusExemplar {
|
|
63
|
-
id: string;
|
|
64
|
-
label: string;
|
|
65
|
-
body: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface VoiceDistilProfileResult {
|
|
69
|
-
profileId: string | null;
|
|
70
|
-
corpusSize: number;
|
|
71
|
-
generatedAt: string | null;
|
|
72
|
-
feedbackEntries: number;
|
|
73
|
-
skipped: boolean;
|
|
74
|
-
skipReason?: "below-threshold" | "recent" | "empty-corpus" | "missing-style-card";
|
|
75
|
-
errorReason?: string;
|
|
76
|
-
/** Present when `mode='sample'`: corpus exemplars for the agent to read. */
|
|
77
|
-
exemplars?: VoiceCorpusExemplar[];
|
|
78
|
-
/** Present when `mode='sample'`: operator-edit intents to fold into the style card. */
|
|
79
|
-
feedbackIntents?: string[];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// The agent produces a YAML style card with at minimum these keys:
|
|
83
|
-
// sentenceLengthDistribution, register, favoured, avoided, taboos,
|
|
84
|
-
// openingPattern, closingPattern, exemplarSentences
|
|
85
|
-
// Exemplar sentences should be quoted verbatim from the returned corpus.
|
|
86
|
-
|
|
87
|
-
export async function voiceDistilProfile(
|
|
88
|
-
params: VoiceDistilProfileParams,
|
|
89
|
-
): Promise<VoiceDistilProfileResult> {
|
|
90
|
-
const { accountId, adminUserId, force = false } = params;
|
|
91
|
-
if (!accountId || !adminUserId) {
|
|
92
|
-
throw new Error("voice-distil-profile: accountId and adminUserId required");
|
|
93
|
-
}
|
|
94
|
-
const session = getSession();
|
|
95
|
-
const now = new Date();
|
|
96
|
-
const nowIso = now.toISOString();
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
// 1. Read existing profile (if any) for cadence-guard comparison.
|
|
100
|
-
const existing = await session.run(
|
|
101
|
-
`MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
|
|
102
|
-
OPTIONAL MATCH (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile)
|
|
103
|
-
RETURN elementId(p) AS profileId, p.corpusSize AS corpusSize,
|
|
104
|
-
p.generatedAt AS generatedAt`,
|
|
105
|
-
{ accountId, adminUserId },
|
|
106
|
-
);
|
|
107
|
-
const existingRow = existing.records[0];
|
|
108
|
-
const prevProfileId = (existingRow?.get("profileId") as string | null) ?? null;
|
|
109
|
-
const prevCorpusSize = toJsNumber(existingRow?.get("corpusSize"));
|
|
110
|
-
const prevGeneratedAt = (existingRow?.get("generatedAt") as string | null) ?? null;
|
|
111
|
-
|
|
112
|
-
// 2. Count corpus. Filter matches human-only + human-led-agent-assisted
|
|
113
|
-
// across the four primary content labels plus conversation-document
|
|
114
|
-
// :Section chunks (chat archives — children of a :KnowledgeDocument
|
|
115
|
-
// with `conversationIdentity` set). See VOICE_CORPUS_WHERE above.
|
|
116
|
-
const countResult = await session.run(
|
|
117
|
-
`MATCH (n)
|
|
118
|
-
WHERE ${VOICE_CORPUS_WHERE}
|
|
119
|
-
RETURN count(n) AS c`,
|
|
120
|
-
{ accountId },
|
|
121
|
-
);
|
|
122
|
-
const corpusSize = toJsNumber(countResult.records[0]?.get("c")) ?? 0;
|
|
123
|
-
|
|
124
|
-
if (corpusSize === 0) {
|
|
125
|
-
process.stderr.write(
|
|
126
|
-
`[voice-distil] adminUser=${adminUserId} skip reason=empty-corpus\n`,
|
|
127
|
-
);
|
|
128
|
-
return {
|
|
129
|
-
profileId: prevProfileId,
|
|
130
|
-
corpusSize: 0,
|
|
131
|
-
generatedAt: prevGeneratedAt,
|
|
132
|
-
feedbackEntries: 0,
|
|
133
|
-
skipped: true,
|
|
134
|
-
skipReason: "empty-corpus",
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 3. Cadence guard.
|
|
139
|
-
if (!force && prevProfileId && prevCorpusSize !== null && prevGeneratedAt) {
|
|
140
|
-
const ageMs = now.getTime() - new Date(prevGeneratedAt).getTime();
|
|
141
|
-
const ageDays = ageMs / (24 * 60 * 60 * 1000);
|
|
142
|
-
// prevCorpusSize is non-null here (the guard above checks). When it's
|
|
143
|
-
// zero we treat growth as infinite — first real corpus always triggers
|
|
144
|
-
// a re-run.
|
|
145
|
-
const growth =
|
|
146
|
-
prevCorpusSize > 0
|
|
147
|
-
? (corpusSize - prevCorpusSize) / prevCorpusSize
|
|
148
|
-
: 1;
|
|
149
|
-
const ageTrigger = ageDays >= RE_RUN_AGE_DAYS;
|
|
150
|
-
const growthTrigger = growth >= RE_RUN_GROWTH_FRACTION;
|
|
151
|
-
if (!ageTrigger && !growthTrigger) {
|
|
152
|
-
const reason = ageDays < 1 ? "recent" : "below-threshold";
|
|
153
|
-
process.stderr.write(
|
|
154
|
-
`[voice-distil] adminUser=${adminUserId} skip reason=${reason} corpusSize=${corpusSize} prevCorpus=${prevCorpusSize} ageDays=${ageDays.toFixed(1)}\n`,
|
|
155
|
-
);
|
|
156
|
-
return {
|
|
157
|
-
profileId: prevProfileId,
|
|
158
|
-
corpusSize,
|
|
159
|
-
generatedAt: prevGeneratedAt,
|
|
160
|
-
feedbackEntries: 0,
|
|
161
|
-
skipped: true,
|
|
162
|
-
skipReason: reason,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 4. Sample corpus bodies up to the char cap. Pick recent first.
|
|
168
|
-
// Timestamp coalesces include `firstMessageAt` for conversation-document
|
|
169
|
-
// :Section chunks so chat archives sort by when the conversation
|
|
170
|
-
// actually happened.
|
|
171
|
-
const sampled = await session.run(
|
|
172
|
-
`MATCH (n)
|
|
173
|
-
WHERE ${VOICE_CORPUS_WHERE}
|
|
174
|
-
WITH n, coalesce(n.dateSent, n.datePublished, n.firstMessageAt, n.dateCreated, n.createdAt) AS ts
|
|
175
|
-
OPTIONAL MATCH (parent:KnowledgeDocument)-[:HAS_SECTION]->(n)
|
|
176
|
-
WHERE 'Section' IN labels(n) AND parent.conversationIdentity IS NOT NULL
|
|
177
|
-
RETURN elementId(n) AS id,
|
|
178
|
-
labels(n) AS labels,
|
|
179
|
-
coalesce(n.body, n.abstract, n.subject, n.title, n.summary, '') AS body,
|
|
180
|
-
parent.conversationIdentity IS NOT NULL AS isConversationChunk,
|
|
181
|
-
ts
|
|
182
|
-
ORDER BY ts DESC NULLS LAST
|
|
183
|
-
LIMIT 500`,
|
|
184
|
-
{ accountId },
|
|
185
|
-
);
|
|
186
|
-
const samples: { id: string; label: string; body: string }[] = [];
|
|
187
|
-
let charBudget = SAMPLE_CHAR_CAP;
|
|
188
|
-
for (const r of sampled.records) {
|
|
189
|
-
const body = (r.get("body") as string | null) ?? "";
|
|
190
|
-
const labels = (r.get("labels") as string[]) ?? [];
|
|
191
|
-
// Conversation-document sections carry `:Section` only after the Task
|
|
192
|
-
// 324 collapse; surface "Conversation" via the parent's
|
|
193
|
-
// `conversationIdentity` so the LLM exemplar header still says "this
|
|
194
|
-
// is chat" rather than the bare base label.
|
|
195
|
-
const isConversationChunk = Boolean(r.get("isConversationChunk"));
|
|
196
|
-
const label = isConversationChunk
|
|
197
|
-
? "Conversation"
|
|
198
|
-
: (labels.find((l) =>
|
|
199
|
-
["KnowledgeDocument", "Message", "SocialPost"].includes(l),
|
|
200
|
-
) ?? labels[0] ?? "Unknown");
|
|
201
|
-
if (body.length === 0) continue;
|
|
202
|
-
const trimmed = body.length > charBudget ? body.slice(0, charBudget) : body;
|
|
203
|
-
samples.push({ id: r.get("id") as string, label, body: trimmed });
|
|
204
|
-
charBudget -= trimmed.length;
|
|
205
|
-
if (charBudget <= 0) break;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// 5. Pull feedback intents from existing :VoiceEdit nodes for this operator.
|
|
209
|
-
const feedback = await session.run(
|
|
210
|
-
`MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
|
|
211
|
-
MATCH (e:VoiceEdit {adminUserId: $adminUserId, accountId: $accountId})
|
|
212
|
-
RETURN e.intent AS intent ORDER BY e.occurredAt DESC LIMIT 50`,
|
|
213
|
-
{ accountId, adminUserId },
|
|
214
|
-
);
|
|
215
|
-
const feedbackIntents = feedback.records
|
|
216
|
-
.map((r) => r.get("intent") as string | null)
|
|
217
|
-
.filter((x): x is string => typeof x === "string" && x.length > 0);
|
|
218
|
-
|
|
219
|
-
// 6. Mode branch (Task 424).
|
|
220
|
-
// - `sample` (default): return the exemplars + feedback intents to the
|
|
221
|
-
// calling agent. The agent composes the YAML style card in its own
|
|
222
|
-
// turn and re-invokes with mode='write'.
|
|
223
|
-
// - `write`: persist the agent-produced style card.
|
|
224
|
-
const mode = params.mode ?? "sample";
|
|
225
|
-
if (mode === "sample") {
|
|
226
|
-
process.stderr.write(
|
|
227
|
-
`[voice-distil] adminUser=${adminUserId} mode=sample corpusSize=${corpusSize} ` +
|
|
228
|
-
`exemplars=${samples.length} feedbackEntries=${feedbackIntents.length}\n`,
|
|
229
|
-
);
|
|
230
|
-
return {
|
|
231
|
-
profileId: prevProfileId,
|
|
232
|
-
corpusSize,
|
|
233
|
-
generatedAt: prevGeneratedAt,
|
|
234
|
-
feedbackEntries: feedbackIntents.length,
|
|
235
|
-
skipped: true,
|
|
236
|
-
skipReason: "missing-style-card",
|
|
237
|
-
exemplars: samples,
|
|
238
|
-
feedbackIntents,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const styleCardYaml = (params.styleCardYaml ?? "").trim();
|
|
243
|
-
if (!styleCardYaml) {
|
|
244
|
-
throw new Error(
|
|
245
|
-
"voice-distil-profile: mode='write' requires styleCardYaml (the YAML you composed from the sample-mode response).",
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// 7. Write the :VoiceProfile node + LEARNED_FROM edges to every sampled node.
|
|
250
|
-
//
|
|
251
|
-
// The :AdminUser must already exist (onboarding/auth is the only
|
|
252
|
-
// legitimate origin for :AdminUser). MATCH not MERGE: a typo in
|
|
253
|
-
// adminUserId, an env mismatch, or a dropped credential surfaces
|
|
254
|
-
// here as a typed error rather than silently minting an orphan
|
|
255
|
-
// AdminUser that bypasses the platform's label-origin protection.
|
|
256
|
-
const writeResult = await session.run(
|
|
257
|
-
`MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
|
|
258
|
-
MERGE (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile {accountId: $accountId, adminUserId: $adminUserId})
|
|
259
|
-
ON CREATE SET p.createdAt = $now
|
|
260
|
-
SET p.styleCard = $styleCard,
|
|
261
|
-
p.generatedAt = $now,
|
|
262
|
-
p.corpusSize = $corpusSize,
|
|
263
|
-
p.feedbackEntries = $feedbackEntries,
|
|
264
|
-
p.updatedAt = $now
|
|
265
|
-
WITH p
|
|
266
|
-
UNWIND $sampleIds AS sid
|
|
267
|
-
MATCH (s) WHERE elementId(s) = sid
|
|
268
|
-
MERGE (p)-[:LEARNED_FROM]->(s)
|
|
269
|
-
RETURN elementId(p) AS profileId`,
|
|
270
|
-
{
|
|
271
|
-
accountId,
|
|
272
|
-
adminUserId,
|
|
273
|
-
styleCard: styleCardYaml,
|
|
274
|
-
now: nowIso,
|
|
275
|
-
corpusSize,
|
|
276
|
-
feedbackEntries: feedbackIntents.length,
|
|
277
|
-
sampleIds: samples.map((s) => s.id),
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
// MATCH-on-:AdminUser failure yields zero records. Throw the typed
|
|
281
|
-
// error here rather than returning a half-baked result.
|
|
282
|
-
if (writeResult.records.length === 0) {
|
|
283
|
-
throw new Error(
|
|
284
|
-
`voice-mirror: :AdminUser {accountId='${accountId}', adminUserId='${adminUserId}'} not found. Onboarding must promote the operator to :AdminUser before distillation.`,
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
const profileId = writeResult.records[0].get("profileId") as string;
|
|
288
|
-
|
|
289
|
-
process.stderr.write(
|
|
290
|
-
`[voice-distil] adminUser=${adminUserId} mode=write corpusSize=${corpusSize} generatedAt=${nowIso} feedbackEntries=${feedbackIntents.length}\n`,
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
return {
|
|
294
|
-
profileId,
|
|
295
|
-
corpusSize,
|
|
296
|
-
generatedAt: nowIso,
|
|
297
|
-
feedbackEntries: feedbackIntents.length,
|
|
298
|
-
skipped: false,
|
|
299
|
-
};
|
|
300
|
-
} finally {
|
|
301
|
-
await session.close();
|
|
302
|
-
}
|
|
303
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* voice-record-feedback — capture an operator's edit on an agent draft
|
|
3
|
-
* as feedback for the next distillation.
|
|
4
|
-
*
|
|
5
|
-
* Task 424: the Haiku diff-intent summarisation moved into the calling
|
|
6
|
-
* agent's turn. The agent compares `originalText` to `editedText` itself
|
|
7
|
-
* and passes the resulting `intent` string as a required parameter. The
|
|
8
|
-
* tool now does the graph writes only.
|
|
9
|
-
*
|
|
10
|
-
* Side effects:
|
|
11
|
-
* 1. Write `(:VoiceEdit)-[:FEEDBACK_FOR]->(:VoiceProfile)` if the profile
|
|
12
|
-
* exists; otherwise write the :VoiceEdit alone so the next distillation
|
|
13
|
-
* back-fills the edge.
|
|
14
|
-
*/
|
|
15
|
-
import { randomUUID } from "node:crypto";
|
|
16
|
-
import { getSession } from "../lib/neo4j.js";
|
|
17
|
-
|
|
18
|
-
export interface VoiceRecordFeedbackParams {
|
|
19
|
-
accountId: string;
|
|
20
|
-
adminUserId: string;
|
|
21
|
-
originalText: string;
|
|
22
|
-
editedText: string;
|
|
23
|
-
/**
|
|
24
|
-
* One sentence describing what changed and what voice preference it reveals.
|
|
25
|
-
* The calling agent summarises this from `originalText` vs `editedText` in
|
|
26
|
-
* its own turn before invoking the tool (Task 424). Examples: "shorter and
|
|
27
|
-
* dropped the apology", "switched to first person", "removed corporate
|
|
28
|
-
* sign-off", "added a specific date".
|
|
29
|
-
*/
|
|
30
|
-
intent: string;
|
|
31
|
-
context?: {
|
|
32
|
-
skill?: string;
|
|
33
|
-
brief?: string;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface VoiceRecordFeedbackResult {
|
|
38
|
-
editId: string;
|
|
39
|
-
intent: string;
|
|
40
|
-
occurredAt: string;
|
|
41
|
-
hadProfile: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function voiceRecordFeedback(
|
|
45
|
-
params: VoiceRecordFeedbackParams,
|
|
46
|
-
): Promise<VoiceRecordFeedbackResult> {
|
|
47
|
-
const { accountId, adminUserId, originalText, editedText, intent: rawIntent, context } = params;
|
|
48
|
-
if (!accountId || !adminUserId) {
|
|
49
|
-
throw new Error("voice-record-feedback: accountId and adminUserId required");
|
|
50
|
-
}
|
|
51
|
-
if (!originalText || !editedText) {
|
|
52
|
-
throw new Error("voice-record-feedback: originalText and editedText required");
|
|
53
|
-
}
|
|
54
|
-
const intent = (rawIntent ?? "").trim();
|
|
55
|
-
if (!intent) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
"voice-record-feedback: intent is required — summarise what the operator changed in-turn before calling this tool (Task 424).",
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const editId = randomUUID();
|
|
62
|
-
const occurredAt = new Date().toISOString();
|
|
63
|
-
const cappedIntent = intent.slice(0, 500);
|
|
64
|
-
|
|
65
|
-
const session = getSession();
|
|
66
|
-
try {
|
|
67
|
-
const result = await session.run(
|
|
68
|
-
`MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
|
|
69
|
-
CREATE (e:VoiceEdit {
|
|
70
|
-
editId: $editId,
|
|
71
|
-
accountId: $accountId,
|
|
72
|
-
adminUserId: $adminUserId,
|
|
73
|
-
originalText: $originalText,
|
|
74
|
-
editedText: $editedText,
|
|
75
|
-
intent: $intent,
|
|
76
|
-
occurredAt: $occurredAt,
|
|
77
|
-
skill: $skill,
|
|
78
|
-
brief: $brief
|
|
79
|
-
})
|
|
80
|
-
CREATE (a)-[:AUTHORED]->(e)
|
|
81
|
-
WITH e
|
|
82
|
-
OPTIONAL MATCH (:AdminUser {accountId: $accountId, adminUserId: $adminUserId})-[:HAS_VOICE_PROFILE]->(p:VoiceProfile)
|
|
83
|
-
FOREACH (_ IN CASE WHEN p IS NULL THEN [] ELSE [1] END |
|
|
84
|
-
MERGE (e)-[:FEEDBACK_FOR]->(p)
|
|
85
|
-
)
|
|
86
|
-
RETURN elementId(e) AS editId, (p IS NOT NULL) AS hadProfile`,
|
|
87
|
-
{
|
|
88
|
-
accountId,
|
|
89
|
-
adminUserId,
|
|
90
|
-
editId,
|
|
91
|
-
originalText,
|
|
92
|
-
editedText,
|
|
93
|
-
intent: cappedIntent,
|
|
94
|
-
occurredAt,
|
|
95
|
-
skill: context?.skill ?? null,
|
|
96
|
-
brief: context?.brief ?? null,
|
|
97
|
-
},
|
|
98
|
-
);
|
|
99
|
-
if (result.records.length === 0) {
|
|
100
|
-
throw new Error(
|
|
101
|
-
`voice-mirror: :AdminUser {accountId='${accountId}', adminUserId='${adminUserId}'} not found. Onboarding must promote the operator to :AdminUser before feedback can be recorded.`,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
const hadProfile = result.records[0].get("hadProfile") === true;
|
|
105
|
-
|
|
106
|
-
process.stderr.write(
|
|
107
|
-
`[voice-record-feedback] adminUser=${adminUserId} intent="${cappedIntent.replace(/"/g, "'")}" diffBytes=${editedText.length - originalText.length}\n`,
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
return { editId, intent: cappedIntent, occurredAt, hadProfile };
|
|
111
|
-
} finally {
|
|
112
|
-
await session.close();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* voice-retrieve-conditioning — return the operator's style card plus K
|
|
3
|
-
* voice-matched exemplars for a drafting brief.
|
|
4
|
-
*
|
|
5
|
-
* Retrieval order (best to worst):
|
|
6
|
-
* 1. If brief.topic is provided AND keyword search on the human-only corpus
|
|
7
|
-
* returns ≥K matches → use those (ordered by recency).
|
|
8
|
-
* 2. Otherwise, return the K most-recent human-only nodes.
|
|
9
|
-
*
|
|
10
|
-
* Embeddings are not used in v1: the corpus is small (single operator's
|
|
11
|
-
* personal writing — typically <500 docs), and recency-with-keyword-filter
|
|
12
|
-
* is both faster and more predictable than vector ANN at that scale.
|
|
13
|
-
* Vector-search is a Task 365-follow-up if the corpus grows beyond the
|
|
14
|
-
* 500-doc breakeven point.
|
|
15
|
-
*
|
|
16
|
-
* Graceful degradation: missing profile, empty corpus, and DB errors all
|
|
17
|
-
* return `{styleCard: null, exemplars: []}` rather than throwing — the
|
|
18
|
-
* calling drafting skill falls back to its existing default register.
|
|
19
|
-
*/
|
|
20
|
-
import { getSession } from "../lib/neo4j.js";
|
|
21
|
-
import { VOICE_CORPUS_WHERE } from "../lib/voice-corpus.js";
|
|
22
|
-
|
|
23
|
-
const DEFAULT_TOKEN_BUDGET_SHORT = 16_000;
|
|
24
|
-
const DEFAULT_TOKEN_BUDGET_LONG = 96_000;
|
|
25
|
-
const CHARS_PER_TOKEN = 4;
|
|
26
|
-
|
|
27
|
-
export interface VoiceRetrieveConditioningParams {
|
|
28
|
-
accountId: string;
|
|
29
|
-
adminUserId: string;
|
|
30
|
-
brief: {
|
|
31
|
-
topic?: string;
|
|
32
|
-
format: "short" | "long";
|
|
33
|
-
register?: string;
|
|
34
|
-
};
|
|
35
|
-
tokenBudget?: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface VoiceExemplar {
|
|
39
|
-
nodeId: string;
|
|
40
|
-
label: string;
|
|
41
|
-
body: string;
|
|
42
|
-
source: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface VoiceRetrieveConditioningResult {
|
|
46
|
-
styleCard: string | null;
|
|
47
|
-
exemplars: VoiceExemplar[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const EMPTY: VoiceRetrieveConditioningResult = { styleCard: null, exemplars: [] };
|
|
51
|
-
|
|
52
|
-
export async function voiceRetrieveConditioning(
|
|
53
|
-
params: VoiceRetrieveConditioningParams,
|
|
54
|
-
): Promise<VoiceRetrieveConditioningResult> {
|
|
55
|
-
const { accountId, adminUserId, brief } = params;
|
|
56
|
-
if (!accountId || !adminUserId) return EMPTY;
|
|
57
|
-
|
|
58
|
-
const k = brief.format === "long" ? 15 : 5;
|
|
59
|
-
const tokenBudget =
|
|
60
|
-
params.tokenBudget ??
|
|
61
|
-
(brief.format === "long" ? DEFAULT_TOKEN_BUDGET_LONG : DEFAULT_TOKEN_BUDGET_SHORT);
|
|
62
|
-
const charBudget = tokenBudget * CHARS_PER_TOKEN;
|
|
63
|
-
|
|
64
|
-
const session = getSession();
|
|
65
|
-
try {
|
|
66
|
-
// 1. Style card.
|
|
67
|
-
const profile = await session.run(
|
|
68
|
-
`MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
|
|
69
|
-
OPTIONAL MATCH (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile)
|
|
70
|
-
RETURN p.styleCard AS styleCard`,
|
|
71
|
-
{ accountId, adminUserId },
|
|
72
|
-
);
|
|
73
|
-
const styleCard = (profile.records[0]?.get("styleCard") as string | null) ?? null;
|
|
74
|
-
|
|
75
|
-
// 2. Exemplars. Keyword-aware filter when brief.topic is set.
|
|
76
|
-
const topic = (brief.topic ?? "").trim();
|
|
77
|
-
const usesTopic = topic.length > 0;
|
|
78
|
-
|
|
79
|
-
const cypher = usesTopic
|
|
80
|
-
? `MATCH (n)
|
|
81
|
-
WHERE ${VOICE_CORPUS_WHERE}
|
|
82
|
-
AND toLower(coalesce(n.body, n.abstract, n.subject, n.title, n.summary, '')) CONTAINS toLower($topic)
|
|
83
|
-
WITH n, coalesce(n.dateSent, n.datePublished, n.firstMessageAt, n.dateCreated, n.createdAt) AS ts
|
|
84
|
-
RETURN elementId(n) AS id,
|
|
85
|
-
labels(n) AS labels,
|
|
86
|
-
coalesce(n.body, n.abstract, n.subject, n.title, n.summary, '') AS body,
|
|
87
|
-
coalesce(n.subject, n.title, n.name, '') AS source,
|
|
88
|
-
ts
|
|
89
|
-
ORDER BY ts DESC NULLS LAST
|
|
90
|
-
LIMIT $k`
|
|
91
|
-
: `MATCH (n)
|
|
92
|
-
WHERE ${VOICE_CORPUS_WHERE}
|
|
93
|
-
WITH n, coalesce(n.dateSent, n.datePublished, n.firstMessageAt, n.dateCreated, n.createdAt) AS ts
|
|
94
|
-
RETURN elementId(n) AS id,
|
|
95
|
-
labels(n) AS labels,
|
|
96
|
-
coalesce(n.body, n.abstract, n.subject, n.title, n.summary, '') AS body,
|
|
97
|
-
coalesce(n.subject, n.title, n.name, '') AS source,
|
|
98
|
-
ts
|
|
99
|
-
ORDER BY ts DESC NULLS LAST
|
|
100
|
-
LIMIT $k`;
|
|
101
|
-
|
|
102
|
-
const result = await session.run(cypher, {
|
|
103
|
-
accountId,
|
|
104
|
-
topic,
|
|
105
|
-
k,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
let charsUsed = 0;
|
|
109
|
-
const exemplars: VoiceExemplar[] = [];
|
|
110
|
-
for (const r of result.records) {
|
|
111
|
-
const body = (r.get("body") as string | null) ?? "";
|
|
112
|
-
if (body.length === 0) continue;
|
|
113
|
-
const labels = (r.get("labels") as string[]) ?? [];
|
|
114
|
-
const label =
|
|
115
|
-
labels.find((l) =>
|
|
116
|
-
["KnowledgeDocument", "Message", "SocialPost", "Conversation"].includes(l),
|
|
117
|
-
) ?? labels[0] ?? "Unknown";
|
|
118
|
-
const remaining = charBudget - charsUsed;
|
|
119
|
-
if (remaining <= 0) break;
|
|
120
|
-
const truncated = body.length > remaining ? body.slice(0, remaining) : body;
|
|
121
|
-
exemplars.push({
|
|
122
|
-
nodeId: r.get("id") as string,
|
|
123
|
-
label,
|
|
124
|
-
body: truncated,
|
|
125
|
-
source: ((r.get("source") as string | null) ?? "").slice(0, 200),
|
|
126
|
-
});
|
|
127
|
-
charsUsed += truncated.length;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
process.stderr.write(
|
|
131
|
-
`[voice-retrieve] task=${brief.topic ?? brief.format} styleCardBytes=${
|
|
132
|
-
styleCard?.length ?? 0
|
|
133
|
-
} exemplarCount=${exemplars.length} tokenBudget=${tokenBudget}\n`,
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
return { styleCard, exemplars };
|
|
137
|
-
} catch (err) {
|
|
138
|
-
process.stderr.write(
|
|
139
|
-
`[voice-retrieve] error: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
140
|
-
);
|
|
141
|
-
return EMPTY;
|
|
142
|
-
} finally {
|
|
143
|
-
await session.close();
|
|
144
|
-
}
|
|
145
|
-
}
|