feat(server): add doc keyword search tool (#12837)

close AI-185

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

- **New Features**
- Introduced a keyword-based document search tool, allowing users to
search for relevant documents within their workspace using keywords.
- Search results include document titles, summaries, and direct links,
enhancing document discovery and navigation.
- **Bug Fixes**
  - None.
- **Tests**
- Added new tests to verify document search by IDs and by keywords,
ensuring accurate and reliable search functionality.
- **Documentation**
  - None.
- **Chores**
- Updated configuration file organization for improved clarity; no
changes to functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->








#### PR Dependency Tree


* **PR #12867**
  * **PR #12863**
    * **PR #12837** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
fengmk2
2025-06-20 18:50:34 +08:00
committed by GitHub
parent 3a124b67bd
commit e978147a16
7 changed files with 148 additions and 26 deletions
@@ -1,3 +1,4 @@
import { Logger } from '@nestjs/common';
import {
CoreAssistantMessage,
CoreUserMessage,
@@ -10,6 +11,7 @@ import {
import { ZodType } from 'zod';
import {
createDocKeywordSearchTool,
createDocSemanticSearchTool,
createExaCrawlTool,
createExaSearchTool,
@@ -381,6 +383,7 @@ export class CitationParser {
export interface CustomAITools extends ToolSet {
doc_semantic_search: ReturnType<typeof createDocSemanticSearchTool>;
doc_keyword_search: ReturnType<typeof createDocKeywordSearchTool>;
web_search_exa: ReturnType<typeof createExaSearchTool>;
web_crawl_exa: ReturnType<typeof createExaCrawlTool>;
}
@@ -404,6 +407,7 @@ export function parseUnknownError(error: unknown) {
}
export class TextStreamParser {
private readonly logger = new Logger(TextStreamParser.name);
private readonly CALLOUT_PREFIX = '\n[!]\n';
private lastType: ChunkType | undefined;
@@ -428,6 +432,9 @@ export class TextStreamParser {
break;
}
case 'tool-call': {
this.logger.debug(
`[tool-call] toolName: ${chunk.toolName}, toolCallId: ${chunk.toolCallId}`
);
result = this.addPrefix(result);
switch (chunk.toolName) {
case 'web_search_exa': {
@@ -438,11 +445,18 @@ export class TextStreamParser {
result += `\nCrawling the web "${chunk.args.url}"\n`;
break;
}
case 'doc_keyword_search': {
result += `\nSearching the keyword "${chunk.args.query}"\n`;
break;
}
}
result = this.markAsCallout(result);
break;
}
case 'tool-result': {
this.logger.debug(
`[tool-result] toolName: ${chunk.toolName}, toolCallId: ${chunk.toolCallId}`
);
result = this.addPrefix(result);
switch (chunk.toolName) {
case 'doc_semantic_search': {
@@ -451,6 +465,13 @@ export class TextStreamParser {
}
break;
}
case 'doc_keyword_search': {
if (Array.isArray(chunk.result)) {
result += `\nFound ${chunk.result.length} document${chunk.result.length !== 1 ? 's' : ''} related to “${chunk.args.query}”.\n`;
result += `\n${this.getKeywordSearchLinks(chunk.result)}\n`;
}
break;
}
case 'web_search_exa': {
if (Array.isArray(chunk.result)) {
result += `\n${this.getWebSearchLinks(chunk.result)}\n`;
@@ -505,6 +526,18 @@ export class TextStreamParser {
}, '');
return links;
}
private getKeywordSearchLinks(
list: {
docId: string;
title: string;
}[]
): string {
const links = list.reduce((acc, result) => {
return acc + `\n\n[${result.title}](${result.docId})\n\n`;
}, '');
return links;
}
}
export class StreamObjectParser {