Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Commit 66911bb

Browse files
committed
split runtime file
1 parent 9afa980 commit 66911bb

3 files changed

Lines changed: 159 additions & 148 deletions

File tree

packages/runtime/src/classify.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/* eslint-disable @typescript-eslint/no-unused-expressions */
2+
// Copyright (c) Microsoft Corporation.
3+
// Licensed under the MIT License.
4+
5+
/**
6+
* GenAIScript supporting runtime
7+
* This module provides core functionality for text classification, data transformation,
8+
* PDF processing, and file system operations in the GenAIScript environment.
9+
*/
10+
import type {
11+
ChatGenerationContext,
12+
Logprob,
13+
PromptGenerator,
14+
PromptGeneratorOptions,
15+
RunPromptUsage,
16+
StringLike,
17+
} from "@genaiscript/core";
18+
import { uniq } from "es-toolkit";
19+
20+
/**
21+
* Options for classifying data using AI models.
22+
*
23+
* @property {boolean} [other] - Inject a 'other' label.
24+
* @property {boolean} [explanations] - Explain answers before returning token.
25+
* @property {ChatGenerationContext} [ctx] - Options runPrompt context.
26+
*/
27+
export type ClassifyOptions = {
28+
/**
29+
* When true, adds an 'other' category to handle cases that don't match defined labels
30+
*/
31+
other?: boolean;
32+
/**
33+
* When true, provides explanatory text before the classification result
34+
*/
35+
explanations?: boolean;
36+
/**
37+
* Context for running the classification prompt
38+
*/
39+
ctx?: ChatGenerationContext;
40+
} & Omit<PromptGeneratorOptions, "choices">;
41+
42+
/**
43+
* Classifies input text into predefined categories using AI.
44+
* Inspired by https://github.com/prefecthq/marvin.
45+
*
46+
* @param text - Text content to classify or a prompt generator function.
47+
* @param labels - Object mapping label names to their descriptions.
48+
* @param options - Configuration options for classification, including whether to add an "other" category, provide explanations, and specify context.
49+
* @returns Classification result containing the chosen label, confidence metrics, log probabilities, the full answer text, and usage statistics.
50+
* @throws Error if fewer than two labels are provided (including "other").
51+
*/
52+
export async function classify<L extends Record<string, string>>(
53+
text: StringLike | PromptGenerator,
54+
labels: L,
55+
options?: ClassifyOptions,
56+
): Promise<{
57+
label: keyof typeof labels | "other";
58+
entropy?: number;
59+
logprob?: number;
60+
probPercent?: number;
61+
answer: string;
62+
logprobs?: Record<keyof typeof labels | "other", Logprob>;
63+
usage?: RunPromptUsage;
64+
}> {
65+
const { other, explanations, ...rest } = options || {};
66+
67+
const entries = Object.entries({
68+
...labels,
69+
...(other
70+
? {
71+
other: "This label is used when the text does not fit any of the available labels.",
72+
}
73+
: {}),
74+
}).map(([k, v]) => [k.trim().toLowerCase(), v]);
75+
76+
if (entries.length < 2) throw Error("classify must have at least two label (including other)");
77+
78+
const choices = entries.map(([k]) => k);
79+
const allChoices = uniq<keyof typeof labels | "other">(choices);
80+
const ctx = options?.ctx || globalPromptContext.env.generator;
81+
82+
const res = await ctx.runPrompt(
83+
async (_) => {
84+
_.$`## Expert Classifier
85+
You are a specialized text classification system.
86+
Your task is to carefully read and classify any input text or image into one
87+
of the predefined labels below.
88+
For each label, you will find a short description. Use these descriptions to guide your decision.
89+
`.role("system");
90+
_.$`## Labels
91+
You must classify the data as one of the following labels.
92+
${entries.map(([id, descr]) => `- Label '${id}': ${descr}`).join("\n")}
93+
94+
## Output
95+
${explanations ? "Provide a single short sentence justification for your choice." : ""}
96+
Output the label as a single word on the last line (do not emit "Label").
97+
98+
`;
99+
_.fence(
100+
`- Label 'yes': funny
101+
- Label 'no': not funny
102+
103+
DATA:
104+
Why did the chicken cross the road? Because moo.
105+
106+
Output:
107+
${explanations ? "It's a classic joke but the ending does not relate to the start of the joke." : ""}
108+
no
109+
110+
`,
111+
{ language: "example" },
112+
);
113+
if (typeof text === "function") await text(_);
114+
else _.def("DATA", text);
115+
},
116+
{
117+
model: "classify",
118+
choices: choices,
119+
label: `classify ${choices.join(", ")}`,
120+
logprobs: true,
121+
topLogprobs: Math.min(3, choices.length),
122+
maxTokens: explanations ? 100 : 1,
123+
system: [
124+
"system.output_plaintext",
125+
"system.safety_jailbreak",
126+
"system.safety_harmful_content",
127+
"system.safety_protected_material",
128+
],
129+
...rest,
130+
},
131+
);
132+
133+
// find the last label
134+
const answer = res.text.toLowerCase();
135+
const indexes = choices.map((l) => answer.lastIndexOf(l));
136+
const labeli = indexes.reduce((previ, _label, i) => {
137+
if (indexes[i] > indexes[previ]) return i;
138+
else return previ;
139+
}, 0);
140+
const label = entries[labeli][0];
141+
const logprobs = res.choices
142+
? (Object.fromEntries(
143+
res.choices.filter((c) => !isNaN(c?.logprob)).map((c, i) => [allChoices[i], c]),
144+
) as Record<keyof typeof labels | "other", Logprob>)
145+
: undefined;
146+
const logprob = logprobs?.[label];
147+
const usage = res.usage;
148+
149+
return {
150+
label,
151+
entropy: logprob?.entropy,
152+
logprob: logprob?.logprob,
153+
probPercent: logprob?.probPercent,
154+
answer,
155+
logprobs,
156+
usage,
157+
};
158+
}

packages/runtime/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from "./log.js";
77
export * from "./nodehost.js";
88
export * from "./playwright.js";
99
export * from "./runtime.js";
10+
export * from "./classify.js";
1011
export * from "./version.js";
1112

1213
import { installGlobals } from "@genaiscript/core";

packages/runtime/src/runtime.ts

Lines changed: 0 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -14,166 +14,18 @@ import type {
1414
FileStats,
1515
JSONSchema,
1616
JSONSchemaArray,
17-
Logprob,
1817
OptionsOrString,
1918
ParsePDFOptions,
2019
PromptContext,
2120
PromptGenerator,
2221
PromptGeneratorOptions,
23-
RunPromptUsage,
2422
StringLike,
2523
WorkspaceFile,
2624
WorkspaceGrepOptions,
2725
} from "@genaiscript/core";
28-
import { delay, uniq, uniqBy, chunk } from "es-toolkit";
29-
import { z } from "zod";
3026

3127
const globalPromptContext: PromptContext = globalThis as unknown as PromptContext;
3228

33-
/**
34-
* Utility functions exported for general use
35-
*/
36-
export { delay, uniq, uniqBy, z, chunk };
37-
/**
38-
* Options for classifying data using AI models.
39-
*
40-
* @property {boolean} [other] - Inject a 'other' label.
41-
* @property {boolean} [explanations] - Explain answers before returning token.
42-
* @property {ChatGenerationContext} [ctx] - Options runPrompt context.
43-
*/
44-
export type ClassifyOptions = {
45-
/**
46-
* When true, adds an 'other' category to handle cases that don't match defined labels
47-
*/
48-
other?: boolean;
49-
/**
50-
* When true, provides explanatory text before the classification result
51-
*/
52-
explanations?: boolean;
53-
/**
54-
* Context for running the classification prompt
55-
*/
56-
ctx?: ChatGenerationContext;
57-
} & Omit<PromptGeneratorOptions, "choices">;
58-
59-
/**
60-
* Classifies input text into predefined categories using AI.
61-
* Inspired by https://github.com/prefecthq/marvin.
62-
*
63-
* @param text - Text content to classify or a prompt generator function.
64-
* @param labels - Object mapping label names to their descriptions.
65-
* @param options - Configuration options for classification, including whether to add an "other" category, provide explanations, and specify context.
66-
* @returns Classification result containing the chosen label, confidence metrics, log probabilities, the full answer text, and usage statistics.
67-
* @throws Error if fewer than two labels are provided (including "other").
68-
*/
69-
export async function classify<L extends Record<string, string>>(
70-
text: StringLike | PromptGenerator,
71-
labels: L,
72-
options?: ClassifyOptions,
73-
): Promise<{
74-
label: keyof typeof labels | "other";
75-
entropy?: number;
76-
logprob?: number;
77-
probPercent?: number;
78-
answer: string;
79-
logprobs?: Record<keyof typeof labels | "other", Logprob>;
80-
usage?: RunPromptUsage;
81-
}> {
82-
const { other, explanations, ...rest } = options || {};
83-
84-
const entries = Object.entries({
85-
...labels,
86-
...(other
87-
? {
88-
other: "This label is used when the text does not fit any of the available labels.",
89-
}
90-
: {}),
91-
}).map(([k, v]) => [k.trim().toLowerCase(), v]);
92-
93-
if (entries.length < 2) throw Error("classify must have at least two label (including other)");
94-
95-
const choices = entries.map(([k]) => k);
96-
const allChoices = uniq<keyof typeof labels | "other">(choices);
97-
const ctx = options?.ctx || globalPromptContext.env.generator;
98-
99-
const res = await ctx.runPrompt(
100-
async (_) => {
101-
_.$`## Expert Classifier
102-
You are a specialized text classification system.
103-
Your task is to carefully read and classify any input text or image into one
104-
of the predefined labels below.
105-
For each label, you will find a short description. Use these descriptions to guide your decision.
106-
`.role("system");
107-
_.$`## Labels
108-
You must classify the data as one of the following labels.
109-
${entries.map(([id, descr]) => `- Label '${id}': ${descr}`).join("\n")}
110-
111-
## Output
112-
${explanations ? "Provide a single short sentence justification for your choice." : ""}
113-
Output the label as a single word on the last line (do not emit "Label").
114-
115-
`;
116-
_.fence(
117-
`- Label 'yes': funny
118-
- Label 'no': not funny
119-
120-
DATA:
121-
Why did the chicken cross the road? Because moo.
122-
123-
Output:
124-
${explanations ? "It's a classic joke but the ending does not relate to the start of the joke." : ""}
125-
no
126-
127-
`,
128-
{ language: "example" },
129-
);
130-
if (typeof text === "function") await text(_);
131-
else _.def("DATA", text);
132-
},
133-
{
134-
model: "classify",
135-
choices: choices,
136-
label: `classify ${choices.join(", ")}`,
137-
logprobs: true,
138-
topLogprobs: Math.min(3, choices.length),
139-
maxTokens: explanations ? 100 : 1,
140-
system: [
141-
"system.output_plaintext",
142-
"system.safety_jailbreak",
143-
"system.safety_harmful_content",
144-
"system.safety_protected_material",
145-
],
146-
...rest,
147-
},
148-
);
149-
150-
// find the last label
151-
const answer = res.text.toLowerCase();
152-
const indexes = choices.map((l) => answer.lastIndexOf(l));
153-
const labeli = indexes.reduce((previ, _label, i) => {
154-
if (indexes[i] > indexes[previ]) return i;
155-
else return previ;
156-
}, 0);
157-
const label = entries[labeli][0];
158-
const logprobs = res.choices
159-
? (Object.fromEntries(
160-
res.choices.filter((c) => !isNaN(c?.logprob)).map((c, i) => [allChoices[i], c]),
161-
) as Record<keyof typeof labels | "other", Logprob>)
162-
: undefined;
163-
const logprob = logprobs?.[label];
164-
const usage = res.usage;
165-
166-
return {
167-
label,
168-
entropy: logprob?.entropy,
169-
logprob: logprob?.logprob,
170-
probPercent: logprob?.probPercent,
171-
answer,
172-
logprobs,
173-
usage,
174-
};
175-
}
176-
17729
/**
17830
* Enhances content generation by applying iterative improvements.
17931
*

0 commit comments

Comments
 (0)