feat: implement AI agent for flowme-life interactions
- Add agent-run module to handle AI interactions with tools and messages. - Create routes for proxying requests to OpenAI and Anthropic APIs. - Implement flowme-life chat route for user queries and task management. - Add services for retrieving and updating life records in the database. - Implement logic for fetching today's tasks and marking tasks as done with next execution time calculation. - Introduce tests for flowme-life functionalities.
This commit is contained in:
44
src/routes/flowme-life/chat.ts
Normal file
44
src/routes/flowme-life/chat.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
import { schema, app, cnb, models } from '@/app.ts'
|
||||
import z from 'zod';
|
||||
import { runAgent } from '@kevisual/ai/agent'
|
||||
|
||||
app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'chat',
|
||||
description: `聊天接口, 对自己的数据进行操作,参数是 question或messages,question是用户的提问,messages是对话消息列表,优先级高于 question`,
|
||||
middleware: ['auth']
|
||||
, metadata: {
|
||||
args: {
|
||||
question: z.string().describe('用户的提问'),
|
||||
messages: z.any().optional().describe('对话消息列表,优先级高于 question'),
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const question = ctx.query.question || '';
|
||||
const _messages = ctx.query.messages;
|
||||
const token = ctx.query.token || '';
|
||||
if (!question && !_messages) {
|
||||
ctx.throw(400, '缺少参数 question 或 messages');
|
||||
}
|
||||
const routes = ctx.app.getList().filter(r => r.path.startsWith('flowme-life') && r.key !== 'chat');
|
||||
const messages = _messages || [
|
||||
{
|
||||
"role": "system" as const,
|
||||
"content": `你是我的智能助手,协助我操作我的数据, 请根据我的提问选择合适的接口进行调用。`
|
||||
},
|
||||
{
|
||||
"role": "user" as const,
|
||||
"content": question
|
||||
}
|
||||
]
|
||||
const res = await runAgent({
|
||||
app: app,
|
||||
messages: messages,
|
||||
languageModel: cnb(models['auto']),
|
||||
// query: 'WHERE path LIKE 'flowme-life%',
|
||||
routes,
|
||||
token,
|
||||
});
|
||||
ctx.body = res
|
||||
}).addTo(app);
|
||||
@@ -1 +1,3 @@
|
||||
import './list.ts'
|
||||
import './today.ts'
|
||||
import './chat.ts'
|
||||
38
src/routes/flowme-life/life.services.ts
Normal file
38
src/routes/flowme-life/life.services.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { schema, db } from '@/app.ts';
|
||||
|
||||
export type LifeItem = typeof schema.life.$inferSelect;
|
||||
|
||||
/**
|
||||
* 根据 id 获取 life 记录
|
||||
*/
|
||||
export async function getLifeItem(id: string): Promise<{ code: number; data?: LifeItem; message?: string }> {
|
||||
try {
|
||||
const result = await db.select().from(schema.life).where(eq(schema.life.id, id)).limit(1);
|
||||
if (result.length === 0) {
|
||||
return { code: 404, message: `记录 ${id} 不存在` };
|
||||
}
|
||||
return { code: 200, data: result[0] };
|
||||
} catch (e) {
|
||||
return { code: 500, message: `获取记录 ${id} 失败: ${e?.message || e}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 life 记录的 effectiveAt(下次执行时间)
|
||||
*/
|
||||
export async function updateLifeEffectiveAt(id: string, effectiveAt: string | Date): Promise<{ code: number; data?: LifeItem; message?: string }> {
|
||||
try {
|
||||
const result = await db
|
||||
.update(schema.life)
|
||||
.set({ effectiveAt: new Date(effectiveAt) })
|
||||
.where(eq(schema.life.id, id))
|
||||
.returning();
|
||||
if (result.length === 0) {
|
||||
return { code: 404, message: `记录 ${id} 不存在` };
|
||||
}
|
||||
return { code: 200, data: result[0] };
|
||||
} catch (e) {
|
||||
return { code: 500, message: `更新记录 ${id} 失败: ${e?.message || e}` };
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { desc, eq, count, or, like, and } from 'drizzle-orm';
|
||||
import { schema, app, db } from '@/app.ts'
|
||||
import z from 'zod';
|
||||
import dayjs from 'dayjs';
|
||||
app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'list',
|
||||
@@ -72,7 +73,7 @@ app.route({
|
||||
link: z.string().describe('链接').optional(),
|
||||
data: z.record(z.string(), z.any()).describe('数据').optional(),
|
||||
effectiveAt: z.string().describe('生效日期').optional(),
|
||||
type: z.string().describe('类型').optional(),
|
||||
type: z.string().describe('类型: 智能, 每年农历, 备忘, 归档等.默认智能').optional(),
|
||||
prompt: z.string().describe('提示词').optional(),
|
||||
taskType: z.string().describe('任务类型').optional(),
|
||||
taskResult: z.record(z.string(), z.any()).describe('任务结果').optional(),
|
||||
@@ -82,6 +83,11 @@ app.route({
|
||||
}).define(async (ctx) => {
|
||||
const { uid, updatedAt, createdAt, ...rest } = ctx.query.data || {};
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
if (rest.effectiveAt && isNaN(Date.parse(rest.effectiveAt))) {
|
||||
rest.effectiveAt = null;
|
||||
} else if (rest.effectiveAt) {
|
||||
rest.effectiveAt = dayjs(rest.effectiveAt).toISOString();
|
||||
}
|
||||
const lifeItem = await db.insert(schema.life).values({
|
||||
title: rest.title || '',
|
||||
summary: rest.summary || '',
|
||||
@@ -90,7 +96,7 @@ app.route({
|
||||
link: rest.link || '',
|
||||
data: rest.data || {},
|
||||
effectiveAt: rest.effectiveAt || '',
|
||||
type: rest.type || '',
|
||||
type: rest.type || '智能',
|
||||
prompt: rest.prompt || '',
|
||||
taskType: rest.taskType || '',
|
||||
taskResult: rest.taskResult || {},
|
||||
@@ -103,7 +109,7 @@ app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'update',
|
||||
middleware: ['auth'],
|
||||
description: '更新一个 flowme-life',
|
||||
description: '更新一个 flowme-life 的数据',
|
||||
metadata: {
|
||||
args: {
|
||||
data: z.object({
|
||||
@@ -135,6 +141,11 @@ app.route({
|
||||
if (existing[0].uid !== tokenUser.id) {
|
||||
ctx.throw(403, '没有权限更新该 flowme-life');
|
||||
}
|
||||
if (rest.effectiveAt && isNaN(Date.parse(rest.effectiveAt))) {
|
||||
rest.effectiveAt = null;
|
||||
} else if (rest.effectiveAt) {
|
||||
rest.effectiveAt = dayjs(rest.effectiveAt).toISOString();
|
||||
}
|
||||
const lifeItem = await db.update(schema.life).set({
|
||||
title: rest.title,
|
||||
summary: rest.summary,
|
||||
@@ -155,17 +166,15 @@ app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'delete',
|
||||
middleware: ['auth'],
|
||||
description: '删除 flowme-life',
|
||||
description: '删除单个 flowme-life, 参数: id 必填',
|
||||
metadata: {
|
||||
args: {
|
||||
data: z.object({
|
||||
id: z.string().describe('ID'),
|
||||
})
|
||||
id: z.string().describe('ID'),
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id } = ctx.query.data || {};
|
||||
const { id } = ctx.query || {};
|
||||
if (!id) {
|
||||
ctx.throw(400, 'id 参数缺失');
|
||||
}
|
||||
@@ -184,10 +193,15 @@ app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'get',
|
||||
middleware: ['auth'],
|
||||
description: '获取单个 flowme-life, 参数: data.id 必填',
|
||||
description: '获取单个 flowme-life, 参数: id 必填',
|
||||
metadata: {
|
||||
args: {
|
||||
id: z.string().describe('ID'),
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id } = ctx.query.data || {};
|
||||
const { id } = ctx.query || {};
|
||||
if (!id) {
|
||||
ctx.throw(400, 'id 参数缺失');
|
||||
}
|
||||
|
||||
174
src/routes/flowme-life/today.ts
Normal file
174
src/routes/flowme-life/today.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { desc, eq, count, like, and, lt } from 'drizzle-orm';
|
||||
import { schema, app, db } from '@/app.ts'
|
||||
import z from 'zod';
|
||||
import dayjs from 'dayjs';
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { createLunarDate, toGregorian } from 'lunar';
|
||||
import { getLifeItem, updateLifeEffectiveAt } from './life.services.ts';
|
||||
app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'today',
|
||||
description: `获取今天需要做的事情列表`,
|
||||
middleware: ['auth'],
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const uid = tokenUser.id;
|
||||
|
||||
const tomorrow = dayjs().add(1, 'day').startOf('day').toDate();
|
||||
|
||||
let whereCondition = eq(schema.life.uid, uid);
|
||||
whereCondition = and(
|
||||
eq(schema.life.uid, uid),
|
||||
eq(schema.life.taskType, '运行中'),
|
||||
lt(schema.life.effectiveAt, tomorrow)
|
||||
);
|
||||
|
||||
const list = await db.select()
|
||||
.from(schema.life)
|
||||
.where(whereCondition)
|
||||
.orderBy(desc(schema.life.effectiveAt));
|
||||
|
||||
console.log('today res', list.map(i => i['title']));
|
||||
if (list.length > 0) {
|
||||
ctx.body = {
|
||||
list,
|
||||
content: list.map(item => {
|
||||
return `任务id:[${item['id']}]\n标题: ${item['title']}。\n启动时间: ${dayjs(item['effectiveAt']).format('YYYY-MM-DD HH:mm:ss')}。标签: ${item['tags'] || '无'} \n总结: ${item['summary'] || '无'}`;
|
||||
}).join('\n')
|
||||
};
|
||||
} else {
|
||||
ctx.body = {
|
||||
list,
|
||||
content: '今天没有需要做的事情了,休息一下吧'
|
||||
}
|
||||
}
|
||||
}).addTo(app);
|
||||
|
||||
|
||||
app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'done',
|
||||
description: `完成某件事情,然后判断下一次运行时间。参数是id(string),数据类型是string。如果多个存在,则是ids的string数组`,
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
args: {
|
||||
id: z.string().optional().describe('记录id'),
|
||||
ids: z.array(z.string()).optional().describe('记录id数组'),
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const id = ctx.query.id;
|
||||
const ids: string[] = ctx.query.ids || [];
|
||||
if (!id && ids.length === 0) {
|
||||
ctx.throw(400, '缺少参数 id');
|
||||
}
|
||||
if (ids.length === 0 && id) {
|
||||
ids.push(String(id));
|
||||
}
|
||||
console.log('id', id, ids);
|
||||
const messages = [];
|
||||
const changeItem = async (id: string) => {
|
||||
// 获取记录详情
|
||||
const recordRes = await getLifeItem(id);
|
||||
if (recordRes.code !== 200) {
|
||||
messages.push({
|
||||
id,
|
||||
content: `获取记录 ${id} 详情失败`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const record = recordRes.data;
|
||||
|
||||
// 检查启动时间是否大于今天
|
||||
const startTime = record.effectiveAt;
|
||||
const today = dayjs().startOf('day');
|
||||
const startDate = dayjs(startTime).startOf('day');
|
||||
|
||||
if (startDate.isAfter(today)) {
|
||||
messages.push({
|
||||
id,
|
||||
content: `记录 ${id} 的启动时间是 ${dayjs(startTime).format('YYYY-MM-DD HH:mm:ss')},还没到今天呢,到时候再做吧`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 计算下一次运行时间
|
||||
// 1. 知道当前时间
|
||||
// 2. 知道任务类型,如果是每日,则加一天;如果是每周,则加七天;如果是每月,则加一个月,如果是每年农历,需要转为新的,如果是其他,需要智能判断
|
||||
// 3. 更新记录
|
||||
const strTime = (time: string | Date) => {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
const currentTime = strTime(new Date().toISOString());
|
||||
const title = record.title || '无标题';
|
||||
const isLuar = record.type?.includes?.('农历') || title.includes('农历');
|
||||
let summay = record.summary || '无';
|
||||
if (summay.length > 200) {
|
||||
summay = summay.substring(0, 200) + '...';
|
||||
}
|
||||
const prompt = record.prompt || '';
|
||||
const type = record.type || '';
|
||||
const content = `上一次执行的时间是${strTime(startTime)},当前时间是${currentTime},请帮我计算下一次的运行时间,如果时间不存在,默认在8点启动。
|
||||
${prompt ? `这是我给你的提示词,帮你更好地理解我的需求:${prompt}` : ''}
|
||||
|
||||
相关资料是
|
||||
任务:${record.title}
|
||||
总结:${summay}
|
||||
类型: ${type}
|
||||
`
|
||||
const ai = useContextKey('ai');
|
||||
await ai.chat([
|
||||
{ role: 'system', content: `你是一个时间计算专家,擅长根据任务类型和时间计算下一次运行时间。只返回我对应的日期的结果,格式是:YYYY-MM-DD HH:mm:ss。如果类型是每日,则加一天;如果是每周,则加七天;如果是每月,则加一个月,如果是每年农历,需要转为新的,如果是其他,需要智能判断` },
|
||||
{ role: 'user', content }
|
||||
])
|
||||
let nextTime = ai.responseText?.trim();
|
||||
try {
|
||||
// 判断返回的时间是否可以格式化
|
||||
if (nextTime && dayjs(nextTime).isValid()) {
|
||||
const time = dayjs(nextTime);
|
||||
if (isLuar) {
|
||||
const festival = createLunarDate({ year: time.year(), month: time.month() + 1, day: time.date() });
|
||||
const { date } = toGregorian(festival);
|
||||
nextTime = dayjs(date).toISOString();
|
||||
} else {
|
||||
nextTime = time.toISOString();
|
||||
}
|
||||
} else {
|
||||
messages.push({
|
||||
id,
|
||||
content: `记录 ${id} 的任务 "${record.title}",AI 返回的时间格式无效,无法格式化,返回内容是:${ai.responseText}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
messages.push({
|
||||
id,
|
||||
content: `记录 ${id} 的任务 "${record.title}",AI 返回结果解析失败,返回内容是:${ai.responseText}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const update = await updateLifeEffectiveAt(id, nextTime);
|
||||
if (update.code !== 200) {
|
||||
messages.push({
|
||||
id,
|
||||
content: `记录 ${id} 的任务 "${record.title}",更新记录失败`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const msg = {
|
||||
id,
|
||||
nextTime,
|
||||
showCNTime: dayjs(nextTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
content: `任务 "${record.title}" 已标记为完成。下一次运行时间是 ${dayjs(nextTime).format('YYYY-MM-DD HH:mm:ss')}`
|
||||
};
|
||||
messages.push(msg);
|
||||
}
|
||||
|
||||
for (const _id of ids) {
|
||||
await changeItem(String(_id));
|
||||
}
|
||||
ctx.body = {
|
||||
content: messages.map(m => m.content).join('\n'),
|
||||
list: messages
|
||||
};
|
||||
|
||||
}).addTo(app);
|
||||
Reference in New Issue
Block a user