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

Commit 7d98bce

Browse files
committed
split helpers in files
1 parent 66911bb commit 7d98bce

9 files changed

Lines changed: 315 additions & 278 deletions

File tree

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"fallbacktools",
9696
"fetchtext",
9797
"ffprobe",
98+
"filetree",
9899
"firstsecond",
99100
"Fmepg",
100101
"frontmatter",
@@ -160,6 +161,7 @@
160161
"makeitbetter",
161162
"managedidentity",
162163
"markdownify",
164+
"markdownifypdf",
163165
"markitdown",
164166
"mcpclient",
165167
"mcpresource",

packages/core/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4945,6 +4945,10 @@ export interface ChatGenerationContext extends ChatTurnGenerationContext {
49454945
): Promise<{ image: WorkspaceFile; revisedPrompt?: string }>;
49464946
}
49474947

4948+
export interface ChatGenerationContextOptions {
4949+
ctx?: ChatGenerationContext;
4950+
}
4951+
49484952
export interface GenerationOutput {
49494953
/**
49504954
* full chat history

packages/runtime/src/cast.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/* eslint-disable @typescript-eslint/no-unused-expressions */
2+
import type {
3+
ChatGenerationContext,
4+
JSONSchema,
5+
JSONSchemaArray,
6+
PromptGenerator,
7+
PromptGeneratorOptions,
8+
StringLike,
9+
} from "@genaiscript/core";
10+
11+
/**
12+
* Converts unstructured text or data into structured JSON format.
13+
* Inspired by https://github.com/prefecthq/marvin.
14+
*
15+
* @param data - Input text or a prompt generator function to convert.
16+
* @param itemSchema - JSON schema defining the target data structure. If `multiple` is true, this will be treated as an array schema.
17+
* @param options - Configuration options for the conversion process, including context, instructions, label, and additional settings. If `multiple` is true, the schema will be treated as an array schema.
18+
* @returns An object containing the converted data, error information if applicable, and the raw text response.
19+
*/
20+
export async function cast(
21+
data: StringLike | PromptGenerator,
22+
itemSchema: JSONSchema,
23+
options?: PromptGeneratorOptions & ChatGenerationContextOptions & {
24+
multiple?: boolean;
25+
instructions?: string | PromptGenerator;
26+
},
27+
): Promise<{ data?: unknown; error?: string; text: string }> {
28+
const {
29+
ctx = globalPromptContext.env.generator,
30+
multiple,
31+
instructions,
32+
label = `cast text to schema`,
33+
...rest
34+
} = options || {};
35+
const responseSchema = multiple
36+
? ({
37+
type: "array",
38+
items: itemSchema,
39+
} satisfies JSONSchemaArray)
40+
: itemSchema;
41+
const res = await ctx.runPrompt(
42+
async (_) => {
43+
if (typeof data === "function") await data(_);
44+
else _.def("SOURCE", data);
45+
_.defSchema("SCHEMA", responseSchema, { format: "json" });
46+
_.$`You are an expert data converter specializing in transforming unstructured text source into structured data.
47+
Convert the contents of <SOURCE> to JSON using schema <SCHEMA>.
48+
- Treat images as <SOURCE> and convert them to JSON.
49+
- Make sure the returned data matches the schema in <SCHEMA>.`;
50+
if (typeof instructions === "string") _.$`${instructions}`;
51+
else if (typeof instructions === "function") await instructions(_);
52+
},
53+
{
54+
responseSchema,
55+
...rest,
56+
label,
57+
},
58+
);
59+
const text = globalPromptContext.parsers.unfence(res.text, "json");
60+
return res.json ? { text, data: res.json } : { text, error: res.error?.message };
61+
}

packages/runtime/src/classify.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export async function classify<L extends Record<string, string>>(
7777

7878
const choices = entries.map(([k]) => k);
7979
const allChoices = uniq<keyof typeof labels | "other">(choices);
80-
const ctx = options?.ctx || globalPromptContext.env.generator;
80+
const ctx: ChatGenerationContext = options?.ctx || globalPromptContext.env.generator;
8181

8282
const res = await ctx.runPrompt(
8383
async (_) => {

packages/runtime/src/filetree.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
Awaitable,
12+
ElementOrArray,
13+
FileStats,
14+
OptionsOrString,
15+
WorkspaceFile,
16+
WorkspaceGrepOptions,
17+
} from "@genaiscript/core";
18+
19+
/**
20+
* Creates a tree representation of files in the workspace.
21+
*
22+
* @param glob - Glob pattern to match files.
23+
* @param options - Configuration options for tree generation.
24+
* @param options.query - Optional search query to filter files.
25+
* @param options.size - Whether to include file sizes in the output.
26+
* @param options.ignore - Patterns to exclude from the results.
27+
* @param options.frontmatter - Frontmatter fields to extract from markdown files. Only applies to markdown files.
28+
* @param options.preview - Custom function to generate file previews based on file and stats.
29+
* @returns A formatted string representing the file tree structure, including metadata and file sizes if specified.
30+
*/
31+
export async function fileTree(
32+
glob: string,
33+
options?: WorkspaceGrepOptions & {
34+
query?: string | RegExp;
35+
size?: boolean;
36+
ignore?: ElementOrArray<string>;
37+
frontmatter?: OptionsOrString<"title" | "description" | "keywords" | "tags">[];
38+
preview?: (file: WorkspaceFile, stats: FileStats) => Awaitable<unknown>;
39+
},
40+
): Promise<string> {
41+
const { frontmatter, preview, query, size, ignore, ...rest } = options || {};
42+
const readText = !!(frontmatter || preview);
43+
// TODO
44+
const files = query
45+
? (await globalPromptContext.workspace.grep(query, glob, { ...rest, readText })).files
46+
: await globalPromptContext.workspace.findFiles(glob, {
47+
ignore,
48+
readText,
49+
});
50+
const tree = await buildTree(files);
51+
return renderTree(tree);
52+
53+
type TreeNode = {
54+
filename: string;
55+
children?: TreeNode[];
56+
stats: FileStats;
57+
metadata: string;
58+
};
59+
async function buildTree(files: WorkspaceFile[]): Promise<TreeNode[]> {
60+
const root: TreeNode[] = [];
61+
62+
for (const file of files) {
63+
const { filename } = file;
64+
const parts = filename.split(/[/\\]/);
65+
let currentLevel = root;
66+
for (let index = 0; index < parts.length; index++) {
67+
const part = parts[index];
68+
let node = currentLevel.find((n) => n.filename === part);
69+
if (!node) {
70+
const stats = await globalPromptContext.workspace.stat(filename);
71+
const metadata: unknown[] = [];
72+
if (frontmatter && /\.mdx?$/i.test(filename)) {
73+
const fm = globalPromptContext.parsers.frontmatter(file) || {};
74+
if (fm)
75+
metadata.push(
76+
...frontmatter
77+
.map((field) => [field, fm[field]])
78+
.filter(([, v]) => v !== undefined)
79+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`),
80+
);
81+
}
82+
if (preview) metadata.push(await preview(file, stats));
83+
node = {
84+
filename: part,
85+
metadata: metadata
86+
.filter((f) => f !== undefined)
87+
.map((s) => String(s))
88+
.map((s) => s.replace(/\n/g, " "))
89+
.join(", "),
90+
stats,
91+
};
92+
currentLevel.push(node);
93+
}
94+
if (index < parts.length - 1) {
95+
if (!node.children) {
96+
node.children = [];
97+
}
98+
currentLevel = node.children;
99+
}
100+
}
101+
}
102+
103+
return root;
104+
}
105+
106+
function renderTree(nodes: TreeNode[], prefix = ""): string {
107+
return nodes
108+
.map((node, index) => {
109+
const isLast = index === nodes.length - 1;
110+
const newPrefix = prefix + (isLast ? " " : "│ ");
111+
const children = node.children?.length ? renderTree(node.children, newPrefix) : "";
112+
const meta = [size ? `${Math.ceil(node.stats.size / 1000)}kb ` : undefined, node.metadata]
113+
.filter((s) => !!s)
114+
.join(", ");
115+
return `${prefix}${isLast ? "└ " : "├ "}${node.filename}${meta ? ` - ${meta}` : ""}\n${children}`;
116+
})
117+
.join("");
118+
}
119+
}

packages/runtime/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
export * from "./version.js";
45
export * from "./docker.js";
56
export * from "./input.js";
67
export * from "./log.js";
78
export * from "./nodehost.js";
89
export * from "./playwright.js";
910
export * from "./runtime.js";
1011
export * from "./classify.js";
11-
export * from "./version.js";
12+
export * from "./makeitbetter.js";
13+
export * from "./cast.js";
14+
export * from "./filetree.js";
15+
export * from "./markdownifypdf.js";
1216

1317
import { installGlobals } from "@genaiscript/core";
1418
import { NodeHost } from "./nodehost.js";
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { ChatGenerationContext } from "@genaiscript/core";
2+
3+
/**
4+
* Enhances content generation by applying iterative improvements.
5+
*
6+
* @param options - Configuration for the improvement process.
7+
* @param options.ctx - Chat generation context to use. Defaults to the environment generator if not provided.
8+
* @param options.repeat - Number of improvement iterations to perform. Defaults to 1.
9+
* @param options.instructions - Custom instructions for improvement. Defaults to "Make it better!".
10+
* The instructions are applied in each iteration.
11+
*/
12+
export function makeItBetter(options?: {
13+
ctx?: ChatGenerationContext;
14+
repeat?: number;
15+
instructions?: string;
16+
}) {
17+
const { repeat = 1, instructions = "Make it better!" } = options || {};
18+
const ctx: ChatGenerationContext = options?.ctx || globalPromptContext.env.generator;
19+
20+
let round = 0;
21+
ctx.defChatParticipant((cctx) => {
22+
if (round++ < repeat) {
23+
cctx.console.log(`make it better (round ${round})`);
24+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
25+
cctx.$`${instructions}`;
26+
}
27+
});
28+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
ParsePDFOptions,
13+
PromptGenerator,
14+
PromptGeneratorOptions,
15+
WorkspaceFile,
16+
} from "@genaiscript/core";
17+
18+
/**
19+
* Converts a PDF file to markdown format with intelligent formatting preservation.
20+
*
21+
* @param file - PDF file to convert.
22+
* @param options - Configuration options for PDF processing and markdown conversion, including instructions, context, and additional settings. The options can include rendering images, providing custom instructions, and specifying the context for processing. The text and images from the PDF are analyzed to ensure accurate markdown formatting.
23+
* @returns An object containing the original pages, rendered images, and markdown content for each page.
24+
*/
25+
export async function markdownifyPdf(
26+
file: WorkspaceFile,
27+
options?: PromptGeneratorOptions &
28+
Omit<ParsePDFOptions, "renderAsImage"> & {
29+
instructions?: string | PromptGenerator;
30+
ctx?: ChatGenerationContext;
31+
},
32+
) {
33+
const {
34+
ctx = globalPromptContext.env.generator,
35+
label = `markdownify PDF`,
36+
model = "ocr",
37+
responseType = "markdown",
38+
instructions,
39+
...rest
40+
} = options || {};
41+
42+
// extract text and render pages as images
43+
const { pages, images = [] } = await globalPromptContext.parsers.PDF(file, {
44+
...rest,
45+
renderAsImage: true,
46+
});
47+
const markdowns: string[] = [];
48+
for (let i = 0; i < pages.length; ++i) {
49+
const page = pages[i];
50+
const image = images[i];
51+
// mix of text and vision
52+
const res = await ctx.runPrompt(
53+
async (_) => {
54+
const previousPages = markdowns.slice(-2).join("\n\n");
55+
if (previousPages.length) _.def("PREVIOUS_PAGES", previousPages);
56+
if (page) _.def("PAGE", page);
57+
if (image) _.defImages(image, { autoCrop: true, greyscale: true });
58+
_.$`You are an expert at converting PDFs to markdown.
59+
60+
## Task
61+
Your task is to analyze the image and extract textual content in markdown format.
62+
63+
The image is a screenshot of the current page in the PDF document.
64+
We used pdfjs-dist to extract the text of the current page in <PAGE>, use it to help with the conversion.
65+
The text from the previous pages is in <PREVIOUS_PAGES>, use it to ensure consistency in the conversion.
66+
67+
## Instructions
68+
- Ensure markdown text formatting for the extracted text is applied properly by analyzing the image.
69+
- Do not change any content in the original extracted text while applying markdown formatting and do not repeat the extracted text.
70+
- Preserve markdown text formatting if present such as horizontal lines, header levels, footers, bullet points, links/urls, or other markdown elements.
71+
- Extract source code snippets in code fences.
72+
- Do not omit any textual content from the markdown formatted extracted text.
73+
- Do not generate page breaks
74+
- Do not repeat the <PREVIOUS_PAGES> content.
75+
- Do not include any additional explanations or comments in the markdown formatted extracted text.
76+
`;
77+
if (image) globalPromptContext.$`- For images, generate a short alt-text description.`;
78+
if (typeof instructions === "string") _.$`${instructions}`;
79+
else if (typeof instructions === "function") await instructions(_);
80+
},
81+
{
82+
...rest,
83+
model,
84+
label: `${label}: page ${i + 1}`,
85+
responseType,
86+
system: ["system", "system.assistant"],
87+
},
88+
);
89+
if (res.error) throw new Error(res.error?.message);
90+
markdowns.push(res.text);
91+
}
92+
93+
return { pages, images, markdowns };
94+
}

0 commit comments

Comments
 (0)