@pithy-sh/support 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/audit/actions.d.ts +9 -0
- package/dist/audit/actions.d.ts.map +1 -1
- package/dist/audit/actions.js +9 -0
- package/dist/capability.d.ts +2 -1
- package/dist/capability.d.ts.map +1 -1
- package/dist/capability.js +30 -6
- package/dist/config/config.d.ts +63 -2
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +63 -5
- package/dist/data/assignment.d.ts +43 -0
- package/dist/data/assignment.d.ts.map +1 -0
- package/dist/data/assignment.js +38 -0
- package/dist/data/embedding.d.ts +21 -0
- package/dist/data/embedding.d.ts.map +1 -0
- package/dist/data/embedding.js +23 -0
- package/dist/data/tables.d.ts +28 -0
- package/dist/data/tables.d.ts.map +1 -1
- package/dist/data/tables.js +10 -2
- package/dist/data/thread.d.ts +9 -4
- package/dist/data/thread.d.ts.map +1 -1
- package/dist/data/thread.js +9 -4
- package/dist/http/guards.d.ts +32 -14
- package/dist/http/guards.d.ts.map +1 -1
- package/dist/http/guards.js +44 -16
- package/dist/http/handlers.d.ts +35 -2
- package/dist/http/handlers.d.ts.map +1 -1
- package/dist/http/handlers.js +81 -8
- package/dist/http/resolve.d.ts +8 -0
- package/dist/http/resolve.d.ts.map +1 -1
- package/dist/http/resolve.js +20 -1
- package/dist/http/responses.d.ts +355 -0
- package/dist/http/responses.d.ts.map +1 -1
- package/dist/http/responses.js +64 -2
- package/dist/http/routes.d.ts +25 -11
- package/dist/http/routes.d.ts.map +1 -1
- package/dist/http/routes.js +34 -5
- package/dist/http/schemas.d.ts +67 -3
- package/dist/http/schemas.d.ts.map +1 -1
- package/dist/http/schemas.js +44 -5
- package/dist/http/scopes.d.ts +10 -5
- package/dist/http/scopes.d.ts.map +1 -1
- package/dist/http/scopes.js +42 -6
- package/dist/http/views.d.ts +9 -1
- package/dist/http/views.d.ts.map +1 -1
- package/dist/http/views.js +40 -1
- package/dist/inbound/ingest.d.ts +1 -1
- package/dist/inbound/ingest.d.ts.map +1 -1
- package/dist/inbound/ingest.js +3 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/migrations/0002_assignment.d.ts +20 -0
- package/dist/migrations/0002_assignment.d.ts.map +1 -0
- package/dist/migrations/0002_assignment.js +45 -0
- package/dist/migrations/0003_embeddings.d.ts +15 -0
- package/dist/migrations/0003_embeddings.d.ts.map +1 -0
- package/dist/migrations/0003_embeddings.js +23 -0
- package/dist/migrations/set.d.ts +13 -0
- package/dist/migrations/set.d.ts.map +1 -0
- package/dist/migrations/set.js +21 -0
- package/dist/presence/room.d.ts +35 -0
- package/dist/presence/room.d.ts.map +1 -0
- package/dist/presence/room.js +66 -0
- package/dist/provision/provisionSupport.d.ts +32 -0
- package/dist/provision/provisionSupport.d.ts.map +1 -1
- package/dist/provision/provisionSupport.js +10 -2
- package/dist/provision/resolveSupportConfig.d.ts.map +1 -1
- package/dist/provision/resolveSupportConfig.js +13 -2
- package/dist/purge/contribution.d.ts +49 -0
- package/dist/purge/contribution.d.ts.map +1 -0
- package/dist/purge/contribution.js +164 -0
- package/dist/secret/registry.d.ts +26 -2
- package/dist/secret/registry.d.ts.map +1 -1
- package/dist/secret/registry.js +15 -3
- package/dist/semantic/documents.d.ts +21 -0
- package/dist/semantic/documents.d.ts.map +1 -0
- package/dist/semantic/documents.js +128 -0
- package/dist/semantic/embed.d.ts +59 -0
- package/dist/semantic/embed.d.ts.map +1 -0
- package/dist/semantic/embed.js +59 -0
- package/dist/semantic/messageIndex.d.ts +46 -0
- package/dist/semantic/messageIndex.d.ts.map +1 -0
- package/dist/semantic/messageIndex.js +67 -0
- package/dist/semantic/query.d.ts +37 -0
- package/dist/semantic/query.d.ts.map +1 -0
- package/dist/semantic/query.js +46 -0
- package/dist/staff/staff.d.ts +22 -0
- package/dist/staff/staff.d.ts.map +1 -0
- package/dist/staff/staff.js +64 -0
- package/dist/store/assignment.d.ts +63 -0
- package/dist/store/assignment.d.ts.map +1 -0
- package/dist/store/assignment.js +86 -0
- package/dist/store/hybrid.d.ts +134 -0
- package/dist/store/hybrid.d.ts.map +1 -0
- package/dist/store/hybrid.js +185 -0
- package/dist/store/paging.d.ts +10 -0
- package/dist/store/paging.d.ts.map +1 -1
- package/dist/store/paging.js +11 -1
- package/dist/store/provisionIndex.d.ts +21 -0
- package/dist/store/provisionIndex.d.ts.map +1 -0
- package/dist/store/provisionIndex.js +36 -0
- package/dist/store/search.d.ts +18 -9
- package/dist/store/search.d.ts.map +1 -1
- package/dist/store/search.js +36 -34
- package/dist/store/searchIndex.d.ts +4 -4
- package/dist/store/searchIndex.js +4 -4
- package/dist/store/threads.d.ts +174 -8
- package/dist/store/threads.d.ts.map +1 -1
- package/dist/store/threads.js +33 -40
- package/dist/submission/submit.d.ts +1 -1
- package/dist/submission/submit.d.ts.map +1 -1
- package/dist/submission/submit.js +2 -1
- package/dist/test-utils/seed.d.ts +33 -0
- package/dist/test-utils/seed.d.ts.map +1 -0
- package/dist/test-utils/seed.js +102 -0
- package/dist/test-utils/semantic.d.ts +21 -0
- package/dist/test-utils/semantic.d.ts.map +1 -0
- package/dist/test-utils/semantic.js +107 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workflows/specs.d.ts +23 -0
- package/dist/workflows/specs.d.ts.map +1 -1
- package/dist/workflows/specs.js +27 -7
- package/dist/workflows/worker.d.ts +15 -2
- package/dist/workflows/worker.d.ts.map +1 -1
- package/dist/workflows/worker.js +48 -4
- package/package.json +9 -7
- package/pithy.manifest.json +21 -3
- package/src/audit/actions.ts +9 -0
- package/src/capability.ts +43 -4
- package/src/config/config.ts +104 -5
- package/src/data/assignment.ts +55 -0
- package/src/data/embedding.ts +27 -0
- package/src/data/tables.ts +8 -0
- package/src/data/thread.ts +26 -4
- package/src/http/guards.ts +52 -15
- package/src/http/handlers.ts +125 -4
- package/src/http/resolve.ts +19 -0
- package/src/http/responses.ts +154 -0
- package/src/http/routes.ts +98 -12
- package/src/http/schemas.ts +117 -28
- package/src/http/scopes.ts +43 -6
- package/src/http/views.ts +44 -0
- package/src/inbound/ingest.ts +5 -2
- package/src/index.ts +5 -0
- package/src/migrations/0002_assignment.ts +65 -0
- package/src/migrations/0003_embeddings.ts +29 -0
- package/src/migrations/set.ts +21 -0
- package/src/presence/room.ts +94 -0
- package/src/provision/provisionSupport.ts +33 -1
- package/src/provision/resolveSupportConfig.ts +24 -13
- package/src/purge/contribution.ts +294 -0
- package/src/secret/registry.ts +17 -2
- package/src/semantic/documents.ts +178 -0
- package/src/semantic/embed.ts +99 -0
- package/src/semantic/messageIndex.ts +74 -0
- package/src/semantic/query.ts +66 -0
- package/src/staff/staff.ts +128 -0
- package/src/store/assignment.ts +179 -0
- package/src/store/hybrid.ts +314 -0
- package/src/store/paging.ts +14 -0
- package/src/store/provisionIndex.ts +46 -0
- package/src/store/search.ts +58 -37
- package/src/store/searchIndex.ts +4 -4
- package/src/store/threads.ts +91 -91
- package/src/submission/submit.ts +3 -2
- package/src/test-utils/seed.ts +141 -0
- package/src/test-utils/semantic.ts +110 -0
- package/src/version.generated.ts +1 -1
- package/src/workflows/specs.ts +34 -0
- package/src/workflows/worker.ts +84 -8
- package/src/workflows/wrangler.jsonc +8 -3
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
/** This package's npm name — the join key against a release feed. */
|
|
4
4
|
export declare const PACKAGE_NAME = "@pithy-sh/support";
|
|
5
5
|
/** This package's version, stamped from its own package.json at generation time. */
|
|
6
|
-
export declare const PACKAGE_VERSION = "0.
|
|
6
|
+
export declare const PACKAGE_VERSION = "0.6.0";
|
|
7
7
|
//# sourceMappingURL=version.generated.d.ts.map
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
/** This package's npm name — the join key against a release feed. */
|
|
5
5
|
const PACKAGE_NAME = "@pithy-sh/support";
|
|
6
6
|
/** This package's version, stamped from its own package.json at generation time. */
|
|
7
|
-
const PACKAGE_VERSION = "0.
|
|
7
|
+
const PACKAGE_VERSION = "0.6.0";
|
|
8
8
|
//#endregion
|
|
9
9
|
export { PACKAGE_NAME, PACKAGE_VERSION };
|
|
@@ -24,6 +24,19 @@ export declare const SupportClassifyParams: z.ZodObject<{
|
|
|
24
24
|
messageId: z.ZodString;
|
|
25
25
|
}, z.core.$strip>;
|
|
26
26
|
export type SupportClassifyParams = z.infer<typeof SupportClassifyParams>;
|
|
27
|
+
/**
|
|
28
|
+
* One re-embed of the semantic message index, started by `pithy vector reprocess --index support-messages`.
|
|
29
|
+
*
|
|
30
|
+
* The parameters are the ones `pithy vector reprocess` sends every index, so the CLI dispatches a capability's
|
|
31
|
+
* index exactly as it dispatches one `vector()` owns. There is no `filter`: the vectors carry no metadata to
|
|
32
|
+
* filter on.
|
|
33
|
+
*/
|
|
34
|
+
export declare const SupportReprocessParams: z.ZodObject<{
|
|
35
|
+
index: z.ZodLiteral<"support-messages">;
|
|
36
|
+
all: z.ZodOptional<z.ZodBoolean>;
|
|
37
|
+
pageSize: z.ZodOptional<z.ZodNumber>;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
export type SupportReprocessParams = z.infer<typeof SupportReprocessParams>;
|
|
27
40
|
/** Support's durable jobs, keyed by job name. The key is the second segment of the `support/<job>` dispatch key. */
|
|
28
41
|
export declare const supportWorkflows: {
|
|
29
42
|
readonly classify: {
|
|
@@ -34,6 +47,16 @@ export declare const supportWorkflows: {
|
|
|
34
47
|
}, z.core.$strip>;
|
|
35
48
|
readonly optional: true;
|
|
36
49
|
};
|
|
50
|
+
readonly reprocess: {
|
|
51
|
+
readonly binding: "SUPPORT_REPROCESS";
|
|
52
|
+
readonly className: "SupportReprocessWorkflow";
|
|
53
|
+
readonly params: z.ZodObject<{
|
|
54
|
+
index: z.ZodLiteral<"support-messages">;
|
|
55
|
+
all: z.ZodOptional<z.ZodBoolean>;
|
|
56
|
+
pageSize: z.ZodOptional<z.ZodNumber>;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
readonly optional: true;
|
|
59
|
+
};
|
|
37
60
|
};
|
|
38
61
|
/**
|
|
39
62
|
* Support's jobs as a dispatch registry, keyed `support/<job>`. Built here rather than through
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specs.d.ts","sourceRoot":"","sources":["../../src/workflows/specs.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAmB,MAAM,kCAAkC,CAAC;AAC1F,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;GAcG;AAEH,uGAAuG;AACvG,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C,iCAAiC;AACjC,eAAO,MAAM,qBAAqB;;iBASsE,CAAC;AACzG,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E,oHAAoH;AACpH,eAAO,MAAM,gBAAgB;;iBAEzB,OAAO,EAAE,kBAAkB;iBAC3B,SAAS,EAAE,yBAAyB;iBACpC,MAAM;;;iBACN,QAAQ;;CAEwB,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,gBAKrC,CAAC"}
|
|
1
|
+
{"version":3,"file":"specs.d.ts","sourceRoot":"","sources":["../../src/workflows/specs.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAmB,MAAM,kCAAkC,CAAC;AAC1F,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;GAcG;AAEH,uGAAuG;AACvG,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C,iCAAiC;AACjC,eAAO,MAAM,qBAAqB;;iBASsE,CAAC;AACzG,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB;;;;iBAgB+C,CAAC;AACnF,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E,oHAAoH;AACpH,eAAO,MAAM,gBAAgB;;iBAEzB,OAAO,EAAE,kBAAkB;iBAC3B,SAAS,EAAE,yBAAyB;iBACpC,MAAM;;;iBACN,QAAQ;;;iBAKR,OAAO,EAAE,mBAAmB;iBAC5B,SAAS,EAAE,0BAA0B;iBACrC,MAAM;;;;;iBACN,QAAQ;;CAEwB,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,gBAKrC,CAAC"}
|
package/dist/workflows/specs.js
CHANGED
|
@@ -22,13 +22,33 @@ import { workflowKey } from "@pithy-sh/core/src/workflow/naming";
|
|
|
22
22
|
const SUPPORT_CAPABILITY = "support";
|
|
23
23
|
/** Which message to classify. */
|
|
24
24
|
const SupportClassifyParams = z.object({ messageId: z.string().min(1).describe("The `pithy_support_messages.id` to classify. The Workflow reads that message, asks the model about it, appends a classification row, and denormalizes the answer onto its thread.") }).describe("The instance parameters of the classification Workflow — the one message it runs against.");
|
|
25
|
+
/**
|
|
26
|
+
* One re-embed of the semantic message index, started by `pithy vector reprocess --index support-messages`.
|
|
27
|
+
*
|
|
28
|
+
* The parameters are the ones `pithy vector reprocess` sends every index, so the CLI dispatches a capability's
|
|
29
|
+
* index exactly as it dispatches one `vector()` owns. There is no `filter`: the vectors carry no metadata to
|
|
30
|
+
* filter on.
|
|
31
|
+
*/
|
|
32
|
+
const SupportReprocessParams = z.object({
|
|
33
|
+
index: z.literal("support-messages").describe("The index to re-embed. Support declares one, `support-messages`."),
|
|
34
|
+
all: z.boolean().optional().describe("Re-embed every inbound message, not only the ones whose stamp differs from the configured model. What a fresh index's backfill runs."),
|
|
35
|
+
pageSize: z.number().int().min(1).optional().describe("Messages per Workflow step. Defaults to the Vectorize upsert ceiling of 1,000.")
|
|
36
|
+
}).describe("The instance parameters of a re-embed of the support message index.");
|
|
25
37
|
/** Support's durable jobs, keyed by job name. The key is the second segment of the `support/<job>` dispatch key. */
|
|
26
|
-
const supportWorkflows = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
const supportWorkflows = {
|
|
39
|
+
classify: {
|
|
40
|
+
binding: "SUPPORT_CLASSIFY",
|
|
41
|
+
className: "SupportClassifyWorkflow",
|
|
42
|
+
params: SupportClassifyParams,
|
|
43
|
+
optional: true
|
|
44
|
+
},
|
|
45
|
+
reprocess: {
|
|
46
|
+
binding: "SUPPORT_REPROCESS",
|
|
47
|
+
className: "SupportReprocessWorkflow",
|
|
48
|
+
params: SupportReprocessParams,
|
|
49
|
+
optional: true
|
|
50
|
+
}
|
|
51
|
+
};
|
|
32
52
|
/**
|
|
33
53
|
* Support's jobs as a dispatch registry, keyed `support/<job>`. Built here rather than through
|
|
34
54
|
* `composeWorkflows` because the inbound handler dispatches from inside the capability, before any
|
|
@@ -45,4 +65,4 @@ const supportWorkflowRegistry = Object.fromEntries(Object.entries(supportWorkflo
|
|
|
45
65
|
}];
|
|
46
66
|
}));
|
|
47
67
|
//#endregion
|
|
48
|
-
export { SUPPORT_CAPABILITY, SupportClassifyParams, supportWorkflowRegistry, supportWorkflows };
|
|
68
|
+
export { SUPPORT_CAPABILITY, SupportClassifyParams, SupportReprocessParams, supportWorkflowRegistry, supportWorkflows };
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from "cloudflare:workers";
|
|
4
4
|
import type { D1Database } from "@cloudflare/workers-types";
|
|
5
|
+
import type { VectorStore } from "@pithy-sh/vector/src/index/index";
|
|
6
|
+
import { type ReprocessReport } from "@pithy-sh/vector/src/workflows/reprocess";
|
|
5
7
|
import type { SupportAi } from "../ai/classify";
|
|
8
|
+
import { SupportReprocessParams } from "./specs";
|
|
6
9
|
/**
|
|
7
10
|
* The prebuilt support worker. `pithy support provision` deploys one per environment; the adopter
|
|
8
11
|
* authors no code for it. It hosts the classification Workflow — a thin durable shell around the
|
|
@@ -23,12 +26,14 @@ import type { SupportAi } from "../ai/classify";
|
|
|
23
26
|
* refusal it exports is the honest body for a host with no request surface; see
|
|
24
27
|
* `@pithy-sh/core/src/workflow/hostEntry`.
|
|
25
28
|
*/
|
|
26
|
-
/** The support worker's env: the app database, the AI binding, and the serialized config. */
|
|
29
|
+
/** The support worker's env: the app database, the AI binding, the message index, and the serialized config. */
|
|
27
30
|
export interface SupportWorkerEnv {
|
|
28
31
|
/** The app database the support tables live in. */
|
|
29
32
|
DB: D1Database;
|
|
30
|
-
/** The Workers AI binding. */
|
|
33
|
+
/** The Workers AI binding — classification and embedding both run on it. */
|
|
31
34
|
AI: SupportAi;
|
|
35
|
+
/** The semantic message index. Bound only when `search.semantic` is on. */
|
|
36
|
+
SUPPORT_VECTORIZE?: VectorStore;
|
|
32
37
|
/** The resolved support config as a JSON string, filled at provision. */
|
|
33
38
|
SUPPORT_CONFIG?: string;
|
|
34
39
|
}
|
|
@@ -40,6 +45,14 @@ export declare class SupportClassifyWorkflow extends WorkflowEntrypoint<SupportW
|
|
|
40
45
|
messageId: string;
|
|
41
46
|
}>, step: WorkflowStep): Promise<void>;
|
|
42
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Re-embed the semantic message index — `@pithy-sh/vector`'s own reprocess orchestration, over support's
|
|
50
|
+
* `DocumentStore`, one journalled page per step. `pithy vector reprocess --index support-messages` starts it,
|
|
51
|
+
* and so does `pithy support provision` once, to backfill an index it just created.
|
|
52
|
+
*/
|
|
53
|
+
export declare class SupportReprocessWorkflow extends WorkflowEntrypoint<SupportWorkerEnv, SupportReprocessParams> {
|
|
54
|
+
run(event: WorkflowEvent<SupportReprocessParams>, step: WorkflowStep): Promise<ReprocessReport>;
|
|
55
|
+
}
|
|
43
56
|
/**
|
|
44
57
|
* The module's default export, and therefore its format. See `hostEntry` for why a Workflow host needs
|
|
45
58
|
* one at all, and why this one refuses rather than being empty: nothing reaches this worker over HTTP —
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/workflows/worker.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/workflows/worker.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAM5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,0CAA0C,CAAC;AAChG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAShD,OAAO,EAAsB,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAErE;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,gHAAgH;AAChH,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,EAAE,EAAE,UAAU,CAAC;IACf,4EAA4E;IAC5E,EAAE,EAAE,SAAS,CAAC;IACd,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,WAAW,CAAC;IAChC,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAOD,wDAAwD;AACxD,qBAAa,uBAAwB,SAAQ,kBAAkB,CAAC,gBAAgB,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;IACvF,GAAG,CAAC,KAAK,EAAE,aAAa,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA+CjG;CACF;AAED;;;;GAIG;AACH,qBAAa,wBAAyB,SAAQ,kBAAkB,CAAC,gBAAgB,EAAE,sBAAsB,CAAC;IACzF,GAAG,CAAC,KAAK,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CA6B7G;CACF;AAED;;;;GAIG"}
|
package/dist/workflows/worker.js
CHANGED
|
@@ -3,18 +3,28 @@
|
|
|
3
3
|
import { SupportConfig } from "../config/config.js";
|
|
4
4
|
import { resolveCategories } from "../data/categories.js";
|
|
5
5
|
import { supportDatabase } from "../data/tables.js";
|
|
6
|
-
import {
|
|
6
|
+
import { supportMessageIndex } from "../semantic/messageIndex.js";
|
|
7
|
+
import { SUPPORT_CAPABILITY, SupportReprocessParams } from "./specs.js";
|
|
7
8
|
import { runClassification } from "./classify.js";
|
|
9
|
+
import { supportMessageDocuments } from "../semantic/documents.js";
|
|
10
|
+
import { runEmbedding } from "../semantic/embed.js";
|
|
8
11
|
import { supportWorkflowRetry } from "./retryPolicy.js";
|
|
12
|
+
import { pithyCommandOf } from "@pithy-sh/core/src/cli/command";
|
|
13
|
+
import { createWorkerLogger } from "@pithy-sh/core/src/logger/worker";
|
|
9
14
|
import { WorkflowEntrypoint } from "cloudflare:workers";
|
|
10
15
|
import { NonRetryableError } from "cloudflare:workflows";
|
|
11
16
|
import { classifiedSteps } from "@pithy-sh/core/src/workflow/faults";
|
|
12
17
|
import { workflowHostEntry } from "@pithy-sh/core/src/workflow/hostEntry";
|
|
18
|
+
import { reprocessIndex } from "@pithy-sh/vector/src/workflows/reprocess";
|
|
13
19
|
//#region src/workflows/worker.ts
|
|
20
|
+
/** Parse the config this worker was deployed with. Absent falls back to the defaults. */
|
|
21
|
+
function workerConfig(env) {
|
|
22
|
+
return SupportConfig.parse(env.SUPPORT_CONFIG ? JSON.parse(env.SUPPORT_CONFIG) : {});
|
|
23
|
+
}
|
|
14
24
|
/** Classify one stored message and write the result. */
|
|
15
25
|
var SupportClassifyWorkflow = class extends WorkflowEntrypoint {
|
|
16
26
|
async run(event, step) {
|
|
17
|
-
const config =
|
|
27
|
+
const config = workerConfig(this.env);
|
|
18
28
|
const deps = {
|
|
19
29
|
db: supportDatabase(this.env.DB),
|
|
20
30
|
ai: this.env.AI,
|
|
@@ -24,9 +34,43 @@ var SupportClassifyWorkflow = class extends WorkflowEntrypoint {
|
|
|
24
34
|
newId: () => crypto.randomUUID(),
|
|
25
35
|
now: () => /* @__PURE__ */ new Date()
|
|
26
36
|
};
|
|
27
|
-
await classifiedSteps(step, supportWorkflowRetry, NonRetryableError).do(`classify-${event.payload.messageId}`, async () => {
|
|
37
|
+
if (config.ai.enabled) await classifiedSteps(step, supportWorkflowRetry, NonRetryableError).do(`classify-${event.payload.messageId}`, async () => {
|
|
28
38
|
await runClassification(deps, event.payload.messageId);
|
|
29
39
|
});
|
|
40
|
+
const index = supportMessageIndex(config.search);
|
|
41
|
+
if (index) await classifiedSteps(step, supportWorkflowRetry, NonRetryableError).do(`embed-${event.payload.messageId}`, () => runEmbedding({
|
|
42
|
+
db: deps.db,
|
|
43
|
+
ai: this.env.AI,
|
|
44
|
+
store: this.env.SUPPORT_VECTORIZE,
|
|
45
|
+
index,
|
|
46
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
47
|
+
log: createWorkerLogger({ name: `${SUPPORT_CAPABILITY}:embed` })
|
|
48
|
+
}, event.payload.messageId));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Re-embed the semantic message index — `@pithy-sh/vector`'s own reprocess orchestration, over support's
|
|
53
|
+
* `DocumentStore`, one journalled page per step. `pithy vector reprocess --index support-messages` starts it,
|
|
54
|
+
* and so does `pithy support provision` once, to backfill an index it just created.
|
|
55
|
+
*/
|
|
56
|
+
var SupportReprocessWorkflow = class extends WorkflowEntrypoint {
|
|
57
|
+
async run(event, step) {
|
|
58
|
+
const params = SupportReprocessParams.parse(event.payload);
|
|
59
|
+
const index = supportMessageIndex(workerConfig(this.env).search);
|
|
60
|
+
if (!index) throw new NonRetryableError("support/reprocess: search.semantic is off in this worker's config, so there is no message index to re-embed");
|
|
61
|
+
const store = this.env.SUPPORT_VECTORIZE;
|
|
62
|
+
if (!store) throw new NonRetryableError(`support/reprocess: the SUPPORT_VECTORIZE binding is absent; run \`${pithyCommandOf("support provision", {}, { runtime: true })}\` to bind the index`);
|
|
63
|
+
return reprocessIndex({
|
|
64
|
+
documents: supportMessageDocuments(supportDatabase(this.env.DB), { store }),
|
|
65
|
+
store,
|
|
66
|
+
ai: this.env.AI,
|
|
67
|
+
index,
|
|
68
|
+
indexName: params.index,
|
|
69
|
+
now: () => /* @__PURE__ */ new Date()
|
|
70
|
+
}, classifiedSteps(step, supportWorkflowRetry, NonRetryableError), {
|
|
71
|
+
...params.all !== void 0 ? { all: params.all } : {},
|
|
72
|
+
...params.pageSize !== void 0 ? { pageSize: params.pageSize } : {}
|
|
73
|
+
});
|
|
30
74
|
}
|
|
31
75
|
};
|
|
32
76
|
/**
|
|
@@ -36,4 +80,4 @@ var SupportClassifyWorkflow = class extends WorkflowEntrypoint {
|
|
|
36
80
|
*/
|
|
37
81
|
var worker_default = workflowHostEntry(SUPPORT_CAPABILITY);
|
|
38
82
|
//#endregion
|
|
39
|
-
export { SupportClassifyWorkflow, worker_default as default };
|
|
83
|
+
export { SupportClassifyWorkflow, SupportReprocessWorkflow, worker_default as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pithy-sh/support",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,12 +40,13 @@
|
|
|
40
40
|
"tldts": "^7.4.10"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@pithy-sh/auth": "^0.
|
|
44
|
-
"@pithy-sh/core": "^0.
|
|
45
|
-
"@pithy-sh/email": "^0.
|
|
46
|
-
"@pithy-sh/payments": "^0.
|
|
47
|
-
"@pithy-sh/secrets": "^0.6.
|
|
48
|
-
"@pithy-sh/storage": "^0.
|
|
43
|
+
"@pithy-sh/auth": "^0.12.0",
|
|
44
|
+
"@pithy-sh/core": "^0.14.0",
|
|
45
|
+
"@pithy-sh/email": "^0.8.0",
|
|
46
|
+
"@pithy-sh/payments": "^0.7.0",
|
|
47
|
+
"@pithy-sh/secrets": "^0.6.2",
|
|
48
|
+
"@pithy-sh/storage": "^0.7.0",
|
|
49
|
+
"@pithy-sh/vector": "^0.5.2",
|
|
49
50
|
"hono": "^4.13.2",
|
|
50
51
|
"kysely": "^0.29.0",
|
|
51
52
|
"zod": "^4.4.0"
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
"@pithy-sh/secrets": "workspace:*",
|
|
68
69
|
"@pithy-sh/storage": "workspace:*",
|
|
69
70
|
"@pithy-sh/tsconfig": "workspace:*",
|
|
71
|
+
"@pithy-sh/vector": "workspace:*",
|
|
70
72
|
"@types/node": "^22.15.0",
|
|
71
73
|
"@vitest/coverage-v8": "^4.1.0",
|
|
72
74
|
"hono": "^4.13.2",
|
package/pithy.manifest.json
CHANGED
|
@@ -10,25 +10,43 @@
|
|
|
10
10
|
"job": "classify",
|
|
11
11
|
"className": "SupportClassifyWorkflow",
|
|
12
12
|
"optional": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"type": "workflow",
|
|
16
|
+
"name": "SUPPORT_REPROCESS",
|
|
17
|
+
"job": "reprocess",
|
|
18
|
+
"className": "SupportReprocessWorkflow",
|
|
19
|
+
"optional": true
|
|
20
|
+
},
|
|
21
|
+
{ "type": "vectorize", "name": "SUPPORT_VECTORIZE", "optional": true, "remote": true },
|
|
22
|
+
{ "type": "ai", "name": "AI", "optional": true, "remote": true },
|
|
23
|
+
{
|
|
24
|
+
"type": "durable_object",
|
|
25
|
+
"name": "PRESENCE_ROOMS",
|
|
26
|
+
"className": "PresenceRoom",
|
|
27
|
+
"classModule": "@pithy-sh/core/src/presence/durableObject"
|
|
13
28
|
}
|
|
14
29
|
],
|
|
15
30
|
"peerCapabilities": ["secrets"],
|
|
16
31
|
"optionalCapabilities": ["controlplane", "email", "auth", "payments", "audit", "storage"],
|
|
17
32
|
"migrationNamespace": "support",
|
|
18
|
-
"
|
|
33
|
+
"devSecrets": [{ "name": "presence-ticket-key", "devValue": "random" }],
|
|
34
|
+
"whenToEnable": "A support inbox that lands mail — and your signed-in users' own reports — in your own D1, classifies both on your own Workers AI binding, and links each sender to the account and purchases your app already knows about. Support is the employee a solo developer cannot hire, and every piece needed to build it was already in the catalog. Mail arrives through Cloudflare Email Routing, is parsed (multipart and attachments included), threaded on In-Reply-To and References so a conversation stays one conversation, and classified three ways at once — category, priority, sentiment — which makes a sortable inbox rather than a labelled one. The taxonomy is federated: eight categories ship, and you add your own with defineSupportCategories. Classification runs on your binding, so the inference lands on your bill and your customers' support mail never leaves your infrastructure. Everything is derived from immutable mail: a wrong classification is recomputed, not repaired, and there is no status workflow, because that is a helpdesk product and this is not one. What a shared inbox does need, it has: a thread can be assigned to somebody, with every handover kept, and a `support.thread` presence room shows who else has a thread open, and who is replying, before you answer it twice. Your signed-in users reach the same inbox without leaving the app: POST /support/feedback opens a thread behind requireAuth(), into the same table, the same classifier, and the same taxonomy — no second inbox, no second console. That channel is where the mail path's hardest problem does not exist, because there is no From: header to spoof: the session is the identity, so the thread records whether its account link was proven by a session or merely matched against an address, and a console can tell the two apart. Replies go out through @pithy-sh/email carrying your domain, your DKIM, and correct threading headers — which only the Worker can set — whichever channel the question arrived on. The admin surface is control-plane and default-denied, except to your own staff through `support({ staff })`, who may assign a thread and join its presence room from a session; the three in-app feedback routes answer to a user's session and to no scope at all. **Cloudflare Email Routing takes over a zone's MX**, so use a subdomain (support@help.yourdomain.com) and never your apex, or you will move your real mail off your existing provider.",
|
|
19
35
|
"scaffold": [
|
|
20
36
|
"Pick the address this inbox will answer on, and put it in `inboundAddresses`. **Use a subdomain, never your apex.** Enabling Email Routing on a zone points its MX at Cloudflare, so `support@yourdomain.com` would move all of your real inbound mail off your current provider. `support@help.yourdomain.com` costs you a subdomain zone and nothing else. Until this is set the inbox is inert and ignores every message.",
|
|
21
37
|
"Ensure the secrets capability is provisioned (`pithy add secrets` then `pithy secrets provision`) — the R2 credentials attachment URLs are signed with are stored and read through @pithy-sh/secrets.",
|
|
22
38
|
"Run `pithy support provision --routing-zone <zone-id> --inbound-address <addr> --app-worker <name>`. It creates the `SUPPORT_BUCKET` R2 bucket, deploys the prebuilt classification worker for each environment, and creates the Email Routing rule that delivers mail to your app worker. Idempotent; re-run it freely.",
|
|
23
39
|
"Add the `DB` (D1) binding to each wrangler.jsonc env. `SUPPORT_BUCKET` (R2) is written for you when your config keeps attachments, the raw MIME copy, or in-app uploads, and left out entirely when it keeps none of them. Add the `SUPPORT_CLASSIFY` Workflow binding your app dispatches to after storing a message — `pithy support provision` writes it.",
|
|
24
|
-
"Add the controlplane capability (`pithy add controlplane`) and run `pithy dashboard connect`. Every
|
|
40
|
+
"Add the controlplane capability (`pithy add controlplane`) and run `pithy dashboard connect`. Every management route is control-plane and default-denied, except to your own staff through `support({ staff })`: without a registered connection the admin surface answers 403, which is the correct posture for an inbox holding other people's private correspondence. Grant the six scopes deliberately — `support:threads:reply` sends mail to real people under your domain and is not the same permission as reading, and `support:threads:assign` decides who answers whom.",
|
|
25
41
|
"Add the email capability (`pithy add email`) if you want to answer from the dashboard. Without it the inbox still receives, threads, and classifies; the reply route returns `support/reply_failed` naming what is missing.",
|
|
26
42
|
"Add auth and payments if you have them. Neither is required, and both are reached by guarded dynamic import — with them installed a thread arrives already knowing who the sender is, what they bought, and whether their last renewal failed. **Auth is what makes the in-app channel work at all**: without it `c.var.auth` is null and every `/support/feedback` route denies, which is the correct posture rather than a bug.",
|
|
27
43
|
"Point your app's feedback form at `POST /support/feedback` and pass the screen, build, and environment as `context`. That set is closed — an undeclared key is refused rather than stored, so this cannot quietly become a telemetry pipe. Review `submission.attachments` before you ship it: the size, count, and content-type allowlist are stated for a direct upload rather than inherited from the mail path, which has no type restriction at all. Set `submission: { enabled: false }` and the routes are not mounted.",
|
|
28
44
|
"Add your own categories with `defineSupportCategories({ ... })` and your own canned replies with `defineSupportReplies({ ... })`. A category's description is prompt input, read by the model verbatim; a canned reply is a starting point a human picks and edits, never something sent automatically.",
|
|
29
45
|
"If your feedback screen offers a chooser, send its answer as `declaredCategory`. It lands in its own column beside `category` and is never overwritten by the classifier — one is a claim by the person writing, the other a judgement about them, and an operator triaging an inbox needs to tell them apart. `GET /support/threads` filters on either. A key outside your taxonomy is refused rather than stored, and a category sent with a `threadId` is refused too: a chooser belongs on the form that opens a request.",
|
|
30
46
|
"`POST /support/feedback` is gated on a session and same-origin and never on a role — a general intake that is role-gated is not one. If your own account model makes some submissions act-on-behalf-of, put that check in your `app` capability's `middleware` over your own `basePath`: every capability's middleware mounts before any capability's routes, so yours runs after auth has resolved the session and before this capability's `requireAuth()`.",
|
|
31
|
-
"
|
|
47
|
+
"If your own signed-in users work the inbox, name two powers in `support({ staff: { readPower, assignPower } })` and give them to a role in your organization catalog's `unassignable` list that `organization({ ownership })` does not confer, so only an operator can confer them — anybody may found an organization and take its ownership role, so `support()` refuses a staff power there. `readPower` lets a staff member join a thread's presence room; `assignPower` lets them assign it at `POST /support/staff/threads/:id/assign`. Without `staff`, both answer to management clients only.",
|
|
48
|
+
"Search the inbox at `GET /support/threads/search` — keyword by default, ranked; the inbox list takes no `q`. Turn on `search: { semantic: true }` to find a paraphrase as well as an exact word: it embeds every inbound message on your own Workers AI binding, a second inference per message on top of classification, so it is off until you choose it. Then run `pithy support provision`, which creates the Vectorize index, binds it, and backfills it once; `pithy vector reprocess --index support-messages` re-embeds after a model change.",
|
|
49
|
+
"Run `pithy support provision` after adding support or upgrading it: `search.fts` is on by default, and the FTS5 index and its triggers are provisioned rather than migrated. Until then search falls back to a `LIKE` scan and `pithy doctor` names the environment. Back the database up with `pithy backup`, which skips the index, and restore it with `pithy restore`, which rebuilds it — a plain `wrangler d1 export` fails on any database holding an FTS5 table."
|
|
32
50
|
],
|
|
33
51
|
"configOptions": [
|
|
34
52
|
{
|
package/src/audit/actions.ts
CHANGED
|
@@ -30,6 +30,15 @@ export const SupportAuditActions = {
|
|
|
30
30
|
threadArchived: "support/thread_archived",
|
|
31
31
|
/** A thread was taken back out of the archive. Recorded separately, because undoing is its own decision. */
|
|
32
32
|
threadUnarchived: "support/thread_unarchived",
|
|
33
|
+
/**
|
|
34
|
+
* A thread was given to somebody, or handed from one person to another (#816). Audited because who has
|
|
35
|
+
* a customer's conversation decides who answers them, and the thread keeps only the current answer.
|
|
36
|
+
* Metadata: the previous assignee, the new one, and the assigner's note — staff words, never mail.
|
|
37
|
+
* Reassigning the current assignee changes nothing and emits nothing.
|
|
38
|
+
*/
|
|
39
|
+
threadAssigned: "support/thread_assigned",
|
|
40
|
+
/** A thread's assignee was cleared. Its own action, because giving a conversation up is its own decision. */
|
|
41
|
+
threadUnassigned: "support/thread_unassigned",
|
|
33
42
|
/**
|
|
34
43
|
* A reply went out. Audited because it leaves under the adopter's domain and their DKIM: to the
|
|
35
44
|
* recipient it is indistinguishable from the founder writing it, so who actually sent it is a
|
package/src/capability.ts
CHANGED
|
@@ -26,10 +26,19 @@ import { supportAdminRoutes } from "./http/scopes";
|
|
|
26
26
|
import { createSupportEmailHandler } from "./inbound/handler";
|
|
27
27
|
import { senderPeers } from "./link/peers";
|
|
28
28
|
import type { SenderPeers } from "./link/sender";
|
|
29
|
-
import {
|
|
29
|
+
import { SUPPORT_MIGRATIONS } from "./migrations/set";
|
|
30
|
+
import { supportPresence } from "./presence/room";
|
|
31
|
+
import { supportUserPurge } from "./purge/contribution";
|
|
30
32
|
import { resolveReplies, type SupportReplySnippets } from "./reply/snippets";
|
|
31
33
|
import { inapplicableAttachmentSecrets, SUPPORT_BUCKET_BINDING, supportSecretsRegistry } from "./secret/registry";
|
|
32
34
|
import { supportExampleSeed } from "./seeds/example";
|
|
35
|
+
import {
|
|
36
|
+
SUPPORT_AI_BINDING,
|
|
37
|
+
SUPPORT_MESSAGES_INDEX,
|
|
38
|
+
SUPPORT_VECTORIZE_BINDING,
|
|
39
|
+
supportMessageIndex,
|
|
40
|
+
} from "./semantic/messageIndex";
|
|
41
|
+
import { assertStaffComposition } from "./staff/staff";
|
|
33
42
|
import { PACKAGE_NAME, PACKAGE_VERSION } from "./version.generated";
|
|
34
43
|
import { supportWorkflows } from "./workflows/specs";
|
|
35
44
|
|
|
@@ -182,7 +191,8 @@ export interface SupportCapability
|
|
|
182
191
|
* The support capability — an inbound support inbox in the adopter's own D1, classified on their own
|
|
183
192
|
* Workers AI binding.
|
|
184
193
|
*
|
|
185
|
-
* It contributes
|
|
194
|
+
* It contributes its tables (and optionally a full-text index, and a semantic message index in Vectorize) to the
|
|
195
|
+
* app `DB`, an `email()` handler
|
|
186
196
|
* that claims the configured addresses out of the Worker's single inbound entry, the classification
|
|
187
197
|
* Workflow that handler dispatches to, and a control-plane admin surface for a dashboard to read and
|
|
188
198
|
* act on.
|
|
@@ -213,13 +223,13 @@ export function support(options: SupportOptions = {}): SupportCapability {
|
|
|
213
223
|
emit: noopEmit,
|
|
214
224
|
};
|
|
215
225
|
|
|
216
|
-
//
|
|
226
|
+
// The whole chain, unconditionally. The full-text index is **not** in it: it is derived from the
|
|
217
227
|
// messages table and rebuildable at any time, so it is a provisioned resource that
|
|
218
228
|
// `pithy support provision` creates and drops (see `store/searchIndex.ts`), not schema whose loss
|
|
219
229
|
// loses data. Keeping it out of the ledger is what makes `search.fts` safe to toggle in either
|
|
220
230
|
// direction — a config flag must never be able to corrupt a migration set shared with every other
|
|
221
231
|
// capability in the database.
|
|
222
|
-
const migrations: Record<string, Migration> =
|
|
232
|
+
const migrations: Record<string, Migration> = SUPPORT_MIGRATIONS;
|
|
223
233
|
|
|
224
234
|
const requiredBindings: BindingSpecInput[] = [
|
|
225
235
|
// The app database every support table lives in.
|
|
@@ -239,6 +249,17 @@ export function support(options: SupportOptions = {}): SupportCapability {
|
|
|
239
249
|
// The classification Workflow the inbound handler dispatches to, derived from the spec rather
|
|
240
250
|
// than listed again — one declaration, so a binding rename cannot leave the two disagreeing.
|
|
241
251
|
...workflowBindings(supportWorkflows),
|
|
252
|
+
// The semantic message index and the model that embeds a query, **only when `search.semantic` is
|
|
253
|
+
// on**. The index's `index_name` is a provisioning output, so `pithy support provision` writes the entry
|
|
254
|
+
// once the index exists; until then it is absent and the search route skips the semantic backend, naming
|
|
255
|
+
// it, which is why the binding is optional. Neither has a local emulation, so both reach the real resource
|
|
256
|
+
// under `wrangler dev`.
|
|
257
|
+
...(supportMessageIndex(resolved.search)
|
|
258
|
+
? [
|
|
259
|
+
{ type: "vectorize" as const, name: SUPPORT_VECTORIZE_BINDING, optional: true, remote: true },
|
|
260
|
+
{ type: "ai" as const, name: SUPPORT_AI_BINDING, remote: true },
|
|
261
|
+
]
|
|
262
|
+
: []),
|
|
242
263
|
];
|
|
243
264
|
|
|
244
265
|
const capability = defineCapability({
|
|
@@ -276,7 +297,12 @@ export function support(options: SupportOptions = {}): SupportCapability {
|
|
|
276
297
|
* able to answer is a coherent and useful thing, so a missing email capability degrades one
|
|
277
298
|
* route instead of refusing to boot.
|
|
278
299
|
*/
|
|
300
|
+
// Support's staff powers, when the adopter names them, so tenancy refuses a catalog that places them on
|
|
301
|
+
// no role. Absent until named: a capability never reserves a power in a catalog that is the adopter's.
|
|
302
|
+
...(resolved.staff ? { powers: [resolved.staff.readPower, resolved.staff.assignPower] } : {}),
|
|
279
303
|
compose: ({ capabilities }) => {
|
|
304
|
+
// A staff block no request could be served by safely is refused here, at boot. See `staff/staff.ts`.
|
|
305
|
+
assertStaffComposition(resolved.staff, capabilities);
|
|
280
306
|
const email = capabilities.find(isEmailCapability);
|
|
281
307
|
wiring.enqueueEmail = email?.enqueue;
|
|
282
308
|
// Auth and payments, found rather than imported (#645), and refused here when this Worker's config needs
|
|
@@ -288,13 +314,26 @@ export function support(options: SupportOptions = {}): SupportCapability {
|
|
|
288
314
|
// for why the list is as short as it is.
|
|
289
315
|
client: (): SupportClientProjection => clientProjection(resolved, mountPath),
|
|
290
316
|
adminRoutes: supportAdminRoutes(mountPath),
|
|
317
|
+
// The semantic message index, so `pithy vector reprocess` re-embeds it beside every `vector()` index.
|
|
318
|
+
// Declared only when it exists to re-embed.
|
|
319
|
+
...(supportMessageIndex(resolved.search)
|
|
320
|
+
? { vectorIndexes: [{ name: SUPPORT_MESSAGES_INDEX, reprocessJob: "reprocess" }] }
|
|
321
|
+
: {}),
|
|
322
|
+
// Who else has a thread open (#816): one `support.thread` room per thread, on core's presence seam.
|
|
323
|
+
// Declaring it is the whole of the wiring — core adds the room's binding, socket and ticket routes.
|
|
324
|
+
presence: supportPresence(mountPath, wiring),
|
|
291
325
|
routes: registerSupportRoutes({
|
|
292
326
|
basePath: mountPath,
|
|
293
327
|
submission: resolved.submission.enabled,
|
|
294
328
|
resolveDeps: makeResolveDeps(wiring),
|
|
329
|
+
staff: resolved.staff,
|
|
295
330
|
}),
|
|
296
331
|
email: createSupportEmailHandler(wiring),
|
|
297
332
|
seeds: [supportExampleSeed],
|
|
333
|
+
// A person's threads — linked to their account, or sent from their address and linked to no other — with every
|
|
334
|
+
// message, attachment and raw mail in them: exported on an access request and erased by their purge, as its own
|
|
335
|
+
// step (#814).
|
|
336
|
+
purge: [supportUserPurge("DB", { semantic: supportMessageIndex(resolved.search) !== undefined })],
|
|
298
337
|
});
|
|
299
338
|
|
|
300
339
|
return Object.assign(capability, {
|
package/src/config/config.ts
CHANGED
|
@@ -324,25 +324,121 @@ export const SupportReplyConfig = z
|
|
|
324
324
|
);
|
|
325
325
|
export type SupportReplyConfig = z.output<typeof SupportReplyConfig>;
|
|
326
326
|
|
|
327
|
+
/**
|
|
328
|
+
* The default embedding model for semantic search — the kit's own default (`DEFAULT_EMBEDDING_MODEL` in
|
|
329
|
+
* `@pithy-sh/cloudflare/src/ai/aiManager.ts`), restated rather than imported because a Worker cannot reach
|
|
330
|
+
* the REST client package and should not.
|
|
331
|
+
*
|
|
332
|
+
* **Multilingual, and that is why.** An inbox takes mail in whatever language the customer writes, and an
|
|
333
|
+
* English-only model would embed a Spanish complaint into the wrong neighborhood with no error.
|
|
334
|
+
*/
|
|
335
|
+
export const DEFAULT_EMBEDDING_MODEL = "@cf/baai/bge-m3";
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* How many components `bge-m3` produces. It is not a Vectorize preset, so the index states its own shape;
|
|
339
|
+
* a model override that produces another width states its `dimensions` beside it.
|
|
340
|
+
*/
|
|
341
|
+
export const DEFAULT_EMBEDDING_DIMENSIONS = 1024;
|
|
342
|
+
|
|
343
|
+
/** The `{ model }` form of `search.semantic`: semantic search on, with a model of your choosing. */
|
|
344
|
+
export const SupportSemanticConfig = z
|
|
345
|
+
.object({
|
|
346
|
+
model: z
|
|
347
|
+
.string()
|
|
348
|
+
.min(1)
|
|
349
|
+
.default(DEFAULT_EMBEDDING_MODEL)
|
|
350
|
+
.describe(
|
|
351
|
+
"The Workers AI embedding model the message index is built and queried with. Pinned: change it and `pithy vector reprocess --index support-messages` re-embeds what the old one wrote.",
|
|
352
|
+
),
|
|
353
|
+
dimensions: z
|
|
354
|
+
.number()
|
|
355
|
+
.int()
|
|
356
|
+
.min(1)
|
|
357
|
+
.max(1536)
|
|
358
|
+
.default(DEFAULT_EMBEDDING_DIMENSIONS)
|
|
359
|
+
.describe(
|
|
360
|
+
"How many components the model produces — 1024 for `bge-m3`, 768 for `bge-base-en-v1.5`. Fixed when the index is created, so a model of another width needs the index dropped and recreated.",
|
|
361
|
+
),
|
|
362
|
+
})
|
|
363
|
+
.describe("Semantic search with a chosen embedding model.");
|
|
364
|
+
export type SupportSemanticConfig = z.output<typeof SupportSemanticConfig>;
|
|
365
|
+
|
|
327
366
|
/**
|
|
328
367
|
* How text search is answered.
|
|
329
368
|
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
369
|
+
* FTS5 by default, with the `LIKE` scan kept for a project that wants no virtual table at all. Semantic
|
|
370
|
+
* search is off by default because it costs a second inference per inbound message.
|
|
332
371
|
*/
|
|
333
372
|
export const SupportSearchConfig = z
|
|
334
373
|
.object({
|
|
374
|
+
semantic: z
|
|
375
|
+
.union([z.boolean(), SupportSemanticConfig])
|
|
376
|
+
.default(false)
|
|
377
|
+
.describe(
|
|
378
|
+
"Search by meaning as well as by words: `true` for the default model (`@cf/baai/bge-m3`), or `{ model }` for your own. **Off by default, and the reason is cost.** Every inbound message is embedded once, on your own Workers AI binding, in the classification Workflow — a second inference per message on top of classification, which roughly doubles this capability's per-message AI cost. The volume guard bounds messages, not spend. On, `pithy support provision` creates a Vectorize index and backfills it once; off, it drops the index. Vectors carry message ids and nothing else.",
|
|
379
|
+
),
|
|
335
380
|
fts: z
|
|
336
381
|
.boolean()
|
|
337
|
-
.default(
|
|
382
|
+
.default(true)
|
|
338
383
|
.describe(
|
|
339
|
-
"Build a SQLite FTS5 index over message subjects and bodies
|
|
384
|
+
"Build a SQLite FTS5 index over message subjects and bodies — a ranked word search instead of an unindexed `LIKE` scan. **On by default.** A plain `wrangler d1 export` of a database holding an FTS5 table fails outright, so `pithy backup` leaves the index and its triggers out and `pithy restore` rebuilds them from `pithy_support_messages`; the bare `wrangler` route has to skip it by hand. The index is derived rather than migrated, so `pithy support provision` creates or drops it to match this flag, and until a project re-provisions, search falls back to the `LIKE` scan and `pithy doctor` names the environment.",
|
|
340
385
|
),
|
|
341
386
|
})
|
|
342
|
-
.describe(
|
|
387
|
+
.describe(
|
|
388
|
+
"Search settings — FTS5 by default, the `LIKE` scan when it is off or not yet provisioned, and semantic search an opt-in priced in resources you own.",
|
|
389
|
+
);
|
|
343
390
|
export type SupportSearchConfig = z.output<typeof SupportSearchConfig>;
|
|
344
391
|
|
|
392
|
+
/**
|
|
393
|
+
* The semantic model and width this configuration asks for, or `undefined` when semantic search is off.
|
|
394
|
+
*
|
|
395
|
+
* The one reading of the three-shaped flag — `false`, `true`, `{ model }` — so the capability's bindings,
|
|
396
|
+
* the search route, the classification Workflow and provisioning cannot each decide what `true` means.
|
|
397
|
+
*/
|
|
398
|
+
export function supportSemantic(search: SupportSearchConfig): SupportSemanticConfig | undefined {
|
|
399
|
+
if (search.semantic === false) return undefined;
|
|
400
|
+
if (search.semantic === true) return { model: DEFAULT_EMBEDDING_MODEL, dimensions: DEFAULT_EMBEDDING_DIMENSIONS };
|
|
401
|
+
return search.semantic;
|
|
402
|
+
}
|
|
403
|
+
|
|
345
404
|
/** The full support configuration. */
|
|
405
|
+
/**
|
|
406
|
+
* **Who of the app's own signed-in users works the inbox (#816).** Off until named.
|
|
407
|
+
*
|
|
408
|
+
* The inbox holds every customer's correspondence, so a signed-in user reaches it only through a power the
|
|
409
|
+
* adopter names in their own role catalog, held in the organization the request acts for — the same seam
|
|
410
|
+
* `media:manage` and `storage:manage` ask, and the same rule `artifacts({ managePower })` follows: a
|
|
411
|
+
* capability never reserves a power name in a catalog that is the adopter's.
|
|
412
|
+
*
|
|
413
|
+
* **Both powers must sit only on roles no organization can hand itself** — listed in
|
|
414
|
+
* `defineRoles({ unassignable })`, and not the role `organization({ ownership })` confers — and `support()`
|
|
415
|
+
* refuses the composition otherwise. A role is the catalog's, not one organization's, and an organization's
|
|
416
|
+
* own administrator may give any assignable role to their own members — so a support power on an assignable
|
|
417
|
+
* role would let any customer's administrator make themselves staff and read every other customer's mail.
|
|
418
|
+
* The ownership role is unassignable and still self-served: anybody may found an organization, nominate
|
|
419
|
+
* themselves its owner, and accept. On any other unassignable role it is conferred by an operator alone,
|
|
420
|
+
* through the control plane.
|
|
421
|
+
*/
|
|
422
|
+
export const SupportStaffConfig = z
|
|
423
|
+
.object({
|
|
424
|
+
readPower: z
|
|
425
|
+
.string()
|
|
426
|
+
.min(1)
|
|
427
|
+
.describe(
|
|
428
|
+
"The power that lets a signed-in user see who else has a thread open — what the `support.thread` presence room's session ticket asks. Your own name, declared in your role catalog on unassignable roles only, and not on the role ownership transfer confers, e.g. `support:read`.",
|
|
429
|
+
),
|
|
430
|
+
assignPower: z
|
|
431
|
+
.string()
|
|
432
|
+
.min(1)
|
|
433
|
+
.describe(
|
|
434
|
+
"The power that lets a signed-in user assign a thread, to themselves or to somebody else, at `POST {basePath}/staff/threads/:id/assign`. Your own name, declared in your role catalog on unassignable roles only, and not on the role ownership transfer confers, e.g. `support:assign`.",
|
|
435
|
+
),
|
|
436
|
+
})
|
|
437
|
+
.describe(
|
|
438
|
+
"Who of your app's own signed-in users works the inbox, by the organization powers their role holds. Requires `organization`, and both powers on unassignable roles other than the one ownership transfer confers. Absent, no signed-in user can assign a thread or join a thread's presence room — management clients still can, through the control plane.",
|
|
439
|
+
);
|
|
440
|
+
export type SupportStaffConfig = z.output<typeof SupportStaffConfig>;
|
|
441
|
+
|
|
346
442
|
export const SupportConfig = z
|
|
347
443
|
.object({
|
|
348
444
|
inboundAddresses: z
|
|
@@ -373,6 +469,9 @@ export const SupportConfig = z
|
|
|
373
469
|
submission: SupportSubmissionConfig.prefault({}).describe("The in-app submission channel."),
|
|
374
470
|
reply: SupportReplyConfig.prefault({}).describe("Reply settings."),
|
|
375
471
|
search: SupportSearchConfig.prefault({}).describe("How text search is answered."),
|
|
472
|
+
staff: SupportStaffConfig.optional().describe(
|
|
473
|
+
"Who of your app's signed-in users works the inbox, by organization power. Absent, the staff session routes refuse everyone.",
|
|
474
|
+
),
|
|
376
475
|
})
|
|
377
476
|
.describe(
|
|
378
477
|
"Configuration for the support capability — which addresses it claims, and how it classifies and bounds them.",
|