TypeChat实战,实现记账功能

2,630 阅读5分钟

背景

前段时间在掘金看了一篇关于typechat的文章,觉得这玩意挺有意思的,就写了个demo玩了玩,这里给大家分享一下。

什么是 TypeChat

TypeChat 入门指南

这篇文章已经解释了什么是TypeChat,我这里就不介绍了,主要是实战。

需求分析

一般的记账小程序或app,基本都是需要自己一项一项输入,比如日期、金额、消费类型、备注等。这样录入感觉有点麻烦,快捷的录入方式是,输入一段文本,系统自动解析里面的关键字,生成对应模型数据。

举个例子

用户输入:昨天买了个西瓜,花了20元。

期望解析出来的数据结构是这样的

{
  date: '2023-08-24',
  amount: 20,
  name: '西瓜'
}

自己写代码去解析难度太大了,用户输入的格式什么的你想不到有哪些,用chatgpt去分析,返回的数据格式有可能不是自己想要的,TypeChat就是干这个的。分析用户的输入,返回固定的数据格式。

实战

初始化一个后端midway项目

npm init midway

安装TypeChat依赖

pnpm i typechat --save

安装dotenv依赖

pnpm i dotenv --save

在项目根目录下建.env文件,配置openai key

OPENAI_MODEL=gpt-3.5-turbo
OPENAI_API_KEY=openai key

src/configuration.ts加载.env中的值到环境变量中

image.png

定义schema

// src/schema/demo.ts
export type Demo = {
  date: string;
  name: string;
  amount?: number;
};

改造home.controller文件

// 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;
    }
  }
}

启动项目,测试

npm run dev

访问 http://127.0.0.1:7001/

测试结果

image.png

返回的数据不是我们想要的,这是因为typechat不知道字段的含义,没办法给你解析。

改造schema

可以通过给字段加注释让typechat知道你的模型是用来干啥的

export type Demo = {
  // 消费日期,输出YYYY-MM-DD格式
  date: string;
  // 消费物品名称
  name: string;
  // 消费金额
  amount?: number;
};

image.png

解决日期问题

现在的数据格式就是我们想要的,但是日期有点问题,我明明输入的时候昨天,返回的却是2022-01-20,这个很奇怪,我猜测可能openai,没办法获取到当前日期,所以随便找了个日期。

想解决这个问题也简单,我们把当前日期注入进去就行了。注释里不能写函数,怎么注入当前日期呢,不知道大家有没有发现,上面创建schema的方法是读取demo.ts这个文件的内容当参数的,那我们在demo.ts写一个占位符,读取后用当前日期给替换掉就行了。

export type Demo = {
  // 消费日期,输出YYYY-MM-DD格式,举个例子:如果输入今天,输出今天的日期,今天日期为{today}
  date: string;
  // 消费物品名称
  name: string;
  // 消费金额
  amount?: number;
};

这里只需要告诉他今天日期就行了,他会自动推算昨天前天,甚至上周五的日期。

image.png

用当前日期给{today}占位符给替换掉

image.png

现在日期就对了

image.png

image.png

提高难度也能正常识别,甚至中文的一百都给你转成数字了。

加大难度

如果不输入日期表示当前日期,怎么做呢,在注释中加个默认值就行了。

image.png

image.png

image.png

提示

node版本不能低于18.0.0,不然项目会报错,因为typechat里面用了node高版本的api。

做个网页

基于上面功能,我们来实现一个小功能。

用户输入一段内容,自动把数据插入到数据库中,并以表格的形式展示出来。

引入typeorm操作数据库表

具体请参考官方文档,www.midwayjs.org/docs/extens…

创建entity

// 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

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

// 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展示

demo1.gif

demo3.gif

总结

感觉这种交互方式,更适合语音输入,后面有时间做一个记账app,对接一下语音识别,就不用手动输入内容那么麻烦了。