背景
前段时间在掘金看了一篇关于typechat的文章,觉得这玩意挺有意思的,就写了个demo玩了玩,这里给大家分享一下。
什么是 TypeChat
这篇文章已经解释了什么是TypeChat,我这里就不介绍了,主要是实战。
需求分析
一般的记账小程序或app,基本都是需要自己一项一项输入,比如日期、金额、消费类型、备注等。这样录入感觉有点麻烦,快捷的录入方式是,输入一段文本,系统自动解析里面的关键字,生成对应模型数据。
举个例子
用户输入:昨天买了个西瓜,花了20元。
期望解析出来的数据结构是这样的
json复制代码{
date: '2023-08-24',
amount: 20,
name: '西瓜'
}
自己写代码去解析难度太大了,用户输入的格式什么的你想不到有哪些,用chatgpt去分析,返回的数据格式有可能不是自己想要的,TypeChat就是干这个的。分析用户的输入,返回固定的数据格式。
实战
初始化一个后端midway项目
sh复制代码npm init midway
安装TypeChat依赖
sh复制代码pnpm i typechat --save
安装dotenv依赖
sh复制代码pnpm i dotenv --save
在项目根目录下建.env文件,配置openai key
env复制代码OPENAI_MODEL=gpt-3.5-turbo OPENAI_API_KEY=openai key
src/configuration.ts加载.env中的值到环境变量中
定义schema
ts复制代码// src/schema/demo.ts
export type Demo = {
date: string;
name: string;
amount?: number;
};
改造home.controller文件
ts复制代码// src/controller/home.controller.ts
import { Controller, Get } from '@midwayjs/core';
import { readFileSync } from 'fs';
import { join } from 'path';
import { createLanguageModel, createJsonTranslator } from 'typechat';
import { Demo } from '../schema/demo';
@Controller('/')
export class APIController {
@Get('/')
async home() {
// 从环境变量里创建模型
const model = createLanguageModel(process.env);
// 读取我们前面定义的schema
const schema = readFileSync(join(__dirname, '../schema/demo.ts'), 'utf8');
// 创建转换器
const translator = createJsonTranslator<Demo>(model, schema, 'Demo');
// 解析输入的内容
const response = await translator.translate('昨天我买了西瓜,花了100元。');
if (response.success) {
return response.data;
}
}
}
启动项目,测试
sh复制代码npm run dev
测试结果
返回的数据不是我们想要的,这是因为typechat不知道字段的含义,没办法给你解析。
改造schema
可以通过给字段加注释让typechat知道你的模型是用来干啥的
ts复制代码export type Demo = {
// 消费日期,输出YYYY-MM-DD格式
date: string;
// 消费物品名称
name: string;
// 消费金额
amount?: number;
};
解决日期问题
现在的数据格式就是我们想要的,但是日期有点问题,我明明输入的时候昨天,返回的却是2022-01-20
,这个很奇怪,我猜测可能openai,没办法获取到当前日期,所以随便找了个日期。
想解决这个问题也简单,我们把当前日期注入进去就行了。注释里不能写函数,怎么注入当前日期呢,不知道大家有没有发现,上面创建schema
的方法是读取demo.ts
这个文件的内容当参数的,那我们在demo.ts
写一个占位符,读取后用当前日期给替换掉就行了。
ts复制代码export type Demo = {
// 消费日期,输出YYYY-MM-DD格式,举个例子:如果输入今天,输出今天的日期,今天日期为{today}
date: string;
// 消费物品名称
name: string;
// 消费金额
amount?: number;
};
这里只需要告诉他今天日期就行了,他会自动推算昨天前天,甚至上周五的日期。
用当前日期给{today}占位符给替换掉
现在日期就对了
提高难度也能正常识别,甚至中文的一百都给你转成数字了。
加大难度
如果不输入日期表示当前日期,怎么做呢,在注释中加个默认值就行了。
提示
node版本不能低于18.0.0,不然项目会报错,因为typechat里面用了node高版本的api。
做个网页
基于上面功能,我们来实现一个小功能。
用户输入一段内容,自动把数据插入到数据库中,并以表格的形式展示出来。
引入typeorm操作数据库表
具体请参考官方文档,www.midwayjs.org/docs/extens…
创建entity
ts复制代码// src/entity/accounting.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Accounting {
@PrimaryGeneratedColumn()
id: number;
@Column({ comment: '物品名称', nullable: true })
name: string;
@Column({ comment: '消费日期', nullable: true })
date: string;
@Column({ comment: '金额', nullable: true })
amount: number;
}
改造controller
ts复制代码import { Body, Controller, Get, Post } from '@midwayjs/core';
import { readFileSync } from 'fs';
import { join } from 'path';
import { createLanguageModel, createJsonTranslator } from 'typechat';
import { Demo } from '../schema/demo';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Accounting } from '../entity/accounting';
import { Repository } from 'typeorm';
@Controller('/')
export class APIController {
@InjectEntityModel(Accounting)
accountingModel: Repository<Accounting>;
@Post('/')
async home(@Body() data: { text: string }) {
// 从环境变量里创建模型
const model = createLanguageModel(process.env);
// 读取我们前面定义的schema
const schema = readFileSync(
join(__dirname, '../schema/demo.ts'),
'utf8'
).replace(/{today}/g, new Date().toDateString());
// 创建转换器
const translator = createJsonTranslator<Demo>(model, schema, 'Demo');
// 解析输入的内容
const response = await translator.translate(data.text);
if (response.success) {
const accounting = new Accounting();
accounting.amount = response.data.amount;
accounting.date = response.data.date;
accounting.name = response.data.name;
// 保存到数据库
await this.accountingModel.save(accounting);
}
}
@Get('/')
async list() {
// 查询列表,倒序返回
return await this.accountingModel.find({ order: { id: 'DESC' } });
}
}
前端项目
脚手架用的是vite,组件库用的是antd
tsx复制代码// src/App.tsx
import { Button, Table, Input, Space } from 'antd'
import { useEffect, useMemo, useState, } from 'react'
function App() {
const columns = useMemo(() => [
{ dataIndex: 'date', title: '日期' },
{ dataIndex: 'amount', title: '金额' },
{ dataIndex: 'name', title: '备注' },
], []);
const [loading, setLoading] = useState(true);
const [text, setText] = useState('');
const [dataSource, setDataSource] = useState([]);
const [translating, setTranslating] = useState(false);
function getTableData() {
setLoading(true);
window.fetch('/api', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
})
.then(res => res.json())
.then(data => {
setDataSource(data);
setLoading(false);
});
}
useEffect(() => {
getTableData();
}, []);
function translate() {
setTranslating(true);
window.fetch('/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text,
}),
}).then(() => {
getTableData();
setTranslating(false);
setText('');
})
}
return (
<div className='p-[20px]'>
<Table scroll={{ y: 500 }} pagination={false} rowKey="id" loading={loading} dataSource={dataSource} columns={columns} />
<Space className='w-[100%] mt-[20px]'>
<Input onPressEnter={translate} value={text} onChange={e => { setText(e.target.value) }} className='w-[800px]' />
<Button type='primary' loading={translating} onClick={translate}>确定</Button>
</Space>
</div>
)
}
export default App
demo展示
总结
感觉这种交互方式,更适合语音输入,后面有时间做一个记账app,对接一下语音识别,就不用手动输入内容那么麻烦了。