feat(core): add exa url crawl tool (#12277)

Close [AI-126](https://linear.app/affine-design/issue/AI-126)

![截屏2025-05-14 17.01.19.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/1a86ac68-f9f1-4740-8ddb-2293838682d2.png)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced a new web crawling tool, allowing users to extract live content from specific web pages in addition to traditional web search.
- **Improvements**
  - Enhanced error handling for web search and web crawl operations, providing clearer failure messages.
  - Updated terminology in AI prompts and user-facing messages to reflect the new web search/crawl capabilities.
  - Improved formatting of web search and crawl results for better readability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
akumatus
2025-05-15 05:22:55 +00:00
parent fcc9b31da9
commit fabcdd3b2c
4 changed files with 80 additions and 24 deletions
@@ -11,7 +11,7 @@ import {
metrics,
UserFriendlyError,
} from '../../../base';
import { createExaSearchTool } from '../tools';
import { createExaCrawlTool, createExaSearchTool } from '../tools';
import { CopilotProvider } from './provider';
import {
ChatMessageRole,
@@ -207,15 +207,23 @@ export class AnthropicProvider
yield prefix;
prefix = null;
}
if (chunk.toolName === 'web_search') {
if (chunk.toolName === 'web_search_exa') {
yield this.markAsCallout(
`\nSearching the web "${chunk.args.query}"\n`
);
}
if (chunk.toolName === 'web_crawl_exa') {
yield this.markAsCallout(
`\nCrawling the web "${chunk.args.url}"\n`
);
}
break;
}
case 'tool-result': {
if (chunk.toolName === 'web_search') {
if (
chunk.toolName === 'web_search_exa' &&
Array.isArray(chunk.result)
) {
if (prefix) {
yield prefix;
prefix = null;
@@ -243,7 +251,8 @@ export class AnthropicProvider
private getTools() {
return {
web_search: createExaSearchTool(this.AFFiNEConfig),
web_search_exa: createExaSearchTool(this.AFFiNEConfig),
web_crawl_exa: createExaCrawlTool(this.AFFiNEConfig),
};
}
@@ -19,7 +19,7 @@ import {
metrics,
UserFriendlyError,
} from '../../../base';
import { createExaSearchTool } from '../tools';
import { createExaCrawlTool, createExaSearchTool } from '../tools';
import { CopilotProvider } from './provider';
import {
ChatMessageRole,
@@ -46,6 +46,7 @@ export type OpenAIConfig = {
type OpenAITools = {
web_search_preview: ReturnType<typeof openai.tools.webSearchPreview>;
web_search_exa: ReturnType<typeof createExaSearchTool>;
web_crawl_exa: ReturnType<typeof createExaCrawlTool>;
};
export class OpenAIProvider
@@ -202,6 +203,7 @@ export class OpenAIProvider
// o series reasoning models
if (model.startsWith('o')) {
tools.web_search_exa = createExaSearchTool(this.AFFiNEConfig);
tools.web_crawl_exa = createExaCrawlTool(this.AFFiNEConfig);
} else {
tools.web_search_preview = openai.tools.webSearchPreview();
}
@@ -330,10 +332,18 @@ export class OpenAIProvider
`\nSearching the web "${chunk.args.query}"\n`
);
}
if (chunk.toolName === 'web_crawl_exa') {
yield this.markAsCallout(
`\nCrawling the web "${chunk.args.url}"\n`
);
}
break;
}
case 'tool-result': {
if (chunk.toolName === 'web_search_exa') {
if (
chunk.toolName === 'web_search_exa' &&
Array.isArray(chunk.result)
) {
yield this.markAsCallout(
`\n${this.getWebSearchLinks(chunk.result)}\n`
);