Skip to content

Commit da7a25b

Browse files
VinciGit00claude
andauthored
feat(sdk): add MIME and PDF processor options (#21)
* feat(search): add contentTypes filter Search now accepts contentTypes so callers can pick which content types get fetched. Adds application/json and text/markdown to fetchContentTypeSchema to match the API enum. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(search): align PDF options with API * fix(sdk): support PDF options across endpoints * fix(sdk): match content validation with API * fix(ci): quote local package rewrite steps * fix(types): allow default PDF page cap --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9197b24 commit da7a25b

6 files changed

Lines changed: 142 additions & 12 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ jobs:
1414
- uses: actions/checkout@v4
1515
- uses: oven-sh/setup-bun@v2
1616
- name: Use local scrapegraph-js package
17-
run: sed -i 's/"scrapegraph-js": "\^2.2.0"/"scrapegraph-js": "file:..\/.."/' packages/ai-sdk/package.json
17+
run: |
18+
sed -i 's/"scrapegraph-js": "\^2.2.0"/"scrapegraph-js": "file:..\/.."/' packages/ai-sdk/package.json
1819
- run: bun install
1920
- run: bun run test
2021

@@ -25,7 +26,8 @@ jobs:
2526
- uses: actions/checkout@v4
2627
- uses: oven-sh/setup-bun@v2
2728
- name: Use local scrapegraph-js package
28-
run: sed -i 's/"scrapegraph-js": "\^2.2.0"/"scrapegraph-js": "file:..\/.."/' packages/ai-sdk/package.json
29+
run: |
30+
sed -i 's/"scrapegraph-js": "\^2.2.0"/"scrapegraph-js": "file:..\/.."/' packages/ai-sdk/package.json
2931
- run: bun install
3032
- run: bun run build
3133
- run: bun run check

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,15 @@ const res = await sgai.search({
155155
timeRange: "past_week", // optional
156156
locationGeoCode: "us", // optional
157157
fetchConfig: { /* ... */ }, // optional
158+
allowedTypes: ["text/html", "application/pdf"], // optional MIME allowlist
158159
});
159160
```
160161

162+
By default `search` accepts every supported content type, including PDFs, and processes up to 25
163+
pages per PDF. You do not need to send `processors` or `maxPages` for this default. Use
164+
`allowedTypes` to restrict accepted MIME types. Only configure `processors` to override the cap;
165+
`{ type: "pdf" }` also defaults to 25, while `maxPages` accepts `1``500`, or `-1` for no page limit.
166+
161167
### crawl
162168

163169
Crawl a website and its linked pages.

src/schemas.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export const htmlModeSchema = z.enum(["normal", "reader", "prune"]);
1010
export const fetchContentTypeSchema = z.enum([
1111
"text/html",
1212
"application/json",
13+
"text/markdown",
14+
"text/plain",
15+
"text/csv",
16+
"application/x-latex",
1317
"application/pdf",
1418
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1519
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
@@ -25,10 +29,27 @@ export const fetchContentTypeSchema = z.enum([
2529
"application/epub+zip",
2630
"application/rtf",
2731
"application/vnd.oasis.opendocument.text",
28-
"text/csv",
29-
"text/plain",
30-
"application/x-latex",
3132
]);
33+
export const pdfProcessorSchema = z.object({
34+
type: z.literal("pdf"),
35+
maxPages: z.union([z.literal(-1), z.number().int().min(1).max(500)]).default(25),
36+
});
37+
export const allowedTypesSchema = z
38+
.array(fetchContentTypeSchema)
39+
.min(1)
40+
.refine((types) => new Set(types).size === types.length, {
41+
message: "duplicate allowed types not allowed",
42+
});
43+
export const processorsSchema = z
44+
.array(pdfProcessorSchema)
45+
.min(1)
46+
.refine(
47+
(processors) =>
48+
new Set(processors.map((processor) => processor.type)).size === processors.length,
49+
{
50+
message: "duplicate processor types not allowed",
51+
},
52+
);
3253
export const userPromptSchema = z.string().min(1).max(10_000);
3354

3455
const PUBLIC_DOMAIN_RE =
@@ -214,6 +235,8 @@ export const scrapeFormatEntrySchema = z.discriminatedUnion("type", [
214235
export const scrapeRequestSchema = z.object({
215236
url: urlSchema,
216237
contentType: fetchContentTypeSchema.optional(),
238+
allowedTypes: allowedTypesSchema.optional(),
239+
processors: processorsSchema.optional(),
217240
fetchConfig: fetchConfigSchema.optional(),
218241
formats: z
219242
.array(scrapeFormatEntrySchema)
@@ -233,6 +256,8 @@ export const extractRequestBaseSchema = z
233256
prompt: userPromptSchema,
234257
schema: z.record(z.string(), z.unknown()).optional(),
235258
contentType: fetchContentTypeSchema.optional(),
259+
allowedTypes: allowedTypesSchema.optional(),
260+
processors: processorsSchema.optional(),
236261
fetchConfig: fetchConfigSchema.optional(),
237262
})
238263
.refine((d) => d.url || d.html || d.markdown, {
@@ -249,6 +274,8 @@ export const searchRequestSchema = z
249274
prompt: userPromptSchema.optional(),
250275
schema: z.record(z.string(), z.unknown()).optional(),
251276
locationGeoCode: z.string().max(10).optional(),
277+
allowedTypes: allowedTypesSchema.optional(),
278+
processors: processorsSchema.optional(),
252279
timeRange: z
253280
.enum(["past_hour", "past_24_hours", "past_week", "past_month", "past_year"])
254281
.optional(),
@@ -517,7 +544,8 @@ export const crawlRequestSchema = z.object({
517544
.describe(
518545
'Glob-style URL patterns to exclude. Use "*/<slug>" for first-level paths and "**/<slug>/**" for nested paths.',
519546
),
520-
contentTypes: z.array(fetchContentTypeSchema).optional(),
547+
allowedTypes: allowedTypesSchema.optional(),
548+
processors: processorsSchema.optional(),
521549
fetchConfig: fetchConfigSchema.optional(),
522550
});
523551

src/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export type HealthResponse = z.infer<typeof healthResponseSchema>;
134134

135135
// ─── scrape ──────────────────────────────────────────────────────────────────
136136

137-
export type ScrapeRequest = z.infer<typeof scrapeRequestSchema>;
137+
export type ScrapeRequest = z.input<typeof scrapeRequestSchema>;
138138
export type FetchConfig = z.infer<typeof fetchConfigSchema>;
139139
export type FetchMode = z.infer<typeof fetchModeSchema>;
140140
export type FetchContentType = z.infer<typeof fetchContentTypeSchema>;
@@ -202,7 +202,7 @@ export type ScrapeEvent =
202202

203203
// ─── extract ─────────────────────────────────────────────────────────────────
204204

205-
export type ExtractRequestBase = z.infer<typeof extractRequestBaseSchema>;
205+
export type ExtractRequestBase = z.input<typeof extractRequestBaseSchema>;
206206
export type LlmConfig = z.infer<typeof llmConfigSchema>;
207207

208208
export type ExtractResponse = z.infer<typeof extractResponseSchema>;
@@ -217,7 +217,7 @@ export type ExtractEvent =
217217

218218
// ─── search ──────────────────────────────────────────────────────────────────
219219

220-
export type SearchRequest = z.infer<typeof searchRequestSchema>;
220+
export type SearchRequest = z.input<typeof searchRequestSchema>;
221221

222222
export type SearchResult = z.infer<typeof searchResultSchema>;
223223
export type SearchMetadata = z.infer<typeof searchMetadataSchema>;
@@ -313,7 +313,7 @@ export type MonitorEvent =
313313

314314
// ─── crawl ───────────────────────────────────────────────────────────────────
315315

316-
export type CrawlRequest = z.infer<typeof crawlRequestSchema>;
316+
export type CrawlRequest = z.input<typeof crawlRequestSchema>;
317317
export type CrawlStatus = z.infer<typeof crawlStatusSchema>;
318318
export type CrawlPageStatus = z.infer<typeof crawlPageStatusSchema>;
319319

tests/schemas.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, expect, test } from "bun:test";
2+
import {
3+
crawlRequestSchema,
4+
extractRequestBaseSchema,
5+
scrapeRequestSchema,
6+
searchRequestSchema,
7+
} from "../src/schemas.js";
8+
import type { SearchRequest } from "../src/types.js";
9+
10+
const requestWithoutExplicitPageCap: SearchRequest = {
11+
query: "example",
12+
processors: [{ type: "pdf" }],
13+
};
14+
15+
const requests = [
16+
["scrape", scrapeRequestSchema, { url: "https://example.com" }],
17+
["extract", extractRequestBaseSchema, { url: "https://example.com", prompt: "title" }],
18+
["search", searchRequestSchema, { query: "example" }],
19+
["crawl", crawlRequestSchema, { url: "https://example.com" }],
20+
] as const;
21+
22+
describe("content type and PDF processor validation", () => {
23+
test("request types and schemas allow the default 25-page PDF cap", () => {
24+
expect(searchRequestSchema.parse(requestWithoutExplicitPageCap).processors).toEqual([
25+
{ type: "pdf", maxPages: 25 },
26+
]);
27+
expect(searchRequestSchema.parse({ query: "example" }).processors).toBeUndefined();
28+
});
29+
30+
for (const [name, schema, base] of requests) {
31+
test(`${name} accepts the documented PDF configuration`, () => {
32+
expect(
33+
schema.safeParse({
34+
...base,
35+
allowedTypes: ["application/pdf"],
36+
processors: [{ type: "pdf", maxPages: 10 }],
37+
}).success,
38+
).toBe(true);
39+
});
40+
41+
test(`${name} rejects empty and duplicate arrays`, () => {
42+
expect(schema.safeParse({ ...base, allowedTypes: [] }).success).toBe(false);
43+
expect(
44+
schema.safeParse({
45+
...base,
46+
allowedTypes: ["application/pdf", "application/pdf"],
47+
}).success,
48+
).toBe(false);
49+
expect(schema.safeParse({ ...base, processors: [] }).success).toBe(false);
50+
expect(
51+
schema.safeParse({
52+
...base,
53+
processors: [
54+
{ type: "pdf", maxPages: 1 },
55+
{ type: "pdf", maxPages: 10 },
56+
],
57+
}).success,
58+
).toBe(false);
59+
});
60+
61+
test(`${name} enforces the documented PDF page limits`, () => {
62+
for (const maxPages of [1, 500, -1]) {
63+
expect(schema.safeParse({ ...base, processors: [{ type: "pdf", maxPages }] }).success).toBe(
64+
true,
65+
);
66+
}
67+
for (const maxPages of [0, -2, 501, 1.5]) {
68+
expect(schema.safeParse({ ...base, processors: [{ type: "pdf", maxPages }] }).success).toBe(
69+
false,
70+
);
71+
}
72+
});
73+
}
74+
});

tests/scrapegraphai.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,25 @@ describe("search", () => {
710710
expect(res.status).toBe("success");
711711
expectRequest(0, "POST", "/search", searchParams);
712712
});
713+
714+
test("with PDF options", async () => {
715+
const body = {
716+
results: [],
717+
metadata: { search: {}, pages: { requested: 1, scraped: 0 } },
718+
};
719+
fetchSpy = spyOn(globalThis, "fetch").mockResolvedValueOnce(json(body));
720+
const searchParams = {
721+
query: "papers",
722+
numResults: 1,
723+
allowedTypes: ["application/pdf"] as const,
724+
processors: [{ type: "pdf" as const, maxPages: 10 }],
725+
};
726+
727+
const res = await sdk.search(API_KEY, searchParams);
728+
729+
expect(res.status).toBe("success");
730+
expectRequest(0, "POST", "/search", searchParams);
731+
});
713732
});
714733

715734
describe("getCredits", () => {
@@ -862,7 +881,7 @@ describe("crawl", () => {
862881
expectRequest(0, "POST", "/crawl", patternParams);
863882
});
864883

865-
test("start with fetchConfig and contentTypes", async () => {
884+
test("start with fetchConfig, allowedTypes, and processors", async () => {
866885
const body = {
867886
id: "crawl-abc",
868887
status: "running",
@@ -874,7 +893,8 @@ describe("crawl", () => {
874893

875894
const configParams = {
876895
url: "https://example.com",
877-
contentTypes: ["text/html" as const, "application/pdf" as const],
896+
allowedTypes: ["text/html" as const, "application/pdf" as const],
897+
processors: [{ type: "pdf" as const, maxPages: 10 }],
878898
fetchConfig: {
879899
mode: "js" as const,
880900
stealth: true,

0 commit comments

Comments
 (0)