@youdotcom-oss/api 0.1.0 → 0.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.
- package/README.md +36 -36
- package/bin/cli.js +233 -403
- package/package.json +2 -2
- package/src/cli.ts +92 -21
- package/src/contents/contents.schemas.ts +8 -7
- package/src/contents/tests/contents.request.spec.ts +109 -0
- package/src/contents/tests/contents.schema-validation.spec.ts +75 -0
- package/src/deep-search/deep-search.schemas.ts +48 -0
- package/src/deep-search/deep-search.utils.ts +75 -0
- package/src/main.ts +4 -3
- package/src/search/search.schemas.ts +65 -6
- package/src/search/search.utils.ts +6 -28
- package/src/search/tests/search.request.spec.ts +122 -0
- package/src/search/tests/search.schema-validation.spec.ts +152 -0
- package/src/search/tests/{search.utils.spec.ts → search.utils.docker.ts} +0 -10
- package/src/shared/api.constants.ts +1 -1
- package/src/shared/check-response-for-errors.ts +1 -1
- package/src/shared/command-runner.ts +95 -0
- package/src/shared/dry-run-utils.ts +141 -0
- package/src/shared/tests/command-runner.spec.ts +210 -0
- package/src/shared/use-get-user-agents.ts +1 -1
- package/src/commands/contents.ts +0 -52
- package/src/commands/express.ts +0 -52
- package/src/commands/search.ts +0 -52
- package/src/express/express.schemas.ts +0 -85
- package/src/express/express.utils.ts +0 -113
- package/src/express/tests/express.utils.spec.ts +0 -83
- /package/src/contents/tests/{contents.utils.spec.ts → contents.utils.docker.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youdotcom-oss/api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "You.com API client with bundled CLI for agents supporting Agent Skills",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
@@ -63,6 +63,6 @@
|
|
|
63
63
|
"test:watch": "bun test --watch"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"zod": "^4.3.
|
|
66
|
+
"zod": "^4.3.6"
|
|
67
67
|
}
|
|
68
68
|
}
|
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Commands:
|
|
6
6
|
* search <query> - Search the web with You.com
|
|
7
|
-
*
|
|
7
|
+
* deep-search <query> - Perform deep research with comprehensive answers
|
|
8
8
|
* contents <url> [url...] - Extract content from URLs
|
|
9
9
|
*
|
|
10
10
|
* Options:
|
|
@@ -14,10 +14,17 @@
|
|
|
14
14
|
* --help, -h - Show help
|
|
15
15
|
*/
|
|
16
16
|
import { parseArgs } from 'node:util'
|
|
17
|
+
import type * as z from 'zod'
|
|
17
18
|
import packageJson from '../package.json' with { type: 'json' }
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { ContentsQuerySchema } from './contents/contents.schemas.ts'
|
|
20
|
+
import { fetchContents } from './contents/contents.utils.ts'
|
|
21
|
+
import { DeepSearchQuerySchema } from './deep-search/deep-search.schemas.ts'
|
|
22
|
+
import { callDeepSearch } from './deep-search/deep-search.utils.ts'
|
|
23
|
+
import { SearchQuerySchema } from './search/search.schemas.ts'
|
|
24
|
+
import { fetchSearchResults } from './search/search.utils.ts'
|
|
25
|
+
import type { GetUserAgent } from './shared/api.types.ts'
|
|
26
|
+
import { type CommandConfig, runCommand } from './shared/command-runner.ts'
|
|
27
|
+
import { buildContentsRequest, buildDeepSearchRequest, buildSearchRequest } from './shared/dry-run-utils.ts'
|
|
21
28
|
import { generateErrorReportLink } from './shared/generate-error-report-link.ts'
|
|
22
29
|
import { useGetUserAgent } from './shared/use-get-user-agents.ts'
|
|
23
30
|
|
|
@@ -35,7 +42,7 @@ Usage: ydc <command> --json <json> [options]
|
|
|
35
42
|
|
|
36
43
|
Commands:
|
|
37
44
|
search Search the web with You.com
|
|
38
|
-
|
|
45
|
+
deep-search Perform deep research with comprehensive answers
|
|
39
46
|
contents Extract content from URLs
|
|
40
47
|
|
|
41
48
|
Global Options:
|
|
@@ -43,6 +50,7 @@ Global Options:
|
|
|
43
50
|
--api-key <key> You.com API key (overrides YDC_API_KEY)
|
|
44
51
|
--client <name> Client name for tracking and debugging
|
|
45
52
|
--schema Output JSON schema for what can be passed to --json
|
|
53
|
+
--dry-run Show request details without making API call
|
|
46
54
|
--help, -h Show this help
|
|
47
55
|
|
|
48
56
|
Environment Variables:
|
|
@@ -55,9 +63,10 @@ Output Format:
|
|
|
55
63
|
|
|
56
64
|
Examples:
|
|
57
65
|
ydc search --json '{"query":"AI developments"}' --client Openclaw
|
|
58
|
-
ydc
|
|
66
|
+
ydc deep-search --json '{"query":"What are the latest breakthroughs in AI?","search_effort":"high"}' --client MyAgent
|
|
59
67
|
ydc contents --json '{"urls":["https://example.com"],"formats":["markdown"]}'
|
|
60
68
|
ydc search --schema # Get JSON schema for search --json input
|
|
69
|
+
ydc search --json '{"query":"AI"}' --dry-run # Inspect request without API call
|
|
61
70
|
ydc search --json '{"query":"AI"}' | jq '.data.results.web[0].title'
|
|
62
71
|
|
|
63
72
|
More info: https://github.com/youdotcom-oss/dx-toolkit/tree/main/packages/api
|
|
@@ -65,22 +74,84 @@ More info: https://github.com/youdotcom-oss/dx-toolkit/tree/main/packages/api
|
|
|
65
74
|
process.exit(command ? 0 : 2)
|
|
66
75
|
}
|
|
67
76
|
|
|
77
|
+
// Command configuration map
|
|
78
|
+
const commands = {
|
|
79
|
+
search: {
|
|
80
|
+
schema: SearchQuerySchema,
|
|
81
|
+
handler: ({
|
|
82
|
+
input,
|
|
83
|
+
YDC_API_KEY,
|
|
84
|
+
getUserAgent,
|
|
85
|
+
}: {
|
|
86
|
+
input: z.infer<typeof SearchQuerySchema>
|
|
87
|
+
YDC_API_KEY: string
|
|
88
|
+
getUserAgent: GetUserAgent
|
|
89
|
+
}) => fetchSearchResults({ searchQuery: input, YDC_API_KEY, getUserAgent }),
|
|
90
|
+
dryRunHandler: ({
|
|
91
|
+
input,
|
|
92
|
+
YDC_API_KEY,
|
|
93
|
+
getUserAgent,
|
|
94
|
+
}: {
|
|
95
|
+
input: z.infer<typeof SearchQuerySchema>
|
|
96
|
+
YDC_API_KEY: string
|
|
97
|
+
getUserAgent: GetUserAgent
|
|
98
|
+
}) => buildSearchRequest({ searchQuery: input, YDC_API_KEY, getUserAgent }),
|
|
99
|
+
},
|
|
100
|
+
'deep-search': {
|
|
101
|
+
schema: DeepSearchQuerySchema,
|
|
102
|
+
handler: ({
|
|
103
|
+
input,
|
|
104
|
+
YDC_API_KEY,
|
|
105
|
+
getUserAgent,
|
|
106
|
+
}: {
|
|
107
|
+
input: z.infer<typeof DeepSearchQuerySchema>
|
|
108
|
+
YDC_API_KEY: string
|
|
109
|
+
getUserAgent: GetUserAgent
|
|
110
|
+
}) => callDeepSearch({ deepSearchQuery: input, YDC_API_KEY, getUserAgent }),
|
|
111
|
+
dryRunHandler: ({
|
|
112
|
+
input,
|
|
113
|
+
YDC_API_KEY,
|
|
114
|
+
getUserAgent,
|
|
115
|
+
}: {
|
|
116
|
+
input: z.infer<typeof DeepSearchQuerySchema>
|
|
117
|
+
YDC_API_KEY: string
|
|
118
|
+
getUserAgent: GetUserAgent
|
|
119
|
+
}) => buildDeepSearchRequest({ deepSearchQuery: input, YDC_API_KEY, getUserAgent }),
|
|
120
|
+
},
|
|
121
|
+
contents: {
|
|
122
|
+
schema: ContentsQuerySchema,
|
|
123
|
+
handler: ({
|
|
124
|
+
input,
|
|
125
|
+
YDC_API_KEY,
|
|
126
|
+
getUserAgent,
|
|
127
|
+
}: {
|
|
128
|
+
input: z.infer<typeof ContentsQuerySchema>
|
|
129
|
+
YDC_API_KEY: string
|
|
130
|
+
getUserAgent: GetUserAgent
|
|
131
|
+
}) => fetchContents({ contentsQuery: input, YDC_API_KEY, getUserAgent }),
|
|
132
|
+
dryRunHandler: ({
|
|
133
|
+
input,
|
|
134
|
+
YDC_API_KEY,
|
|
135
|
+
getUserAgent,
|
|
136
|
+
}: {
|
|
137
|
+
input: z.infer<typeof ContentsQuerySchema>
|
|
138
|
+
YDC_API_KEY: string
|
|
139
|
+
getUserAgent: GetUserAgent
|
|
140
|
+
}) => buildContentsRequest({ contentsQuery: input, YDC_API_KEY, getUserAgent }),
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Validate command
|
|
145
|
+
if (!(command in commands)) {
|
|
146
|
+
console.error(`Unknown command: ${command}`)
|
|
147
|
+
console.error(`Run 'ydc --help' for usage`)
|
|
148
|
+
process.exit(2)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Execute command
|
|
68
152
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await searchCommand(args)
|
|
72
|
-
break
|
|
73
|
-
case 'express':
|
|
74
|
-
await expressCommand(args)
|
|
75
|
-
break
|
|
76
|
-
case 'contents':
|
|
77
|
-
await contentsCommand(args)
|
|
78
|
-
break
|
|
79
|
-
default:
|
|
80
|
-
console.error(`Unknown command: ${command}`)
|
|
81
|
-
console.error(`Run 'ydc --help' for usage`)
|
|
82
|
-
process.exit(2)
|
|
83
|
-
}
|
|
153
|
+
// Type assertion is safe because we validated command exists above
|
|
154
|
+
await runCommand(args, commands[command as keyof typeof commands] as CommandConfig<unknown, unknown>)
|
|
84
155
|
process.exit(0)
|
|
85
156
|
} catch (error) {
|
|
86
157
|
console.error(error)
|
|
@@ -21,20 +21,21 @@ export type ContentsQuery = z.infer<typeof ContentsQuerySchema>
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Schema for a single content item in the API response
|
|
24
|
+
* Based on OpenAPI spec: https://you.com/specs/openapi_contents.yaml
|
|
24
25
|
*/
|
|
25
26
|
const ContentsItemSchema = z.object({
|
|
26
27
|
url: z.string().describe('URL'),
|
|
27
|
-
title: z.string().optional().describe('Title'),
|
|
28
|
-
html: z.string().optional().describe('HTML content'),
|
|
29
|
-
markdown: z.string().optional().describe('Markdown content'),
|
|
28
|
+
title: z.string().optional().describe('Title (optional in actual API responses)'),
|
|
29
|
+
html: z.string().nullable().optional().describe('HTML content'),
|
|
30
|
+
markdown: z.string().nullable().optional().describe('Markdown content'),
|
|
30
31
|
metadata: z
|
|
31
32
|
.object({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
twitter: z.record(z.string(), z.string()).optional().describe('Twitter Card metadata'),
|
|
33
|
+
site_name: z.string().nullable().optional().describe('OpenGraph site name'),
|
|
34
|
+
favicon_url: z.string().describe('Favicon URL'),
|
|
35
35
|
})
|
|
36
|
+
.nullable()
|
|
36
37
|
.optional()
|
|
37
|
-
.describe('
|
|
38
|
+
.describe('Page metadata (only when metadata format requested)'),
|
|
38
39
|
})
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { CONTENTS_API_URL } from '../../shared/api.constants.ts'
|
|
3
|
+
import { buildContentsRequest } from '../../shared/dry-run-utils.ts'
|
|
4
|
+
|
|
5
|
+
describe('buildContentsRequest', () => {
|
|
6
|
+
const getUserAgent = () => 'test-agent'
|
|
7
|
+
const YDC_API_KEY = 'test-key'
|
|
8
|
+
|
|
9
|
+
test('builds basic contents request with markdown format', () => {
|
|
10
|
+
const request = buildContentsRequest({
|
|
11
|
+
contentsQuery: { urls: ['https://example.com'] },
|
|
12
|
+
YDC_API_KEY,
|
|
13
|
+
getUserAgent,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(request.url).toBe(CONTENTS_API_URL)
|
|
17
|
+
expect(request.method).toBe('POST')
|
|
18
|
+
expect(request.headers['X-API-Key']).toBe('test-key')
|
|
19
|
+
expect(request.headers['Content-Type']).toBe('application/json')
|
|
20
|
+
expect(request.headers['User-Agent']).toBe('test-agent')
|
|
21
|
+
|
|
22
|
+
const body = JSON.parse(request.body!)
|
|
23
|
+
expect(body.urls).toEqual(['https://example.com'])
|
|
24
|
+
expect(body.formats).toEqual(['markdown'])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('builds request with multiple URLs', () => {
|
|
28
|
+
const request = buildContentsRequest({
|
|
29
|
+
contentsQuery: {
|
|
30
|
+
urls: ['https://a.com', 'https://b.com', 'https://c.com'],
|
|
31
|
+
},
|
|
32
|
+
YDC_API_KEY,
|
|
33
|
+
getUserAgent,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const body = JSON.parse(request.body!)
|
|
37
|
+
expect(body.urls).toEqual(['https://a.com', 'https://b.com', 'https://c.com'])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('builds request with multiple formats', () => {
|
|
41
|
+
const request = buildContentsRequest({
|
|
42
|
+
contentsQuery: {
|
|
43
|
+
urls: ['https://example.com'],
|
|
44
|
+
formats: ['html', 'markdown', 'metadata'],
|
|
45
|
+
},
|
|
46
|
+
YDC_API_KEY,
|
|
47
|
+
getUserAgent,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const body = JSON.parse(request.body!)
|
|
51
|
+
expect(body.formats).toEqual(['html', 'markdown', 'metadata'])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('builds request with deprecated format parameter', () => {
|
|
55
|
+
const request = buildContentsRequest({
|
|
56
|
+
contentsQuery: {
|
|
57
|
+
urls: ['https://example.com'],
|
|
58
|
+
format: 'html',
|
|
59
|
+
},
|
|
60
|
+
YDC_API_KEY,
|
|
61
|
+
getUserAgent,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const body = JSON.parse(request.body!)
|
|
65
|
+
expect(body.formats).toEqual(['html'])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('prefers formats array over deprecated format parameter', () => {
|
|
69
|
+
const request = buildContentsRequest({
|
|
70
|
+
contentsQuery: {
|
|
71
|
+
urls: ['https://example.com'],
|
|
72
|
+
formats: ['markdown', 'metadata'],
|
|
73
|
+
format: 'html',
|
|
74
|
+
},
|
|
75
|
+
YDC_API_KEY,
|
|
76
|
+
getUserAgent,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const body = JSON.parse(request.body!)
|
|
80
|
+
expect(body.formats).toEqual(['markdown', 'metadata'])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('includes crawl_timeout when provided', () => {
|
|
84
|
+
const request = buildContentsRequest({
|
|
85
|
+
contentsQuery: {
|
|
86
|
+
urls: ['https://example.com'],
|
|
87
|
+
crawl_timeout: 30,
|
|
88
|
+
},
|
|
89
|
+
YDC_API_KEY,
|
|
90
|
+
getUserAgent,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const body = JSON.parse(request.body!)
|
|
94
|
+
expect(body.crawl_timeout).toBe(30)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('omits crawl_timeout when not provided', () => {
|
|
98
|
+
const request = buildContentsRequest({
|
|
99
|
+
contentsQuery: {
|
|
100
|
+
urls: ['https://example.com'],
|
|
101
|
+
},
|
|
102
|
+
YDC_API_KEY,
|
|
103
|
+
getUserAgent,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const body = JSON.parse(request.body!)
|
|
107
|
+
expect(body.crawl_timeout).toBeUndefined()
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { ContentsQuerySchema } from '../contents.schemas.ts'
|
|
3
|
+
|
|
4
|
+
describe('ContentsQuerySchema OpenAPI validation', () => {
|
|
5
|
+
test('accepts valid contents queries', () => {
|
|
6
|
+
const validQueries = [
|
|
7
|
+
{ urls: ['https://example.com'] },
|
|
8
|
+
{ urls: ['https://example.com'], formats: ['markdown'] },
|
|
9
|
+
{ urls: ['https://example.com'], formats: ['html', 'markdown'] },
|
|
10
|
+
{ urls: ['https://example.com'], formats: ['markdown', 'metadata'] },
|
|
11
|
+
{ urls: ['https://example.com'], formats: ['html', 'markdown', 'metadata'] },
|
|
12
|
+
{ urls: ['https://example.com'], format: 'html' }, // Deprecated but still supported
|
|
13
|
+
{ urls: ['https://example.com'], crawl_timeout: 30 },
|
|
14
|
+
{ urls: ['https://a.com', 'https://b.com', 'https://c.com'], formats: ['markdown'] },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
for (const validQuery of validQueries) {
|
|
18
|
+
expect(() => ContentsQuerySchema.parse(validQuery)).not.toThrow()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('rejects invalid contents queries', () => {
|
|
23
|
+
const invalidQueries = [
|
|
24
|
+
{}, // Missing urls
|
|
25
|
+
{ urls: [] }, // Empty urls array
|
|
26
|
+
{ urls: ['not-a-url'] }, // Invalid URL
|
|
27
|
+
{ urls: ['https://example.com'], formats: ['invalid'] }, // Invalid format
|
|
28
|
+
{ urls: ['https://example.com'], crawl_timeout: 0 }, // Timeout too low
|
|
29
|
+
{ urls: ['https://example.com'], crawl_timeout: 61 }, // Timeout too high
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
for (const invalidQuery of invalidQueries) {
|
|
33
|
+
expect(() => ContentsQuerySchema.parse(invalidQuery)).toThrow()
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('accepts metadata format', () => {
|
|
38
|
+
const query = {
|
|
39
|
+
urls: ['https://example.com'],
|
|
40
|
+
formats: ['metadata'],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
expect(() => ContentsQuerySchema.parse(query)).not.toThrow()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('accepts all format combinations', () => {
|
|
47
|
+
const formatCombinations = [
|
|
48
|
+
['html'],
|
|
49
|
+
['markdown'],
|
|
50
|
+
['metadata'],
|
|
51
|
+
['html', 'markdown'],
|
|
52
|
+
['html', 'metadata'],
|
|
53
|
+
['markdown', 'metadata'],
|
|
54
|
+
['html', 'markdown', 'metadata'],
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
for (const formats of formatCombinations) {
|
|
58
|
+
expect(() => ContentsQuerySchema.parse({ urls: ['https://example.com'], formats })).not.toThrow()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('crawl_timeout validation', () => {
|
|
63
|
+
// Valid timeouts (1-60)
|
|
64
|
+
const validTimeouts = [1, 30, 60]
|
|
65
|
+
for (const timeout of validTimeouts) {
|
|
66
|
+
expect(() => ContentsQuerySchema.parse({ urls: ['https://example.com'], crawl_timeout: timeout })).not.toThrow()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Invalid timeouts
|
|
70
|
+
const invalidTimeouts = [0, -1, 61, 100]
|
|
71
|
+
for (const timeout of invalidTimeouts) {
|
|
72
|
+
expect(() => ContentsQuerySchema.parse({ urls: ['https://example.com'], crawl_timeout: timeout })).toThrow()
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as z from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Search effort levels for deep-search API
|
|
5
|
+
* Controls computation budget and response time
|
|
6
|
+
*/
|
|
7
|
+
export const SearchEffortSchema = z.enum(['low', 'medium', 'high']).describe('Search effort level')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Input schema for deep-search API
|
|
11
|
+
* Based on OpenAPI spec: https://docs.you.com/api-reference/deep-search/v1-deep_search
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export const DeepSearchQuerySchema = z.object({
|
|
16
|
+
query: z
|
|
17
|
+
.string()
|
|
18
|
+
.min(1, 'Query is required')
|
|
19
|
+
.describe('The research question or complex query requiring in-depth investigation and multi-step reasoning'),
|
|
20
|
+
search_effort: SearchEffortSchema.optional()
|
|
21
|
+
.default('medium')
|
|
22
|
+
.describe('Computation budget: low (<30s), medium (<60s, default), high (<300s)'),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export type DeepSearchQuery = z.infer<typeof DeepSearchQuerySchema>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Schema for a single source in the deep-search response
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
const DeepSearchSourceSchema = z.object({
|
|
33
|
+
url: z.string().describe('Source webpage URL'),
|
|
34
|
+
title: z.string().describe('Source webpage title'),
|
|
35
|
+
snippets: z.array(z.string()).describe('Relevant excerpts from the source page used in generating the answer'),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Response schema for deep-search API
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export const DeepSearchResponseSchema = z.object({
|
|
44
|
+
answer: z.string().describe('Comprehensive response with inline citations, formatted in Markdown'),
|
|
45
|
+
results: z.array(DeepSearchSourceSchema).describe('List of web sources used to generate the answer'),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export type DeepSearchResponse = z.infer<typeof DeepSearchResponseSchema>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type * as z from 'zod'
|
|
2
|
+
import { DEEP_SEARCH_API_URL } from '../shared/api.constants.ts'
|
|
3
|
+
import type { GetUserAgent } from '../shared/api.types.ts'
|
|
4
|
+
import { checkResponseForErrors } from '../shared/check-response-for-errors.ts'
|
|
5
|
+
import { type DeepSearchQuery, DeepSearchResponseSchema } from './deep-search.schemas.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Perform deep research using You.com Deep Search API
|
|
9
|
+
*
|
|
10
|
+
* @param params - Deep search query parameters
|
|
11
|
+
* @returns Deep search response with comprehensive answer and sources
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export const callDeepSearch = async ({
|
|
16
|
+
deepSearchQuery,
|
|
17
|
+
YDC_API_KEY,
|
|
18
|
+
getUserAgent,
|
|
19
|
+
}: {
|
|
20
|
+
deepSearchQuery: DeepSearchQuery
|
|
21
|
+
YDC_API_KEY: string
|
|
22
|
+
getUserAgent: GetUserAgent
|
|
23
|
+
}) => {
|
|
24
|
+
const response = await fetch(DEEP_SEARCH_API_URL, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: new Headers({
|
|
27
|
+
'X-API-Key': YDC_API_KEY || '',
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'User-Agent': getUserAgent(),
|
|
30
|
+
}),
|
|
31
|
+
body: JSON.stringify(deepSearchQuery),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
await checkResponseForErrors(response)
|
|
35
|
+
const data = await response.json()
|
|
36
|
+
|
|
37
|
+
return DeepSearchResponseSchema.parse(data)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format deep-search response for display
|
|
42
|
+
* Returns markdown-formatted text with answer and sources
|
|
43
|
+
*
|
|
44
|
+
* @param response - Deep search API response
|
|
45
|
+
* @returns Formatted markdown string
|
|
46
|
+
*
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
export const formatDeepSearchResponse = (response: z.infer<typeof DeepSearchResponseSchema>): string => {
|
|
50
|
+
const parts: string[] = []
|
|
51
|
+
|
|
52
|
+
// Add the comprehensive answer
|
|
53
|
+
parts.push('# Answer\n')
|
|
54
|
+
parts.push(response.answer)
|
|
55
|
+
parts.push('\n')
|
|
56
|
+
|
|
57
|
+
// Add sources section
|
|
58
|
+
if (response.results && response.results.length > 0) {
|
|
59
|
+
parts.push('\n## Sources\n')
|
|
60
|
+
|
|
61
|
+
for (const [index, source] of response.results.entries()) {
|
|
62
|
+
parts.push(`\n### ${index + 1}. ${source.title}\n`)
|
|
63
|
+
parts.push(`**URL:** ${source.url}\n`)
|
|
64
|
+
|
|
65
|
+
if (source.snippets && source.snippets.length > 0) {
|
|
66
|
+
parts.push('\n**Key Excerpts:**\n')
|
|
67
|
+
for (const snippet of source.snippets) {
|
|
68
|
+
parts.push(`> ${snippet}\n`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return parts.join('\n')
|
|
75
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
// Contents
|
|
11
11
|
export * from './contents/contents.schemas.ts'
|
|
12
12
|
export * from './contents/contents.utils.ts'
|
|
13
|
-
//
|
|
14
|
-
export * from './
|
|
15
|
-
export * from './
|
|
13
|
+
// Deep-Search
|
|
14
|
+
export * from './deep-search/deep-search.schemas.ts'
|
|
15
|
+
export * from './deep-search/deep-search.utils.ts'
|
|
16
16
|
// Search
|
|
17
17
|
export * from './search/search.schemas.ts'
|
|
18
18
|
export * from './search/search.utils.ts'
|
|
19
19
|
// Shared
|
|
20
20
|
export * from './shared/api.constants.ts'
|
|
21
21
|
export * from './shared/api.types.ts'
|
|
22
|
+
export * from './shared/dry-run-utils.ts'
|
|
22
23
|
export * from './shared/generate-error-report-link.ts'
|
|
@@ -1,7 +1,70 @@
|
|
|
1
1
|
import * as z from 'zod'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Language codes supported by You.com Search API (BCP 47 format)
|
|
5
|
+
* Based on OpenAPI spec: https://you.com/specs/openapi_search_v1.yaml
|
|
6
|
+
*/
|
|
7
|
+
export const LanguageSchema = z.enum([
|
|
8
|
+
'AR',
|
|
9
|
+
'EU',
|
|
10
|
+
'BN',
|
|
11
|
+
'BG',
|
|
12
|
+
'CA',
|
|
13
|
+
'ZH-HANS',
|
|
14
|
+
'ZH-HANT',
|
|
15
|
+
'HR',
|
|
16
|
+
'CS',
|
|
17
|
+
'DA',
|
|
18
|
+
'NL',
|
|
19
|
+
'EN',
|
|
20
|
+
'EN-GB',
|
|
21
|
+
'ET',
|
|
22
|
+
'FI',
|
|
23
|
+
'FR',
|
|
24
|
+
'GL',
|
|
25
|
+
'DE',
|
|
26
|
+
'EL',
|
|
27
|
+
'GU',
|
|
28
|
+
'HE',
|
|
29
|
+
'HI',
|
|
30
|
+
'HU',
|
|
31
|
+
'IS',
|
|
32
|
+
'IT',
|
|
33
|
+
'JP',
|
|
34
|
+
'KN',
|
|
35
|
+
'KO',
|
|
36
|
+
'LV',
|
|
37
|
+
'LT',
|
|
38
|
+
'MS',
|
|
39
|
+
'ML',
|
|
40
|
+
'MR',
|
|
41
|
+
'NB',
|
|
42
|
+
'PL',
|
|
43
|
+
'PT-BR',
|
|
44
|
+
'PT-PT',
|
|
45
|
+
'PA',
|
|
46
|
+
'RO',
|
|
47
|
+
'RU',
|
|
48
|
+
'SR',
|
|
49
|
+
'SK',
|
|
50
|
+
'SL',
|
|
51
|
+
'ES',
|
|
52
|
+
'SV',
|
|
53
|
+
'TA',
|
|
54
|
+
'TE',
|
|
55
|
+
'TH',
|
|
56
|
+
'TR',
|
|
57
|
+
'UK',
|
|
58
|
+
'VI',
|
|
59
|
+
])
|
|
60
|
+
|
|
3
61
|
export const SearchQuerySchema = z.object({
|
|
4
|
-
query: z
|
|
62
|
+
query: z
|
|
63
|
+
.string()
|
|
64
|
+
.min(1, 'Query is required')
|
|
65
|
+
.describe(
|
|
66
|
+
'Search query. Supports operators: site:domain.com (domain filter), filetype:pdf (file type), +term (include), -term (exclude), AND/OR/NOT (boolean logic), lang:en (language). Example: "machine learning (Python OR PyTorch) -TensorFlow filetype:pdf"',
|
|
67
|
+
),
|
|
5
68
|
count: z.number().int().min(1).max(100).optional().describe('Max results per section'),
|
|
6
69
|
freshness: z.string().optional().describe('day/week/month/year or YYYY-MM-DDtoYYYY-MM-DD'),
|
|
7
70
|
offset: z.number().int().min(0).max(9).optional().describe('Pagination offset'),
|
|
@@ -32,6 +95,7 @@ export const SearchQuerySchema = z.object({
|
|
|
32
95
|
'CN',
|
|
33
96
|
'PL',
|
|
34
97
|
'PT',
|
|
98
|
+
'PT-BR',
|
|
35
99
|
'PH',
|
|
36
100
|
'RU',
|
|
37
101
|
'SA',
|
|
@@ -47,11 +111,6 @@ export const SearchQuerySchema = z.object({
|
|
|
47
111
|
.optional()
|
|
48
112
|
.describe('Country code'),
|
|
49
113
|
safesearch: z.enum(['off', 'moderate', 'strict']).optional().describe('Filter level'),
|
|
50
|
-
site: z.string().optional().describe('Specific domain'),
|
|
51
|
-
fileType: z.string().optional().describe('File type'),
|
|
52
|
-
language: z.string().optional().describe('ISO 639-1 language code'),
|
|
53
|
-
excludeTerms: z.string().optional().describe('Terms to exclude (pipe-separated)'),
|
|
54
|
-
exactTerms: z.string().optional().describe('Exact terms (pipe-separated)'),
|
|
55
114
|
livecrawl: z.enum(['web', 'news', 'all']).optional().describe('Live-crawl sections for full content'),
|
|
56
115
|
livecrawl_formats: z.enum(['html', 'markdown']).optional().describe('Format for crawled content'),
|
|
57
116
|
})
|
|
@@ -5,7 +5,7 @@ import { type SearchQuery, SearchResponseSchema } from './search.schemas.ts'
|
|
|
5
5
|
|
|
6
6
|
export const fetchSearchResults = async ({
|
|
7
7
|
YDC_API_KEY = process.env.YDC_API_KEY,
|
|
8
|
-
searchQuery
|
|
8
|
+
searchQuery,
|
|
9
9
|
getUserAgent,
|
|
10
10
|
}: {
|
|
11
11
|
searchQuery: SearchQuery
|
|
@@ -16,33 +16,11 @@ export const fetchSearchResults = async ({
|
|
|
16
16
|
|
|
17
17
|
const searchParams = new URLSearchParams()
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
const searchQuery
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (exactTerms && excludeTerms) {
|
|
25
|
-
throw new Error('Cannot specify both exactTerms and excludeTerms - please use only one')
|
|
26
|
-
}
|
|
27
|
-
exactTerms &&
|
|
28
|
-
searchQuery.push(
|
|
29
|
-
exactTerms
|
|
30
|
-
.split('|')
|
|
31
|
-
.map((term) => `+${term}`)
|
|
32
|
-
.join(' AND '),
|
|
33
|
-
)
|
|
34
|
-
excludeTerms &&
|
|
35
|
-
searchQuery.push(
|
|
36
|
-
excludeTerms
|
|
37
|
-
.split('|')
|
|
38
|
-
.map((term) => `-${term}`)
|
|
39
|
-
.join(' AND '),
|
|
40
|
-
)
|
|
41
|
-
searchParams.append('query', searchQuery.join(' '))
|
|
42
|
-
|
|
43
|
-
// Append additional advanced Params
|
|
44
|
-
for (const [name, value] of Object.entries(rest)) {
|
|
45
|
-
if (value) searchParams.append(name, `${value}`)
|
|
19
|
+
// Append all query parameters
|
|
20
|
+
for (const [name, value] of Object.entries(searchQuery)) {
|
|
21
|
+
if (value !== undefined && value !== null) {
|
|
22
|
+
searchParams.append(name, `${value}`)
|
|
23
|
+
}
|
|
46
24
|
}
|
|
47
25
|
|
|
48
26
|
url.search = searchParams.toString()
|