@bpmnkit/casen-worker-ai 0.1.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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-check.log +5 -0
- package/.turbo/turbo-test.log +18 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +35 -0
- package/dist/config.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/llm.d.ts +18 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +57 -0
- package/dist/llm.js.map +1 -0
- package/dist/operations/classify.d.ts +16 -0
- package/dist/operations/classify.d.ts.map +1 -0
- package/dist/operations/classify.js +74 -0
- package/dist/operations/classify.js.map +1 -0
- package/dist/operations/classify.test.d.ts +2 -0
- package/dist/operations/classify.test.d.ts.map +1 -0
- package/dist/operations/classify.test.js +74 -0
- package/dist/operations/classify.test.js.map +1 -0
- package/dist/operations/decide.d.ts +16 -0
- package/dist/operations/decide.d.ts.map +1 -0
- package/dist/operations/decide.js +60 -0
- package/dist/operations/decide.js.map +1 -0
- package/dist/operations/decide.test.d.ts +2 -0
- package/dist/operations/decide.test.d.ts.map +1 -0
- package/dist/operations/decide.test.js +65 -0
- package/dist/operations/decide.test.js.map +1 -0
- package/dist/operations/extract.d.ts +16 -0
- package/dist/operations/extract.d.ts.map +1 -0
- package/dist/operations/extract.js +70 -0
- package/dist/operations/extract.js.map +1 -0
- package/dist/operations/extract.test.d.ts +2 -0
- package/dist/operations/extract.test.d.ts.map +1 -0
- package/dist/operations/extract.test.js +65 -0
- package/dist/operations/extract.test.js.map +1 -0
- package/dist/operations/summarize.d.ts +16 -0
- package/dist/operations/summarize.d.ts.map +1 -0
- package/dist/operations/summarize.js +59 -0
- package/dist/operations/summarize.js.map +1 -0
- package/dist/operations/summarize.test.d.ts +2 -0
- package/dist/operations/summarize.test.d.ts.map +1 -0
- package/dist/operations/summarize.test.js +42 -0
- package/dist/operations/summarize.test.js.map +1 -0
- package/package.json +69 -0
- package/src/config.test.ts +40 -0
- package/src/config.ts +27 -0
- package/src/index.ts +54 -0
- package/src/llm.ts +67 -0
- package/src/operations/classify.test.ts +101 -0
- package/src/operations/classify.ts +85 -0
- package/src/operations/decide.test.ts +82 -0
- package/src/operations/decide.ts +70 -0
- package/src/operations/extract.test.ts +80 -0
- package/src/operations/extract.ts +80 -0
- package/src/operations/summarize.test.ts +48 -0
- package/src/operations/summarize.ts +68 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { WorkerJob, WorkerJobResult } from "@bpmnkit/cli-sdk"
|
|
2
|
+
import type { AiWorkerConfig } from "../config.js"
|
|
3
|
+
import { RetryableError, callLlm, parseJsonResponse } from "../llm.js"
|
|
4
|
+
|
|
5
|
+
interface ExtractResponse {
|
|
6
|
+
extracted: Record<string, unknown>
|
|
7
|
+
missingFields: string[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SYSTEM_PROMPT = `You are a structured data extraction assistant.
|
|
11
|
+
The user will provide unstructured text and a list of fields to extract.
|
|
12
|
+
Respond ONLY with valid JSON matching this exact shape:
|
|
13
|
+
{ "extracted": { "<field>": <value or null>, ... }, "missingFields": ["<fields not found>"] }
|
|
14
|
+
Use null for fields you cannot confidently determine. List those field names in missingFields.
|
|
15
|
+
Do not include any prose outside the JSON object.`
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extracts structured fields from job.variables.input.
|
|
19
|
+
*
|
|
20
|
+
* Required job variables:
|
|
21
|
+
* - input: string — the unstructured text
|
|
22
|
+
* - fields: string[] — field names to extract
|
|
23
|
+
*
|
|
24
|
+
* Optional:
|
|
25
|
+
* - schema: Record<string, string> — type hints per field, e.g. { amount: "number", date: "ISO 8601 string" }
|
|
26
|
+
*
|
|
27
|
+
* Output variables: extracted (object), missingFields (array), aiModel, processedAt
|
|
28
|
+
*/
|
|
29
|
+
export async function extract(job: WorkerJob, config: AiWorkerConfig): Promise<WorkerJobResult> {
|
|
30
|
+
const input = String(job.variables.input ?? "")
|
|
31
|
+
const fields = job.variables.fields
|
|
32
|
+
if (!Array.isArray(fields) || fields.length === 0) {
|
|
33
|
+
return {
|
|
34
|
+
outcome: "error",
|
|
35
|
+
errorCode: "AI_INVALID_INPUT",
|
|
36
|
+
errorMessage: 'Job variable "fields" must be a non-empty array of field name strings.',
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const schema = job.variables.schema
|
|
40
|
+
const schemaHint =
|
|
41
|
+
schema && typeof schema === "object" && !Array.isArray(schema)
|
|
42
|
+
? `\nType hints: ${JSON.stringify(schema)}`
|
|
43
|
+
: ""
|
|
44
|
+
const userMessage = `Fields to extract: ${JSON.stringify(fields)}${schemaHint}\n\nText:\n${input}`
|
|
45
|
+
|
|
46
|
+
let raw: string
|
|
47
|
+
try {
|
|
48
|
+
raw = await callLlm(SYSTEM_PROMPT, userMessage, config)
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err instanceof RetryableError) {
|
|
51
|
+
return { outcome: "fail", errorMessage: err.message, retries: 2, retryBackOff: 30_000 }
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
outcome: "error",
|
|
55
|
+
errorCode: "AI_API_ERROR",
|
|
56
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let parsed: ExtractResponse
|
|
61
|
+
try {
|
|
62
|
+
parsed = parseJsonResponse<ExtractResponse>(raw)
|
|
63
|
+
} catch {
|
|
64
|
+
return {
|
|
65
|
+
outcome: "error",
|
|
66
|
+
errorCode: "AI_PARSE_ERROR",
|
|
67
|
+
errorMessage: `Model returned non-JSON response: ${raw.slice(0, 200)}`,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
outcome: "complete",
|
|
73
|
+
variables: {
|
|
74
|
+
extracted: parsed.extracted,
|
|
75
|
+
missingFields: parsed.missingFields ?? [],
|
|
76
|
+
aiModel: config.model,
|
|
77
|
+
processedAt: new Date().toISOString(),
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest"
|
|
2
|
+
import type { AiWorkerConfig } from "../config.js"
|
|
3
|
+
import * as llmModule from "../llm.js"
|
|
4
|
+
import { summarize } from "./summarize.js"
|
|
5
|
+
|
|
6
|
+
const cfg: AiWorkerConfig = {
|
|
7
|
+
apiKey: "test",
|
|
8
|
+
model: "claude-test",
|
|
9
|
+
maxTokens: 512,
|
|
10
|
+
timeoutMs: 5000,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeJob(variables: Record<string, unknown>) {
|
|
14
|
+
return {
|
|
15
|
+
jobKey: "1",
|
|
16
|
+
processDefinitionId: "p1",
|
|
17
|
+
elementId: "e1",
|
|
18
|
+
processInstanceKey: "i1",
|
|
19
|
+
variables,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("summarize", () => {
|
|
24
|
+
it("returns complete with summary and wordCount", async () => {
|
|
25
|
+
vi.spyOn(llmModule, "callLlm").mockResolvedValue(
|
|
26
|
+
JSON.stringify({ summary: "A short summary.", wordCount: 3 }),
|
|
27
|
+
)
|
|
28
|
+
const result = await summarize(makeJob({ input: "Long text here..." }), cfg)
|
|
29
|
+
expect(result.outcome).toBe("complete")
|
|
30
|
+
if (result.outcome === "complete") {
|
|
31
|
+
expect(result.variables.summary).toBe("A short summary.")
|
|
32
|
+
expect(result.variables.wordCount).toBe(3)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("returns error on parse failure", async () => {
|
|
37
|
+
vi.spyOn(llmModule, "callLlm").mockResolvedValue("Here is the summary: blah blah.")
|
|
38
|
+
const result = await summarize(makeJob({ input: "text" }), cfg)
|
|
39
|
+
expect(result.outcome).toBe("error")
|
|
40
|
+
if (result.outcome === "error") expect(result.errorCode).toBe("AI_PARSE_ERROR")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("returns fail on RetryableError", async () => {
|
|
44
|
+
vi.spyOn(llmModule, "callLlm").mockRejectedValue(new llmModule.RetryableError("timeout"))
|
|
45
|
+
const result = await summarize(makeJob({ input: "text" }), cfg)
|
|
46
|
+
expect(result.outcome).toBe("fail")
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { WorkerJob, WorkerJobResult } from "@bpmnkit/cli-sdk"
|
|
2
|
+
import type { AiWorkerConfig } from "../config.js"
|
|
3
|
+
import { RetryableError, callLlm, parseJsonResponse } from "../llm.js"
|
|
4
|
+
|
|
5
|
+
interface SummarizeResponse {
|
|
6
|
+
summary: string
|
|
7
|
+
wordCount: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SYSTEM_PROMPT = `You are a text summarization assistant.
|
|
11
|
+
The user will provide text to summarize along with a target length and style.
|
|
12
|
+
Respond ONLY with valid JSON matching this exact shape:
|
|
13
|
+
{ "summary": "<the summary text>", "wordCount": <integer> }
|
|
14
|
+
Do not include any prose outside the JSON object.`
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Summarizes job.variables.input.
|
|
18
|
+
*
|
|
19
|
+
* Required job variables:
|
|
20
|
+
* - input: string — the text to summarize
|
|
21
|
+
*
|
|
22
|
+
* Optional:
|
|
23
|
+
* - maxWords: number — target word count (default: 100)
|
|
24
|
+
* - style: "bullet" | "paragraph" — output style (default: "paragraph")
|
|
25
|
+
*
|
|
26
|
+
* Output variables: summary, wordCount, aiModel, processedAt
|
|
27
|
+
*/
|
|
28
|
+
export async function summarize(job: WorkerJob, config: AiWorkerConfig): Promise<WorkerJobResult> {
|
|
29
|
+
const input = String(job.variables.input ?? "")
|
|
30
|
+
const maxWords = Number(job.variables.maxWords ?? 100)
|
|
31
|
+
const style = String(job.variables.style ?? "paragraph")
|
|
32
|
+
const userMessage = `Summarize the following text in ${style} style, targeting ${maxWords} words:\n\n${input}`
|
|
33
|
+
|
|
34
|
+
let raw: string
|
|
35
|
+
try {
|
|
36
|
+
raw = await callLlm(SYSTEM_PROMPT, userMessage, config)
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err instanceof RetryableError) {
|
|
39
|
+
return { outcome: "fail", errorMessage: err.message, retries: 2, retryBackOff: 30_000 }
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
outcome: "error",
|
|
43
|
+
errorCode: "AI_API_ERROR",
|
|
44
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let parsed: SummarizeResponse
|
|
49
|
+
try {
|
|
50
|
+
parsed = parseJsonResponse<SummarizeResponse>(raw)
|
|
51
|
+
} catch {
|
|
52
|
+
return {
|
|
53
|
+
outcome: "error",
|
|
54
|
+
errorCode: "AI_PARSE_ERROR",
|
|
55
|
+
errorMessage: `Model returned non-JSON response: ${raw.slice(0, 200)}`,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
outcome: "complete",
|
|
61
|
+
variables: {
|
|
62
|
+
summary: parsed.summary,
|
|
63
|
+
wordCount: parsed.wordCount,
|
|
64
|
+
aiModel: config.model,
|
|
65
|
+
processedAt: new Date().toISOString(),
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|