@youdotcom-oss/api 0.1.1 → 0.2.1
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 -400
- package/package.json +1 -1
- 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 +79 -0
- package/src/deep-search/tests/deep-search.request.spec.ts +109 -0
- package/src/deep-search/tests/deep-search.schema-validation.spec.ts +71 -0
- package/src/deep-search/tests/deep-search.utils.docker.ts +139 -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
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,79 @@
|
|
|
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 = process.env.YDC_API_KEY,
|
|
18
|
+
getUserAgent,
|
|
19
|
+
}: {
|
|
20
|
+
deepSearchQuery: DeepSearchQuery
|
|
21
|
+
YDC_API_KEY?: string
|
|
22
|
+
getUserAgent: GetUserAgent
|
|
23
|
+
}) => {
|
|
24
|
+
if (!YDC_API_KEY) {
|
|
25
|
+
throw new Error('YDC_API_KEY is required for Deep Search API')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const response = await fetch(DEEP_SEARCH_API_URL, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: new Headers({
|
|
31
|
+
'X-API-Key': YDC_API_KEY,
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'User-Agent': getUserAgent(),
|
|
34
|
+
}),
|
|
35
|
+
body: JSON.stringify(deepSearchQuery),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await checkResponseForErrors(response)
|
|
39
|
+
const data = await response.json()
|
|
40
|
+
|
|
41
|
+
return DeepSearchResponseSchema.parse(data)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Format deep-search response for display
|
|
46
|
+
* Returns markdown-formatted text with answer and sources
|
|
47
|
+
*
|
|
48
|
+
* @param response - Deep search API response
|
|
49
|
+
* @returns Formatted markdown string
|
|
50
|
+
*
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
export const formatDeepSearchResponse = (response: z.infer<typeof DeepSearchResponseSchema>): string => {
|
|
54
|
+
const parts: string[] = []
|
|
55
|
+
|
|
56
|
+
// Add the comprehensive answer
|
|
57
|
+
parts.push('# Answer\n')
|
|
58
|
+
parts.push(response.answer)
|
|
59
|
+
parts.push('\n')
|
|
60
|
+
|
|
61
|
+
// Add sources section
|
|
62
|
+
if (response.results && response.results.length > 0) {
|
|
63
|
+
parts.push('\n## Sources\n')
|
|
64
|
+
|
|
65
|
+
for (const [index, source] of response.results.entries()) {
|
|
66
|
+
parts.push(`\n### ${index + 1}. ${source.title}\n`)
|
|
67
|
+
parts.push(`**URL:** ${source.url}\n`)
|
|
68
|
+
|
|
69
|
+
if (source.snippets && source.snippets.length > 0) {
|
|
70
|
+
parts.push('\n**Key Excerpts:**\n')
|
|
71
|
+
for (const snippet of source.snippets) {
|
|
72
|
+
parts.push(`> ${snippet}\n`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return parts.join('\n')
|
|
79
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { DEEP_SEARCH_API_URL } from '../../shared/api.constants.ts'
|
|
3
|
+
import { buildDeepSearchRequest } from '../../shared/dry-run-utils.ts'
|
|
4
|
+
|
|
5
|
+
describe('buildDeepSearchRequest', () => {
|
|
6
|
+
const getUserAgent = () => 'test-agent'
|
|
7
|
+
const YDC_API_KEY = 'test-key'
|
|
8
|
+
|
|
9
|
+
test('builds basic deep-search request with query only', () => {
|
|
10
|
+
const request = buildDeepSearchRequest({
|
|
11
|
+
deepSearchQuery: { query: 'What is AI?' },
|
|
12
|
+
YDC_API_KEY,
|
|
13
|
+
getUserAgent,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(request.url).toBe(DEEP_SEARCH_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.query).toBe('What is AI?')
|
|
24
|
+
// search_effort not in body when not provided (default applied by schema validation, not dry-run)
|
|
25
|
+
expect(body.search_effort).toBeUndefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('builds request with explicit medium search effort', () => {
|
|
29
|
+
const request = buildDeepSearchRequest({
|
|
30
|
+
deepSearchQuery: { query: 'What is AI?', search_effort: 'medium' },
|
|
31
|
+
YDC_API_KEY,
|
|
32
|
+
getUserAgent,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const body = JSON.parse(request.body!)
|
|
36
|
+
expect(body.query).toBe('What is AI?')
|
|
37
|
+
expect(body.search_effort).toBe('medium')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('builds request with low search effort', () => {
|
|
41
|
+
const request = buildDeepSearchRequest({
|
|
42
|
+
deepSearchQuery: {
|
|
43
|
+
query: 'Quick explanation of JWT',
|
|
44
|
+
search_effort: 'low',
|
|
45
|
+
},
|
|
46
|
+
YDC_API_KEY,
|
|
47
|
+
getUserAgent,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const body = JSON.parse(request.body!)
|
|
51
|
+
expect(body.query).toBe('Quick explanation of JWT')
|
|
52
|
+
expect(body.search_effort).toBe('low')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('builds request with high search effort', () => {
|
|
56
|
+
const request = buildDeepSearchRequest({
|
|
57
|
+
deepSearchQuery: {
|
|
58
|
+
query: 'Comprehensive analysis of climate change impacts',
|
|
59
|
+
search_effort: 'high',
|
|
60
|
+
},
|
|
61
|
+
YDC_API_KEY,
|
|
62
|
+
getUserAgent,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const body = JSON.parse(request.body!)
|
|
66
|
+
expect(body.query).toBe('Comprehensive analysis of climate change impacts')
|
|
67
|
+
expect(body.search_effort).toBe('high')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('builds request with complex research question', () => {
|
|
71
|
+
const complexQuery = `What are the key differences between microservices and monolithic architecture?
|
|
72
|
+
Include pros and cons of each approach, best use cases, and migration strategies.`
|
|
73
|
+
|
|
74
|
+
const request = buildDeepSearchRequest({
|
|
75
|
+
deepSearchQuery: {
|
|
76
|
+
query: complexQuery,
|
|
77
|
+
search_effort: 'high',
|
|
78
|
+
},
|
|
79
|
+
YDC_API_KEY,
|
|
80
|
+
getUserAgent,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const body = JSON.parse(request.body!)
|
|
84
|
+
expect(body.query).toBe(complexQuery)
|
|
85
|
+
expect(body.search_effort).toBe('high')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('uses correct API URL', () => {
|
|
89
|
+
const request = buildDeepSearchRequest({
|
|
90
|
+
deepSearchQuery: { query: 'test' },
|
|
91
|
+
YDC_API_KEY,
|
|
92
|
+
getUserAgent,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
expect(request.url).toBe('https://api.you.com/v1/deep_search')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('includes all required headers', () => {
|
|
99
|
+
const request = buildDeepSearchRequest({
|
|
100
|
+
deepSearchQuery: { query: 'test' },
|
|
101
|
+
YDC_API_KEY: 'my-api-key',
|
|
102
|
+
getUserAgent: () => 'CustomAgent/1.0',
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
expect(request.headers['X-API-Key']).toBe('my-api-key')
|
|
106
|
+
expect(request.headers['Content-Type']).toBe('application/json')
|
|
107
|
+
expect(request.headers['User-Agent']).toBe('CustomAgent/1.0')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { DeepSearchQuerySchema, SearchEffortSchema } from '../deep-search.schemas.ts'
|
|
3
|
+
|
|
4
|
+
describe('DeepSearchQuerySchema OpenAPI validation', () => {
|
|
5
|
+
test('accepts valid query parameters', () => {
|
|
6
|
+
const validQueries = [
|
|
7
|
+
{ query: 'What is quantum computing?' },
|
|
8
|
+
{ query: 'Explain machine learning', search_effort: 'low' },
|
|
9
|
+
{ query: 'Latest AI developments', search_effort: 'medium' },
|
|
10
|
+
{ query: 'Comprehensive research on climate change', search_effort: 'high' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
for (const validQuery of validQueries) {
|
|
14
|
+
expect(() => DeepSearchQuerySchema.parse(validQuery)).not.toThrow()
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('rejects invalid query parameters', () => {
|
|
19
|
+
const invalidQueries = [
|
|
20
|
+
{}, // Missing query
|
|
21
|
+
{ query: '' }, // Empty query
|
|
22
|
+
{ query: 'test', search_effort: 'invalid' }, // Invalid search_effort
|
|
23
|
+
{ query: 'test', search_effort: 'extreme' }, // Invalid effort level
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
for (const invalidQuery of invalidQueries) {
|
|
27
|
+
expect(() => DeepSearchQuerySchema.parse(invalidQuery)).toThrow()
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('defaults search_effort to medium when not provided', () => {
|
|
32
|
+
const result = DeepSearchQuerySchema.parse({ query: 'test query' })
|
|
33
|
+
expect(result.search_effort).toBe('medium')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('SearchEffortSchema accepts all valid effort levels', () => {
|
|
37
|
+
const validEfforts = ['low', 'medium', 'high']
|
|
38
|
+
|
|
39
|
+
for (const effort of validEfforts) {
|
|
40
|
+
expect(() => SearchEffortSchema.parse(effort)).not.toThrow()
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('SearchEffortSchema rejects invalid effort levels', () => {
|
|
45
|
+
const invalidEfforts = ['none', 'extreme', 'ultra', 'minimal', '']
|
|
46
|
+
|
|
47
|
+
for (const effort of invalidEfforts) {
|
|
48
|
+
expect(() => SearchEffortSchema.parse(effort)).toThrow()
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('accepts complex research questions', () => {
|
|
53
|
+
const complexQueries = [
|
|
54
|
+
{
|
|
55
|
+
query: 'What are the key differences between REST and GraphQL APIs, and when should each be used?',
|
|
56
|
+
search_effort: 'high',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
query: 'Compare the advantages and disadvantages of microservices architecture versus monolithic architecture',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
query: 'What happened in AI research during 2024? Provide a comprehensive summary with key breakthroughs.',
|
|
63
|
+
search_effort: 'high',
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
for (const query of complexQueries) {
|
|
68
|
+
expect(() => DeepSearchQuerySchema.parse(query)).not.toThrow()
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
})
|