Skip to content

Commit d4ccd61

Browse files
feat: add optional You.com search integration
- Add YouComMcpClient for You.com web search API - Extend search web command with --provider flag (dashscope|youcom) - Support optional YDC_API_KEY env var (falls back to keyless API) - MCP-compatible tool interface for agent integration - Graceful error handling and fallback behavior - Maintain backward compatibility with existing DashScope search - Add comprehensive documentation and usage examples Resolves youdotcom-oss/integration-tracking#187
1 parent 94f9dbb commit d4ccd61

5 files changed

Lines changed: 479 additions & 51 deletions

File tree

YOUCOM_INTEGRATION.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# You.com Web Search Integration
2+
3+
This CLI now supports You.com as an optional web search provider alongside the default DashScope WebSearch service.
4+
5+
## Setup
6+
7+
### Environment Variables
8+
9+
- `YDC_API_KEY` (optional): You.com API key for authenticated requests
10+
- `YOUCOM_BASE_URL` (optional): Custom You.com API base URL (default: https://api.you.com)
11+
12+
### Usage Examples
13+
14+
```bash
15+
# Use default DashScope WebSearch
16+
bailian-cli search web --query "latest AI developments"
17+
18+
# Use You.com search explicitly
19+
bailian-cli search web --query "latest AI developments" --provider youcom
20+
21+
# Use You.com with API key authentication
22+
export YDC_API_KEY="your-api-key-here"
23+
bailian-cli search web --query "TypeScript features" --provider youcom --count 5
24+
25+
# List available tools from You.com
26+
bailian-cli search web --list-tools --provider youcom
27+
```
28+
29+
## Features
30+
31+
### Keyless Operation
32+
You.com search works without an API key (100 free searches per day) but performs better with authentication.
33+
34+
### MCP Tool Integration
35+
When used as an MCP server, the You.com integration exposes:
36+
37+
- **Tool**: `youcom_web_search`
38+
- **Description**: Search the web using You.com. Returns relevant results with titles, URLs, and snippets.
39+
- **Parameters**:
40+
- `query` (required): The search query string
41+
- `count` (optional): Number of results (1-20, default: 10)
42+
- `safesearch` (optional): Safe search setting ("strict", "moderate", "off", default: "moderate")
43+
- `country` (optional): Country code for localized results (e.g. "US", "GB")
44+
45+
### Error Handling
46+
47+
The integration gracefully handles:
48+
- Network timeouts and connection errors
49+
- API rate limits (HTTP 429)
50+
- Authentication failures (HTTP 401)
51+
- Invalid queries and malformed responses
52+
- Fallback behavior when API key is invalid
53+
54+
### Output Formats
55+
56+
Results are available in both JSON and human-readable text formats, with structured metadata including:
57+
- Page titles and URLs
58+
- Content snippets
59+
- Publication age (when available)
60+
- Provider identification for mixed workflows
61+
62+
## Architecture
63+
64+
The You.com integration is implemented as:
65+
1. **YouComMcpClient**: MCP-compatible client for You.com API
66+
2. **Provider Selection**: Optional `--provider` flag in existing search commands
67+
3. **Environment Configuration**: Standard environment variable configuration
68+
4. **Graceful Fallback**: Falls back to keyless API when authentication fails
69+
70+
## Contributing
71+
72+
The You.com integration follows the existing CLI patterns:
73+
- MCP protocol compliance for tool interoperability
74+
- Structured error handling with BailianError
75+
- Consistent CLI flag naming and behavior
76+
- Environment-based configuration
77+
- Comprehensive test coverage (when test infrastructure is available)

packages/commands/src/commands/search/web.ts

Lines changed: 135 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
BailianError,
44
detectOutputFormat,
55
mcpWebSearchPath,
6+
YouComMcpClient,
67
type FlagsDef,
78
} from "bailian-cli-core";
89
import { createSpinner, emitResult } from "bailian-cli-runtime";
@@ -16,38 +17,71 @@ const WEB_SEARCH_FLAGS = {
1617
description: "Number of search results (default: 10)",
1718
},
1819
listTools: { type: "switch", description: "List available MCP tools and exit" },
20+
provider: {
21+
type: "string",
22+
valueHint: "<name>",
23+
description: "Search provider: 'dashscope' (default) or 'youcom'"
24+
},
1925
} satisfies FlagsDef;
2026

2127
export default defineCommand({
22-
description: "Search the web using DashScope MCP WebSearch service",
23-
auth: "apiKey",
24-
usageArgs: "--query <text> [flags]",
28+
description: "Search the web using DashScope WebSearch or You.com",
29+
auth: "optionalApiKey",
30+
usageArgs: "--query <text> [--provider <name>] [flags]",
2531
flags: WEB_SEARCH_FLAGS,
2632
exampleArgs: [
2733
'--query "Alibaba Cloud Bailian latest features"',
2834
'--query "TypeScript 5.9 new features" --count 5',
29-
'--query "Today\'s news"',
30-
"--list-tools",
35+
'--query "Today\'s news" --provider youcom',
36+
'--query "AI developments" --provider dashscope',
37+
"--list-tools --provider youcom",
3138
],
32-
validate: (f) => (!f.listTools && !f.query ? "Missing required flag: --query" : undefined),
39+
validate: (f) => {
40+
if (!f.listTools && !f.query) return "Missing required flag: --query";
41+
if (f.provider && !["dashscope", "youcom"].includes(f.provider)) {
42+
return "Invalid provider. Use 'dashscope' or 'youcom'";
43+
}
44+
return undefined;
45+
},
3346
async run(ctx) {
3447
const { settings, flags } = ctx;
3548
const format = detectOutputFormat(settings.output);
49+
50+
// Determine provider
51+
const provider = flags.provider || "dashscope";
52+
const useYouCom = provider === "youcom";
3653

3754
// --- List tools mode ---
3855
if (flags.listTools) {
3956
if (settings.dryRun) {
40-
emitResult({ endpoint: ctx.client.url(mcpWebSearchPath()), action: "tools/list" }, format);
57+
const endpoint = useYouCom
58+
? "https://api.you.com"
59+
: ctx.client.url(mcpWebSearchPath());
60+
emitResult({ endpoint, action: "tools/list", provider }, format);
4161
return;
4262
}
4363

4464
try {
45-
const client = ctx.client.mcp(mcpWebSearchPath());
46-
await client.initialize();
47-
const tools = await client.listTools();
48-
emitResult({ tools }, format);
65+
if (useYouCom) {
66+
const config = YouComMcpClient.getConfig();
67+
const youcomClient = YouComMcpClient.fromClient(ctx.client, config.apiKey, config.baseUrl);
68+
await youcomClient.initialize();
69+
const tools = await youcomClient.listTools();
70+
emitResult({ tools, provider: "youcom" }, format);
71+
} else {
72+
const client = ctx.client.mcp(mcpWebSearchPath());
73+
await client.initialize();
74+
const tools = await client.listTools();
75+
emitResult({ tools, provider: "dashscope" }, format);
76+
}
4977
} catch (error) {
50-
rethrowWithWebSearchActivateHint(error);
78+
if (useYouCom) {
79+
// You.com specific error handling
80+
if (error instanceof BailianError) throw error;
81+
throw new BailianError(`You.com search error: ${error instanceof Error ? error.message : 'Unknown error'}`, 1);
82+
} else {
83+
rethrowWithWebSearchActivateHint(error);
84+
}
5185
}
5286
return;
5387
}
@@ -56,11 +90,17 @@ export default defineCommand({
5690
const query = flags.query;
5791

5892
if (settings.dryRun) {
93+
const endpoint = useYouCom
94+
? "https://api.you.com/api/search"
95+
: ctx.client.url(mcpWebSearchPath());
96+
const toolName = useYouCom ? "youcom_web_search" : "bailian_web_search";
97+
5998
emitResult(
6099
{
61-
endpoint: ctx.client.url(mcpWebSearchPath()),
100+
endpoint,
62101
action: "tools/call",
63-
tool: "bailian_web_search",
102+
tool: toolName,
103+
provider,
64104
arguments: {
65105
query: query!,
66106
count: flags.count || undefined,
@@ -71,62 +111,106 @@ export default defineCommand({
71111
return;
72112
}
73113

74-
// Initialize MCP client
75-
const client = ctx.client.mcp(mcpWebSearchPath());
114+
// Initialize appropriate client
76115
const spinner = createSpinner("Initializing search...");
77116

78117
if (!settings.quiet) spinner.start();
79118

80119
try {
81-
await client.initialize();
120+
if (useYouCom) {
121+
// Use You.com MCP client
122+
const config = YouComMcpClient.getConfig();
123+
const youcomClient = YouComMcpClient.fromClient(ctx.client, config.apiKey, config.baseUrl);
124+
await youcomClient.initialize();
82125

83-
if (!settings.quiet) spinner.update("Searching...");
126+
if (!settings.quiet) spinner.update("Searching with You.com...");
84127

85-
// Build tool arguments
86-
const toolArgs: Record<string, unknown> = { query: query! };
87-
if (flags.count) toolArgs.count = flags.count;
128+
// Build tool arguments
129+
const toolArgs: Record<string, unknown> = { query: query! };
130+
if (flags.count) toolArgs.count = flags.count;
88131

89-
// Call the search tool
90-
const result = await client.callTool("bailian_web_search", toolArgs);
132+
// Call the search tool
133+
const result = await youcomClient.callTool("youcom_web_search", toolArgs);
91134

92-
// Handle error response
93-
if (result.isError) {
94-
const errText = result.content.map((c) => c.text || "").join("\n");
95-
throw new BailianError(`Search error: ${errText}`);
96-
}
135+
// Handle error response
136+
if (result.isError) {
137+
const errText = result.content.map((c) => c.text || "").join("\n");
138+
throw new BailianError(`You.com search error: ${errText}`);
139+
}
97140

98-
if (!settings.quiet) spinner.stop("Done.");
141+
if (!settings.quiet) spinner.stop("Done.");
142+
143+
// Output results
144+
if (format === "json") {
145+
emitResult({ ...result, provider: "youcom" }, format);
146+
} else {
147+
// Text mode - You.com results are already formatted
148+
for (const item of result.content) {
149+
if (item.type === "text" && item.text) {
150+
emitResult({ text: item.text, provider: "youcom" }, format);
151+
}
152+
}
153+
}
99154

100-
// Output results — always structured to stdout
101-
if (format === "json") {
102-
emitResult(result, format);
103155
} else {
104-
// Text mode: try to extract pages for human-friendly display
105-
for (const item of result.content) {
106-
if (item.type === "text" && item.text) {
107-
try {
108-
const data = JSON.parse(item.text) as {
109-
pages?: Array<{
110-
title?: string;
111-
url?: string;
112-
snippet?: string;
113-
hostname?: string;
114-
}>;
115-
};
116-
if (data.pages && Array.isArray(data.pages)) {
117-
emitResult({ pages: data.pages, total: data.pages.length }, format);
118-
} else {
119-
emitResult(data, format);
156+
// Use DashScope MCP client
157+
const client = ctx.client.mcp(mcpWebSearchPath());
158+
await client.initialize();
159+
160+
if (!settings.quiet) spinner.update("Searching with DashScope...");
161+
162+
// Build tool arguments
163+
const toolArgs: Record<string, unknown> = { query: query! };
164+
if (flags.count) toolArgs.count = flags.count;
165+
166+
// Call the search tool
167+
const result = await client.callTool("bailian_web_search", toolArgs);
168+
169+
// Handle error response
170+
if (result.isError) {
171+
const errText = result.content.map((c) => c.text || "").join("\n");
172+
throw new BailianError(`Search error: ${errText}`);
173+
}
174+
175+
if (!settings.quiet) spinner.stop("Done.");
176+
177+
// Output results — always structured to stdout
178+
if (format === "json") {
179+
emitResult({ ...result, provider: "dashscope" }, format);
180+
} else {
181+
// Text mode: try to extract pages for human-friendly display
182+
for (const item of result.content) {
183+
if (item.type === "text" && item.text) {
184+
try {
185+
const data = JSON.parse(item.text) as {
186+
pages?: Array<{
187+
title?: string;
188+
url?: string;
189+
snippet?: string;
190+
hostname?: string;
191+
}>;
192+
};
193+
if (data.pages && Array.isArray(data.pages)) {
194+
emitResult({ pages: data.pages, total: data.pages.length, provider: "dashscope" }, format);
195+
} else {
196+
emitResult({ ...data, provider: "dashscope" }, format);
197+
}
198+
} catch {
199+
emitResult({ text: item.text, provider: "dashscope" }, format);
120200
}
121-
} catch {
122-
emitResult({ text: item.text }, format);
123201
}
124202
}
125203
}
126204
}
127205
} catch (error) {
128206
spinner.stop("Failed.");
129-
rethrowWithWebSearchActivateHint(error);
207+
if (useYouCom) {
208+
// You.com specific error handling
209+
if (error instanceof BailianError) throw error;
210+
throw new BailianError(`You.com search error: ${error instanceof Error ? error.message : 'Unknown error'}`, 1);
211+
} else {
212+
rethrowWithWebSearchActivateHint(error);
213+
}
130214
}
131215
},
132216
});

packages/core/src/client/endpoints.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ export function mcpWebSearchPath(): string {
100100
return "/api/v1/mcps/WebSearch/mcp";
101101
}
102102

103+
export function mcpYouComSearchPath(): string {
104+
return "/api/v1/mcps/YouComSearch/mcp";
105+
}
106+
103107
// ---- Datasets / Fine-tune Files ----
104108

105109
/**

packages/core/src/client/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export {
1313
memoryNodePath,
1414
memorySearchPath,
1515
mcpWebSearchPath,
16+
mcpYouComSearchPath,
1617
profileSchemaPath,
1718
speechRecognizePath,
1819
speechSynthesizePath,
@@ -59,5 +60,6 @@ export {
5960
} from "./acs.ts";
6061
export type { McpTool, McpToolResult } from "./mcp.ts";
6162
export { McpClient, bailianMcpPath } from "./mcp.ts";
63+
export { YouComMcpClient } from "./youcom-mcp.ts";
6264
export type { ServerSentEvent } from "./stream.ts";
6365
export { parseSSE } from "./stream.ts";

0 commit comments

Comments
 (0)