mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
feat: ai integration (#6283)
* feat: ai * fix: build * fix: plugin * fix: bug * fix: version * chore: add debug info * chore: allows to enter a model name manually * chore: update * chore: optimize * fix: bug * fix: version * fix: build * fix: test
This commit is contained in:
parent
2bad77ac83
commit
4720244eb3
2
packages/plugins/@nocobase/plugin-ai/.npmignore
Normal file
2
packages/plugins/@nocobase/plugin-ai/.npmignore
Normal file
@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/src
|
1
packages/plugins/@nocobase/plugin-ai/README.md
Normal file
1
packages/plugins/@nocobase/plugin-ai/README.md
Normal file
@ -0,0 +1 @@
|
||||
# @nocobase/plugin-ai
|
165
packages/plugins/@nocobase/plugin-ai/build.config.ts
Normal file
165
packages/plugins/@nocobase/plugin-ai/build.config.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import { defineConfig } from '@nocobase/build';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
async function generateIndexFiles(packageJsonPath: string) {
|
||||
// Read and parse package.json
|
||||
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
|
||||
const exports = packageJson.exports;
|
||||
|
||||
// Collect all export paths excluding package.json
|
||||
const exportPaths = Object.entries(exports)
|
||||
.filter(([key]) => key !== './package.json')
|
||||
.map(([key, value]: [string, any]) => {
|
||||
// Remove './' prefix from the key
|
||||
const normalizedKey = key.startsWith('./') ? key.slice(2) : key;
|
||||
|
||||
// Get the paths for different formats
|
||||
const paths = {
|
||||
js: typeof value === 'object' ? value.import?.replace(/^\.\//, '') : value?.replace(/^\.\//, ''),
|
||||
dts: typeof value === 'object' ? value.types?.import?.replace(/^\.\//, '') : null,
|
||||
};
|
||||
|
||||
return { key: normalizedKey, paths };
|
||||
});
|
||||
|
||||
// Generate JavaScript index content
|
||||
const jsContent = `// This file is auto-generated from package.json exports
|
||||
// Do not edit this file manually
|
||||
|
||||
${exportPaths.map(({ key, paths }) => `export * from './${paths.js.replace(/\.js$/, '')}';`).join('\n')}
|
||||
`;
|
||||
|
||||
// Generate CommonJS index content
|
||||
const cjsContent = `// This file is auto-generated from package.json exports
|
||||
// Do not edit this file manually
|
||||
|
||||
${exportPaths.map(({ key, paths }) => `module.exports = require('../${paths.js.replace(/\.js$/, '')}');`).join('\n')}
|
||||
`;
|
||||
|
||||
// Generate TypeScript declaration content
|
||||
const dtsContent = `// This file is auto-generated from package.json exports
|
||||
// Do not edit this file manually
|
||||
|
||||
${exportPaths
|
||||
.map(({ key, paths }) => {
|
||||
const dtsPath = paths.dts?.replace(/\.d\.ts$/, '') || paths.js.replace(/\.js$/, '');
|
||||
return `export * from './${dtsPath}';`;
|
||||
})
|
||||
.join('\n')}
|
||||
`;
|
||||
|
||||
// Generate CommonJS declaration content
|
||||
const ctsContent = `// This file is auto-generated from package.json exports
|
||||
// Do not edit this file manually
|
||||
|
||||
${exportPaths
|
||||
.map(({ key, paths }) => {
|
||||
const dtsPath = paths.dts?.replace(/\.d\.ts$/, '') || paths.js.replace(/\.js$/, '');
|
||||
return `export * from './${dtsPath}';`;
|
||||
})
|
||||
.join('\n')}
|
||||
`;
|
||||
|
||||
// Generate package.json exports for index files
|
||||
const indexExports = {
|
||||
'.': {
|
||||
types: {
|
||||
import: './index.d.ts',
|
||||
require: './index.d.cts',
|
||||
default: './index.d.ts',
|
||||
},
|
||||
import: './index.js',
|
||||
require: './index.cjs',
|
||||
},
|
||||
...exports, // Keep existing exports
|
||||
};
|
||||
|
||||
return {
|
||||
js: jsContent,
|
||||
cjs: cjsContent,
|
||||
dts: dtsContent,
|
||||
cts: ctsContent,
|
||||
exports: indexExports,
|
||||
};
|
||||
}
|
||||
|
||||
async function writeIndexFiles(pkgPath: string) {
|
||||
try {
|
||||
const contents = await generateIndexFiles(pkgPath);
|
||||
const outputDir = path.dirname(pkgPath);
|
||||
|
||||
// Write index.js
|
||||
await fs.writeFile(path.join(outputDir, 'index.js'), contents.js);
|
||||
await fs.writeFile(path.join(outputDir, 'dist', 'index.js'), contents.js);
|
||||
console.log('Generated index.js');
|
||||
|
||||
// Write index.js
|
||||
await fs.writeFile(path.join(outputDir, 'index.cjs'), contents.cjs);
|
||||
await fs.writeFile(path.join(outputDir, 'dist', 'index.cjs'), contents.cjs);
|
||||
console.log('Generated index.cjs');
|
||||
|
||||
// Write index.d.ts
|
||||
await fs.writeFile(path.join(outputDir, 'index.d.ts'), contents.dts);
|
||||
await fs.writeFile(path.join(outputDir, 'dist', 'index.d.ts'), contents.dts);
|
||||
console.log('Generated index.d.ts');
|
||||
|
||||
// Write index.d.cts
|
||||
await fs.writeFile(path.join(outputDir, 'index.d.cts'), contents.cts);
|
||||
console.log('Generated index.d.cts');
|
||||
|
||||
// Update package.json with new exports
|
||||
const packageJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
||||
packageJson.exports = contents.exports;
|
||||
await fs.writeFile(pkgPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||
console.log('Updated package.json exports');
|
||||
} catch (error) {
|
||||
console.error('Error generating index files:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
beforeBuild: async (log) => {
|
||||
await fs.promises.rm(path.resolve(__dirname, 'dist'), { recursive: true, force: true });
|
||||
const pkgPath = path.resolve(process.cwd(), 'node_modules/@langchain/core/package.json');
|
||||
await writeIndexFiles(pkgPath);
|
||||
},
|
||||
|
||||
afterBuild: async (log) => {
|
||||
log('copying deps');
|
||||
const deps = [
|
||||
'decamelize',
|
||||
'zod',
|
||||
'zod-to-json-schema',
|
||||
'langsmith',
|
||||
'p-retry',
|
||||
'p-queue',
|
||||
'p-timeout',
|
||||
'p-finally',
|
||||
'mustache',
|
||||
// 'js-tiktoken/lite',
|
||||
'@cfworker/json-schema',
|
||||
];
|
||||
for (const dep of deps) {
|
||||
const depPath = path.resolve(process.cwd(), 'node_modules', dep);
|
||||
await fs.promises.cp(depPath, path.resolve(__dirname, 'dist/node_modules/@langchain/core/node_modules', dep), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
log('copying js-tiktoken/lite');
|
||||
const files = ['lite.d.ts', 'lite.js', 'lite.cjs'];
|
||||
for (const file of files) {
|
||||
const depPath = path.dirname(require.resolve('js-tiktoken/lite'));
|
||||
const filePath = path.resolve(depPath, file);
|
||||
await fs.promises.cp(
|
||||
filePath,
|
||||
path.resolve(__dirname, 'dist/node_modules/@langchain/core/node_modules/js-tiktoken', file),
|
||||
{
|
||||
recursive: true,
|
||||
force: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
2
packages/plugins/@nocobase/plugin-ai/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-ai/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './dist/client';
|
||||
export { default } from './dist/client';
|
1
packages/plugins/@nocobase/plugin-ai/client.js
Normal file
1
packages/plugins/@nocobase/plugin-ai/client.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/client/index.js');
|
20
packages/plugins/@nocobase/plugin-ai/package.json
Normal file
20
packages/plugins/@nocobase/plugin-ai/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-ai",
|
||||
"displayName": "AI integration",
|
||||
"displayName.zh-CN": "AI 集成",
|
||||
"description": "Support integration with AI services, providing AI-related workflow nodes to enhance business processing capabilities.",
|
||||
"description.zh-CN": "支持接入 AI 服务,提供 AI 相关的工作流节点,增强业务处理能力。",
|
||||
"version": "1.6.0-beta.16",
|
||||
"main": "dist/server/index.js",
|
||||
"peerDependencies": {
|
||||
"@nocobase/client": "1.x",
|
||||
"@nocobase/plugin-workflow": "1.x",
|
||||
"@nocobase/server": "1.x",
|
||||
"@nocobase/test": "1.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@langchain/core": "^0.3.39",
|
||||
"@langchain/deepseek": "^0.0.1",
|
||||
"@langchain/openai": "^0.4.3"
|
||||
}
|
||||
}
|
2
packages/plugins/@nocobase/plugin-ai/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-ai/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './dist/server';
|
||||
export { default } from './dist/server';
|
1
packages/plugins/@nocobase/plugin-ai/server.js
Normal file
1
packages/plugins/@nocobase/plugin-ai/server.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/server/index.js');
|
@ -0,0 +1,234 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { namespace, useT } from '../locale';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import { ArrayCollapse, FormLayout } from '@formily/antd-v5';
|
||||
import { useField, observer } from '@formily/react';
|
||||
import { Field } from '@formily/core';
|
||||
|
||||
const UserMessage: React.FC = observer(() => {
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
content: {
|
||||
title: t('Content'),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'WorkflowVariableRawTextArea',
|
||||
'x-component-props': {
|
||||
autoSize: {
|
||||
minRows: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const Content: React.FC = observer(() => {
|
||||
const t = useT();
|
||||
const field = useField();
|
||||
const role = field.query('.role').take() as Field;
|
||||
|
||||
if (role.value === 'user') {
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ UserMessage }}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
content: {
|
||||
type: 'array',
|
||||
'x-component': 'ArrayCollapse',
|
||||
'x-component-props': {
|
||||
size: 'small',
|
||||
bordered: false,
|
||||
},
|
||||
default: [{ type: 'text' }],
|
||||
'x-decorator': 'FormItem',
|
||||
items: {
|
||||
type: 'object',
|
||||
'x-component': 'ArrayCollapse.CollapsePanel',
|
||||
'x-component-props': {
|
||||
header: t('Content'),
|
||||
},
|
||||
properties: {
|
||||
form: {
|
||||
type: 'void',
|
||||
'x-component': 'FormLayout',
|
||||
'x-component-props': {
|
||||
layout: 'vertical',
|
||||
},
|
||||
properties: {
|
||||
type: {
|
||||
title: t('Type'),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: [{ label: t('Text'), value: 'text' }],
|
||||
default: 'text',
|
||||
},
|
||||
user: {
|
||||
type: 'void',
|
||||
'x-component': 'UserMessage',
|
||||
},
|
||||
},
|
||||
},
|
||||
moveUp: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.MoveUp',
|
||||
},
|
||||
moveDown: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.MoveDown',
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
addition: {
|
||||
type: 'void',
|
||||
title: tval('Add content', { ns: namespace }),
|
||||
'x-component': 'ArrayCollapse.Addition',
|
||||
'x-component-props': {
|
||||
defaultValue: { type: 'text' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
message: {
|
||||
title: tval('Content', { ns: namespace }),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'WorkflowVariableRawTextArea',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const Messages: React.FC = () => {
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ ArrayCollapse, FormLayout, Content }}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
messages: {
|
||||
type: 'array',
|
||||
'x-component': 'ArrayCollapse',
|
||||
'x-component-props': {
|
||||
size: 'small',
|
||||
},
|
||||
'x-decorator': 'FormItem',
|
||||
default: [{ role: 'user', content: [{ type: 'text' }] }],
|
||||
items: {
|
||||
type: 'object',
|
||||
'x-component': 'ArrayCollapse.CollapsePanel',
|
||||
'x-component-props': {
|
||||
header: t('Message'),
|
||||
},
|
||||
properties: {
|
||||
form: {
|
||||
type: 'void',
|
||||
'x-component': 'FormLayout',
|
||||
'x-component-props': {
|
||||
layout: 'vertical',
|
||||
},
|
||||
properties: {
|
||||
role: {
|
||||
title: tval('Role', { ns: namespace }),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: [
|
||||
{ label: 'System', value: 'system' },
|
||||
{ label: 'User', value: 'user' },
|
||||
{ label: 'Assistant', value: 'assistant' },
|
||||
],
|
||||
default: 'user',
|
||||
},
|
||||
content: {
|
||||
type: 'void',
|
||||
'x-component': 'Content',
|
||||
},
|
||||
},
|
||||
},
|
||||
moveUp: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.MoveUp',
|
||||
},
|
||||
moveDown: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.MoveDown',
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
addition: {
|
||||
type: 'void',
|
||||
title: tval('Add prompt', { ns: namespace }),
|
||||
'x-component': 'ArrayCollapse.Addition',
|
||||
'x-component-props': {
|
||||
defaultValue: { role: 'user', content: [{ type: 'text' }] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const MessagesSettings: React.FC = () => {
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ Messages }}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
messages: {
|
||||
type: 'void',
|
||||
'x-component': 'Messages',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
249
packages/plugins/@nocobase/plugin-ai/src/client/client.d.ts
vendored
Normal file
249
packages/plugins/@nocobase/plugin-ai/src/client/client.d.ts
vendored
Normal file
@ -0,0 +1,249 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
// CSS modules
|
||||
type CSSModuleClasses = { readonly [key: string]: string };
|
||||
|
||||
declare module '*.module.css' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.scss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.sass' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.less' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.styl' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.stylus' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.pcss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.sss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
|
||||
// CSS
|
||||
declare module '*.css' { }
|
||||
declare module '*.scss' { }
|
||||
declare module '*.sass' { }
|
||||
declare module '*.less' { }
|
||||
declare module '*.styl' { }
|
||||
declare module '*.stylus' { }
|
||||
declare module '*.pcss' { }
|
||||
declare module '*.sss' { }
|
||||
|
||||
// Built-in asset types
|
||||
// see `src/node/constants.ts`
|
||||
|
||||
// images
|
||||
declare module '*.apng' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.png' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jpg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jpeg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jfif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pjpeg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pjp' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.gif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.svg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ico' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.webp' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.avif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// media
|
||||
declare module '*.mp4' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.webm' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ogg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.mp3' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.wav' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.flac' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.aac' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.opus' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.mov' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.m4a' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.vtt' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// fonts
|
||||
declare module '*.woff' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.woff2' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.eot' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ttf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.otf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// other
|
||||
declare module '*.webmanifest' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pdf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.txt' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// wasm?init
|
||||
declare module '*.wasm?init' {
|
||||
const initWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>;
|
||||
export default initWasm;
|
||||
}
|
||||
|
||||
// web worker
|
||||
declare module '*?worker' {
|
||||
const workerConstructor: {
|
||||
new(options?: { name?: string }): Worker;
|
||||
};
|
||||
export default workerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?worker&inline' {
|
||||
const workerConstructor: {
|
||||
new(options?: { name?: string }): Worker;
|
||||
};
|
||||
export default workerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?worker&url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker' {
|
||||
const sharedWorkerConstructor: {
|
||||
new(options?: { name?: string }): SharedWorker;
|
||||
};
|
||||
export default sharedWorkerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker&inline' {
|
||||
const sharedWorkerConstructor: {
|
||||
new(options?: { name?: string }): SharedWorker;
|
||||
};
|
||||
export default sharedWorkerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker&url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?raw' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?inline' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
61
packages/plugins/@nocobase/plugin-ai/src/client/index.tsx
Normal file
61
packages/plugins/@nocobase/plugin-ai/src/client/index.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Plugin, lazy } from '@nocobase/client';
|
||||
import { AIManager } from './manager/ai-manager';
|
||||
import { openaiProviderOptions } from './llm-providers/openai';
|
||||
import { deepseekProviderOptions } from './llm-providers/deepseek';
|
||||
import PluginWorkflowClient from '@nocobase/plugin-workflow/client';
|
||||
import { LLMInstruction } from './workflow/nodes/llm';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import { namespace } from './locale';
|
||||
const { LLMServices } = lazy(() => import('./llm-services/LLMServices'), 'LLMServices');
|
||||
const { MessagesSettings } = lazy(() => import('./chat-settings/Messages'), 'MessagesSettings');
|
||||
const { Chat } = lazy(() => import('./llm-providers/components/Chat'), 'Chat');
|
||||
const { ModelSelect } = lazy(() => import('./llm-providers/components/ModelSelect'), 'ModelSelect');
|
||||
|
||||
export class PluginAIClient extends Plugin {
|
||||
aiManager = new AIManager();
|
||||
|
||||
async afterAdd() {
|
||||
// await this.app.pm.add()
|
||||
}
|
||||
|
||||
async beforeLoad() {}
|
||||
|
||||
// You can get and modify the app instance here
|
||||
async load() {
|
||||
this.app.pluginSettingsManager.add('ai', {
|
||||
icon: 'RobotOutlined',
|
||||
title: tval('AI integration', { ns: namespace }),
|
||||
aclSnippet: 'pm.ai',
|
||||
});
|
||||
this.app.pluginSettingsManager.add('ai.llm-services', {
|
||||
icon: 'LinkOutlined',
|
||||
title: tval('LLM services', { ns: namespace }),
|
||||
aclSnippet: 'pm.ai.llm-services',
|
||||
Component: LLMServices,
|
||||
});
|
||||
|
||||
this.aiManager.registerLLMProvider('openai', openaiProviderOptions);
|
||||
this.aiManager.registerLLMProvider('deepseek', deepseekProviderOptions);
|
||||
this.aiManager.chatSettings.set('messages', {
|
||||
title: tval('Messages'),
|
||||
Component: MessagesSettings,
|
||||
});
|
||||
|
||||
const workflow = this.app.pm.get('workflow') as PluginWorkflowClient;
|
||||
workflow.registerInstructionGroup('ai', { label: tval('AI', { ns: namespace }) });
|
||||
workflow.registerInstruction('llm', LLMInstruction);
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginAIClient;
|
||||
export { Chat, ModelSelect };
|
||||
export type { LLMProviderOptions } from './manager/ai-manager';
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Tabs } from 'antd';
|
||||
import { usePlugin } from '@nocobase/client';
|
||||
import PluginAIClient from '../..';
|
||||
import { Schema } from '@formily/react';
|
||||
import { useT } from '../../locale';
|
||||
|
||||
export const Chat: React.FC = () => {
|
||||
const t = useT();
|
||||
const plugin = usePlugin('ai') as PluginAIClient;
|
||||
const chatSettings = Array.from(plugin.aiManager.chatSettings.entries());
|
||||
const items = chatSettings.map(([key, { title, Component }]) => ({
|
||||
key,
|
||||
label: Schema.compile(title, { t }),
|
||||
children: <Component />,
|
||||
}));
|
||||
return <Tabs items={items} />;
|
||||
};
|
@ -0,0 +1,250 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { namespace, useT } from '../../locale';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import { ArrayCollapse, FormLayout } from '@formily/antd-v5';
|
||||
import { useField, observer } from '@formily/react';
|
||||
import { Field } from '@formily/core';
|
||||
import { WorkflowVariableInput } from '@nocobase/plugin-workflow/client';
|
||||
|
||||
const UserMessage: React.FC = observer(() => {
|
||||
const t = useT();
|
||||
const field = useField();
|
||||
const type = field.query('.type').take() as Field;
|
||||
|
||||
if (type.value === 'image_url') {
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ WorkflowVariableInput }}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
image_url: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
title: tval('Image', { ns: namespace }),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'WorkflowVariableInput',
|
||||
'x-component-props': {
|
||||
changeOnSelect: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
content: {
|
||||
title: t('Content'),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'WorkflowVariableRawTextArea',
|
||||
'x-component-props': {
|
||||
autoSize: {
|
||||
minRows: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const Content: React.FC = observer(() => {
|
||||
const t = useT();
|
||||
const field = useField();
|
||||
const role = field.query('.role').take() as Field;
|
||||
|
||||
if (role.value === 'user') {
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ UserMessage }}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
content: {
|
||||
type: 'array',
|
||||
'x-component': 'ArrayCollapse',
|
||||
'x-component-props': {
|
||||
size: 'small',
|
||||
bordered: false,
|
||||
},
|
||||
default: [{ type: 'text' }],
|
||||
'x-decorator': 'FormItem',
|
||||
items: {
|
||||
type: 'object',
|
||||
'x-component': 'ArrayCollapse.CollapsePanel',
|
||||
'x-component-props': {
|
||||
header: t('Content'),
|
||||
},
|
||||
properties: {
|
||||
form: {
|
||||
type: 'void',
|
||||
'x-component': 'FormLayout',
|
||||
'x-component-props': {
|
||||
layout: 'vertical',
|
||||
},
|
||||
properties: {
|
||||
type: {
|
||||
title: t('Type'),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: [
|
||||
{ label: t('Text'), value: 'text' },
|
||||
{ label: t('Image'), value: 'image_url' },
|
||||
],
|
||||
default: 'text',
|
||||
},
|
||||
user: {
|
||||
type: 'void',
|
||||
'x-component': 'UserMessage',
|
||||
},
|
||||
},
|
||||
},
|
||||
moveUp: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.MoveUp',
|
||||
},
|
||||
moveDown: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.MoveDown',
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
addition: {
|
||||
type: 'void',
|
||||
title: tval('Add content', { ns: namespace }),
|
||||
'x-component': 'ArrayCollapse.Addition',
|
||||
'x-component-props': {
|
||||
defaultValue: { type: 'text' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
message: {
|
||||
title: tval('Content', { ns: namespace }),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'WorkflowVariableRawTextArea',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const Messages: React.FC = () => {
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ ArrayCollapse, FormLayout, Content }}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
messages: {
|
||||
type: 'array',
|
||||
'x-component': 'ArrayCollapse',
|
||||
'x-component-props': {
|
||||
size: 'small',
|
||||
},
|
||||
'x-decorator': 'FormItem',
|
||||
default: [{ role: 'user', content: [{ type: 'text' }] }],
|
||||
items: {
|
||||
type: 'object',
|
||||
'x-component': 'ArrayCollapse.CollapsePanel',
|
||||
'x-component-props': {
|
||||
header: t('Message'),
|
||||
},
|
||||
properties: {
|
||||
form: {
|
||||
type: 'void',
|
||||
'x-component': 'FormLayout',
|
||||
'x-component-props': {
|
||||
layout: 'vertical',
|
||||
},
|
||||
properties: {
|
||||
role: {
|
||||
title: tval('Role', { ns: namespace }),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: [
|
||||
{ label: 'System', value: 'system' },
|
||||
{ label: 'User', value: 'user' },
|
||||
{ label: 'Assistant', value: 'assistant' },
|
||||
],
|
||||
default: 'user',
|
||||
},
|
||||
content: {
|
||||
type: 'void',
|
||||
'x-component': 'Content',
|
||||
},
|
||||
},
|
||||
},
|
||||
moveUp: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.MoveUp',
|
||||
},
|
||||
moveDown: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.MoveDown',
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCollapse.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
addition: {
|
||||
type: 'void',
|
||||
title: tval('Add prompt', { ns: namespace }),
|
||||
'x-component': 'ArrayCollapse.Addition',
|
||||
'x-component-props': {
|
||||
defaultValue: { role: 'user', content: [{ type: 'text' }] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useAPIClient, useActionContext, useRequest } from '@nocobase/client';
|
||||
import { useForm, useField } from '@formily/react';
|
||||
import { AutoComplete, Spin } from 'antd';
|
||||
import { Field } from '@formily/core';
|
||||
|
||||
export const ModelSelect: React.FC = () => {
|
||||
const field = useField<Field>();
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const ctx = useActionContext();
|
||||
const [options, setOptions] = useState([]);
|
||||
const { data: models, loading } = useRequest<
|
||||
{
|
||||
label: string;
|
||||
value: string;
|
||||
}[]
|
||||
>(
|
||||
() =>
|
||||
api
|
||||
.resource('ai')
|
||||
.listModels({
|
||||
llmService: form.values?.llmService,
|
||||
})
|
||||
.then(
|
||||
(res) =>
|
||||
res?.data?.data?.map(
|
||||
({ id }) =>
|
||||
({
|
||||
label: id,
|
||||
value: id,
|
||||
}) || [],
|
||||
),
|
||||
),
|
||||
{
|
||||
ready: !!form.values?.llmService && ctx.visible,
|
||||
refreshDeps: [form.values?.llmService],
|
||||
onSuccess: (data) => setOptions(data),
|
||||
},
|
||||
);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
if (!models) {
|
||||
setOptions([]);
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
setOptions(models);
|
||||
return;
|
||||
}
|
||||
const searchOptions = models.filter((option) => {
|
||||
return option.label.toLowerCase().includes(value.toLowerCase());
|
||||
});
|
||||
setOptions(searchOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
onSearch={handleSearch}
|
||||
options={options}
|
||||
notFoundContent={loading ? <Spin size="small" /> : null}
|
||||
value={field.value}
|
||||
onChange={(val) => (field.value = val)}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useT } from '../../locale';
|
||||
import { WorkflowVariableJSON } from '@nocobase/plugin-workflow/client';
|
||||
|
||||
export const StructuredOutput: React.FC = () => {
|
||||
const t = useT();
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ WorkflowVariableJSON }}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
structuredOutput: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
schema: {
|
||||
title: 'JSON Schema',
|
||||
type: 'string',
|
||||
description: (
|
||||
<>
|
||||
{t('Syntax references')}:{' '}
|
||||
<a href="https://json-schema.org" target="_blank" rel="noreferrer">
|
||||
JSON Schema
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'WorkflowVariableJSON',
|
||||
'x-component-props': {
|
||||
json5: true,
|
||||
autoSize: {
|
||||
minRows: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
name: {
|
||||
title: t('Name'),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
description: {
|
||||
title: t('Description'),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
},
|
||||
strict: {
|
||||
title: 'Strict',
|
||||
type: 'boolean',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,170 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import { namespace, useT } from '../../locale';
|
||||
import { Collapse } from 'antd';
|
||||
import { WorkflowVariableRawTextArea } from '@nocobase/plugin-workflow/client';
|
||||
import { ModelSelect } from '../components/ModelSelect';
|
||||
import { Chat } from '../components/Chat';
|
||||
|
||||
const Options: React.FC = () => {
|
||||
const t = useT();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<Collapse
|
||||
bordered={false}
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: 'options',
|
||||
label: t('Options'),
|
||||
forceRender: true,
|
||||
children: (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
name: 'deepseek',
|
||||
properties: {
|
||||
frequencyPenalty: {
|
||||
title: tval('Frequency penalty', { ns: namespace }),
|
||||
description: tval('Frequency penalty description', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 0.0,
|
||||
'x-component-props': {
|
||||
step: 0.1,
|
||||
min: -2.0,
|
||||
max: 2.0,
|
||||
},
|
||||
},
|
||||
maxCompletionTokens: {
|
||||
title: tval('Max completion tokens', { ns: namespace }),
|
||||
description: tval('Max completion tokens description', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: -1,
|
||||
},
|
||||
presencePenalty: {
|
||||
title: tval('Presence penalty', { ns: namespace }),
|
||||
description: tval('Presence penalty description', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 0.0,
|
||||
'x-component-props': {
|
||||
step: 0.1,
|
||||
min: -2.0,
|
||||
max: 2.0,
|
||||
},
|
||||
},
|
||||
temperature: {
|
||||
title: tval('Temperature', { ns: namespace }),
|
||||
description: tval('Temperature description', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 0.7,
|
||||
'x-component-props': {
|
||||
step: 0.1,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
},
|
||||
},
|
||||
topP: {
|
||||
title: tval('Top P', { ns: namespace }),
|
||||
description: tval('Top P description', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 1.0,
|
||||
'x-component-props': {
|
||||
step: 0.5,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
},
|
||||
},
|
||||
responseFormat: {
|
||||
title: tval('Response format', { ns: namespace }),
|
||||
description: tval('Response format description', { ns: namespace }),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: [
|
||||
{
|
||||
label: t('Text'),
|
||||
value: 'text',
|
||||
},
|
||||
{
|
||||
label: t('JSON'),
|
||||
value: 'json_object',
|
||||
},
|
||||
],
|
||||
default: 'text',
|
||||
},
|
||||
timeout: {
|
||||
title: tval('Timeout (ms)', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 60000,
|
||||
},
|
||||
maxRetries: {
|
||||
title: tval('Max retries', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModelSettingsForm: React.FC = () => {
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ Options, WorkflowVariableRawTextArea, ModelSelect, Chat }}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
model: {
|
||||
title: tval('Model', { ns: namespace }),
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ModelSelect',
|
||||
},
|
||||
options: {
|
||||
type: 'void',
|
||||
'x-component': 'Options',
|
||||
},
|
||||
chat: {
|
||||
type: 'void',
|
||||
'x-component': 'Chat',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { LLMProviderOptions } from '../../manager/ai-manager';
|
||||
import { ProviderSettingsForm } from '../openai/ProviderSettings';
|
||||
import { ModelSettingsForm } from './ModelSettings';
|
||||
|
||||
export const deepseekProviderOptions: LLMProviderOptions = {
|
||||
components: {
|
||||
ProviderSettingsForm,
|
||||
ModelSettingsForm,
|
||||
},
|
||||
};
|
@ -0,0 +1,174 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import { namespace, useT } from '../../locale';
|
||||
import { Collapse } from 'antd';
|
||||
import { WorkflowVariableRawTextArea } from '@nocobase/plugin-workflow/client';
|
||||
import { ModelSelect } from '../components/ModelSelect';
|
||||
import { Chat } from '../components/Chat';
|
||||
|
||||
const Options: React.FC = () => {
|
||||
const t = useT();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<Collapse
|
||||
bordered={false}
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: 'options',
|
||||
label: t('Options'),
|
||||
forceRender: true,
|
||||
children: (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
name: 'openai',
|
||||
properties: {
|
||||
frequencyPenalty: {
|
||||
title: tval('Frequency penalty', { ns: namespace }),
|
||||
description: tval('Frequency penalty description', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 0.0,
|
||||
'x-component-props': {
|
||||
step: 0.1,
|
||||
min: -2.0,
|
||||
max: 2.0,
|
||||
},
|
||||
},
|
||||
maxCompletionTokens: {
|
||||
title: tval('Max completion tokens', { ns: namespace }),
|
||||
description: tval('Max completion tokens description', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: -1,
|
||||
},
|
||||
presencePenalty: {
|
||||
title: tval('Presence penalty', { ns: namespace }),
|
||||
description: tval('Presence penalty description', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 0.0,
|
||||
'x-component-props': {
|
||||
step: 0.1,
|
||||
min: -2.0,
|
||||
max: 2.0,
|
||||
},
|
||||
},
|
||||
temperature: {
|
||||
title: tval('Temperature', { ns: namespace }),
|
||||
description: tval('Temperature description', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 0.7,
|
||||
'x-component-props': {
|
||||
step: 0.1,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
},
|
||||
},
|
||||
topP: {
|
||||
title: tval('Top P', { ns: namespace }),
|
||||
description: tval('Top P description', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 1.0,
|
||||
'x-component-props': {
|
||||
step: 0.5,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
},
|
||||
},
|
||||
responseFormat: {
|
||||
title: tval('Response format', { ns: namespace }),
|
||||
description: tval('Response format description', { ns: namespace }),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: [
|
||||
{
|
||||
label: t('Text'),
|
||||
value: 'text',
|
||||
},
|
||||
{
|
||||
label: t('JSON'),
|
||||
value: 'json_object',
|
||||
},
|
||||
{
|
||||
label: t('JSON Schema'),
|
||||
value: 'json_schema',
|
||||
},
|
||||
],
|
||||
default: 'text',
|
||||
},
|
||||
timeout: {
|
||||
title: tval('Timeout (ms)', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 60000,
|
||||
},
|
||||
maxRetries: {
|
||||
title: tval('Max retries', { ns: namespace }),
|
||||
type: 'number',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModelSettingsForm: React.FC = () => {
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ Options, WorkflowVariableRawTextArea, ModelSelect, Chat }}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
model: {
|
||||
title: tval('Model', { ns: namespace }),
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ModelSelect',
|
||||
},
|
||||
options: {
|
||||
type: 'void',
|
||||
'x-component': 'Options',
|
||||
},
|
||||
chat: {
|
||||
type: 'void',
|
||||
'x-component': 'Chat',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import { namespace } from '../../locale';
|
||||
|
||||
export const ProviderSettingsForm: React.FC = () => {
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
apiKey: {
|
||||
title: tval('API key', { ns: namespace }),
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'TextAreaWithGlobalScope',
|
||||
},
|
||||
baseURL: {
|
||||
title: tval('Base URL', { ns: namespace }),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'TextAreaWithGlobalScope',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { LLMProviderOptions } from '../../manager/ai-manager';
|
||||
import { ModelSettingsForm } from './ModelSettings';
|
||||
import { ProviderSettingsForm } from './ProviderSettings';
|
||||
|
||||
export const openaiProviderOptions: LLMProviderOptions = {
|
||||
components: {
|
||||
ProviderSettingsForm,
|
||||
ModelSettingsForm,
|
||||
},
|
||||
};
|
@ -0,0 +1,206 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionContextProvider,
|
||||
ExtendCollectionsProvider,
|
||||
SchemaComponent,
|
||||
useAPIClient,
|
||||
useActionContext,
|
||||
useCollection,
|
||||
useCollectionRecordData,
|
||||
useDataBlockRequest,
|
||||
useDataBlockResource,
|
||||
usePlugin,
|
||||
useRequest,
|
||||
} from '@nocobase/client';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import { useT } from '../locale';
|
||||
import { Button, Dropdown, App } from 'antd';
|
||||
import { PlusOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import llmServices from '../../collections/llm-services';
|
||||
import { llmsSchema, createLLMSchema } from '../schemas/llms';
|
||||
import { LLMProviderContext, LLMProvidersContext, useLLMProviders } from './llm-providers';
|
||||
import { Schema, useForm, observer } from '@formily/react';
|
||||
import { createForm } from '@formily/core';
|
||||
import { uid } from '@formily/shared';
|
||||
import PluginAIClient from '..';
|
||||
|
||||
const useCreateFormProps = () => {
|
||||
const { provider } = useContext(LLMProviderContext);
|
||||
const form = useMemo(
|
||||
() =>
|
||||
createForm({
|
||||
initialValues: {
|
||||
name: `v_${uid()}`,
|
||||
provider,
|
||||
},
|
||||
}),
|
||||
[provider],
|
||||
);
|
||||
return {
|
||||
form,
|
||||
};
|
||||
};
|
||||
|
||||
const useEditFormProps = () => {
|
||||
const record = useCollectionRecordData();
|
||||
const form = useMemo(
|
||||
() =>
|
||||
createForm({
|
||||
initialValues: record,
|
||||
}),
|
||||
[record],
|
||||
);
|
||||
return {
|
||||
form,
|
||||
};
|
||||
};
|
||||
|
||||
const useCancelActionProps = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
const form = useForm();
|
||||
return {
|
||||
type: 'default',
|
||||
onClick() {
|
||||
setVisible(false);
|
||||
form.reset();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const useCreateActionProps = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
const { message } = App.useApp();
|
||||
const form = useForm();
|
||||
const resource = useDataBlockResource();
|
||||
const { refresh } = useDataBlockRequest();
|
||||
const t = useT();
|
||||
|
||||
return {
|
||||
type: 'primary',
|
||||
async onClick() {
|
||||
await form.submit();
|
||||
const values = form.values;
|
||||
await resource.create({
|
||||
values,
|
||||
});
|
||||
refresh();
|
||||
message.success(t('Saved successfully'));
|
||||
setVisible(false);
|
||||
form.reset();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const useEditActionProps = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
const { message } = App.useApp();
|
||||
const form = useForm();
|
||||
const resource = useDataBlockResource();
|
||||
const { refresh } = useDataBlockRequest();
|
||||
const collection = useCollection();
|
||||
const filterTk = collection.getFilterTargetKey();
|
||||
const t = useT();
|
||||
|
||||
return {
|
||||
type: 'primary',
|
||||
async onClick() {
|
||||
await form.submit();
|
||||
const values = form.values;
|
||||
await resource.update({
|
||||
values,
|
||||
filterByTk: values[filterTk],
|
||||
});
|
||||
refresh();
|
||||
message.success(t('Saved successfully'));
|
||||
setVisible(false);
|
||||
form.reset();
|
||||
},
|
||||
};
|
||||
};
|
||||
const AddNew = () => {
|
||||
const t = useT();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [provider, setProvider] = useState('');
|
||||
const providers = useLLMProviders();
|
||||
const items = providers.map((item) => ({
|
||||
...item,
|
||||
onClick: () => {
|
||||
setVisible(true);
|
||||
setProvider(item.value);
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<LLMProviderContext.Provider value={{ provider }}>
|
||||
<Dropdown menu={{ items }}>
|
||||
<Button icon={<PlusOutlined />} type={'primary'}>
|
||||
{t('Add new')} <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<SchemaComponent scope={{ setProvider, useCreateFormProps }} schema={createLLMSchema} />
|
||||
</LLMProviderContext.Provider>
|
||||
</ActionContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useProviderSettingsForm = (provider: string) => {
|
||||
const plugin = usePlugin('ai') as PluginAIClient;
|
||||
const p = plugin.aiManager.llmProviders.get(provider);
|
||||
return p?.components?.ProviderSettingsForm;
|
||||
};
|
||||
|
||||
export const Settings = observer(
|
||||
() => {
|
||||
const form = useForm();
|
||||
const record = useCollectionRecordData();
|
||||
const Component = useProviderSettingsForm(form.values.provider || record.provider);
|
||||
return Component ? <Component /> : null;
|
||||
},
|
||||
{ displayName: 'LLMProviderSettings' },
|
||||
);
|
||||
|
||||
export const LLMServices: React.FC = () => {
|
||||
const t = useT();
|
||||
const [providers, setProviders] = useState([]);
|
||||
const api = useAPIClient();
|
||||
useRequest(
|
||||
() =>
|
||||
api
|
||||
.resource('ai')
|
||||
.listLLMProviders()
|
||||
.then((res) => {
|
||||
const providers = res?.data?.data || [];
|
||||
return providers.map((provider: { name: string; title?: string }) => ({
|
||||
key: provider.name,
|
||||
label: Schema.compile(provider.title || provider.name, { t }),
|
||||
value: provider.name,
|
||||
}));
|
||||
}),
|
||||
{
|
||||
onSuccess: (providers) => {
|
||||
setProviders(providers);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<LLMProvidersContext.Provider value={{ providers }}>
|
||||
<ExtendCollectionsProvider collections={[llmServices]}>
|
||||
<SchemaComponent
|
||||
schema={llmsSchema}
|
||||
components={{ AddNew, Settings }}
|
||||
scope={{ t, providers, useEditFormProps, useCancelActionProps, useCreateActionProps, useEditActionProps }}
|
||||
/>
|
||||
</ExtendCollectionsProvider>
|
||||
</LLMProvidersContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export const LLMProviderContext = createContext<{
|
||||
provider: string;
|
||||
}>({ provider: '' });
|
||||
LLMProviderContext.displayName = 'LLMProvidersContext';
|
||||
|
||||
export const LLMProvidersContext = createContext<{
|
||||
providers: {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
}>({ providers: [] });
|
||||
LLMProvidersContext.displayName = 'LLMProviderssContext';
|
||||
|
||||
export const useLLMProviders = () => {
|
||||
const { providers } = useContext(LLMProvidersContext);
|
||||
return providers;
|
||||
};
|
23
packages/plugins/@nocobase/plugin-ai/src/client/locale.ts
Normal file
23
packages/plugins/@nocobase/plugin-ai/src/client/locale.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import pkg from './../../package.json';
|
||||
import { useApp } from '@nocobase/client';
|
||||
|
||||
export const namespace = pkg.name;
|
||||
|
||||
export function useT() {
|
||||
const app = useApp();
|
||||
return (str: string) => app.i18n.t(str, { ns: [pkg.name, 'client'] });
|
||||
}
|
||||
|
||||
export function tStr(key: string) {
|
||||
return `{{t(${JSON.stringify(key)}, { ns: ['${pkg.name}', 'client'], nsMode: 'fallback' })}}`;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Registry } from '@nocobase/utils/client';
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
export type LLMProviderOptions = {
|
||||
components: {
|
||||
ProviderSettingsForm?: ComponentType;
|
||||
ModelSettingsForm?: ComponentType;
|
||||
};
|
||||
};
|
||||
|
||||
export class AIManager {
|
||||
llmProviders = new Registry<LLMProviderOptions>();
|
||||
chatSettings = new Map<
|
||||
string,
|
||||
{
|
||||
title: string;
|
||||
Component: ComponentType;
|
||||
}
|
||||
>();
|
||||
|
||||
registerLLMProvider(name: string, options: LLMProviderOptions) {
|
||||
this.llmProviders.register(name, options);
|
||||
}
|
||||
}
|
255
packages/plugins/@nocobase/plugin-ai/src/client/schemas/llms.ts
Normal file
255
packages/plugins/@nocobase/plugin-ai/src/client/schemas/llms.ts
Normal file
@ -0,0 +1,255 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export const createLLMSchema = {
|
||||
type: 'void',
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
title: '{{ t("Add new") }}',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-decorator': 'FormV2',
|
||||
'x-use-decorator-props': 'useCreateFormProps',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
title: '{{ t("UID") }}',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
title: '{{ t("Title") }}',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
options: {
|
||||
type: 'object',
|
||||
'x-component': 'Settings',
|
||||
},
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{ t("Cancel") }}',
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useCancelActionProps',
|
||||
},
|
||||
submit: {
|
||||
title: '{{ t("Submit") }}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
'x-use-component-props': 'useCreateActionProps',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const llmsSchema = {
|
||||
type: 'void',
|
||||
properties: {
|
||||
card: {
|
||||
type: 'void',
|
||||
'x-component': 'CardItem',
|
||||
'x-component-props': {
|
||||
heightMode: 'fullHeight',
|
||||
},
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'llmServices',
|
||||
action: 'list',
|
||||
},
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
filter: {
|
||||
'x-component': 'Filter.Action',
|
||||
'x-use-component-props': 'useFilterActionProps',
|
||||
title: "{{t('Filter')}}",
|
||||
'x-component-props': {
|
||||
icon: 'FilterOutlined',
|
||||
},
|
||||
'x-align': 'left',
|
||||
},
|
||||
refresh: {
|
||||
title: "{{t('Refresh')}}",
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useRefreshActionProps',
|
||||
'x-component-props': {
|
||||
icon: 'ReloadOutlined',
|
||||
},
|
||||
},
|
||||
bulkDelete: {
|
||||
title: "{{t('Delete')}}",
|
||||
'x-action': 'destroy',
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useBulkDestroyActionProps',
|
||||
'x-component-props': {
|
||||
icon: 'DeleteOutlined',
|
||||
confirm: {
|
||||
title: "{{t('Delete record')}}",
|
||||
content: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
add: {
|
||||
type: 'void',
|
||||
'x-component': 'AddNew',
|
||||
title: "{{t('Add new')}}",
|
||||
'x-align': 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
table: {
|
||||
type: 'array',
|
||||
'x-component': 'TableV2',
|
||||
'x-use-component-props': 'useTableBlockProps',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
column1: {
|
||||
type: 'void',
|
||||
title: '{{ t("UID") }}',
|
||||
'x-component': 'TableV2.Column',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column2: {
|
||||
type: 'void',
|
||||
title: '{{ t("Title") }}',
|
||||
'x-component': 'TableV2.Column',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column3: {
|
||||
type: 'void',
|
||||
title: '{{ t("Provider") }}',
|
||||
'x-component': 'TableV2.Column',
|
||||
properties: {
|
||||
provider: {
|
||||
type: 'string',
|
||||
'x-component': 'Select',
|
||||
'x-read-pretty': true,
|
||||
enum: '{{ providers }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
column4: {
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
properties: {
|
||||
edit: {
|
||||
type: 'void',
|
||||
title: '{{ t("Edit") }}',
|
||||
'x-action': 'update',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
icon: 'EditOutlined',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
title: '{{ t("Edit record") }}',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-decorator': 'FormV2',
|
||||
'x-use-decorator-props': 'useEditFormProps',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
title: '{{ t("Title") }}',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
options: {
|
||||
type: 'object',
|
||||
'x-component': 'Settings',
|
||||
},
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{ t("Cancel") }}',
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useCancelActionProps',
|
||||
},
|
||||
submit: {
|
||||
title: '{{ t("Submit") }}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
'x-use-component-props': 'useEditActionProps',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destroy: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-action': 'destroy',
|
||||
'x-component': 'Action.Link',
|
||||
'x-use-component-props': 'useDestroyActionProps',
|
||||
'x-component-props': {
|
||||
confirm: {
|
||||
title: "{{t('Delete record')}}",
|
||||
content: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { observer, useForm } from '@formily/react';
|
||||
import { useAPIClient, usePlugin, useRequest } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import PluginAIClient from '../../../';
|
||||
|
||||
export const useModelSettingsForm = (provider: string) => {
|
||||
const plugin = usePlugin(PluginAIClient);
|
||||
const p = plugin.aiManager.llmProviders.get(provider);
|
||||
return p?.components?.ModelSettingsForm;
|
||||
};
|
||||
|
||||
export const Settings = observer(
|
||||
() => {
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const { data, loading } = useRequest<{ provider: string }>(
|
||||
() =>
|
||||
api
|
||||
.resource('llmServices')
|
||||
.get({ filterByTk: form.values?.llmService })
|
||||
.then((res) => res?.data?.data),
|
||||
{
|
||||
ready: !!form.values?.llmService,
|
||||
refreshDeps: [form.values?.llmService],
|
||||
},
|
||||
);
|
||||
const Component = useModelSettingsForm(data?.provider);
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
return Component ? <Component /> : null;
|
||||
},
|
||||
{ displayName: 'WorkflowLLMModelSettingsForm' },
|
||||
);
|
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Instruction } from '@nocobase/plugin-workflow/client';
|
||||
import { RobotOutlined } from '@ant-design/icons';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import { namespace } from '../../../locale';
|
||||
import { Settings } from './ModelSettings';
|
||||
|
||||
export class LLMInstruction extends Instruction {
|
||||
title = 'LLM';
|
||||
type = 'llm';
|
||||
group = 'ai';
|
||||
// @ts-ignore
|
||||
icon = (<RobotOutlined />);
|
||||
fieldset = {
|
||||
llmService: {
|
||||
type: 'string',
|
||||
title: tval('LLM service', { ns: namespace }),
|
||||
name: 'llmService',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'RemoteSelect',
|
||||
'x-component-props': {
|
||||
manual: false,
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
},
|
||||
service: {
|
||||
resource: 'llmServices',
|
||||
action: 'list',
|
||||
params: {
|
||||
fields: ['title', 'name'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
type: 'void',
|
||||
'x-component': 'Settings',
|
||||
},
|
||||
};
|
||||
components = {
|
||||
Settings,
|
||||
};
|
||||
|
||||
isAvailable({ engine, workflow }) {
|
||||
return !engine.isWorkflowSync(workflow);
|
||||
}
|
||||
|
||||
useVariables(node, options) {
|
||||
return {
|
||||
label: node.title,
|
||||
value: node.key,
|
||||
children: [
|
||||
{
|
||||
value: 'content',
|
||||
label: 'Content',
|
||||
},
|
||||
{
|
||||
value: 'structuredContent',
|
||||
label: 'Structured content',
|
||||
},
|
||||
{
|
||||
value: 'additionalKwargs',
|
||||
label: 'Additional Kwargs',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'llmServices',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'uid',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Title")}}',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'provider',
|
||||
type: 'string',
|
||||
interface: 'select',
|
||||
uiSchema: {
|
||||
title: '{{t("Provider")}}',
|
||||
'x-component': 'Select',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
type: 'jsonb',
|
||||
},
|
||||
],
|
||||
};
|
11
packages/plugins/@nocobase/plugin-ai/src/index.ts
Normal file
11
packages/plugins/@nocobase/plugin-ai/src/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export * from './server';
|
||||
export { default } from './server';
|
25
packages/plugins/@nocobase/plugin-ai/src/locale/en-US.json
Normal file
25
packages/plugins/@nocobase/plugin-ai/src/locale/en-US.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"AI integration": "AI integration",
|
||||
"LLM services": "LLM services",
|
||||
"LLM service": "LLM service",
|
||||
"Model": "Model",
|
||||
"Messages": "Messages",
|
||||
"Structured output": "Structured output",
|
||||
"Message": "Message",
|
||||
"Role": "Role",
|
||||
"UID": "UID",
|
||||
"Add content": "Add content",
|
||||
"Add prompt": "Add prompt",
|
||||
"Provider": "Provider",
|
||||
"Text": "Text",
|
||||
"Image": "Image",
|
||||
"Timout (ms)": "Timout (ms)",
|
||||
"Max retries": "Max retries",
|
||||
"Frequency penalty description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.",
|
||||
"Max completion tokens description": "An upper bound for the number of tokens that can be generated for a completion, including visible output tokens and reasoning tokens.",
|
||||
"Presence penalty description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.",
|
||||
"Response format description": "Important: when using JSON mode, you must also instruct the model to produce JSON yourself via a system or user message.",
|
||||
"Temperature description": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.",
|
||||
"Top P description": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.",
|
||||
"Get models list failed, you can enter a model name manually.": "Get models list failed, you can enter a model name manually."
|
||||
}
|
25
packages/plugins/@nocobase/plugin-ai/src/locale/zh-CN.json
Normal file
25
packages/plugins/@nocobase/plugin-ai/src/locale/zh-CN.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"AI integration": "AI 集成",
|
||||
"LLM services": "LLM 服务",
|
||||
"LLM service": "LLM 服务",
|
||||
"Model": "模型",
|
||||
"UID": "唯一标识",
|
||||
"Provider": "LLM 类型",
|
||||
"Messages": "消息",
|
||||
"Structured output": "结构化输出",
|
||||
"Message": "消息",
|
||||
"Role": "角色",
|
||||
"Add content": "添加内容",
|
||||
"Add prompt": "添加提示",
|
||||
"Text": "文本",
|
||||
"Image": "图片",
|
||||
"Timout (ms)": "超时时间(毫秒)",
|
||||
"Max retries": "最大重试次数",
|
||||
"Frequency penalty description": "介于 -2.0 和 2.0 之间的数字。如果该值为正,那么新 token 会根据其在已有文本中的出现频率受到相应的惩罚,降低模型重复相同内容的可能性。",
|
||||
"Max completion tokens description": "限制一次请求中模型生成 completion 的最大 token 数。输入 token 和输出 token 的总长度受模型的上下文长度的限制。",
|
||||
"Presence penalty description": "介于 -2.0 和 2.0 之间的数字。如果该值为正,那么新 token 会根据其是否已在已有文本中出现受到相应的惩罚,从而增加模型谈论新主题的可能性。",
|
||||
"Response format description": "使用 JSON 模式时,你还必须通过系统或用户消息指示模型生成 JSON。",
|
||||
"Temperature description": "采样温度,介于 0 和 2 之间。更高的值,如 0.8,会使输出更随机,而更低的值,如 0.2,会使其更加集中和确定。",
|
||||
"Top P description": "作为调节采样温度的替代方案,模型会考虑前 top_p 概率的 token 的结果。所以 0.1 就意味着只有包括在最高 10% 概率中的 token 会被考虑。",
|
||||
"Get models list failed, you can enter a model name manually.": "获取模型列表失败,你可以手动输入模型名称。"
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { defineCollection } from '@nocobase/database';
|
||||
import llmServices from '../../collections/llm-services';
|
||||
|
||||
export default defineCollection({
|
||||
migrationRules: ['overwrite', 'schema-only'],
|
||||
autoGenId: false,
|
||||
...llmServices,
|
||||
});
|
12
packages/plugins/@nocobase/plugin-ai/src/server/index.ts
Normal file
12
packages/plugins/@nocobase/plugin-ai/src/server/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export { default } from './plugin';
|
||||
export { LLMProvider } from './llm-providers/provider';
|
||||
export { LLMProviderOptions } from './manager/ai-manager';
|
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { ChatDeepSeek } from '@langchain/deepseek';
|
||||
import { LLMProvider } from './provider';
|
||||
import { LLMProviderOptions } from '../manager/ai-manager';
|
||||
|
||||
export class DeepSeekProvider extends LLMProvider {
|
||||
baseURL = 'https://api.deepseek.com';
|
||||
|
||||
createModel() {
|
||||
const { baseURL, apiKey } = this.serviceOptions || {};
|
||||
const { responseFormat } = this.modelOptions || {};
|
||||
|
||||
return new ChatDeepSeek({
|
||||
apiKey,
|
||||
...this.modelOptions,
|
||||
modelKwargs: {
|
||||
response_format: {
|
||||
type: responseFormat,
|
||||
},
|
||||
},
|
||||
configuration: {
|
||||
baseURL: baseURL || this.baseURL,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const deepseekProviderOptions: LLMProviderOptions = {
|
||||
title: 'DeepSeek',
|
||||
provider: DeepSeekProvider,
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
type Message = {
|
||||
role: 'user' | 'system' | 'assistant';
|
||||
message?: string;
|
||||
content?: {
|
||||
type: 'text';
|
||||
content?: string;
|
||||
}[];
|
||||
};
|
||||
import { AIMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
|
||||
|
||||
async function parseMessage(message: Message) {
|
||||
switch (message.role) {
|
||||
case 'system':
|
||||
return new SystemMessage(message.message);
|
||||
case 'assistant':
|
||||
return new AIMessage(message.message);
|
||||
case 'user': {
|
||||
if (message.content.length === 1) {
|
||||
const msg = message.content[0];
|
||||
return new HumanMessage(msg.content);
|
||||
}
|
||||
const content = [];
|
||||
for (const c of message.content) {
|
||||
if (c.type === 'text') {
|
||||
content.push({
|
||||
type: 'text',
|
||||
text: c.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
return new HumanMessage({
|
||||
content,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseMessages() {
|
||||
const msgs = [];
|
||||
for (const message of this.messages) {
|
||||
const msg = await parseMessage(message);
|
||||
msgs.push(msg);
|
||||
}
|
||||
this.messages = msgs;
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { LLMProvider } from './provider';
|
||||
|
||||
export class OpenAIProvider extends LLMProvider {
|
||||
baseURL = 'https://api.openai.com/v1';
|
||||
|
||||
createModel() {
|
||||
const { baseURL, apiKey } = this.serviceOptions || {};
|
||||
const { responseFormat, structuredOutput } = this.modelOptions || {};
|
||||
const { schema } = structuredOutput || {};
|
||||
const responseFormatOptions = {
|
||||
type: responseFormat,
|
||||
};
|
||||
if (responseFormat === 'json_schema' && schema) {
|
||||
responseFormatOptions['json_schema'] = schema;
|
||||
}
|
||||
return new ChatOpenAI({
|
||||
apiKey,
|
||||
...this.modelOptions,
|
||||
modelKwargs: {
|
||||
response_format: responseFormatOptions,
|
||||
},
|
||||
configuration: {
|
||||
baseURL: baseURL || this.baseURL,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const openaiProviderOptions = {
|
||||
title: 'OpenAI',
|
||||
provider: OpenAIProvider,
|
||||
};
|
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import axios from 'axios';
|
||||
import { parseMessages } from './handlers/parse-messages';
|
||||
import { Application } from '@nocobase/server';
|
||||
|
||||
export abstract class LLMProvider {
|
||||
baseURL?: string;
|
||||
serviceOptions: Record<string, any>;
|
||||
modelOptions: Record<string, any>;
|
||||
messages: any[];
|
||||
chatModel: any;
|
||||
chatHandlers = new Map<string, () => Promise<void> | void>();
|
||||
|
||||
abstract createModel(): BaseChatModel;
|
||||
|
||||
constructor(opts: {
|
||||
app: Application;
|
||||
serviceOptions: any;
|
||||
chatOptions?: {
|
||||
messages?: any[];
|
||||
[key: string]: any;
|
||||
};
|
||||
}) {
|
||||
const { app, serviceOptions, chatOptions } = opts;
|
||||
this.serviceOptions = app.environment.renderJsonTemplate(serviceOptions);
|
||||
if (chatOptions) {
|
||||
const { messages, ...modelOptions } = chatOptions;
|
||||
this.modelOptions = modelOptions;
|
||||
this.messages = messages;
|
||||
this.chatModel = this.createModel();
|
||||
this.registerChatHandler('parse-messages', parseMessages);
|
||||
}
|
||||
}
|
||||
|
||||
registerChatHandler(name: string, handler: () => Promise<void> | void) {
|
||||
this.chatHandlers.set(name, handler.bind(this));
|
||||
}
|
||||
|
||||
async invokeChat() {
|
||||
for (const handler of this.chatHandlers.values()) {
|
||||
await handler();
|
||||
}
|
||||
return this.chatModel.invoke(this.messages);
|
||||
}
|
||||
|
||||
async listModels(): Promise<{
|
||||
models?: { id: string }[];
|
||||
code?: number;
|
||||
errMsg?: string;
|
||||
}> {
|
||||
const options = this.serviceOptions || {};
|
||||
const apiKey = options.apiKey;
|
||||
let baseURL = options.baseURL || this.baseURL;
|
||||
if (!baseURL) {
|
||||
return { code: 400, errMsg: 'baseURL is required' };
|
||||
}
|
||||
if (!apiKey) {
|
||||
return { code: 400, errMsg: 'API Key required' };
|
||||
}
|
||||
if (baseURL && baseURL.endsWith('/')) {
|
||||
baseURL = baseURL.slice(0, -1);
|
||||
}
|
||||
try {
|
||||
if (baseURL && baseURL.endsWith('/')) {
|
||||
baseURL = baseURL.slice(0, -1);
|
||||
}
|
||||
const res = await axios.get(`${baseURL}/models`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
return { models: res?.data.data };
|
||||
} catch (e) {
|
||||
return { code: 500, errMsg: e.message };
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Application } from '@nocobase/server';
|
||||
import { LLMProvider } from '../llm-providers/provider';
|
||||
|
||||
export type LLMProviderOptions = {
|
||||
title: string;
|
||||
provider: new (opts: { app: Application; serviceOptions?: any; chatOptions?: any }) => LLMProvider;
|
||||
};
|
||||
|
||||
export class AIManager {
|
||||
llmProviders = new Map<string, LLMProviderOptions>();
|
||||
|
||||
registerLLMProvider(name: string, options: LLMProviderOptions) {
|
||||
this.llmProviders.set(name, options);
|
||||
}
|
||||
|
||||
listLLMProviders() {
|
||||
const providers = this.llmProviders.entries();
|
||||
return Array.from(providers).map(([name, { title }]) => ({ name, title }));
|
||||
}
|
||||
}
|
52
packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts
Normal file
52
packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { AIManager } from './manager/ai-manager';
|
||||
import { openaiProviderOptions } from './llm-providers/openai';
|
||||
import { deepseekProviderOptions } from './llm-providers/deepseek';
|
||||
import aiResource from './resource/ai';
|
||||
import PluginWorkflowServer from '@nocobase/plugin-workflow';
|
||||
import { LLMInstruction } from './workflow/nodes/llm';
|
||||
|
||||
export class PluginAIServer extends Plugin {
|
||||
aiManager = new AIManager();
|
||||
|
||||
async afterAdd() {}
|
||||
|
||||
async beforeLoad() {}
|
||||
|
||||
async load() {
|
||||
this.aiManager.registerLLMProvider('openai', openaiProviderOptions);
|
||||
this.aiManager.registerLLMProvider('deepseek', deepseekProviderOptions);
|
||||
|
||||
this.app.resourceManager.define(aiResource);
|
||||
this.app.acl.registerSnippet({
|
||||
name: `pm.${this.name}.llm-services`,
|
||||
actions: ['ai:*', 'llmServices:*'],
|
||||
});
|
||||
const workflowSnippet = this.app.acl.snippetManager.snippets.get('pm.workflow.workflows');
|
||||
if (workflowSnippet) {
|
||||
workflowSnippet.actions.push('ai:listModels');
|
||||
}
|
||||
|
||||
const workflow = this.app.pm.get('workflow') as PluginWorkflowServer;
|
||||
workflow.registerInstruction('llm', LLMInstruction);
|
||||
}
|
||||
|
||||
async install() {}
|
||||
|
||||
async afterEnable() {}
|
||||
|
||||
async afterDisable() {}
|
||||
|
||||
async remove() {}
|
||||
}
|
||||
|
||||
export default PluginAIServer;
|
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { ResourceOptions } from '@nocobase/resourcer';
|
||||
import { PluginAIServer } from '../plugin';
|
||||
|
||||
const aiResource: ResourceOptions = {
|
||||
name: 'ai',
|
||||
actions: {
|
||||
listLLMProviders: async (ctx, next) => {
|
||||
const plugin = ctx.app.pm.get('ai') as PluginAIServer;
|
||||
ctx.body = plugin.aiManager.listLLMProviders();
|
||||
await next();
|
||||
},
|
||||
listModels: async (ctx, next) => {
|
||||
const { llmService } = ctx.action.params;
|
||||
const plugin = ctx.app.pm.get('ai') as PluginAIServer;
|
||||
const service = await ctx.db.getRepository('llmServices').findOne({
|
||||
filter: {
|
||||
name: llmService,
|
||||
},
|
||||
});
|
||||
if (!service) {
|
||||
ctx.throw(400, 'invalid llm service');
|
||||
}
|
||||
const providerOptions = plugin.aiManager.llmProviders.get(service.provider);
|
||||
if (!providerOptions) {
|
||||
ctx.throw(400, 'invalid llm provider');
|
||||
}
|
||||
const options = service.options;
|
||||
const Provider = providerOptions.provider;
|
||||
const provider = new Provider({
|
||||
app: ctx.app,
|
||||
serviceOptions: options,
|
||||
});
|
||||
const res = await provider.listModels();
|
||||
if (res.errMsg) {
|
||||
ctx.log.error(res.errMsg);
|
||||
ctx.throw(500, ctx.t('Get models list failed, you can enter a model name manually.'));
|
||||
}
|
||||
ctx.body = res.models || [];
|
||||
return next();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default aiResource;
|
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { FlowNodeModel, Instruction, JOB_STATUS, Processor } from '@nocobase/plugin-workflow';
|
||||
import PluginAIServer from '../../../plugin';
|
||||
import { LLMProvider } from '../../../llm-providers/provider';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class LLMInstruction extends Instruction {
|
||||
async getLLMProvider(llmService: string, chatOptions: any) {
|
||||
const service = await this.workflow.db.getRepository('llmServices').findOne({
|
||||
filter: {
|
||||
name: llmService,
|
||||
},
|
||||
});
|
||||
if (!service) {
|
||||
throw new Error('invalid llm service');
|
||||
}
|
||||
const plugin = this.workflow.app.pm.get('ai') as PluginAIServer;
|
||||
const providerOptions = plugin.aiManager.llmProviders.get(service.provider);
|
||||
if (!providerOptions) {
|
||||
throw new Error('invalid llm provider');
|
||||
}
|
||||
const Provider = providerOptions.provider;
|
||||
const provider = new Provider({ app: this.workflow.app, serviceOptions: service.options, chatOptions });
|
||||
return provider;
|
||||
}
|
||||
|
||||
async run(node: FlowNodeModel, input: any, processor: Processor) {
|
||||
const { llmService, ...chatOptions } = processor.getParsedValue(node.config, node.id);
|
||||
let provider: LLMProvider;
|
||||
try {
|
||||
provider = await this.getLLMProvider(llmService, chatOptions);
|
||||
} catch (e) {
|
||||
return {
|
||||
status: JOB_STATUS.ERROR,
|
||||
result: e.message,
|
||||
};
|
||||
}
|
||||
|
||||
const job = await processor.saveJob({
|
||||
status: JOB_STATUS.PENDING,
|
||||
nodeId: node.id,
|
||||
nodeKey: node.key,
|
||||
upstreamId: input?.id ?? null,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
provider
|
||||
.invokeChat()
|
||||
.then((aiMsg) => {
|
||||
let raw = aiMsg;
|
||||
if (aiMsg.raw) {
|
||||
raw = aiMsg.raw;
|
||||
}
|
||||
|
||||
job.set({
|
||||
status: JOB_STATUS.RESOLVED,
|
||||
result: {
|
||||
id: raw.id,
|
||||
content: raw.content,
|
||||
additionalKwargs: raw.additional_kwargs,
|
||||
responseMetadata: raw.response_metadata,
|
||||
toolCalls: raw.tool_calls,
|
||||
structuredContent: aiMsg.parsed,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
processor.logger.error(`llm invoke failed, ${e.message}`, {
|
||||
node: node.id,
|
||||
error: e,
|
||||
chatOptions: _.omit(chatOptions, 'messages'),
|
||||
});
|
||||
job.set({
|
||||
status: JOB_STATUS.ERROR,
|
||||
result: e.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setImmediate(() => {
|
||||
this.workflow.resume(job);
|
||||
});
|
||||
});
|
||||
|
||||
processor.logger.trace(`llm invoke, waiting for response...`, {
|
||||
node: node.id,
|
||||
});
|
||||
return processor.exit();
|
||||
}
|
||||
|
||||
resume(node: FlowNodeModel, job: any, processor: Processor) {
|
||||
const { ignoreFail } = node.config;
|
||||
if (ignoreFail) {
|
||||
job.set('status', JOB_STATUS.RESOLVED);
|
||||
}
|
||||
return job;
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@
|
||||
"@nocobase/plugin-audit-logs": "1.6.0-beta.16",
|
||||
"@nocobase/plugin-auth": "1.6.0-beta.16",
|
||||
"@nocobase/plugin-auth-sms": "1.6.0-beta.16",
|
||||
"@nocobase/plugin-ai": "1.6.0-beta.16",
|
||||
"@nocobase/plugin-backup-restore": "1.6.0-beta.16",
|
||||
"@nocobase/plugin-block-iframe": "1.6.0-beta.16",
|
||||
"@nocobase/plugin-block-workbench": "1.6.0-beta.16",
|
||||
@ -94,6 +95,7 @@
|
||||
"@nocobase/plugin-action-print",
|
||||
"@nocobase/plugin-auth",
|
||||
"@nocobase/plugin-async-task-manager",
|
||||
"@nocobase/plugin-ai",
|
||||
"@nocobase/plugin-block-iframe",
|
||||
"@nocobase/plugin-block-workbench",
|
||||
"@nocobase/plugin-calendar",
|
||||
|
Loading…
x
Reference in New Issue
Block a user