feat(server): add read doc tool (#12811)

close AI-186















#### PR Dependency Tree


* **PR #12811** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

- **New Features**
- Added a new tool enabling users to read document content and metadata
within a workspace, with enforced access control.
- **Improvements**
- Updated tool interfaces and outputs to support the document reading
functionality seamlessly.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fengmk2
2025-06-30 11:37:34 +08:00
committed by GitHub
parent 6b2639cbbb
commit 5599c39e97
4 changed files with 106 additions and 0 deletions

View File

@@ -11,14 +11,17 @@ import {
} from '../../../base';
import { DocReader } from '../../../core/doc';
import { AccessController } from '../../../core/permission';
import { Models } from '../../../models';
import { IndexerService } from '../../indexer';
import { CopilotContextService } from '../context';
import {
buildContentGetter,
buildDocContentGetter,
buildDocKeywordSearchGetter,
buildDocSearchGetter,
createDocEditTool,
createDocKeywordSearchTool,
createDocReadTool,
createDocSemanticSearchTool,
createExaCrawlTool,
createExaSearchTool,
@@ -182,6 +185,14 @@ export abstract class CopilotProvider<C = any> {
}
break;
}
case 'docRead': {
const ac = this.moduleRef.get(AccessController, { strict: false });
const models = this.moduleRef.get(Models, { strict: false });
const docReader = this.moduleRef.get(DocReader, { strict: false });
const getDoc = buildDocContentGetter(ac, docReader, models);
tools.doc_read = createDocReadTool(getDoc.bind(null, options));
break;
}
case 'webSearch': {
tools.web_search_exa = createExaSearchTool(this.AFFiNEConfig);
tools.web_crawl_exa = createExaCrawlTool(this.AFFiNEConfig);

View File

@@ -13,6 +13,7 @@ import { ZodType } from 'zod';
import {
createDocEditTool,
createDocKeywordSearchTool,
createDocReadTool,
createDocSemanticSearchTool,
createExaCrawlTool,
createExaSearchTool,
@@ -386,6 +387,7 @@ export interface CustomAITools extends ToolSet {
doc_edit: ReturnType<typeof createDocEditTool>;
doc_semantic_search: ReturnType<typeof createDocSemanticSearchTool>;
doc_keyword_search: ReturnType<typeof createDocKeywordSearchTool>;
doc_read: ReturnType<typeof createDocReadTool>;
web_search_exa: ReturnType<typeof createExaSearchTool>;
web_crawl_exa: ReturnType<typeof createExaCrawlTool>;
}
@@ -451,6 +453,10 @@ export class TextStreamParser {
result += `\nSearching the keyword "${chunk.args.query}"\n`;
break;
}
case 'doc_read': {
result += `\nReading the doc "${chunk.args.doc_id}"\n`;
break;
}
}
result = this.markAsCallout(result);
break;

View File

@@ -0,0 +1,88 @@
import { Logger } from '@nestjs/common';
import { tool } from 'ai';
import { z } from 'zod';
import { DocReader } from '../../../core/doc';
import { AccessController } from '../../../core/permission';
import { Models, publicUserSelect } from '../../../models';
import type { CopilotChatOptions } from '../providers';
import { toolError } from './error';
const logger = new Logger('DocReadTool');
export const buildDocContentGetter = (
ac: AccessController,
docReader: DocReader,
models: Models
) => {
const getDoc = async (options: CopilotChatOptions, docId?: string) => {
if (!options?.user || !options?.workspace || !docId) {
return;
}
const canAccess = await ac
.user(options.user)
.workspace(options.workspace)
.doc(docId)
.can('Doc.Read');
if (!canAccess) {
logger.warn(
`User ${options.user} does not have access to doc ${docId} in workspace ${options.workspace}`
);
return;
}
const docMeta = await models.doc.getSnapshot(options.workspace, docId, {
select: {
createdAt: true,
updatedAt: true,
createdByUser: {
select: publicUserSelect,
},
updatedByUser: {
select: publicUserSelect,
},
},
});
if (!docMeta) {
return;
}
const content = await docReader.getDocMarkdown(options.workspace, docId);
if (!content) {
return;
}
return {
title: content.title,
markdown: content.markdown,
createdAt: docMeta.createdAt,
updatedAt: docMeta.updatedAt,
createdByUser: docMeta.createdByUser,
updatedByUser: docMeta.updatedByUser,
};
};
return getDoc;
};
export const createDocReadTool = (
getDoc: (targetId?: string) => Promise<object | undefined>
) => {
return tool({
description: 'Read the content of a doc in the current workspace',
parameters: z.object({
doc_id: z.string().describe('The target doc to read'),
}),
execute: async ({ doc_id }) => {
try {
const doc = await getDoc(doc_id);
if (!doc) {
return;
}
return { ...doc };
} catch (err: any) {
logger.error(`Failed to read the doc ${doc_id}`, err);
return toolError('Doc Read Failed', err.message);
}
},
});
};

View File

@@ -1,5 +1,6 @@
export * from './doc-edit';
export * from './doc-keyword-search';
export * from './doc-read';
export * from './doc-semantic-search';
export * from './error';
export * from './exa-crawl';