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:
@@ -11,14 +11,17 @@ import {
|
|||||||
} from '../../../base';
|
} from '../../../base';
|
||||||
import { DocReader } from '../../../core/doc';
|
import { DocReader } from '../../../core/doc';
|
||||||
import { AccessController } from '../../../core/permission';
|
import { AccessController } from '../../../core/permission';
|
||||||
|
import { Models } from '../../../models';
|
||||||
import { IndexerService } from '../../indexer';
|
import { IndexerService } from '../../indexer';
|
||||||
import { CopilotContextService } from '../context';
|
import { CopilotContextService } from '../context';
|
||||||
import {
|
import {
|
||||||
buildContentGetter,
|
buildContentGetter,
|
||||||
|
buildDocContentGetter,
|
||||||
buildDocKeywordSearchGetter,
|
buildDocKeywordSearchGetter,
|
||||||
buildDocSearchGetter,
|
buildDocSearchGetter,
|
||||||
createDocEditTool,
|
createDocEditTool,
|
||||||
createDocKeywordSearchTool,
|
createDocKeywordSearchTool,
|
||||||
|
createDocReadTool,
|
||||||
createDocSemanticSearchTool,
|
createDocSemanticSearchTool,
|
||||||
createExaCrawlTool,
|
createExaCrawlTool,
|
||||||
createExaSearchTool,
|
createExaSearchTool,
|
||||||
@@ -182,6 +185,14 @@ export abstract class CopilotProvider<C = any> {
|
|||||||
}
|
}
|
||||||
break;
|
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': {
|
case 'webSearch': {
|
||||||
tools.web_search_exa = createExaSearchTool(this.AFFiNEConfig);
|
tools.web_search_exa = createExaSearchTool(this.AFFiNEConfig);
|
||||||
tools.web_crawl_exa = createExaCrawlTool(this.AFFiNEConfig);
|
tools.web_crawl_exa = createExaCrawlTool(this.AFFiNEConfig);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ZodType } from 'zod';
|
|||||||
import {
|
import {
|
||||||
createDocEditTool,
|
createDocEditTool,
|
||||||
createDocKeywordSearchTool,
|
createDocKeywordSearchTool,
|
||||||
|
createDocReadTool,
|
||||||
createDocSemanticSearchTool,
|
createDocSemanticSearchTool,
|
||||||
createExaCrawlTool,
|
createExaCrawlTool,
|
||||||
createExaSearchTool,
|
createExaSearchTool,
|
||||||
@@ -386,6 +387,7 @@ export interface CustomAITools extends ToolSet {
|
|||||||
doc_edit: ReturnType<typeof createDocEditTool>;
|
doc_edit: ReturnType<typeof createDocEditTool>;
|
||||||
doc_semantic_search: ReturnType<typeof createDocSemanticSearchTool>;
|
doc_semantic_search: ReturnType<typeof createDocSemanticSearchTool>;
|
||||||
doc_keyword_search: ReturnType<typeof createDocKeywordSearchTool>;
|
doc_keyword_search: ReturnType<typeof createDocKeywordSearchTool>;
|
||||||
|
doc_read: ReturnType<typeof createDocReadTool>;
|
||||||
web_search_exa: ReturnType<typeof createExaSearchTool>;
|
web_search_exa: ReturnType<typeof createExaSearchTool>;
|
||||||
web_crawl_exa: ReturnType<typeof createExaCrawlTool>;
|
web_crawl_exa: ReturnType<typeof createExaCrawlTool>;
|
||||||
}
|
}
|
||||||
@@ -451,6 +453,10 @@ export class TextStreamParser {
|
|||||||
result += `\nSearching the keyword "${chunk.args.query}"\n`;
|
result += `\nSearching the keyword "${chunk.args.query}"\n`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'doc_read': {
|
||||||
|
result += `\nReading the doc "${chunk.args.doc_id}"\n`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result = this.markAsCallout(result);
|
result = this.markAsCallout(result);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './doc-edit';
|
export * from './doc-edit';
|
||||||
export * from './doc-keyword-search';
|
export * from './doc-keyword-search';
|
||||||
|
export * from './doc-read';
|
||||||
export * from './doc-semantic-search';
|
export * from './doc-semantic-search';
|
||||||
export * from './error';
|
export * from './error';
|
||||||
export * from './exa-crawl';
|
export * from './exa-crawl';
|
||||||
|
|||||||
Reference in New Issue
Block a user