@botpress/zai 1.0.1 → 1.2.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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/build.ts +9 -0
  3. package/dist/adapters/adapter.js +2 -0
  4. package/dist/adapters/botpress-table.js +168 -0
  5. package/dist/adapters/memory.js +12 -0
  6. package/dist/index.d.ts +111 -609
  7. package/dist/index.js +9 -1873
  8. package/dist/operations/check.js +153 -0
  9. package/dist/operations/constants.js +2 -0
  10. package/dist/operations/errors.js +15 -0
  11. package/dist/operations/extract.js +232 -0
  12. package/dist/operations/filter.js +191 -0
  13. package/dist/operations/label.js +249 -0
  14. package/dist/operations/rewrite.js +123 -0
  15. package/dist/operations/summarize.js +133 -0
  16. package/dist/operations/text.js +47 -0
  17. package/dist/utils.js +37 -0
  18. package/dist/zai.js +100 -0
  19. package/e2e/data/botpress_docs.txt +26040 -0
  20. package/e2e/data/cache.jsonl +107 -0
  21. package/e2e/utils.ts +89 -0
  22. package/package.json +33 -29
  23. package/src/adapters/adapter.ts +35 -0
  24. package/src/adapters/botpress-table.ts +210 -0
  25. package/src/adapters/memory.ts +13 -0
  26. package/src/index.ts +11 -0
  27. package/src/operations/check.ts +201 -0
  28. package/src/operations/constants.ts +2 -0
  29. package/src/operations/errors.ts +9 -0
  30. package/src/operations/extract.ts +309 -0
  31. package/src/operations/filter.ts +244 -0
  32. package/src/operations/label.ts +345 -0
  33. package/src/operations/rewrite.ts +161 -0
  34. package/src/operations/summarize.ts +195 -0
  35. package/src/operations/text.ts +65 -0
  36. package/src/utils.ts +52 -0
  37. package/src/zai.ts +147 -0
  38. package/tsconfig.json +3 -23
  39. package/dist/index.cjs +0 -1903
  40. package/dist/index.cjs.map +0 -1
  41. package/dist/index.d.cts +0 -916
  42. package/dist/index.js.map +0 -1
  43. package/tsup.config.ts +0 -16
  44. package/vitest.config.ts +0 -9
  45. package/vitest.setup.ts +0 -24
@@ -0,0 +1,153 @@
1
+ import { z } from "@bpinternal/zui";
2
+ import { fastHash, stringify, takeUntilTokens } from "../utils";
3
+ import { Zai } from "../zai";
4
+ import { PROMPT_INPUT_BUFFER } from "./constants";
5
+ const Example = z.object({
6
+ input: z.any(),
7
+ check: z.boolean(),
8
+ reason: z.string().optional()
9
+ });
10
+ const Options = z.object({
11
+ examples: z.array(Example).describe("Examples to check the condition against").default([])
12
+ });
13
+ const TRUE = "\u25A0TRUE\u25A0";
14
+ const FALSE = "\u25A0FALSE\u25A0";
15
+ const END = "\u25A0END\u25A0";
16
+ Zai.prototype.check = async function(input, condition, _options) {
17
+ const options = Options.parse(_options ?? {});
18
+ const tokenizer = await this.getTokenizer();
19
+ await this.fetchModelDetails();
20
+ const PROMPT_COMPONENT = Math.max(this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER, 100);
21
+ const taskId = this.taskId;
22
+ const taskType = "zai.check";
23
+ const PROMPT_TOKENS = {
24
+ INPUT: Math.floor(0.5 * PROMPT_COMPONENT),
25
+ CONDITION: Math.floor(0.2 * PROMPT_COMPONENT)
26
+ };
27
+ const inputAsString = tokenizer.truncate(stringify(input), PROMPT_TOKENS.INPUT);
28
+ condition = tokenizer.truncate(condition, PROMPT_TOKENS.CONDITION);
29
+ const EXAMPLES_TOKENS = PROMPT_COMPONENT - tokenizer.count(inputAsString) - tokenizer.count(condition);
30
+ const Key = fastHash(
31
+ JSON.stringify({
32
+ taskType,
33
+ taskId,
34
+ input: inputAsString,
35
+ condition
36
+ })
37
+ );
38
+ const examples = taskId ? await this.adapter.getExamples({
39
+ input: inputAsString,
40
+ taskType,
41
+ taskId
42
+ }) : [];
43
+ const exactMatch = examples.find((x) => x.key === Key);
44
+ if (exactMatch) {
45
+ return exactMatch.output;
46
+ }
47
+ const defaultExamples = [
48
+ { input: "50 Cent", check: true, reason: "50 Cent is widely recognized as a public personality." },
49
+ {
50
+ input: ["apple", "banana", "carrot", "house"],
51
+ check: false,
52
+ reason: "The list contains a house, which is not a fruit. Also, the list contains a carrot, which is a vegetable."
53
+ }
54
+ ];
55
+ const userExamples = [
56
+ ...examples.map((e) => ({ input: e.input, check: e.output, reason: e.explanation })),
57
+ ...options.examples
58
+ ];
59
+ let exampleId = 1;
60
+ const formatInput = (input2, condition2) => {
61
+ const header = userExamples.length ? `Expert Example #${exampleId++}` : `Example of condition: "${condition2}"`;
62
+ return `
63
+ ${header}
64
+ <|start_input|>
65
+ ${input2.trim()}
66
+ <|end_input|>
67
+ `.trim();
68
+ };
69
+ const formatOutput = (answer2, justification) => {
70
+ return `
71
+ Analysis: ${justification}
72
+ Final Answer: ${answer2 ? TRUE : FALSE}
73
+ ${END}
74
+ `.trim();
75
+ };
76
+ const formatExample = (example) => [
77
+ { type: "text", content: formatInput(stringify(example.input ?? null), condition), role: "user" },
78
+ {
79
+ type: "text",
80
+ content: formatOutput(example.check, example.reason ?? ""),
81
+ role: "assistant"
82
+ }
83
+ ];
84
+ const allExamples = takeUntilTokens(
85
+ userExamples.length ? userExamples : defaultExamples,
86
+ EXAMPLES_TOKENS,
87
+ (el) => tokenizer.count(stringify(el.input)) + tokenizer.count(el.reason ?? "")
88
+ ).map(formatExample).flat();
89
+ const specialInstructions = userExamples.length ? `
90
+ - You have been provided with examples from previous experts. Make sure to read them carefully before making your decision.
91
+ - Make sure to refer to the examples provided by the experts to justify your decision (when applicable).
92
+ - When in doubt, ground your decision on the examples provided by the experts instead of your own intuition.
93
+ - When no example is similar to the input, make sure to provide a clear justification for your decision while inferring the decision-making process from the examples provided by the experts.
94
+ `.trim() : "";
95
+ const { output, meta } = await this.callModel({
96
+ systemPrompt: `
97
+ Check if the following condition is true or false for the given input. Before answering, make sure to read the input and the condition carefully.
98
+ Justify your answer, then answer with either ${TRUE} or ${FALSE} at the very end, then add ${END} to finish the response.
99
+ IMPORTANT: Make sure to answer with either ${TRUE} or ${FALSE} at the end of your response, but NOT both.
100
+ ---
101
+ Expert Examples (#1 to #${exampleId - 1}):
102
+ ${specialInstructions}
103
+ `.trim(),
104
+ stopSequences: [END],
105
+ messages: [
106
+ ...allExamples,
107
+ {
108
+ type: "text",
109
+ content: `
110
+ Considering the below input and above examples, is the following condition true or false?
111
+ ${formatInput(inputAsString, condition)}
112
+ In your "Analysis", please refer to the Expert Examples # to justify your decision.`.trim(),
113
+ role: "user"
114
+ }
115
+ ]
116
+ });
117
+ const answer = output.choices[0]?.content;
118
+ const hasTrue = answer.includes(TRUE);
119
+ const hasFalse = answer.includes(FALSE);
120
+ if (!hasTrue && !hasFalse) {
121
+ throw new Error(`The model did not return a valid answer. The response was: ${answer}`);
122
+ }
123
+ let finalAnswer;
124
+ if (hasTrue && hasFalse) {
125
+ finalAnswer = answer.lastIndexOf(TRUE) > answer.lastIndexOf(FALSE);
126
+ } else {
127
+ finalAnswer = hasTrue;
128
+ }
129
+ if (taskId) {
130
+ await this.adapter.saveExample({
131
+ key: Key,
132
+ taskType,
133
+ taskId,
134
+ input: inputAsString,
135
+ instructions: condition,
136
+ metadata: {
137
+ cost: {
138
+ input: meta.cost.input,
139
+ output: meta.cost.output
140
+ },
141
+ latency: meta.latency,
142
+ model: this.Model,
143
+ tokens: {
144
+ input: meta.tokens.input,
145
+ output: meta.tokens.output
146
+ }
147
+ },
148
+ output: finalAnswer,
149
+ explanation: answer.replace(TRUE, "").replace(FALSE, "").replace(END, "").replace("Final Answer:", "").trim()
150
+ });
151
+ }
152
+ return finalAnswer;
153
+ };
@@ -0,0 +1,2 @@
1
+ export const PROMPT_INPUT_BUFFER = 1048;
2
+ export const PROMPT_OUTPUT_BUFFER = 512;
@@ -0,0 +1,15 @@
1
+ export class JsonParsingError extends Error {
2
+ constructor(json, error) {
3
+ const message = `Error parsing JSON:
4
+
5
+ ---JSON---
6
+ ${json}
7
+
8
+ ---Error---
9
+
10
+ ${error}`;
11
+ super(message);
12
+ this.json = json;
13
+ this.error = error;
14
+ }
15
+ }
@@ -0,0 +1,232 @@
1
+ import { z } from "@bpinternal/zui";
2
+ import JSON5 from "json5";
3
+ import { jsonrepair } from "jsonrepair";
4
+ import { chunk, isArray } from "lodash-es";
5
+ import { fastHash, stringify, takeUntilTokens } from "../utils";
6
+ import { Zai } from "../zai";
7
+ import { PROMPT_INPUT_BUFFER } from "./constants";
8
+ import { JsonParsingError } from "./errors";
9
+ const Options = z.object({
10
+ instructions: z.string().optional().describe("Instructions to guide the user on how to extract the data"),
11
+ chunkLength: z.number().min(100).max(1e5).optional().describe("The maximum number of tokens per chunk").default(16e3)
12
+ });
13
+ const START = "\u25A0json_start\u25A0";
14
+ const END = "\u25A0json_end\u25A0";
15
+ const NO_MORE = "\u25A0NO_MORE_ELEMENT\u25A0";
16
+ Zai.prototype.extract = async function(input, schema, _options) {
17
+ const options = Options.parse(_options ?? {});
18
+ const tokenizer = await this.getTokenizer();
19
+ await this.fetchModelDetails();
20
+ const taskId = this.taskId;
21
+ const taskType = "zai.extract";
22
+ const PROMPT_COMPONENT = Math.max(this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER, 100);
23
+ let isArrayOfObjects = false;
24
+ const originalSchema = schema;
25
+ const baseType = (schema.naked ? schema.naked() : schema)?.constructor?.name ?? "unknown";
26
+ if (baseType === "ZodObject") {
27
+ } else if (baseType === "ZodArray") {
28
+ let elementType = schema.element;
29
+ if (elementType.naked) {
30
+ elementType = elementType.naked();
31
+ }
32
+ if (elementType?.constructor?.name === "ZodObject") {
33
+ isArrayOfObjects = true;
34
+ schema = elementType;
35
+ } else {
36
+ throw new Error("Schema must be a ZodObject or a ZodArray<ZodObject>");
37
+ }
38
+ } else {
39
+ throw new Error("Schema must be either a ZuiObject or a ZuiArray<ZuiObject>");
40
+ }
41
+ const schemaTypescript = schema.toTypescript({ declaration: false });
42
+ const schemaLength = tokenizer.count(schemaTypescript);
43
+ options.chunkLength = Math.min(
44
+ options.chunkLength,
45
+ this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER - schemaLength
46
+ );
47
+ const keys = Object.keys(schema.shape);
48
+ let inputAsString = stringify(input);
49
+ if (tokenizer.count(inputAsString) > options.chunkLength) {
50
+ if (isArrayOfObjects) {
51
+ const tokens = tokenizer.split(inputAsString);
52
+ const chunks = chunk(tokens, options.chunkLength).map((x) => x.join(""));
53
+ const all = await Promise.all(chunks.map((chunk2) => this.extract(chunk2, originalSchema)));
54
+ return all.flat();
55
+ } else {
56
+ inputAsString = tokenizer.truncate(stringify(input), options.chunkLength);
57
+ }
58
+ }
59
+ const instructions = [];
60
+ if (options.instructions) {
61
+ instructions.push(options.instructions);
62
+ }
63
+ const shape = `{ ${keys.map((key) => `"${key}": ...`).join(", ")} }`;
64
+ const abbv = "{ ... }";
65
+ if (isArrayOfObjects) {
66
+ instructions.push("You may have multiple elements, or zero elements in the input.");
67
+ instructions.push("You must extract each element separately.");
68
+ instructions.push(`Each element must be a JSON object with exactly the format: ${START}${shape}${END}`);
69
+ instructions.push(`When you are done extracting all elements, type "${NO_MORE}" to finish.`);
70
+ instructions.push(`For example, if you have zero elements, the output should look like this: ${NO_MORE}`);
71
+ instructions.push(
72
+ `For example, if you have two elements, the output should look like this: ${START}${abbv}${END}${START}${abbv}${END}${NO_MORE}`
73
+ );
74
+ } else {
75
+ instructions.push("You may have exactly one element in the input.");
76
+ instructions.push(`The element must be a JSON object with exactly the format: ${START}${shape}${END}`);
77
+ }
78
+ const EXAMPLES_TOKENS = PROMPT_COMPONENT - tokenizer.count(inputAsString) - tokenizer.count(instructions.join("\n"));
79
+ const Key = fastHash(
80
+ JSON.stringify({
81
+ taskType,
82
+ taskId,
83
+ input: inputAsString,
84
+ instructions: options.instructions
85
+ })
86
+ );
87
+ const examples = taskId ? await this.adapter.getExamples({
88
+ input: inputAsString,
89
+ taskType,
90
+ taskId
91
+ }) : [];
92
+ const exactMatch = examples.find((x) => x.key === Key);
93
+ if (exactMatch) {
94
+ return exactMatch.output;
95
+ }
96
+ const defaultExample = isArrayOfObjects ? {
97
+ input: `The story goes as follow.
98
+ Once upon a time, there was a person named Alice who was 30 years old.
99
+ Then, there was a person named Bob who was 25 years old.
100
+ The end.`,
101
+ schema: "Array<{ name: string, age: number }>",
102
+ instructions: "Extract all people",
103
+ extracted: [
104
+ {
105
+ name: "Alice",
106
+ age: 30
107
+ },
108
+ {
109
+ name: "Bob",
110
+ age: 25
111
+ }
112
+ ]
113
+ } : {
114
+ input: `The story goes as follow.
115
+ Once upon a time, there was a person named Alice who was 30 years old.
116
+ The end.`,
117
+ schema: "{ name: string, age: number }",
118
+ instructions: "Extract the person",
119
+ extracted: { name: "Alice", age: 30 }
120
+ };
121
+ const userExamples = examples.map((e) => ({
122
+ input: e.input,
123
+ extracted: e.output,
124
+ schema: schemaTypescript,
125
+ instructions: options.instructions
126
+ }));
127
+ let exampleId = 1;
128
+ const formatInput = (input2, schema2, instructions2) => {
129
+ const header = userExamples.length ? `Expert Example #${exampleId++}` : "Here's an example to help you understand the format:";
130
+ return `
131
+ ${header}
132
+
133
+ <|start_schema|>
134
+ ${schema2}
135
+ <|end_schema|>
136
+
137
+ <|start_instructions|>
138
+ ${instructions2 ?? "No specific instructions, just follow the schema above."}
139
+ <|end_instructions|>
140
+
141
+ <|start_input|>
142
+ ${input2.trim()}
143
+ <|end_input|>
144
+ `.trim();
145
+ };
146
+ const formatOutput = (extracted) => {
147
+ extracted = isArray(extracted) ? extracted : [extracted];
148
+ return extracted.map(
149
+ (x) => `
150
+ ${START}
151
+ ${JSON.stringify(x, null, 2)}
152
+ ${END}`.trim()
153
+ ).join("\n") + NO_MORE;
154
+ };
155
+ const formatExample = (example) => [
156
+ {
157
+ type: "text",
158
+ content: formatInput(stringify(example.input ?? null), example.schema, example.instructions),
159
+ role: "user"
160
+ },
161
+ {
162
+ type: "text",
163
+ content: formatOutput(example.extracted),
164
+ role: "assistant"
165
+ }
166
+ ];
167
+ const allExamples = takeUntilTokens(
168
+ userExamples.length ? userExamples : [defaultExample],
169
+ EXAMPLES_TOKENS,
170
+ (el) => tokenizer.count(stringify(el.input)) + tokenizer.count(stringify(el.extracted))
171
+ ).map(formatExample).flat();
172
+ const { output, meta } = await this.callModel({
173
+ systemPrompt: `
174
+ Extract the following information from the input:
175
+ ${schemaTypescript}
176
+ ====
177
+
178
+ ${instructions.map((x) => `\u2022 ${x}`).join("\n")}
179
+ `.trim(),
180
+ stopSequences: [isArrayOfObjects ? NO_MORE : END],
181
+ messages: [
182
+ ...allExamples,
183
+ {
184
+ role: "user",
185
+ type: "text",
186
+ content: formatInput(inputAsString, schemaTypescript, options.instructions ?? "")
187
+ }
188
+ ]
189
+ });
190
+ const answer = output.choices[0]?.content;
191
+ const elements = answer.split(START).filter((x) => x.trim().length > 0).map((x) => {
192
+ try {
193
+ const json = x.slice(0, x.indexOf(END)).trim();
194
+ const repairedJson = jsonrepair(json);
195
+ const parsedJson = JSON5.parse(repairedJson);
196
+ return schema.parse(parsedJson);
197
+ } catch (error) {
198
+ throw new JsonParsingError(x, error instanceof Error ? error : new Error("Unknown error"));
199
+ }
200
+ }).filter((x) => x !== null);
201
+ let final;
202
+ if (isArrayOfObjects) {
203
+ final = elements;
204
+ } else if (elements.length === 0) {
205
+ final = schema.parse({});
206
+ } else {
207
+ final = elements[0];
208
+ }
209
+ if (taskId) {
210
+ await this.adapter.saveExample({
211
+ key: Key,
212
+ taskId: `zai/${taskId}`,
213
+ taskType,
214
+ instructions: options.instructions ?? "No specific instructions",
215
+ input: inputAsString,
216
+ output: final,
217
+ metadata: {
218
+ cost: {
219
+ input: meta.cost.input,
220
+ output: meta.cost.output
221
+ },
222
+ latency: meta.latency,
223
+ model: this.Model,
224
+ tokens: {
225
+ input: meta.tokens.input,
226
+ output: meta.tokens.output
227
+ }
228
+ }
229
+ });
230
+ }
231
+ return final;
232
+ };
@@ -0,0 +1,191 @@
1
+ import { z } from "@bpinternal/zui";
2
+ import { clamp } from "lodash-es";
3
+ import { fastHash, stringify, takeUntilTokens } from "../utils";
4
+ import { Zai } from "../zai";
5
+ import { PROMPT_INPUT_BUFFER, PROMPT_OUTPUT_BUFFER } from "./constants";
6
+ const Example = z.object({
7
+ input: z.any(),
8
+ filter: z.boolean(),
9
+ reason: z.string().optional()
10
+ });
11
+ const Options = z.object({
12
+ tokensPerItem: z.number().min(1).max(1e5).optional().describe("The maximum number of tokens per item").default(250),
13
+ examples: z.array(Example).describe("Examples to filter the condition against").default([])
14
+ });
15
+ const END = "\u25A0END\u25A0";
16
+ Zai.prototype.filter = async function(input, condition, _options) {
17
+ const options = Options.parse(_options ?? {});
18
+ const tokenizer = await this.getTokenizer();
19
+ await this.fetchModelDetails();
20
+ const taskId = this.taskId;
21
+ const taskType = "zai.filter";
22
+ const MAX_ITEMS_PER_CHUNK = 50;
23
+ const TOKENS_TOTAL_MAX = this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER - PROMPT_OUTPUT_BUFFER;
24
+ const TOKENS_EXAMPLES_MAX = Math.floor(Math.max(250, TOKENS_TOTAL_MAX * 0.5));
25
+ const TOKENS_CONDITION_MAX = clamp(TOKENS_TOTAL_MAX * 0.25, 250, tokenizer.count(condition));
26
+ const TOKENS_INPUT_ARRAY_MAX = TOKENS_TOTAL_MAX - TOKENS_EXAMPLES_MAX - TOKENS_CONDITION_MAX;
27
+ condition = tokenizer.truncate(condition, TOKENS_CONDITION_MAX);
28
+ let chunks = [];
29
+ let currentChunk = [];
30
+ let currentChunkTokens = 0;
31
+ for (const element of input) {
32
+ const elementAsString = tokenizer.truncate(stringify(element, false), options.tokensPerItem);
33
+ const elementTokens = tokenizer.count(elementAsString);
34
+ if (currentChunkTokens + elementTokens > TOKENS_INPUT_ARRAY_MAX || currentChunk.length >= MAX_ITEMS_PER_CHUNK) {
35
+ chunks.push(currentChunk);
36
+ currentChunk = [];
37
+ currentChunkTokens = 0;
38
+ }
39
+ currentChunk.push(element);
40
+ currentChunkTokens += elementTokens;
41
+ }
42
+ if (currentChunk.length > 0) {
43
+ chunks.push(currentChunk);
44
+ }
45
+ chunks = chunks.filter((x) => x.length > 0);
46
+ const formatInput = (input2, condition2) => {
47
+ return `
48
+ Condition to check:
49
+ ${condition2}
50
+
51
+ Items (from \u25A00 to \u25A0${input2.length - 1})
52
+ ==============================
53
+ ${input2.map((x, idx) => `\u25A0${idx} = ${stringify(x.input ?? null, false)}`).join("\n")}
54
+ `.trim();
55
+ };
56
+ const formatExamples = (examples) => {
57
+ return `
58
+ ${examples.map((x, idx) => `\u25A0${idx}:${!!x.filter ? "true" : "false"}`).join("")}
59
+ ${END}
60
+ ====
61
+ Here's the reasoning behind each example:
62
+ ${examples.map((x, idx) => `\u25A0${idx}:${!!x.filter ? "true" : "false"}:${x.reason ?? "No reason provided"}`).join("\n")}
63
+ `.trim();
64
+ };
65
+ const genericExamples = [
66
+ {
67
+ input: "apple",
68
+ filter: true,
69
+ reason: "Apples are fruits"
70
+ },
71
+ {
72
+ input: "Apple Inc.",
73
+ filter: false,
74
+ reason: "Apple Inc. is a company, not a fruit"
75
+ },
76
+ {
77
+ input: "banana",
78
+ filter: true,
79
+ reason: "Bananas are fruits"
80
+ },
81
+ {
82
+ input: "potato",
83
+ filter: false,
84
+ reason: "Potatoes are vegetables"
85
+ }
86
+ ];
87
+ const genericExamplesMessages = [
88
+ {
89
+ type: "text",
90
+ content: formatInput(genericExamples, "is a fruit"),
91
+ role: "user"
92
+ },
93
+ {
94
+ type: "text",
95
+ content: formatExamples(genericExamples),
96
+ role: "assistant"
97
+ }
98
+ ];
99
+ const filterChunk = async (chunk) => {
100
+ const examples = taskId ? await this.adapter.getExamples({
101
+ // The Table API can't search for a huge input string
102
+ input: JSON.stringify(chunk).slice(0, 1e3),
103
+ taskType,
104
+ taskId
105
+ }).then(
106
+ (x) => x.map((y) => ({ filter: y.output, input: y.input, reason: y.explanation }))
107
+ ) : [];
108
+ const allExamples = takeUntilTokens(
109
+ [...examples, ...options.examples ?? []],
110
+ TOKENS_EXAMPLES_MAX,
111
+ (el) => tokenizer.count(stringify(el.input))
112
+ );
113
+ const exampleMessages = [
114
+ {
115
+ type: "text",
116
+ content: formatInput(allExamples, condition),
117
+ role: "user"
118
+ },
119
+ {
120
+ type: "text",
121
+ content: formatExamples(allExamples),
122
+ role: "assistant"
123
+ }
124
+ ];
125
+ const { output, meta } = await this.callModel({
126
+ systemPrompt: `
127
+ You are given a list of items. Your task is to filter out the items that meet the condition below.
128
+ You need to return the full list of items with the format:
129
+ \u25A0x:true\u25A0y:false\u25A0z:true (where x, y, z are the indices of the items in the list)
130
+ You need to start with "\u25A00" and go up to the last index "\u25A0${chunk.length - 1}".
131
+ If an item meets the condition, you should return ":true", otherwise ":false".
132
+
133
+ IMPORTANT: Make sure to read the condition and the examples carefully before making your decision.
134
+ The condition is: "${condition}"
135
+ `.trim(),
136
+ stopSequences: [END],
137
+ messages: [
138
+ ...exampleMessages.length ? exampleMessages : genericExamplesMessages,
139
+ {
140
+ type: "text",
141
+ content: formatInput(
142
+ chunk.map((x) => ({ input: x })),
143
+ condition
144
+ ),
145
+ role: "user"
146
+ }
147
+ ]
148
+ });
149
+ const answer = output.choices[0]?.content;
150
+ const indices = answer.trim().split("\u25A0").filter((x) => x.length > 0).map((x) => {
151
+ const [idx, filter] = x.split(":");
152
+ return { idx: parseInt(idx?.trim() ?? ""), filter: filter?.toLowerCase().trim() === "true" };
153
+ });
154
+ const partial = chunk.filter((_, idx) => {
155
+ return indices.find((x) => x.idx === idx)?.filter ?? false;
156
+ });
157
+ if (taskId) {
158
+ const key = fastHash(
159
+ stringify({
160
+ taskId,
161
+ taskType,
162
+ input: JSON.stringify(chunk),
163
+ condition
164
+ })
165
+ );
166
+ await this.adapter.saveExample({
167
+ key,
168
+ taskType,
169
+ taskId,
170
+ input: JSON.stringify(chunk),
171
+ output: partial,
172
+ instructions: condition,
173
+ metadata: {
174
+ cost: {
175
+ input: meta.cost.input,
176
+ output: meta.cost.output
177
+ },
178
+ latency: meta.latency,
179
+ model: this.Model,
180
+ tokens: {
181
+ input: meta.tokens.input,
182
+ output: meta.tokens.output
183
+ }
184
+ }
185
+ });
186
+ }
187
+ return partial;
188
+ };
189
+ const filteredChunks = await Promise.all(chunks.map(filterChunk));
190
+ return filteredChunks.flat();
191
+ };