@youdotcom-oss/api 0.2.0 → 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/bin/cli.js +6 -3
- package/package.json +1 -1
- package/src/deep-search/deep-search.utils.ts +7 -3
- 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/bin/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { parseArgs as parseArgs2 } from "node:util";
|
|
|
15
15
|
// package.json
|
|
16
16
|
var package_default = {
|
|
17
17
|
name: "@youdotcom-oss/api",
|
|
18
|
-
version: "0.2.
|
|
18
|
+
version: "0.2.1",
|
|
19
19
|
description: "You.com API client with bundled CLI for agents supporting Agent Skills",
|
|
20
20
|
license: "MIT",
|
|
21
21
|
engines: {
|
|
@@ -4856,13 +4856,16 @@ var DeepSearchResponseSchema = object({
|
|
|
4856
4856
|
// src/deep-search/deep-search.utils.ts
|
|
4857
4857
|
var callDeepSearch = async ({
|
|
4858
4858
|
deepSearchQuery,
|
|
4859
|
-
YDC_API_KEY,
|
|
4859
|
+
YDC_API_KEY = process.env.YDC_API_KEY,
|
|
4860
4860
|
getUserAgent
|
|
4861
4861
|
}) => {
|
|
4862
|
+
if (!YDC_API_KEY) {
|
|
4863
|
+
throw new Error("YDC_API_KEY is required for Deep Search API");
|
|
4864
|
+
}
|
|
4862
4865
|
const response = await fetch(DEEP_SEARCH_API_URL, {
|
|
4863
4866
|
method: "POST",
|
|
4864
4867
|
headers: new Headers({
|
|
4865
|
-
"X-API-Key": YDC_API_KEY
|
|
4868
|
+
"X-API-Key": YDC_API_KEY,
|
|
4866
4869
|
"Content-Type": "application/json",
|
|
4867
4870
|
"User-Agent": getUserAgent()
|
|
4868
4871
|
}),
|
package/package.json
CHANGED
|
@@ -14,17 +14,21 @@ import { type DeepSearchQuery, DeepSearchResponseSchema } from './deep-search.sc
|
|
|
14
14
|
*/
|
|
15
15
|
export const callDeepSearch = async ({
|
|
16
16
|
deepSearchQuery,
|
|
17
|
-
YDC_API_KEY,
|
|
17
|
+
YDC_API_KEY = process.env.YDC_API_KEY,
|
|
18
18
|
getUserAgent,
|
|
19
19
|
}: {
|
|
20
20
|
deepSearchQuery: DeepSearchQuery
|
|
21
|
-
YDC_API_KEY
|
|
21
|
+
YDC_API_KEY?: string
|
|
22
22
|
getUserAgent: GetUserAgent
|
|
23
23
|
}) => {
|
|
24
|
+
if (!YDC_API_KEY) {
|
|
25
|
+
throw new Error('YDC_API_KEY is required for Deep Search API')
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
const response = await fetch(DEEP_SEARCH_API_URL, {
|
|
25
29
|
method: 'POST',
|
|
26
30
|
headers: new Headers({
|
|
27
|
-
'X-API-Key': YDC_API_KEY
|
|
31
|
+
'X-API-Key': YDC_API_KEY,
|
|
28
32
|
'Content-Type': 'application/json',
|
|
29
33
|
'User-Agent': getUserAgent(),
|
|
30
34
|
}),
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { callDeepSearch } from '../deep-search.utils.ts'
|
|
3
|
+
|
|
4
|
+
const getUserAgent = () => 'API/test (You.com;TEST)'
|
|
5
|
+
|
|
6
|
+
describe('callDeepSearch', () => {
|
|
7
|
+
test(
|
|
8
|
+
'returns valid response structure for basic query',
|
|
9
|
+
async () => {
|
|
10
|
+
const result = await callDeepSearch({
|
|
11
|
+
deepSearchQuery: {
|
|
12
|
+
query: 'What is TypeScript?',
|
|
13
|
+
search_effort: 'low',
|
|
14
|
+
},
|
|
15
|
+
getUserAgent,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
expect(result).toHaveProperty('answer')
|
|
19
|
+
expect(result).toHaveProperty('results')
|
|
20
|
+
expect(typeof result.answer).toBe('string')
|
|
21
|
+
expect(Array.isArray(result.results)).toBe(true)
|
|
22
|
+
expect(result.answer.length).toBeGreaterThan(0)
|
|
23
|
+
},
|
|
24
|
+
{ retry: 2 },
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
test(
|
|
28
|
+
'handles medium search effort',
|
|
29
|
+
async () => {
|
|
30
|
+
const result = await callDeepSearch({
|
|
31
|
+
deepSearchQuery: {
|
|
32
|
+
query: 'Explain REST API principles',
|
|
33
|
+
search_effort: 'medium',
|
|
34
|
+
},
|
|
35
|
+
getUserAgent,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(result).toHaveProperty('answer')
|
|
39
|
+
expect(result).toHaveProperty('results')
|
|
40
|
+
expect(typeof result.answer).toBe('string')
|
|
41
|
+
expect(Array.isArray(result.results)).toBe(true)
|
|
42
|
+
},
|
|
43
|
+
{ retry: 2 },
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
test(
|
|
47
|
+
'validates response schema with sources',
|
|
48
|
+
async () => {
|
|
49
|
+
const result = await callDeepSearch({
|
|
50
|
+
deepSearchQuery: {
|
|
51
|
+
query: 'What are the benefits of microservices?',
|
|
52
|
+
search_effort: 'low',
|
|
53
|
+
},
|
|
54
|
+
getUserAgent,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Test that results have required properties
|
|
58
|
+
expect(result.results.length).toBeGreaterThan(0)
|
|
59
|
+
|
|
60
|
+
const source = result.results[0]
|
|
61
|
+
expect(source).toBeDefined()
|
|
62
|
+
expect(source).toHaveProperty('url')
|
|
63
|
+
expect(source).toHaveProperty('title')
|
|
64
|
+
expect(source).toHaveProperty('snippets')
|
|
65
|
+
expect(typeof source?.url).toBe('string')
|
|
66
|
+
expect(typeof source?.title).toBe('string')
|
|
67
|
+
expect(Array.isArray(source?.snippets)).toBe(true)
|
|
68
|
+
},
|
|
69
|
+
{ retry: 2 },
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
test(
|
|
73
|
+
'answer contains markdown with inline citations',
|
|
74
|
+
async () => {
|
|
75
|
+
const result = await callDeepSearch({
|
|
76
|
+
deepSearchQuery: {
|
|
77
|
+
query: 'What is JWT authentication?',
|
|
78
|
+
search_effort: 'low',
|
|
79
|
+
},
|
|
80
|
+
getUserAgent,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Answer should be non-empty markdown string
|
|
84
|
+
expect(typeof result.answer).toBe('string')
|
|
85
|
+
expect(result.answer.length).toBeGreaterThan(0)
|
|
86
|
+
|
|
87
|
+
// Answer typically contains citations in the format [1], [2], etc.
|
|
88
|
+
// This is a soft check - citations may or may not be present
|
|
89
|
+
if (result.answer.includes('[')) {
|
|
90
|
+
expect(result.answer).toMatch(/\[\d+\]/)
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{ retry: 2 },
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
test(
|
|
97
|
+
'handles complex multi-part questions',
|
|
98
|
+
async () => {
|
|
99
|
+
const result = await callDeepSearch({
|
|
100
|
+
deepSearchQuery: {
|
|
101
|
+
query: 'What is GraphQL and how does it differ from REST?',
|
|
102
|
+
search_effort: 'low',
|
|
103
|
+
},
|
|
104
|
+
getUserAgent,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
expect(result).toHaveProperty('answer')
|
|
108
|
+
expect(result).toHaveProperty('results')
|
|
109
|
+
expect(result.results.length).toBeGreaterThan(0)
|
|
110
|
+
|
|
111
|
+
// Complex questions should have substantial answers
|
|
112
|
+
expect(result.answer.length).toBeGreaterThan(100)
|
|
113
|
+
},
|
|
114
|
+
{ retry: 2 },
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
test(
|
|
118
|
+
'sources include relevant snippets',
|
|
119
|
+
async () => {
|
|
120
|
+
const result = await callDeepSearch({
|
|
121
|
+
deepSearchQuery: {
|
|
122
|
+
query: 'What is Docker containerization?',
|
|
123
|
+
search_effort: 'low',
|
|
124
|
+
},
|
|
125
|
+
getUserAgent,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Check that at least one source has snippets
|
|
129
|
+
const sourcesWithSnippets = result.results.filter((source) => source.snippets.length > 0)
|
|
130
|
+
expect(sourcesWithSnippets.length).toBeGreaterThan(0)
|
|
131
|
+
|
|
132
|
+
// Snippets should be non-empty strings
|
|
133
|
+
const firstSource = sourcesWithSnippets[0]
|
|
134
|
+
expect(firstSource).toBeDefined()
|
|
135
|
+
expect(firstSource?.snippets[0]?.length).toBeGreaterThan(0)
|
|
136
|
+
},
|
|
137
|
+
{ retry: 2 },
|
|
138
|
+
)
|
|
139
|
+
})
|