feat(core): add ai tool call error type and ui (#12941)

<img width="775" alt="截屏2025-06-26 16 17 05"
src="https://github.com/user-attachments/assets/ed6bcae3-94af-4eb1-81e8-710f36ef5e46"
/>


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

## Summary by CodeRabbit

* **New Features**
  * Introduced a tool to crawl web pages and extract key information.
* Added a visual component to display tool call failures in the AI
interface.
* Enhanced error reporting for document and web search tools with
structured error messages.

* **Improvements**
* Updated error handling across AI tools and components for more
consistent and informative feedback.
* Default values added for tool card components to improve reliability
and display.

* **Bug Fixes**
* Improved handling of error and empty states in web crawl and web
search result displays.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wu Yue
2025-06-26 17:38:19 +08:00
committed by GitHub
parent 2171d1bfe2
commit a7185e419c
15 changed files with 240 additions and 121 deletions
@@ -4,6 +4,7 @@ import { z } from 'zod';
import type { AccessController } from '../../../core/permission';
import type { IndexerService, SearchDoc } from '../../indexer';
import type { CopilotChatOptions } from '../providers';
import { toolError } from './error';
export const buildDocKeywordSearchGetter = (
ac: AccessController,
@@ -56,8 +57,8 @@ export const createDocKeywordSearchTool = (
createdByUser: doc.createdByUser,
updatedByUser: doc.updatedByUser,
}));
} catch {
return 'Failed to search documents.';
} catch (e: any) {
return toolError('Doc Keyword Search Failed', e.message);
}
},
});
@@ -5,6 +5,7 @@ import type { AccessController } from '../../../core/permission';
import type { ChunkSimilarity } from '../../../models';
import type { CopilotContextService } from '../context';
import type { CopilotChatOptions } from '../providers';
import { toolError } from './error';
export const buildDocSearchGetter = (
ac: AccessController,
@@ -46,8 +47,8 @@ export const createDocSemanticSearchTool = (
execute: async ({ query }) => {
try {
return await searchDocs(query);
} catch {
return 'Failed to search documents.';
} catch (e: any) {
return toolError('Doc Semantic Search Failed', e.message);
}
},
});
@@ -0,0 +1,11 @@
export interface ToolError {
type: 'error';
name: string;
message: string;
}
export const toolError = (name: string, message: string): ToolError => ({
type: 'error',
name,
message,
});
@@ -0,0 +1,39 @@
import { tool } from 'ai';
import Exa from 'exa-js';
import { z } from 'zod';
import { Config } from '../../../base';
import { toolError } from './error';
export const createExaCrawlTool = (config: Config) => {
return tool({
description: 'Crawl the web url for information',
parameters: z.object({
url: z
.string()
.describe('The URL to crawl (including http:// or https://)'),
}),
execute: async ({ url }) => {
try {
const { key } = config.copilot.exa;
const exa = new Exa(key);
const result = await exa.getContents([url], {
livecrawl: 'always',
text: {
maxCharacters: 100000,
},
});
return result.results.map(data => ({
title: data.title,
url: data.url,
content: data.text,
favicon: data.favicon,
publishedDate: data.publishedDate,
author: data.author,
}));
} catch (e: any) {
return toolError('Exa Crawl Failed', e.message);
}
},
});
};
@@ -3,6 +3,7 @@ import Exa from 'exa-js';
import { z } from 'zod';
import { Config } from '../../../base';
import { toolError } from './error';
export const createExaSearchTool = (config: Config) => {
return tool({
@@ -30,41 +31,8 @@ export const createExaSearchTool = (config: Config) => {
publishedDate: data.publishedDate,
author: data.author,
}));
} catch {
return 'Failed to search the web';
}
},
});
};
export const createExaCrawlTool = (config: Config) => {
return tool({
description: 'Crawl the web url for information',
parameters: z.object({
url: z
.string()
.describe('The URL to crawl (including http:// or https://)'),
}),
execute: async ({ url }) => {
try {
const { key } = config.copilot.exa;
const exa = new Exa(key);
const result = await exa.getContents([url], {
livecrawl: 'always',
text: {
maxCharacters: 100000,
},
});
return result.results.map(data => ({
title: data.title,
url: data.url,
content: data.text,
favicon: data.favicon,
publishedDate: data.publishedDate,
author: data.author,
}));
} catch {
return 'Failed to crawl the web url';
} catch (e: any) {
return toolError('Exa Search Failed', e.message);
}
},
});
@@ -1,3 +1,5 @@
export * from './doc-keyword-search';
export * from './doc-semantic-search';
export * from './web-search';
export * from './error';
export * from './exa-crawl';
export * from './exa-search';