Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: chore: llm gen #269

Open
wants to merge 5 commits into
base: epic-vue
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"site:home": "cross-env NODE_ENV=production webpack --progress --config scripts/sites/webpack.prod.home.js",
"site:mobile": "cross-env NODE_ENV=production webpack --progress --config scripts/sites/webpack.prod.mobile.js",
"site:pc": "cross-env NODE_ENV=production webpack --progress --config scripts/sites/webpack.prod.pc.js",
"llm:gen": "node scripts/llm/gen.mjs",
"dev:demo": "node scripts/dev/dev-demo.js $@"
},
"author": "[email protected]",
Expand Down
82 changes: 82 additions & 0 deletions scripts/llm/code.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import _ from 'lodash';
import { statSync, readdirSync, readFileSync } from 'fs';
import { join } from 'path';

///// 辅助函数 /////
// 递归函数来遍历文件夹
function walkDir(dir, callback) {
readdirSync(dir).forEach(f => {
let dirPath = join(dir, f);
let isDirectory = statSync(dirPath).isDirectory();
isDirectory ? walkDir(dirPath, callback) : callback(join(dir, f));
});
}

// 源代码相关处理类,获取文件列表
class Code {
constructor(dir, fileReg) {
this._dir = dir;
this._fileReg = fileReg;
}
// 获取文件列表
fileList() {
const fileList = [];
walkDir(this._dir, filePath => {
if (filePath.match(this._fileReg)) {
fileList.push(filePath);
}
});
return fileList;
}

// 获取目录内容
directory() {
const fileList = this.fileList();

// 生成目录结构
const contents = [
`The source code directory is ${this._dir}, which contains the following files:`,
...fileList.map(i => '- ' + i),
'\n',
];

return contents.join(',');
}

// 获取目录内容和文件内容
directoryAndContent() {
const fileList = this.fileList();

// 生成目录结构
const contents = [
`The source code directory is ${this.fileList}, which contains the following files:`,
...fileList.map(i => '- ' + i),
'\n',
];

// 生成每个文件内容
for (const filePath of fileList) {
const fileContent = readFileSync(filePath, { encoding: 'utf-8' }).trim();
contents.push(`Code for ${filePath}: `, fileContent, '\n');
}
return contents.join('\n');
}
}

// React 源代码
export class ReactCode extends Code {
constructor(comp) {
const dir = `packages/arcodesign/components/${_.snakeCase(comp)}/`;
const fileReg = /.(ts|tsx|js|jsx|less)$/;
super(dir, fileReg);
}
}

// Vue 源代码
export class VueCode extends Code {
constructor(comp) {
const dir = `packages/arcodesign-vue/components/${_.snakeCase(comp)}/`;
const fileReg = /.(vue|ts|js|less)$/;
super(dir, fileReg);
}
}
178 changes: 178 additions & 0 deletions scripts/llm/coze.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import axios from 'axios';
import _ from 'lodash';
import { writeFileSync, appendFileSync } from 'fs';

///// 一些常量 /////
const CHAT_URL = 'https://bots.byteintl.net/open_api/v2/chat';

// 对话日志,基础类
class ChatLog {
constructor() {
// 初始化变量
this.chatHistory = [];
this.chatId = `${_.now()}${_.random(100000, 999999)}`;
this.logFiles = [
`./scripts/llm/log/realtime.log`,
`./scripts/llm/log/${new Date().toISOString()}.log`,
];

// 初始化文件
this.logFiles.forEach(i => {
writeFileSync(i, `${new Date().toISOString()} Chat Id:\n${this.chatId}\n\n`);
});
}

// 写入日志文件,内部方法
_append(content) {
this.logFiles.forEach(i => appendFileSync(i, content));
}
}

// 对话日志(非流式)
class ChatLogNonStream extends ChatLog {
// 处理查询日志
query(query) {
this._append(`${new Date().toISOString()} Query:\n${query} \n\n`);
this.chatHistory.push({ role: 'user', content_type: 'text', content: query });
}

// 处理响应日志(非流式),并返回 answer
response(data) {
this._append(`${new Date().toISOString()} `);
let answer = '';
data?.messages?.forEach(({ type, content }) => {
if (type === 'verbose') return;
if (type === 'answer') answer += content;
this._append(`${_.startCase(type)}:\n${content}\n\n`);
this.chatHistory.push({ role: 'assistant', content_type: 'text', type, content });
});
return answer;
}
}

// 对话日志(流式)
class ChatLogStream extends ChatLog {
// 处理查询日志
query(query) {
this._append(`${new Date().toISOString()} Query:\n${query} \n\n`);
this.chatHistory.push({ role: 'user', content_type: 'text', content: query });
}

// 开始写入响应日志(分块)
responseChunkStart() {
this._chunk = {};
this._append(`${new Date().toISOString()} Response:\n`);
}
// 写入响应日志(分块)
responseChunk(raw) {
const regex = /data:(\{.*?\})(?=\s|$)/g;
const matches = String(raw).match(regex);

for (const match of matches) {
const data = JSON.parse(match.replace('data:', ''));

const event = data?.event,
type = data?.message?.type,
content = data?.message?.content;

// 拼接数据
if (event === 'message' && content) {
if (!this._chunk?.[type]) this._chunk[type] = '';
this._chunk[type] += content;

// 处理 answer
if (type === 'answer') {
this._append(content);
}
}
}
}
// 结束响应(分块)
responseChunkEnd() {
this._append(`\n\n`);
_.forEach(this._chunk, (content, type) => {
if (type === 'verbose') return;
if (type !== 'answer') {
this._append(`${_.startCase(type)}:\n${content}\n\n`);
}
this.chatHistory.push({ role: 'assistant', content_type: 'text', type, content });
});
return this._chunk['answer'];
}
}

// AI 机器人
export class AiBotNonStream {
constructor(botId, token) {
this.log = new ChatLogNonStream();
this.botId = botId;
this.token = token;
}

// 非流式对话
async chat(query, withHistory = true) {
query = query.trim();

const headers = { Authorization: `Bearer ${this.token}` };
const body = {
conversation_id: this.log.chatId,
bot_id: this.botId,
user: 'Aex',
query,
chat_history: withHistory ? this.log.chatHistory : [],
// stream: true,
};
this.log.query(query);

// 开始请求
const res = await axios.post(CHAT_URL, body, { headers });
const answer = this.log.response(res.data);

return answer;
}
}

export class AiBot {
constructor(botId, token) {
this.log = new ChatLogStream();
this.botId = botId;
this.token = token;
}

// 流式对话
async chat(query, withHistory = true) {
query = query.trim();

const headers = { Authorization: `Bearer ${this.token}` };
const body = {
conversation_id: this.log.chatId,
bot_id: this.botId,
user: 'Aex',
query,
chat_history: withHistory ? this.log.chatHistory : [],
stream: true, // 将请求变为流式
};
this.log.query(query);

// 开始请求 - 注意这里使用流式处理方式
const res = await axios.post(CHAT_URL, body, { headers, responseType: 'stream' });

this.log.responseChunkStart();

// 设置一个数组来收集流式数据片段
res.data.on('data', chunk => {
this.log.responseChunk(chunk.toString('utf-8'));
});

return new Promise((resolve, reject) => {
res.data.on('end', () => {
const data = this.log.responseChunkEnd();
resolve(data);
});

res.data.on('error', err => {
reject(err);
});
});
}
}
20 changes: 20 additions & 0 deletions scripts/llm/gen-article.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AiBotNonStream } from './coze.mjs';

// const BOT_ID = '7372403809576009729'; // Claude 3 Opus 模型
// const BOT_ID = '7369473402367361041'; // GPT-4 Turbo 模型
const BOT_ID = '7373886057160753169'; // GPT-4o 模型

const TOKEN = process.env.COZE_TOKEN_ADM;

const prompt_article = `
今天又是努力工作的一天,帮我写一篇今天的日记,100 字左右

`;

///// 主流程开始 /////

const bot = new AiBotNonStream(BOT_ID, TOKEN);
// 主指令
const answer = await bot.chat(prompt_article);

console.log(answer);
30 changes: 30 additions & 0 deletions scripts/llm/gen-game.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { AiBot } from './coze.mjs';

const BOT_ID = '7372403809576009729'; // Claude 3 Opus 模型
// const BOT_ID = '7369473402367361041'; // GPT-4 Turbo 模型
// const BOT_ID = '7373886057160753169'; // GPT-4o 模型
const TOKEN = process.env.COZE_TOKEN_ADM;

// 一个游戏,测试 AI 上下文能力
const prompt_game = `
现在我们玩一个游戏:

当我发送一个数字,你应该将这个数字+1,并将结果发给我。
当我发送 "Next",你应该继续+1,并将结果发给我。
当再次接收到数字后,游戏重新开始。

严格按照游戏规则回复,不要回复其他内容。

首先我给你一个数字是 1

`;

///// 主流程开始 /////

const bot = new AiBot(BOT_ID, TOKEN);
// 主指令
await bot.chat(prompt_game);
await bot.chat('Next');
await bot.chat('Next');
await bot.chat('Next');
await bot.chat('Next');
66 changes: 66 additions & 0 deletions scripts/llm/gen.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { writeFileSync } from 'fs';
import { ReactCode, VueCode } from './code.mjs';
import { AiBot } from './coze.mjs';

// const BOT_ID = '7372403809576009729'; // Claude 3 Opus 模型
// const BOT_ID = '7369473402367361041'; // GPT-4 Turbo 模型
const BOT_ID = '7373886057160753169'; // GPT-4o 模型

const TOKEN = process.env.COZE_TOKEN_ADM;

// 指令一,以下指令为示例代码
const prompt1 = `

# 角色

你是一个优秀的前端工程师,熟悉 React 和 Vue 框架,你的主要工作是将 Arco Design Mobile 已有的 React 组件改写为 Vue 组件。

接下来我为你提供 Arco Design Mobile 某些组件的源代码,你需要学习这些源代码的目录结构、代码风格和命名习惯。

第一个是 Notify 组件,下面是 React 版本的源代码
${new ReactCode('notify').directoryAndContent()}
将其改写为 Vue 版本后的代码如下
${new VueCode('notify').directoryAndContent()}

第二个是 Cell 组件,下面是 React 版本的源代码
${new ReactCode('cell').directoryAndContent()}
将其改写为 Vue 版本后的代码如下
${new VueCode('cell').directoryAndContent()}

第三个是 Loading 组件,这个组件只有 React 版本的源代码
${new ReactCode('loading').directoryAndContent()}

根据以上的代码,开始你的工作:你需要严格参考 Cell 组件和 Notify 组件两个版本源代码的目录结构、命名习惯,将 Loading 的 React 组件改写为 Vue 组件。

我帮你完成了 Loading 组件 Vue 版本的目录结构,具体如下
${new VueCode('loading').directory()}

在进行改写的时候,你需要严格遵守并满足以下要求:
- 组件的功能和表现应与 React 代码完全一致
- 需要将 React 的 JSX/TSX 转换为 Vue 文件要使用模版语法
- 生成的代码首行需要添加注释 "Note: Generated by AI, needs verification"
- 生成的代码需要添加中英文双语注释
- 源代码要完整清晰,关键部分不要做任何的省略
- 代码的格式和内容要符合业界最佳实践,特别要注意 TS 的一些类型

当你准备好了,请回复 Ready。
接着我会引导你一步一步完成代码编写,每当我发送文件路径时,你需要直接回复该文件的代码,回复的格式满足以下要求:
- 回复的内容必须是代码,如果有额外的补充说明或解析,需要使用注释的方式
- 除了源代码和注释本身,不要回复任何其他无关的内容
`;

///// 主流程开始 /////

const bot = new AiBot(BOT_ID, TOKEN);
// 主指令
await bot.chat(prompt1);

// 单文件代码生成
for (const filePath of new VueCode('loading').fileList()) {
console.log('即将生成文件代码:', filePath);
const content = await bot.chat(filePath);

// 匹配代码部分并写入文件
const match = content.match(/```(?:\w*\n)?([\s\S]*?)```/);
writeFileSync(filePath, match?.[1] ?? content);
}
1 change: 1 addition & 0 deletions scripts/llm/log/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.log
Loading