This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import type { ToolDef } from '../../llm/openai-compat.js';
|
||||
import type { ToolContext, ToolResult } from './core.js';
|
||||
import type { Repository } from '../../db/repository.js';
|
||||
|
||||
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const TIME_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/;
|
||||
const MAX_TITLE_LEN = 200;
|
||||
const MAX_DESC_LEN = 4000;
|
||||
|
||||
let _repo: Repository | null = null;
|
||||
|
||||
export function setCalendarRepo(repo: Repository | null): void {
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
const ADD_CALENDAR_EVENT_DEF: ToolDef = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'AddCalendarEvent',
|
||||
description:
|
||||
'実行中タスクのスペースのカレンダーに予定を1件追加する(created_by=agent)。詳細は ReadToolDoc({ name: "AddCalendarEvent" }) で取得可能。',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: '予定のタイトル(必須)' },
|
||||
date: { type: 'string', description: '開始日 YYYY-MM-DD(ローカル日付、必須)' },
|
||||
end_date: { type: 'string', description: '終了日 YYYY-MM-DD。複数日にまたがる予定のとき指定。省略すると単日' },
|
||||
time: { type: 'string', description: 'HH:MM。省略すると終日扱い' },
|
||||
description: { type: 'string', description: '予定の補足説明(任意)' },
|
||||
},
|
||||
required: ['title', 'date'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const LIST_CALENDAR_EVENTS_DEF: ToolDef = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'ListCalendarEvents',
|
||||
description:
|
||||
'実行中タスクのスペースのカレンダー予定を日付範囲で一覧する。詳細は ReadToolDoc({ name: "AddCalendarEvent" }) で取得可能。',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string', description: '範囲開始 YYYY-MM-DD(任意)' },
|
||||
to: { type: 'string', description: '範囲終了 YYYY-MM-DD(任意)' },
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TOOL_DEFS: Record<string, ToolDef> = {
|
||||
AddCalendarEvent: ADD_CALENDAR_EVENT_DEF,
|
||||
ListCalendarEvents: LIST_CALENDAR_EVENTS_DEF,
|
||||
};
|
||||
|
||||
export async function executeTool(
|
||||
name: string,
|
||||
input: Record<string, unknown>,
|
||||
ctx: ToolContext,
|
||||
): Promise<ToolResult | null> {
|
||||
if (name === 'AddCalendarEvent') return executeAddCalendarEvent(input, ctx);
|
||||
if (name === 'ListCalendarEvents') return executeListCalendarEvents(input, ctx);
|
||||
return null;
|
||||
}
|
||||
|
||||
async function executeAddCalendarEvent(
|
||||
input: Record<string, unknown>,
|
||||
ctx: ToolContext,
|
||||
): Promise<ToolResult> {
|
||||
if (!_repo) {
|
||||
return { output: 'Calendar repo is not initialized', isError: true };
|
||||
}
|
||||
const spaceId = ctx.spaceId;
|
||||
if (!spaceId) {
|
||||
return {
|
||||
output:
|
||||
'このタスクはスペースに属していないため予定を追加できません(旧 local タスクには spaceId がありません)',
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
const title = input['title'];
|
||||
const date = input['date'];
|
||||
const endDateInput = input['end_date'];
|
||||
const time = input['time'];
|
||||
const description = input['description'];
|
||||
|
||||
if (typeof title !== 'string' || !title.trim()) {
|
||||
return { output: 'title は必須です', isError: true };
|
||||
}
|
||||
if (title.length > MAX_TITLE_LEN) {
|
||||
return { output: `title が長すぎます(最大 ${MAX_TITLE_LEN} 文字)`, isError: true };
|
||||
}
|
||||
if (typeof date !== 'string' || !DATE_PATTERN.test(date)) {
|
||||
return { output: 'date は YYYY-MM-DD 形式で指定してください', isError: true };
|
||||
}
|
||||
let endDate: string | null = null;
|
||||
if (endDateInput !== undefined && endDateInput !== null && endDateInput !== '') {
|
||||
if (typeof endDateInput !== 'string' || !DATE_PATTERN.test(endDateInput)) {
|
||||
return { output: 'end_date は YYYY-MM-DD 形式で指定してください', isError: true };
|
||||
}
|
||||
if (endDateInput < date) {
|
||||
return { output: 'end_date は date 以降の日付にしてください', isError: true };
|
||||
}
|
||||
endDate = endDateInput;
|
||||
}
|
||||
if (time !== undefined && time !== null && time !== '') {
|
||||
if (typeof time !== 'string' || !TIME_PATTERN.test(time)) {
|
||||
return { output: 'time は HH:MM 形式で指定してください', isError: true };
|
||||
}
|
||||
}
|
||||
if (description !== undefined && description !== null) {
|
||||
if (typeof description !== 'string') {
|
||||
return { output: 'description は文字列で指定してください', isError: true };
|
||||
}
|
||||
if (description.length > MAX_DESC_LEN) {
|
||||
return { output: `description が長すぎます(最大 ${MAX_DESC_LEN} 文字)`, isError: true };
|
||||
}
|
||||
}
|
||||
|
||||
const sourceTaskId = ctx.taskId ? Number(ctx.taskId) : null;
|
||||
const ev = await _repo.createCalendarEvent({
|
||||
spaceId,
|
||||
ownerId: ctx.ownerId ?? null,
|
||||
date,
|
||||
endDate,
|
||||
time: typeof time === 'string' && time !== '' ? time : null,
|
||||
title: title.trim(),
|
||||
description: typeof description === 'string' ? description : null,
|
||||
createdBy: 'agent',
|
||||
sourceTaskId: Number.isFinite(sourceTaskId) ? sourceTaskId : null,
|
||||
});
|
||||
return {
|
||||
output: `予定を追加しました: ${ev.date}${ev.time ? ` ${ev.time}` : '(終日)'} 「${ev.title}」(id=${ev.id})`,
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeListCalendarEvents(
|
||||
input: Record<string, unknown>,
|
||||
ctx: ToolContext,
|
||||
): Promise<ToolResult> {
|
||||
if (!_repo) {
|
||||
return { output: 'Calendar repo is not initialized', isError: true };
|
||||
}
|
||||
const spaceId = ctx.spaceId;
|
||||
if (!spaceId) {
|
||||
return {
|
||||
output: 'このタスクはスペースに属していないため予定を一覧できません',
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
const from = input['from'];
|
||||
const to = input['to'];
|
||||
if (from !== undefined && (typeof from !== 'string' || !DATE_PATTERN.test(from))) {
|
||||
return { output: 'from は YYYY-MM-DD 形式で指定してください', isError: true };
|
||||
}
|
||||
if (to !== undefined && (typeof to !== 'string' || !DATE_PATTERN.test(to))) {
|
||||
return { output: 'to は YYYY-MM-DD 形式で指定してください', isError: true };
|
||||
}
|
||||
const events = await _repo.listCalendarEvents(spaceId, {
|
||||
from: typeof from === 'string' ? from : undefined,
|
||||
to: typeof to === 'string' ? to : undefined,
|
||||
});
|
||||
if (events.length === 0) {
|
||||
return { output: '指定範囲に予定はありません', isError: false };
|
||||
}
|
||||
const lines = events.map(
|
||||
(e) => `- ${e.date}${e.time ? ` ${e.time}` : '(終日)'} 「${e.title}」(id=${e.id}, ${e.createdBy})`,
|
||||
);
|
||||
return { output: lines.join('\n'), isError: false };
|
||||
}
|
||||
Reference in New Issue
Block a user