@graphext/cuery 0.2.1 → 0.3.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/esm/src/tools/classifier.d.ts +17 -0
- package/esm/src/tools/classifier.d.ts.map +1 -1
- package/esm/src/tools/classifier.js +77 -1
- package/esm/src/tools/topics.d.ts.map +1 -1
- package/esm/src/tools/topics.js +7 -1
- package/esm/src/tools/translate.d.ts.map +1 -1
- package/esm/src/tools/translate.js +4 -1
- package/package.json +1 -1
- package/script/src/tools/classifier.d.ts +17 -0
- package/script/src/tools/classifier.d.ts.map +1 -1
- package/script/src/tools/classifier.js +77 -0
- package/script/src/tools/topics.d.ts.map +1 -1
- package/script/src/tools/topics.js +7 -1
- package/script/src/tools/translate.d.ts.map +1 -1
- package/script/src/tools/translate.js +4 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AIParams } from '../openai.js';
|
|
1
2
|
/**
|
|
2
3
|
* Classifies a single data record into one of the provided categories using an LLM call.
|
|
3
4
|
*/
|
|
@@ -14,4 +15,20 @@ export declare function label(record: Record<string, unknown> | null, labels: Re
|
|
|
14
15
|
* Assigns labels to multiple data records concurrently while preserving order.
|
|
15
16
|
*/
|
|
16
17
|
export declare function labelBatch(records: Array<Record<string, unknown> | null>, labels: Record<string, string>, instructions?: string, model?: string, maxConcurrency?: number): Promise<Array<Array<string> | null>>;
|
|
18
|
+
export interface LabelExtractionOptions {
|
|
19
|
+
records: Array<Record<string, unknown>>;
|
|
20
|
+
nLabels?: number;
|
|
21
|
+
instructions?: string;
|
|
22
|
+
maxSamples?: number;
|
|
23
|
+
model?: string;
|
|
24
|
+
modelParams?: AIParams;
|
|
25
|
+
maxRetries?: number;
|
|
26
|
+
language?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Extracts a set of classification labels from an array of records using an LLM.
|
|
30
|
+
* Returns a Record<string, string> mapping label names to descriptions,
|
|
31
|
+
* which can be used directly with classify() and classifyBatch().
|
|
32
|
+
*/
|
|
33
|
+
export declare function extractLabels({ records, nLabels, instructions, maxSamples, model, modelParams, maxRetries, language }: LabelExtractionOptions): Promise<Record<string, string>>;
|
|
17
34
|
//# sourceMappingURL=classifier.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../../../src/src/tools/classifier.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../../../src/src/tools/classifier.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAoG5D;;GAEG;AACH,wBAAsB,QAAQ,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,YAAY,GAAE,MAAW,EACzB,KAAK,GAAE,MAAuB,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC5B,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,EAC9C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,YAAY,GAAE,MAAW,EACzB,KAAK,GAAE,MAAuB,EAC9B,cAAc,GAAE,MAAY,GAC1B,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAM/B;AAED;;GAEG;AACH,wBAAsB,KAAK,CAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,YAAY,GAAE,MAAW,EACzB,KAAK,GAAE,MAAuB,GAC5B,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAiB/B;AAED;;GAEG;AACH,wBAAgB,UAAU,CACzB,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,EAC9C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,YAAY,GAAE,MAAW,EACzB,KAAK,GAAE,MAAuB,EAC9B,cAAc,GAAE,MAAY,GAC1B,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAMtC;AA4CD,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,EACnC,OAAO,EACP,OAAY,EACZ,YAAiB,EACjB,UAAgB,EAChB,KAAiB,EACjB,WAAgB,EAChB,UAAc,EACd,QAA6C,EAC7C,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAsC1D"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from '../../deps/jsr.io/@zod/zod/4.1.12/src/index.js';
|
|
2
2
|
import { mapParallel } from '../async.js';
|
|
3
3
|
import { askOpenAISafe } from '../openai.js';
|
|
4
|
-
import { dedent } from '../utils.js';
|
|
4
|
+
import { dedent, formatRecordsAttrWise } from '../utils.js';
|
|
5
5
|
const PROMPT_TEMPLATE = dedent(`
|
|
6
6
|
# Instructions
|
|
7
7
|
|
|
@@ -133,3 +133,79 @@ export async function label(record, labels, instructions = '', model = 'gpt-4.1-
|
|
|
133
133
|
export function labelBatch(records, labels, instructions = '', model = 'gpt-4.1-mini', maxConcurrency = 100) {
|
|
134
134
|
return mapParallel(records, maxConcurrency, record => label(record, labels, instructions, model));
|
|
135
135
|
}
|
|
136
|
+
// =============================================================================
|
|
137
|
+
// Label Extraction
|
|
138
|
+
// =============================================================================
|
|
139
|
+
const EXTRACT_LABELS_PROMPT = dedent(`
|
|
140
|
+
# Instructions
|
|
141
|
+
|
|
142
|
+
From the data records below, extract a flat list of classification labels.
|
|
143
|
+
The output should be a JSON object with a "labels" array, where each item has a "name" and "description".
|
|
144
|
+
The list should not contain more than {n_labels} labels.
|
|
145
|
+
|
|
146
|
+
Make sure labels are generalizable and capture broad themes.
|
|
147
|
+
Each label should have a clear, concise description explaining what it represents.
|
|
148
|
+
Labels and descriptions must be written in {language}.
|
|
149
|
+
|
|
150
|
+
The labels should follow the MECE framework (Mutually Exclusive, Collectively Exhaustive):
|
|
151
|
+
- Mutually Exclusive: Labels should not overlap; each record should fit clearly into one category.
|
|
152
|
+
- Collectively Exhaustive: Labels should cover all the data; every record should have a fitting category.
|
|
153
|
+
- If needed, include an "Other" category for records that don't fit well into the main labels.
|
|
154
|
+
|
|
155
|
+
If the records contain mainly textual content (e.g., articles, posts, comments, descriptions),
|
|
156
|
+
the labels should represent the main topics or subject areas covered by the text,
|
|
157
|
+
unless the user provides different instructions below.
|
|
158
|
+
|
|
159
|
+
{instructions}
|
|
160
|
+
|
|
161
|
+
# Data Records
|
|
162
|
+
|
|
163
|
+
{records}
|
|
164
|
+
`);
|
|
165
|
+
/**
|
|
166
|
+
* Schema for extracted labels - an array of label objects.
|
|
167
|
+
* Uses array format instead of record because OpenAI doesn't support propertyNames in JSON schema.
|
|
168
|
+
*/
|
|
169
|
+
const ExtractedLabelsSchema = z.object({
|
|
170
|
+
labels: z.array(z.object({
|
|
171
|
+
name: z.string(),
|
|
172
|
+
description: z.string()
|
|
173
|
+
}))
|
|
174
|
+
});
|
|
175
|
+
/**
|
|
176
|
+
* Extracts a set of classification labels from an array of records using an LLM.
|
|
177
|
+
* Returns a Record<string, string> mapping label names to descriptions,
|
|
178
|
+
* which can be used directly with classify() and classifyBatch().
|
|
179
|
+
*/
|
|
180
|
+
export async function extractLabels({ records, nLabels = 10, instructions = '', maxSamples = 500, model = 'gpt-4.1', modelParams = {}, maxRetries = 8, language = 'The same language as the records' }) {
|
|
181
|
+
if (!records || records.length === 0) {
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
const sampledRecords = records.length > maxSamples
|
|
185
|
+
? records.slice(0, maxSamples)
|
|
186
|
+
: records;
|
|
187
|
+
const formattedRecords = formatRecordsAttrWise(sampledRecords);
|
|
188
|
+
const prompt = EXTRACT_LABELS_PROMPT
|
|
189
|
+
.replace('{n_labels}', String(nLabels))
|
|
190
|
+
.replace('{instructions}', instructions)
|
|
191
|
+
.replace('{records}', formattedRecords)
|
|
192
|
+
.replace('{language}', language);
|
|
193
|
+
const { parsed, output_text, error } = await askOpenAISafe(prompt, model, ExtractedLabelsSchema, modelParams, maxRetries, 'return');
|
|
194
|
+
if (error != null) {
|
|
195
|
+
if (output_text == null) {
|
|
196
|
+
throw new Error('Failed to get response from OpenAI');
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const extracted = JSON.parse(output_text);
|
|
200
|
+
const { labels } = ExtractedLabelsSchema.parse(extracted);
|
|
201
|
+
return Object.fromEntries(labels.map(l => [l.name, l.description]));
|
|
202
|
+
}
|
|
203
|
+
catch (parseError) {
|
|
204
|
+
throw new Error(`Failed to parse response: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (parsed == null) {
|
|
208
|
+
throw new Error('Failed to parse response from OpenAI');
|
|
209
|
+
}
|
|
210
|
+
return Object.fromEntries(parsed.labels.map(l => [l.name, l.description]));
|
|
211
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topics.d.ts","sourceRoot":"","sources":["../../../src/src/tools/topics.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,gDAAgD,CAAC;AAEnE,OAAO,EAAsC,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAEjF,OAAO,EAEN,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,MAAM,6BAA6B,CAAC;AAIrC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;AAuEpD;;;GAGG;AACH,eAAO,MAAM,KAAK;;;iBAShB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;iBAEvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,QAAQ;;;;;iBAEnB,CAAC;AAEH;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAKpH;AACD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY;;;;;;;kBAmBvD;
|
|
1
|
+
{"version":3,"file":"topics.d.ts","sourceRoot":"","sources":["../../../src/src/tools/topics.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,gDAAgD,CAAC;AAEnE,OAAO,EAAsC,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAEjF,OAAO,EAEN,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,MAAM,6BAA6B,CAAC;AAIrC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;AAuEpD;;;GAGG;AACH,eAAO,MAAM,KAAK;;;iBAShB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;iBAEvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,QAAQ;;;;;iBAEnB,CAAC;AAEH;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAKpH;AACD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY;;;;;;;kBAmBvD;AA+CD;;GAEG;AACH,wBAAsB,WAAW,CAChC,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,QAAQ,EAAE,YAAY,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,MAAM,EAClD,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,EAClC,KAAK,GAAE,MAAkB,EACzB,WAAW,GAAE,QAA4C,GACvD,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA2B5B;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC3B,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,EAC3B,QAAQ,EAAE,YAAY,GAAG,KAAK,CAAC,SAAS,CAAC,EACzC,KAAK,GAAE,MAAkB,EACzB,WAAW,GAAE,QAA4C,EACzD,cAAc,GAAE,MAAY,GAC1B,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,CAgBnC;AAED,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,EACnC,OAAO,EACP,OAAY,EACZ,UAAc,EACd,YAAiB,EACjB,UAAgB,EAChB,KAAiB,EACjB,WAAgB,EAChB,UAAc,EACd,QAA6C,EAC7C,EAAE,sBAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CAsChD"}
|
package/esm/src/tools/topics.js
CHANGED
|
@@ -126,12 +126,18 @@ const TOPICS_PROMPT = dedent(`
|
|
|
126
126
|
From the data records below, extract a two-level nested list of topics.
|
|
127
127
|
The output should be a JSON object with top-level topics as keys and lists of subtopics as values.
|
|
128
128
|
The top-level should not contain more than {n_topics} topics, and each top-level
|
|
129
|
-
should not contain more than {n_subtopics} subtopics.
|
|
129
|
+
should not contain more than {n_subtopics} subtopics. Fewer topics are acceptable, and appropriate if
|
|
130
|
+
the data does not support that many or if there are too few records.
|
|
130
131
|
|
|
131
132
|
Make sure top-level topics are generalizable and capture broad themes.
|
|
132
133
|
Subtopics should represent more specific categories within each theme.
|
|
133
134
|
Both topics and subtopics must be written in {language}.
|
|
134
135
|
|
|
136
|
+
The taxonomy should follow the MECE framework (Mutually Exclusive, Collectively Exhaustive):
|
|
137
|
+
- Mutually Exclusive: Topics and subtopics should not overlap; each record should fit clearly into one category.
|
|
138
|
+
- Collectively Exhaustive: The taxonomy should cover all the data; every record should have a fitting topic and subtopic.
|
|
139
|
+
- If needed, include an "Other" topic or subtopic for records that don't fit well into the main categories.
|
|
140
|
+
|
|
135
141
|
{instructions}
|
|
136
142
|
|
|
137
143
|
# Data Records
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../../src/src/tools/translate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AA+C/D,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,YAAY,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC9B,EAAE,OAAO,EAAE,QAAe,EAAE,KAAoB,EAAE,YAAY,EAAE,YAAiB,EAAE,EAAE,eAAe,GAClG,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED;;GAEG;AAEH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,YAAY,CAAC;CAC3B;AAED,wBAAsB,cAAc,CACnC,MAAM,EAAE,oBAAoB,GAC1B,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAQxB;
|
|
1
|
+
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../../src/src/tools/translate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AA+C/D,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,YAAY,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC9B,EAAE,OAAO,EAAE,QAAe,EAAE,KAAoB,EAAE,YAAY,EAAE,YAAiB,EAAE,EAAE,eAAe,GAClG,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED;;GAEG;AAEH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,YAAY,CAAC;CAC3B;AAED,wBAAsB,cAAc,CACnC,MAAM,EAAE,oBAAoB,GAC1B,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAQxB;AA+BD,MAAM,WAAW,sBAAsB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACrC,EAAE,MAAM,EAAE,QAAe,EAAE,KAAsB,EAAE,EAAE,sBAAsB,GACzE,OAAO,CAAC,MAAM,CAAC,CAWjB;AAED,MAAM,WAAW,2BAA2B;IAC3C,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAC1C,MAAM,EAAE,2BAA2B,GACjC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAQxB"}
|
|
@@ -71,7 +71,7 @@ You are an expert in understanding search intent.
|
|
|
71
71
|
Convert the following natural language prompt into an equivalent Google search keyword.
|
|
72
72
|
The keyword should:
|
|
73
73
|
|
|
74
|
-
- Be concise
|
|
74
|
+
- Be concise
|
|
75
75
|
- Capture the core search intent
|
|
76
76
|
- Remove conversational filler and politeness
|
|
77
77
|
- Use common search terms
|
|
@@ -84,6 +84,9 @@ For example:
|
|
|
84
84
|
|
|
85
85
|
Make sure the keyword is in the language "{language}".
|
|
86
86
|
|
|
87
|
+
It's mandatory that the keyword contains no more than 4 words, ideally 2-3 words.
|
|
88
|
+
Otherwise, Google keyword planner will not accept it.
|
|
89
|
+
|
|
87
90
|
# Prompt
|
|
88
91
|
|
|
89
92
|
{prompt}
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AIParams } from '../openai.js';
|
|
1
2
|
/**
|
|
2
3
|
* Classifies a single data record into one of the provided categories using an LLM call.
|
|
3
4
|
*/
|
|
@@ -14,4 +15,20 @@ export declare function label(record: Record<string, unknown> | null, labels: Re
|
|
|
14
15
|
* Assigns labels to multiple data records concurrently while preserving order.
|
|
15
16
|
*/
|
|
16
17
|
export declare function labelBatch(records: Array<Record<string, unknown> | null>, labels: Record<string, string>, instructions?: string, model?: string, maxConcurrency?: number): Promise<Array<Array<string> | null>>;
|
|
18
|
+
export interface LabelExtractionOptions {
|
|
19
|
+
records: Array<Record<string, unknown>>;
|
|
20
|
+
nLabels?: number;
|
|
21
|
+
instructions?: string;
|
|
22
|
+
maxSamples?: number;
|
|
23
|
+
model?: string;
|
|
24
|
+
modelParams?: AIParams;
|
|
25
|
+
maxRetries?: number;
|
|
26
|
+
language?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Extracts a set of classification labels from an array of records using an LLM.
|
|
30
|
+
* Returns a Record<string, string> mapping label names to descriptions,
|
|
31
|
+
* which can be used directly with classify() and classifyBatch().
|
|
32
|
+
*/
|
|
33
|
+
export declare function extractLabels({ records, nLabels, instructions, maxSamples, model, modelParams, maxRetries, language }: LabelExtractionOptions): Promise<Record<string, string>>;
|
|
17
34
|
//# sourceMappingURL=classifier.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../../../src/src/tools/classifier.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../../../src/src/tools/classifier.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAoG5D;;GAEG;AACH,wBAAsB,QAAQ,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,YAAY,GAAE,MAAW,EACzB,KAAK,GAAE,MAAuB,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC5B,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,EAC9C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,YAAY,GAAE,MAAW,EACzB,KAAK,GAAE,MAAuB,EAC9B,cAAc,GAAE,MAAY,GAC1B,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAM/B;AAED;;GAEG;AACH,wBAAsB,KAAK,CAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,YAAY,GAAE,MAAW,EACzB,KAAK,GAAE,MAAuB,GAC5B,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAiB/B;AAED;;GAEG;AACH,wBAAgB,UAAU,CACzB,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,EAC9C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,YAAY,GAAE,MAAW,EACzB,KAAK,GAAE,MAAuB,EAC9B,cAAc,GAAE,MAAY,GAC1B,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAMtC;AA4CD,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,EACnC,OAAO,EACP,OAAY,EACZ,YAAiB,EACjB,UAAgB,EAChB,KAAiB,EACjB,WAAgB,EAChB,UAAc,EACd,QAA6C,EAC7C,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAsC1D"}
|
|
@@ -4,6 +4,7 @@ exports.classify = classify;
|
|
|
4
4
|
exports.classifyBatch = classifyBatch;
|
|
5
5
|
exports.label = label;
|
|
6
6
|
exports.labelBatch = labelBatch;
|
|
7
|
+
exports.extractLabels = extractLabels;
|
|
7
8
|
const index_js_1 = require("../../deps/jsr.io/@zod/zod/4.1.12/src/index.js");
|
|
8
9
|
const async_js_1 = require("../async.js");
|
|
9
10
|
const openai_js_1 = require("../openai.js");
|
|
@@ -139,3 +140,79 @@ async function label(record, labels, instructions = '', model = 'gpt-4.1-mini')
|
|
|
139
140
|
function labelBatch(records, labels, instructions = '', model = 'gpt-4.1-mini', maxConcurrency = 100) {
|
|
140
141
|
return (0, async_js_1.mapParallel)(records, maxConcurrency, record => label(record, labels, instructions, model));
|
|
141
142
|
}
|
|
143
|
+
// =============================================================================
|
|
144
|
+
// Label Extraction
|
|
145
|
+
// =============================================================================
|
|
146
|
+
const EXTRACT_LABELS_PROMPT = (0, utils_js_1.dedent)(`
|
|
147
|
+
# Instructions
|
|
148
|
+
|
|
149
|
+
From the data records below, extract a flat list of classification labels.
|
|
150
|
+
The output should be a JSON object with a "labels" array, where each item has a "name" and "description".
|
|
151
|
+
The list should not contain more than {n_labels} labels.
|
|
152
|
+
|
|
153
|
+
Make sure labels are generalizable and capture broad themes.
|
|
154
|
+
Each label should have a clear, concise description explaining what it represents.
|
|
155
|
+
Labels and descriptions must be written in {language}.
|
|
156
|
+
|
|
157
|
+
The labels should follow the MECE framework (Mutually Exclusive, Collectively Exhaustive):
|
|
158
|
+
- Mutually Exclusive: Labels should not overlap; each record should fit clearly into one category.
|
|
159
|
+
- Collectively Exhaustive: Labels should cover all the data; every record should have a fitting category.
|
|
160
|
+
- If needed, include an "Other" category for records that don't fit well into the main labels.
|
|
161
|
+
|
|
162
|
+
If the records contain mainly textual content (e.g., articles, posts, comments, descriptions),
|
|
163
|
+
the labels should represent the main topics or subject areas covered by the text,
|
|
164
|
+
unless the user provides different instructions below.
|
|
165
|
+
|
|
166
|
+
{instructions}
|
|
167
|
+
|
|
168
|
+
# Data Records
|
|
169
|
+
|
|
170
|
+
{records}
|
|
171
|
+
`);
|
|
172
|
+
/**
|
|
173
|
+
* Schema for extracted labels - an array of label objects.
|
|
174
|
+
* Uses array format instead of record because OpenAI doesn't support propertyNames in JSON schema.
|
|
175
|
+
*/
|
|
176
|
+
const ExtractedLabelsSchema = index_js_1.z.object({
|
|
177
|
+
labels: index_js_1.z.array(index_js_1.z.object({
|
|
178
|
+
name: index_js_1.z.string(),
|
|
179
|
+
description: index_js_1.z.string()
|
|
180
|
+
}))
|
|
181
|
+
});
|
|
182
|
+
/**
|
|
183
|
+
* Extracts a set of classification labels from an array of records using an LLM.
|
|
184
|
+
* Returns a Record<string, string> mapping label names to descriptions,
|
|
185
|
+
* which can be used directly with classify() and classifyBatch().
|
|
186
|
+
*/
|
|
187
|
+
async function extractLabels({ records, nLabels = 10, instructions = '', maxSamples = 500, model = 'gpt-4.1', modelParams = {}, maxRetries = 8, language = 'The same language as the records' }) {
|
|
188
|
+
if (!records || records.length === 0) {
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
const sampledRecords = records.length > maxSamples
|
|
192
|
+
? records.slice(0, maxSamples)
|
|
193
|
+
: records;
|
|
194
|
+
const formattedRecords = (0, utils_js_1.formatRecordsAttrWise)(sampledRecords);
|
|
195
|
+
const prompt = EXTRACT_LABELS_PROMPT
|
|
196
|
+
.replace('{n_labels}', String(nLabels))
|
|
197
|
+
.replace('{instructions}', instructions)
|
|
198
|
+
.replace('{records}', formattedRecords)
|
|
199
|
+
.replace('{language}', language);
|
|
200
|
+
const { parsed, output_text, error } = await (0, openai_js_1.askOpenAISafe)(prompt, model, ExtractedLabelsSchema, modelParams, maxRetries, 'return');
|
|
201
|
+
if (error != null) {
|
|
202
|
+
if (output_text == null) {
|
|
203
|
+
throw new Error('Failed to get response from OpenAI');
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const extracted = JSON.parse(output_text);
|
|
207
|
+
const { labels } = ExtractedLabelsSchema.parse(extracted);
|
|
208
|
+
return Object.fromEntries(labels.map(l => [l.name, l.description]));
|
|
209
|
+
}
|
|
210
|
+
catch (parseError) {
|
|
211
|
+
throw new Error(`Failed to parse response: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (parsed == null) {
|
|
215
|
+
throw new Error('Failed to parse response from OpenAI');
|
|
216
|
+
}
|
|
217
|
+
return Object.fromEntries(parsed.labels.map(l => [l.name, l.description]));
|
|
218
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topics.d.ts","sourceRoot":"","sources":["../../../src/src/tools/topics.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,gDAAgD,CAAC;AAEnE,OAAO,EAAsC,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAEjF,OAAO,EAEN,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,MAAM,6BAA6B,CAAC;AAIrC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;AAuEpD;;;GAGG;AACH,eAAO,MAAM,KAAK;;;iBAShB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;iBAEvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,QAAQ;;;;;iBAEnB,CAAC;AAEH;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAKpH;AACD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY;;;;;;;kBAmBvD;
|
|
1
|
+
{"version":3,"file":"topics.d.ts","sourceRoot":"","sources":["../../../src/src/tools/topics.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,gDAAgD,CAAC;AAEnE,OAAO,EAAsC,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAEjF,OAAO,EAEN,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,MAAM,6BAA6B,CAAC;AAIrC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;AAuEpD;;;GAGG;AACH,eAAO,MAAM,KAAK;;;iBAShB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;iBAEvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,QAAQ;;;;;iBAEnB,CAAC;AAEH;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAKpH;AACD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY;;;;;;;kBAmBvD;AA+CD;;GAEG;AACH,wBAAsB,WAAW,CAChC,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,QAAQ,EAAE,YAAY,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,MAAM,EAClD,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,EAClC,KAAK,GAAE,MAAkB,EACzB,WAAW,GAAE,QAA4C,GACvD,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA2B5B;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC3B,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,EAC3B,QAAQ,EAAE,YAAY,GAAG,KAAK,CAAC,SAAS,CAAC,EACzC,KAAK,GAAE,MAAkB,EACzB,WAAW,GAAE,QAA4C,EACzD,cAAc,GAAE,MAAY,GAC1B,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,CAgBnC;AAED,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,EACnC,OAAO,EACP,OAAY,EACZ,UAAc,EACd,YAAiB,EACjB,UAAgB,EAChB,KAAiB,EACjB,WAAgB,EAChB,UAAc,EACd,QAA6C,EAC7C,EAAE,sBAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CAsChD"}
|
|
@@ -134,12 +134,18 @@ const TOPICS_PROMPT = (0, utils_js_1.dedent)(`
|
|
|
134
134
|
From the data records below, extract a two-level nested list of topics.
|
|
135
135
|
The output should be a JSON object with top-level topics as keys and lists of subtopics as values.
|
|
136
136
|
The top-level should not contain more than {n_topics} topics, and each top-level
|
|
137
|
-
should not contain more than {n_subtopics} subtopics.
|
|
137
|
+
should not contain more than {n_subtopics} subtopics. Fewer topics are acceptable, and appropriate if
|
|
138
|
+
the data does not support that many or if there are too few records.
|
|
138
139
|
|
|
139
140
|
Make sure top-level topics are generalizable and capture broad themes.
|
|
140
141
|
Subtopics should represent more specific categories within each theme.
|
|
141
142
|
Both topics and subtopics must be written in {language}.
|
|
142
143
|
|
|
144
|
+
The taxonomy should follow the MECE framework (Mutually Exclusive, Collectively Exhaustive):
|
|
145
|
+
- Mutually Exclusive: Topics and subtopics should not overlap; each record should fit clearly into one category.
|
|
146
|
+
- Collectively Exhaustive: The taxonomy should cover all the data; every record should have a fitting topic and subtopic.
|
|
147
|
+
- If needed, include an "Other" topic or subtopic for records that don't fit well into the main categories.
|
|
148
|
+
|
|
143
149
|
{instructions}
|
|
144
150
|
|
|
145
151
|
# Data Records
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../../src/src/tools/translate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AA+C/D,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,YAAY,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC9B,EAAE,OAAO,EAAE,QAAe,EAAE,KAAoB,EAAE,YAAY,EAAE,YAAiB,EAAE,EAAE,eAAe,GAClG,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED;;GAEG;AAEH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,YAAY,CAAC;CAC3B;AAED,wBAAsB,cAAc,CACnC,MAAM,EAAE,oBAAoB,GAC1B,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAQxB;
|
|
1
|
+
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../../src/src/tools/translate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AA+C/D,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,YAAY,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC9B,EAAE,OAAO,EAAE,QAAe,EAAE,KAAoB,EAAE,YAAY,EAAE,YAAiB,EAAE,EAAE,eAAe,GAClG,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED;;GAEG;AAEH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,YAAY,CAAC;CAC3B;AAED,wBAAsB,cAAc,CACnC,MAAM,EAAE,oBAAoB,GAC1B,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAQxB;AA+BD,MAAM,WAAW,sBAAsB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACrC,EAAE,MAAM,EAAE,QAAe,EAAE,KAAsB,EAAE,EAAE,sBAAsB,GACzE,OAAO,CAAC,MAAM,CAAC,CAWjB;AAED,MAAM,WAAW,2BAA2B;IAC3C,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAC1C,MAAM,EAAE,2BAA2B,GACjC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAQxB"}
|
|
@@ -77,7 +77,7 @@ You are an expert in understanding search intent.
|
|
|
77
77
|
Convert the following natural language prompt into an equivalent Google search keyword.
|
|
78
78
|
The keyword should:
|
|
79
79
|
|
|
80
|
-
- Be concise
|
|
80
|
+
- Be concise
|
|
81
81
|
- Capture the core search intent
|
|
82
82
|
- Remove conversational filler and politeness
|
|
83
83
|
- Use common search terms
|
|
@@ -90,6 +90,9 @@ For example:
|
|
|
90
90
|
|
|
91
91
|
Make sure the keyword is in the language "{language}".
|
|
92
92
|
|
|
93
|
+
It's mandatory that the keyword contains no more than 4 words, ideally 2-3 words.
|
|
94
|
+
Otherwise, Google keyword planner will not accept it.
|
|
95
|
+
|
|
93
96
|
# Prompt
|
|
94
97
|
|
|
95
98
|
{prompt}
|