mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +08:00
refactor(core): call copilot in tools (#13024)
fix AI-298 #### PR Dependency Tree * **PR #13024** 👈 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** * Document and code artifact generation tools now use a single prompt field for user input, enabling more flexible content creation powered by AI. * **Bug Fixes** * Improved error handling for missing prompt templates or providers during document and code artifact generation. * **Refactor** * Simplified input schemas for document and code artifact tools, consolidating multiple input fields into a single prompt. * Output schemas updated to remove metadata and other unused fields for a cleaner result. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -14,6 +14,7 @@ import { AccessController } from '../../../core/permission';
|
|||||||
import { Models } from '../../../models';
|
import { Models } from '../../../models';
|
||||||
import { IndexerService } from '../../indexer';
|
import { IndexerService } from '../../indexer';
|
||||||
import { CopilotContextService } from '../context';
|
import { CopilotContextService } from '../context';
|
||||||
|
import { PromptService } from '../prompt';
|
||||||
import {
|
import {
|
||||||
buildContentGetter,
|
buildContentGetter,
|
||||||
buildDocContentGetter,
|
buildDocContentGetter,
|
||||||
@@ -201,11 +202,23 @@ export abstract class CopilotProvider<C = any> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'docCompose': {
|
case 'docCompose': {
|
||||||
tools.doc_compose = createDocComposeTool();
|
const promptService = this.moduleRef.get(PromptService, {
|
||||||
|
strict: false,
|
||||||
|
});
|
||||||
|
tools.doc_compose = createDocComposeTool(
|
||||||
|
promptService,
|
||||||
|
this.factory
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'codeArtifact': {
|
case 'codeArtifact': {
|
||||||
tools.code_artifact = createCodeArtifactTool();
|
const promptService = this.moduleRef.get(PromptService, {
|
||||||
|
strict: false,
|
||||||
|
});
|
||||||
|
tools.code_artifact = createCodeArtifactTool(
|
||||||
|
promptService,
|
||||||
|
this.factory
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Logger } from '@nestjs/common';
|
|||||||
import { tool } from 'ai';
|
import { tool } from 'ai';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { PromptService } from '../prompt';
|
||||||
|
import type { CopilotProviderFactory } from '../providers';
|
||||||
import { toolError } from './error';
|
import { toolError } from './error';
|
||||||
|
|
||||||
const logger = new Logger('CodeArtifactTool');
|
const logger = new Logger('CodeArtifactTool');
|
||||||
@@ -12,7 +14,10 @@ const logger = new Logger('CodeArtifactTool');
|
|||||||
* it can be saved as a single .html file and opened in any browser with no
|
* it can be saved as a single .html file and opened in any browser with no
|
||||||
* external dependencies.
|
* external dependencies.
|
||||||
*/
|
*/
|
||||||
export const createCodeArtifactTool = () => {
|
export const createCodeArtifactTool = (
|
||||||
|
promptService: PromptService,
|
||||||
|
factory: CopilotProviderFactory
|
||||||
|
) => {
|
||||||
return tool({
|
return tool({
|
||||||
description:
|
description:
|
||||||
'Generate a single-file HTML snippet (with inline <style> and <script>) that accomplishes the requested functionality. The final HTML should be runnable when saved as an .html file and opened in a browser. Do NOT reference external resources (CSS, JS, images) except through data URIs.',
|
'Generate a single-file HTML snippet (with inline <style> and <script>) that accomplishes the requested functionality. The final HTML should be runnable when saved as an .html file and opened in a browser. Do NOT reference external resources (CSS, JS, images) except through data URIs.',
|
||||||
@@ -22,58 +27,49 @@ export const createCodeArtifactTool = () => {
|
|||||||
*/
|
*/
|
||||||
title: z.string().describe('The title of the HTML page'),
|
title: z.string().describe('The title of the HTML page'),
|
||||||
/**
|
/**
|
||||||
* The raw HTML that should be placed inside <body>. *Do not* include
|
* The optimized user prompt
|
||||||
* <body> tags here – the tool will wrap it for you.
|
|
||||||
*/
|
*/
|
||||||
body: z
|
userPrompt: z
|
||||||
.string()
|
.string()
|
||||||
.describe('HTML markup that goes inside the <body> element'),
|
.describe(
|
||||||
/**
|
'The user description of the code artifact, will be used to generate the code artifact'
|
||||||
* Optional CSS rules to be wrapped in a single <style> tag inside <head>.
|
),
|
||||||
*/
|
|
||||||
css: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe('CSS to inline in a <style> tag (omit if none).'),
|
|
||||||
/**
|
|
||||||
* Optional JavaScript code to be wrapped in a single <script> tag before
|
|
||||||
* </body>.
|
|
||||||
*/
|
|
||||||
js: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe('JavaScript to inline in a <script> tag (omit if none).'),
|
|
||||||
}),
|
}),
|
||||||
execute: async ({ title, body, css = '', js = '' }) => {
|
execute: async ({ title, userPrompt }) => {
|
||||||
try {
|
try {
|
||||||
const parts: string[] = [];
|
const prompt = await promptService.get('Make it real with text');
|
||||||
parts.push('<!DOCTYPE html>');
|
if (!prompt) {
|
||||||
parts.push('<html lang="en">');
|
throw new Error('Prompt not found');
|
||||||
parts.push('<head>');
|
|
||||||
parts.push('<meta charset="UTF-8" />');
|
|
||||||
parts.push(`<title>${title}</title>`);
|
|
||||||
if (css.trim().length) {
|
|
||||||
parts.push('<style>');
|
|
||||||
parts.push(css);
|
|
||||||
parts.push('</style>');
|
|
||||||
}
|
}
|
||||||
parts.push('</head>');
|
|
||||||
parts.push('<body>');
|
|
||||||
parts.push(body);
|
|
||||||
if (js.trim().length) {
|
|
||||||
parts.push('<script>');
|
|
||||||
parts.push(js);
|
|
||||||
parts.push('</script>');
|
|
||||||
}
|
|
||||||
parts.push('</body>');
|
|
||||||
parts.push('</html>');
|
|
||||||
|
|
||||||
const html = parts.join('\n');
|
const provider = await factory.getProviderByModel(prompt.model);
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error('Provider not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await provider.text(
|
||||||
|
{
|
||||||
|
modelId: prompt.model,
|
||||||
|
},
|
||||||
|
[...prompt.finish({}), { role: 'user', content: userPrompt }]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove surrounding ``` or ```html fences if present
|
||||||
|
let stripped = content.trim();
|
||||||
|
if (stripped.startsWith('```')) {
|
||||||
|
const firstNewline = stripped.indexOf('\n');
|
||||||
|
if (firstNewline !== -1) {
|
||||||
|
stripped = stripped.slice(firstNewline + 1);
|
||||||
|
}
|
||||||
|
if (stripped.endsWith('```')) {
|
||||||
|
stripped = stripped.slice(0, -3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
html,
|
html: stripped,
|
||||||
size: html.length,
|
size: stripped.length,
|
||||||
};
|
};
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error(`Failed to compose code artifact (${title})`, err);
|
logger.error(`Failed to compose code artifact (${title})`, err);
|
||||||
|
|||||||
@@ -2,64 +2,51 @@ import { Logger } from '@nestjs/common';
|
|||||||
import { tool } from 'ai';
|
import { tool } from 'ai';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { PromptService } from '../prompt';
|
||||||
|
import type { CopilotProviderFactory } from '../providers';
|
||||||
import { toolError } from './error';
|
import { toolError } from './error';
|
||||||
|
|
||||||
const logger = new Logger('DocComposeTool');
|
const logger = new Logger('DocComposeTool');
|
||||||
|
|
||||||
export const createDocComposeTool = () => {
|
export const createDocComposeTool = (
|
||||||
|
promptService: PromptService,
|
||||||
|
factory: CopilotProviderFactory
|
||||||
|
) => {
|
||||||
return tool({
|
return tool({
|
||||||
description:
|
description:
|
||||||
'Write a new document with markdown content. This tool creates structured markdown content for documents including titles, sections, and formatting.',
|
'Write a new document with markdown content. This tool creates structured markdown content for documents including titles, sections, and formatting.',
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
title: z.string().describe('The title of the document'),
|
title: z.string().describe('The title of the document'),
|
||||||
content: z
|
userPrompt: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
'The main content to write in markdown format. Include proper markdown formatting like headers (# ## ###), lists (- * 1.), links [text](url), code blocks ```code```, and other markdown elements as needed.'
|
'The user description of the document, will be used to generate the document'
|
||||||
),
|
),
|
||||||
sections: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
heading: z.string().describe('Section heading'),
|
|
||||||
content: z.string().describe('Section content in markdown'),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional()
|
|
||||||
.describe('Optional structured sections for the document'),
|
|
||||||
metadata: z
|
|
||||||
.object({
|
|
||||||
tags: z
|
|
||||||
.array(z.string())
|
|
||||||
.optional()
|
|
||||||
.describe('Optional tags for the document'),
|
|
||||||
description: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe('Optional brief description of the document'),
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.describe('Optional metadata for the document'),
|
|
||||||
}),
|
}),
|
||||||
execute: async ({ title, content, sections, metadata }) => {
|
execute: async ({ title, userPrompt }) => {
|
||||||
try {
|
try {
|
||||||
let markdownContent = '';
|
const prompt = await promptService.get('Write an article about this');
|
||||||
|
if (!prompt) {
|
||||||
markdownContent += `${content}\n\n`;
|
throw new Error('Prompt not found');
|
||||||
|
|
||||||
if (sections && sections.length > 0) {
|
|
||||||
for (const section of sections) {
|
|
||||||
markdownContent += `## ${section.heading}\n\n`;
|
|
||||||
markdownContent += `${section.content}\n\n`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const provider = await factory.getProviderByModel(prompt.model);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error('Provider not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await provider.text(
|
||||||
|
{
|
||||||
|
modelId: prompt.model,
|
||||||
|
},
|
||||||
|
[...prompt.finish({}), { role: 'user', content: userPrompt }]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
markdown: markdownContent.trim(),
|
markdown: content,
|
||||||
wordCount: content.split(/\s+/).length,
|
wordCount: content.split(/\s+/).length,
|
||||||
metadata: metadata || {},
|
|
||||||
tags: metadata?.tags || [],
|
|
||||||
description: metadata?.description || '',
|
|
||||||
};
|
};
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error(`Failed to write document: ${title}`, err);
|
logger.error(`Failed to write document: ${title}`, err);
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ interface DocComposeToolResult {
|
|||||||
title: string;
|
title: string;
|
||||||
markdown: string;
|
markdown: string;
|
||||||
wordCount: number;
|
wordCount: number;
|
||||||
metadata: Record<string, unknown>;
|
|
||||||
}
|
}
|
||||||
| ToolError
|
| ToolError
|
||||||
| null;
|
| null;
|
||||||
|
|||||||
Reference in New Issue
Block a user