Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions YOUCOM_INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# You.com Web Search Integration

This CLI now supports You.com as an optional web search provider alongside the default DashScope WebSearch service.

## Setup

### Environment Variables

- `YDC_API_KEY` (optional): You.com API key for authenticated requests
- `YOUCOM_BASE_URL` (optional): Custom You.com API base URL (default: https://api.you.com)

### Usage Examples

```bash
# Use default DashScope WebSearch
bailian-cli search web --query "latest AI developments"

# Use You.com search explicitly
bailian-cli search web --query "latest AI developments" --provider youcom

# Use You.com with API key authentication
export YDC_API_KEY="your-api-key-here"
bailian-cli search web --query "TypeScript features" --provider youcom --count 5

# List available tools from You.com
bailian-cli search web --list-tools --provider youcom
```

## Features

### Keyless Operation
You.com search works without an API key (100 free searches per day) but performs better with authentication.

### MCP Tool Integration
When used as an MCP server, the You.com integration exposes:

- **Tool**: `youcom_web_search`
- **Description**: Search the web using You.com. Returns relevant results with titles, URLs, and snippets.
- **Parameters**:
- `query` (required): The search query string
- `count` (optional): Number of results (1-20, default: 10)
- `safesearch` (optional): Safe search setting ("strict", "moderate", "off", default: "moderate")
- `country` (optional): Country code for localized results (e.g. "US", "GB")

### Error Handling

The integration gracefully handles:
- Network timeouts and connection errors
- API rate limits (HTTP 429)
- Authentication failures (HTTP 401)
- Invalid queries and malformed responses
- Fallback behavior when API key is invalid

### Output Formats

Results are available in both JSON and human-readable text formats, with structured metadata including:
- Page titles and URLs
- Content snippets
- Publication age (when available)
- Provider identification for mixed workflows

## Architecture

The You.com integration is implemented as:
1. **YouComMcpClient**: MCP-compatible client for You.com API
2. **Provider Selection**: Optional `--provider` flag in existing search commands
3. **Environment Configuration**: Standard environment variable configuration
4. **Graceful Fallback**: Falls back to keyless API when authentication fails

## Contributing

The You.com integration follows the existing CLI patterns:
- MCP protocol compliance for tool interoperability
- Structured error handling with BailianError
- Consistent CLI flag naming and behavior
- Environment-based configuration
- Comprehensive test coverage (when test infrastructure is available)
186 changes: 135 additions & 51 deletions packages/commands/src/commands/search/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
BailianError,
detectOutputFormat,
mcpWebSearchPath,
YouComMcpClient,
type FlagsDef,
} from "bailian-cli-core";
import { createSpinner, emitResult } from "bailian-cli-runtime";
Expand All @@ -16,38 +17,71 @@ const WEB_SEARCH_FLAGS = {
description: "Number of search results (default: 10)",
},
listTools: { type: "switch", description: "List available MCP tools and exit" },
provider: {
type: "string",
valueHint: "<name>",
description: "Search provider: 'dashscope' (default) or 'youcom'"
},
} satisfies FlagsDef;

export default defineCommand({
description: "Search the web using DashScope MCP WebSearch service",
auth: "apiKey",
usageArgs: "--query <text> [flags]",
description: "Search the web using DashScope WebSearch or You.com",
auth: "optionalApiKey",
usageArgs: "--query <text> [--provider <name>] [flags]",
flags: WEB_SEARCH_FLAGS,
exampleArgs: [
'--query "Alibaba Cloud Bailian latest features"',
'--query "TypeScript 5.9 new features" --count 5',
'--query "Today\'s news"',
"--list-tools",
'--query "Today\'s news" --provider youcom',
'--query "AI developments" --provider dashscope',
"--list-tools --provider youcom",
],
validate: (f) => (!f.listTools && !f.query ? "Missing required flag: --query" : undefined),
validate: (f) => {
if (!f.listTools && !f.query) return "Missing required flag: --query";
if (f.provider && !["dashscope", "youcom"].includes(f.provider)) {
return "Invalid provider. Use 'dashscope' or 'youcom'";
}
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);

// Determine provider
const provider = flags.provider || "dashscope";
const useYouCom = provider === "youcom";

// --- List tools mode ---
if (flags.listTools) {
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(mcpWebSearchPath()), action: "tools/list" }, format);
const endpoint = useYouCom
? "https://api.you.com"
: ctx.client.url(mcpWebSearchPath());
emitResult({ endpoint, action: "tools/list", provider }, format);
return;
}

try {
const client = ctx.client.mcp(mcpWebSearchPath());
await client.initialize();
const tools = await client.listTools();
emitResult({ tools }, format);
if (useYouCom) {
const config = YouComMcpClient.getConfig();
const youcomClient = YouComMcpClient.fromClient(ctx.client, config.apiKey, config.baseUrl);
await youcomClient.initialize();
const tools = await youcomClient.listTools();
emitResult({ tools, provider: "youcom" }, format);
} else {
const client = ctx.client.mcp(mcpWebSearchPath());
await client.initialize();
const tools = await client.listTools();
emitResult({ tools, provider: "dashscope" }, format);
}
} catch (error) {
rethrowWithWebSearchActivateHint(error);
if (useYouCom) {
// You.com specific error handling
if (error instanceof BailianError) throw error;
throw new BailianError(`You.com search error: ${error instanceof Error ? error.message : 'Unknown error'}`, 1);
} else {
rethrowWithWebSearchActivateHint(error);
}
}
return;
}
Expand All @@ -56,11 +90,17 @@ export default defineCommand({
const query = flags.query;

if (settings.dryRun) {
const endpoint = useYouCom
? "https://api.you.com/api/search"
: ctx.client.url(mcpWebSearchPath());
const toolName = useYouCom ? "youcom_web_search" : "bailian_web_search";

emitResult(
{
endpoint: ctx.client.url(mcpWebSearchPath()),
endpoint,
action: "tools/call",
tool: "bailian_web_search",
tool: toolName,
provider,
arguments: {
query: query!,
count: flags.count || undefined,
Expand All @@ -71,62 +111,106 @@ export default defineCommand({
return;
}

// Initialize MCP client
const client = ctx.client.mcp(mcpWebSearchPath());
// Initialize appropriate client
const spinner = createSpinner("Initializing search...");

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

try {
await client.initialize();
if (useYouCom) {
// Use You.com MCP client
const config = YouComMcpClient.getConfig();
const youcomClient = YouComMcpClient.fromClient(ctx.client, config.apiKey, config.baseUrl);
await youcomClient.initialize();

if (!settings.quiet) spinner.update("Searching...");
if (!settings.quiet) spinner.update("Searching with You.com...");

// Build tool arguments
const toolArgs: Record<string, unknown> = { query: query! };
if (flags.count) toolArgs.count = flags.count;
// Build tool arguments
const toolArgs: Record<string, unknown> = { query: query! };
if (flags.count) toolArgs.count = flags.count;

// Call the search tool
const result = await client.callTool("bailian_web_search", toolArgs);
// Call the search tool
const result = await youcomClient.callTool("youcom_web_search", toolArgs);

// Handle error response
if (result.isError) {
const errText = result.content.map((c) => c.text || "").join("\n");
throw new BailianError(`Search error: ${errText}`);
}
// Handle error response
if (result.isError) {
const errText = result.content.map((c) => c.text || "").join("\n");
throw new BailianError(`You.com search error: ${errText}`);
}

if (!settings.quiet) spinner.stop("Done.");
if (!settings.quiet) spinner.stop("Done.");

// Output results
if (format === "json") {
emitResult({ ...result, provider: "youcom" }, format);
} else {
// Text mode - You.com results are already formatted
for (const item of result.content) {
if (item.type === "text" && item.text) {
emitResult({ text: item.text, provider: "youcom" }, format);
}
}
}

// Output results — always structured to stdout
if (format === "json") {
emitResult(result, format);
} else {
// Text mode: try to extract pages for human-friendly display
for (const item of result.content) {
if (item.type === "text" && item.text) {
try {
const data = JSON.parse(item.text) as {
pages?: Array<{
title?: string;
url?: string;
snippet?: string;
hostname?: string;
}>;
};
if (data.pages && Array.isArray(data.pages)) {
emitResult({ pages: data.pages, total: data.pages.length }, format);
} else {
emitResult(data, format);
// Use DashScope MCP client
const client = ctx.client.mcp(mcpWebSearchPath());
await client.initialize();

if (!settings.quiet) spinner.update("Searching with DashScope...");

// Build tool arguments
const toolArgs: Record<string, unknown> = { query: query! };
if (flags.count) toolArgs.count = flags.count;

// Call the search tool
const result = await client.callTool("bailian_web_search", toolArgs);

// Handle error response
if (result.isError) {
const errText = result.content.map((c) => c.text || "").join("\n");
throw new BailianError(`Search error: ${errText}`);
}

if (!settings.quiet) spinner.stop("Done.");

// Output results — always structured to stdout
if (format === "json") {
emitResult({ ...result, provider: "dashscope" }, format);
} else {
// Text mode: try to extract pages for human-friendly display
for (const item of result.content) {
if (item.type === "text" && item.text) {
try {
const data = JSON.parse(item.text) as {
pages?: Array<{
title?: string;
url?: string;
snippet?: string;
hostname?: string;
}>;
};
if (data.pages && Array.isArray(data.pages)) {
emitResult({ pages: data.pages, total: data.pages.length, provider: "dashscope" }, format);
} else {
emitResult({ ...data, provider: "dashscope" }, format);
}
} catch {
emitResult({ text: item.text, provider: "dashscope" }, format);
}
} catch {
emitResult({ text: item.text }, format);
}
}
}
}
} catch (error) {
spinner.stop("Failed.");
rethrowWithWebSearchActivateHint(error);
if (useYouCom) {
// You.com specific error handling
if (error instanceof BailianError) throw error;
throw new BailianError(`You.com search error: ${error instanceof Error ? error.message : 'Unknown error'}`, 1);
} else {
rethrowWithWebSearchActivateHint(error);
}
}
},
});
4 changes: 4 additions & 0 deletions packages/core/src/client/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ export function mcpWebSearchPath(): string {
return "/api/v1/mcps/WebSearch/mcp";
}

export function mcpYouComSearchPath(): string {
return "/api/v1/mcps/YouComSearch/mcp";
}

// ---- Datasets / Fine-tune Files ----

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export {
memoryNodePath,
memorySearchPath,
mcpWebSearchPath,
mcpYouComSearchPath,
profileSchemaPath,
speechRecognizePath,
speechSynthesizePath,
Expand Down Expand Up @@ -59,5 +60,6 @@ export {
} from "./acs.ts";
export type { McpTool, McpToolResult } from "./mcp.ts";
export { McpClient, bailianMcpPath } from "./mcp.ts";
export { YouComMcpClient } from "./youcom-mcp.ts";
export type { ServerSentEvent } from "./stream.ts";
export { parseSSE } from "./stream.ts";
Loading