haroo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/index.js +84883 -0
- package/package.json +73 -0
- package/src/__tests__/e2e/EventService.test.ts +211 -0
- package/src/__tests__/unit/Event.test.ts +89 -0
- package/src/__tests__/unit/Memory.test.ts +130 -0
- package/src/application/graph/builder.ts +106 -0
- package/src/application/graph/edges.ts +37 -0
- package/src/application/graph/nodes/addEvent.ts +113 -0
- package/src/application/graph/nodes/chat.ts +128 -0
- package/src/application/graph/nodes/extractMemory.ts +135 -0
- package/src/application/graph/nodes/index.ts +8 -0
- package/src/application/graph/nodes/query.ts +194 -0
- package/src/application/graph/nodes/respond.ts +26 -0
- package/src/application/graph/nodes/router.ts +82 -0
- package/src/application/graph/nodes/toolExecutor.ts +79 -0
- package/src/application/graph/nodes/types.ts +2 -0
- package/src/application/index.ts +4 -0
- package/src/application/services/DiaryService.ts +188 -0
- package/src/application/services/EventService.ts +61 -0
- package/src/application/services/index.ts +2 -0
- package/src/application/tools/calendarTool.ts +179 -0
- package/src/application/tools/diaryTool.ts +182 -0
- package/src/application/tools/index.ts +68 -0
- package/src/config/env.ts +33 -0
- package/src/config/index.ts +1 -0
- package/src/domain/entities/DiaryEntry.ts +16 -0
- package/src/domain/entities/Event.ts +13 -0
- package/src/domain/entities/Memory.ts +20 -0
- package/src/domain/index.ts +5 -0
- package/src/domain/interfaces/IDiaryRepository.ts +21 -0
- package/src/domain/interfaces/IEventsRepository.ts +12 -0
- package/src/domain/interfaces/ILanguageModel.ts +23 -0
- package/src/domain/interfaces/IMemoriesRepository.ts +15 -0
- package/src/domain/interfaces/IMemory.ts +19 -0
- package/src/domain/interfaces/index.ts +4 -0
- package/src/domain/state/AgentState.ts +30 -0
- package/src/index.ts +5 -0
- package/src/infrastructure/database/factory.ts +52 -0
- package/src/infrastructure/database/index.ts +21 -0
- package/src/infrastructure/database/sqlite-checkpointer.ts +179 -0
- package/src/infrastructure/database/sqlite-client.ts +69 -0
- package/src/infrastructure/database/sqlite-diary-repository.ts +209 -0
- package/src/infrastructure/database/sqlite-events-repository.ts +167 -0
- package/src/infrastructure/database/sqlite-memories-repository.ts +284 -0
- package/src/infrastructure/database/sqlite-schema.ts +98 -0
- package/src/infrastructure/index.ts +3 -0
- package/src/infrastructure/llm/base.ts +14 -0
- package/src/infrastructure/llm/gemini.ts +139 -0
- package/src/infrastructure/llm/index.ts +22 -0
- package/src/infrastructure/llm/ollama.ts +126 -0
- package/src/infrastructure/llm/openai.ts +148 -0
- package/src/infrastructure/memory/checkpointer.ts +19 -0
- package/src/infrastructure/memory/index.ts +2 -0
- package/src/infrastructure/settings/index.ts +96 -0
- package/src/interface/cli/calendar.ts +120 -0
- package/src/interface/cli/chat.ts +185 -0
- package/src/interface/cli/commands.ts +337 -0
- package/src/interface/cli/printer.ts +65 -0
- package/src/interface/index.ts +1 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ToolMessage } from "@langchain/core/messages";
|
|
2
|
+
import type { GraphStateType } from "../../../domain/state/AgentState";
|
|
3
|
+
import type { ToolCall } from "../../../domain/interfaces/ILanguageModel";
|
|
4
|
+
import type { ToolRegistry } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface ToolExecutorNodeDeps {
|
|
7
|
+
tools: ToolRegistry;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Executes tool calls from the LLM response and adds results as ToolMessages.
|
|
12
|
+
* This node processes pending tool calls and returns their results.
|
|
13
|
+
*/
|
|
14
|
+
export async function toolExecutorNode(
|
|
15
|
+
state: GraphStateType,
|
|
16
|
+
deps: ToolExecutorNodeDeps
|
|
17
|
+
): Promise<Partial<GraphStateType>> {
|
|
18
|
+
const lastMessage = state.messages[state.messages.length - 1];
|
|
19
|
+
|
|
20
|
+
// Check if there are tool calls to execute
|
|
21
|
+
const toolCalls = (lastMessage as { additional_kwargs?: { tool_calls?: ToolCall[] } })
|
|
22
|
+
?.additional_kwargs?.tool_calls;
|
|
23
|
+
|
|
24
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const toolMessages: ToolMessage[] = [];
|
|
29
|
+
|
|
30
|
+
for (const toolCall of toolCalls) {
|
|
31
|
+
const executor = deps.tools.get(toolCall.name);
|
|
32
|
+
|
|
33
|
+
if (!executor) {
|
|
34
|
+
toolMessages.push(
|
|
35
|
+
new ToolMessage({
|
|
36
|
+
content: JSON.stringify({ error: `Unknown tool: ${toolCall.name}` }),
|
|
37
|
+
tool_call_id: toolCall.id,
|
|
38
|
+
name: toolCall.name,
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await executor(toolCall.arguments);
|
|
46
|
+
toolMessages.push(
|
|
47
|
+
new ToolMessage({
|
|
48
|
+
content: result,
|
|
49
|
+
tool_call_id: toolCall.id,
|
|
50
|
+
name: toolCall.name,
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
55
|
+
toolMessages.push(
|
|
56
|
+
new ToolMessage({
|
|
57
|
+
content: JSON.stringify({ error: errorMessage }),
|
|
58
|
+
tool_call_id: toolCall.id,
|
|
59
|
+
name: toolCall.name,
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
messages: toolMessages,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Checks if the last message contains tool calls that need to be executed.
|
|
72
|
+
*/
|
|
73
|
+
export function hasToolCalls(state: GraphStateType): boolean {
|
|
74
|
+
const lastMessage = state.messages[state.messages.length - 1];
|
|
75
|
+
const toolCalls = (lastMessage as { additional_kwargs?: { tool_calls?: ToolCall[] } })
|
|
76
|
+
?.additional_kwargs?.tool_calls;
|
|
77
|
+
|
|
78
|
+
return Array.isArray(toolCalls) && toolCalls.length > 0;
|
|
79
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { HumanMessage, AIMessage, SystemMessage, BaseMessage } from "@langchain/core/messages";
|
|
4
|
+
import type { ILanguageModel } from "../../domain/interfaces/ILanguageModel";
|
|
5
|
+
import type { ICheckpointer } from "../../domain/interfaces/IMemory";
|
|
6
|
+
import type { IDiaryRepository } from "../../domain/interfaces/IDiaryRepository";
|
|
7
|
+
import type { DiaryEntry } from "../../domain/entities/DiaryEntry";
|
|
8
|
+
|
|
9
|
+
const DiaryExtractionSchema = z.object({
|
|
10
|
+
summary: z.string().describe("2-3 sentence summary of the day's conversations"),
|
|
11
|
+
mood: z.string().describe("Primary mood/tone detected (e.g., calm, anxious, happy, reflective)"),
|
|
12
|
+
moodScore: z
|
|
13
|
+
.number()
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(10)
|
|
16
|
+
.describe("Mood positivity score: 1=very negative, 10=very positive"),
|
|
17
|
+
therapeuticAdvice: z.string().describe("One sentence of supportive, therapeutic advice"),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
type DiaryExtraction = z.infer<typeof DiaryExtractionSchema>;
|
|
21
|
+
|
|
22
|
+
const DIARY_GENERATION_PROMPT = `You are a compassionate journal writer and emotional wellness assistant.
|
|
23
|
+
Analyze the following conversation(s) and create a diary entry that:
|
|
24
|
+
|
|
25
|
+
1. Summarizes the key topics and interactions (2-3 sentences)
|
|
26
|
+
2. Identifies the overall mood/emotional tone (use a single word like: calm, anxious, happy, reflective, stressed, content, frustrated, hopeful, etc.)
|
|
27
|
+
3. Rates the mood on a 1-10 scale (1=very negative, 10=very positive)
|
|
28
|
+
4. Provides one sentence of gentle, supportive therapeutic advice
|
|
29
|
+
|
|
30
|
+
Be warm, understanding, and focus on the human's emotional journey.
|
|
31
|
+
Your response must be valid JSON with the following structure:
|
|
32
|
+
{
|
|
33
|
+
"summary": "...",
|
|
34
|
+
"mood": "...",
|
|
35
|
+
"moodScore": 5,
|
|
36
|
+
"therapeuticAdvice": "..."
|
|
37
|
+
}`;
|
|
38
|
+
|
|
39
|
+
export class DiaryService {
|
|
40
|
+
constructor(
|
|
41
|
+
private readonly diaryRepository: IDiaryRepository,
|
|
42
|
+
private readonly checkpointer: ICheckpointer,
|
|
43
|
+
private readonly llm: ILanguageModel
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate a diary entry for a specific date by summarizing all sessions from that day.
|
|
48
|
+
*/
|
|
49
|
+
async generateDiaryEntry(targetDate: Date): Promise<DiaryEntry | null> {
|
|
50
|
+
// Check if entry already exists for this date
|
|
51
|
+
const existing = await this.diaryRepository.getByDate(targetDate);
|
|
52
|
+
if (existing) {
|
|
53
|
+
throw new Error(`Diary entry already exists for ${targetDate.toISOString().split("T")[0]}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Get sessions for the target date
|
|
57
|
+
const sessionIds = await this.getSessionsForDate(targetDate);
|
|
58
|
+
|
|
59
|
+
if (sessionIds.length === 0) {
|
|
60
|
+
return null; // No conversations for this date
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Load all messages from the sessions
|
|
64
|
+
const allMessages: BaseMessage[] = [];
|
|
65
|
+
for (const sessionId of sessionIds) {
|
|
66
|
+
const state = await this.checkpointer.load(sessionId);
|
|
67
|
+
if (state) {
|
|
68
|
+
allMessages.push(...state.messages);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (allMessages.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Format messages for the LLM
|
|
77
|
+
const conversationText = this.formatMessagesForSummary(allMessages);
|
|
78
|
+
|
|
79
|
+
// Use LLM to generate diary entry
|
|
80
|
+
const targetDateStr = targetDate.toISOString().split("T")[0];
|
|
81
|
+
const response = await this.llm.generate([
|
|
82
|
+
new SystemMessage(DIARY_GENERATION_PROMPT),
|
|
83
|
+
new HumanMessage(`Analyze these conversations from ${targetDateStr}:\n\n${conversationText}`),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
// Parse the JSON response
|
|
87
|
+
const parsed = this.parseExtractionResponse(response.content);
|
|
88
|
+
|
|
89
|
+
// Create and save diary entry
|
|
90
|
+
const now = new Date();
|
|
91
|
+
const entry: DiaryEntry = {
|
|
92
|
+
id: randomUUID(),
|
|
93
|
+
entryDate: targetDate,
|
|
94
|
+
summary: parsed.summary,
|
|
95
|
+
mood: parsed.mood,
|
|
96
|
+
moodScore: parsed.moodScore,
|
|
97
|
+
therapeuticAdvice: parsed.therapeuticAdvice,
|
|
98
|
+
sessionIds,
|
|
99
|
+
messageCount: allMessages.length,
|
|
100
|
+
createdAt: now,
|
|
101
|
+
updatedAt: now,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return this.diaryRepository.create(entry);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generate diary entry for yesterday (end of day trigger).
|
|
109
|
+
*/
|
|
110
|
+
async generateYesterdayEntry(): Promise<DiaryEntry | null> {
|
|
111
|
+
const yesterday = new Date();
|
|
112
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
113
|
+
yesterday.setHours(0, 0, 0, 0);
|
|
114
|
+
return this.generateDiaryEntry(yesterday);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get recent diary entries.
|
|
119
|
+
*/
|
|
120
|
+
async getRecent(limit = 7): Promise<DiaryEntry[]> {
|
|
121
|
+
return this.diaryRepository.getRecent(limit);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get diary entry by date.
|
|
126
|
+
*/
|
|
127
|
+
async getByDate(date: Date): Promise<DiaryEntry | null> {
|
|
128
|
+
return this.diaryRepository.getByDate(date);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async getSessionsForDate(date: Date): Promise<string[]> {
|
|
132
|
+
// Use the checkpointer's getSessionsForDate if available
|
|
133
|
+
if (this.checkpointer.getSessionsForDate) {
|
|
134
|
+
return this.checkpointer.getSessionsForDate(date);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fallback: list all sessions and filter by metadata
|
|
138
|
+
const allSessionIds = await this.checkpointer.list();
|
|
139
|
+
const sessionsForDate: string[] = [];
|
|
140
|
+
|
|
141
|
+
for (const sessionId of allSessionIds) {
|
|
142
|
+
if (this.checkpointer.getMetadata) {
|
|
143
|
+
const metadata = await this.checkpointer.getMetadata(sessionId);
|
|
144
|
+
if (metadata) {
|
|
145
|
+
const sessionDate = metadata.updatedAt;
|
|
146
|
+
if (
|
|
147
|
+
sessionDate.getFullYear() === date.getFullYear() &&
|
|
148
|
+
sessionDate.getMonth() === date.getMonth() &&
|
|
149
|
+
sessionDate.getDate() === date.getDate()
|
|
150
|
+
) {
|
|
151
|
+
sessionsForDate.push(sessionId);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return sessionsForDate;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private formatMessagesForSummary(messages: BaseMessage[]): string {
|
|
161
|
+
return messages
|
|
162
|
+
.filter((msg) => msg instanceof HumanMessage || msg instanceof AIMessage)
|
|
163
|
+
.map((msg) => {
|
|
164
|
+
const role = msg instanceof HumanMessage ? "User" : "Assistant";
|
|
165
|
+
return `${role}: ${String(msg.content)}`;
|
|
166
|
+
})
|
|
167
|
+
.join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private parseExtractionResponse(content: string): DiaryExtraction {
|
|
171
|
+
// Try to extract JSON from the response
|
|
172
|
+
let jsonStr = content;
|
|
173
|
+
|
|
174
|
+
// If wrapped in markdown code block, extract it
|
|
175
|
+
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
176
|
+
if (jsonMatch) {
|
|
177
|
+
jsonStr = jsonMatch[1];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Parse and validate
|
|
181
|
+
const parsed = DiaryExtractionSchema.safeParse(JSON.parse(jsonStr.trim()));
|
|
182
|
+
if (!parsed.success) {
|
|
183
|
+
throw new Error(`Failed to parse diary extraction: ${parsed.error.message}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return parsed.data;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { Event } from "../../domain/entities/Event";
|
|
3
|
+
import type { IEventsRepository } from "../../domain/interfaces/IEventsRepository";
|
|
4
|
+
|
|
5
|
+
export class EventService {
|
|
6
|
+
constructor(private readonly repository: IEventsRepository) {}
|
|
7
|
+
|
|
8
|
+
async add(event: Omit<Event, "id" | "createdAt">): Promise<Event> {
|
|
9
|
+
const newEvent: Event = {
|
|
10
|
+
...event,
|
|
11
|
+
id: randomUUID(),
|
|
12
|
+
createdAt: new Date(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return this.repository.create(newEvent);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async getById(id: string): Promise<Event | null> {
|
|
19
|
+
return this.repository.getById(id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async getByDate(date: Date): Promise<Event[]> {
|
|
23
|
+
const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
24
|
+
const endOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1);
|
|
25
|
+
|
|
26
|
+
return this.repository.getByDateRange(startOfDay, endOfDay);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getByRange(start: Date, end: Date): Promise<Event[]> {
|
|
30
|
+
return this.repository.getByDateRange(start, end);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getToday(): Promise<Event[]> {
|
|
34
|
+
return this.repository.getToday();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getUpcoming(days: number): Promise<Event[]> {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
const futureDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000);
|
|
40
|
+
|
|
41
|
+
return this.repository.getByDateRange(now, futureDate);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async search(query: string): Promise<Event[]> {
|
|
45
|
+
return this.repository.searchByTitle(query);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async update(id: string, data: Partial<Omit<Event, "id" | "createdAt">>): Promise<Event> {
|
|
49
|
+
const updated = await this.repository.update(id, data);
|
|
50
|
+
|
|
51
|
+
if (!updated) {
|
|
52
|
+
throw new Error(`Event with id ${id} not found`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return updated;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async delete(id: string): Promise<boolean> {
|
|
59
|
+
return this.repository.delete(id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDefinition } from "../../domain/interfaces/ILanguageModel";
|
|
3
|
+
import type { ToolExecutor } from "../graph/nodes";
|
|
4
|
+
import type { IEventsRepository } from "../../domain/interfaces/IEventsRepository";
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Schemas
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export const AddEventSchema = z.object({
|
|
11
|
+
title: z.string().describe("Event title"),
|
|
12
|
+
datetime: z.string().describe("ISO 8601 datetime"),
|
|
13
|
+
endTime: z.string().optional().describe("ISO 8601 end time"),
|
|
14
|
+
notes: z.string().optional().describe("Additional notes"),
|
|
15
|
+
tags: z.array(z.string()).optional().describe("Event tags"),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type AddEventInput = z.infer<typeof AddEventSchema>;
|
|
19
|
+
|
|
20
|
+
export const QueryEventsSchema = z.object({
|
|
21
|
+
type: z.enum(["today", "tomorrow", "week", "date", "range"]),
|
|
22
|
+
date: z.string().optional().describe("Specific date for 'date' type"),
|
|
23
|
+
startDate: z.string().optional().describe("Start date for 'range' type"),
|
|
24
|
+
endDate: z.string().optional().describe("End date for 'range' type"),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type QueryEventsInput = z.infer<typeof QueryEventsSchema>;
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Tool Definitions (for LLM binding)
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
export const addEventToolDefinition: ToolDefinition = {
|
|
34
|
+
name: "add_calendar_event",
|
|
35
|
+
description: "Add a new event to the calendar",
|
|
36
|
+
parameters: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
title: { type: "string", description: "Event title" },
|
|
40
|
+
datetime: { type: "string", description: "ISO 8601 datetime" },
|
|
41
|
+
endTime: { type: "string", description: "ISO 8601 end time" },
|
|
42
|
+
notes: { type: "string", description: "Additional notes" },
|
|
43
|
+
tags: { type: "array", items: { type: "string" }, description: "Event tags" },
|
|
44
|
+
},
|
|
45
|
+
required: ["title", "datetime"],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const queryEventsToolDefinition: ToolDefinition = {
|
|
50
|
+
name: "query_calendar_events",
|
|
51
|
+
description: "Query calendar events by date or range",
|
|
52
|
+
parameters: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
type: {
|
|
56
|
+
type: "string",
|
|
57
|
+
enum: ["today", "tomorrow", "week", "date", "range"],
|
|
58
|
+
description: "Query type",
|
|
59
|
+
},
|
|
60
|
+
date: { type: "string", description: "Specific date for 'date' type (YYYY-MM-DD)" },
|
|
61
|
+
startDate: { type: "string", description: "Start date for 'range' type (YYYY-MM-DD)" },
|
|
62
|
+
endDate: { type: "string", description: "End date for 'range' type (YYYY-MM-DD)" },
|
|
63
|
+
},
|
|
64
|
+
required: ["type"],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const calendarToolDefinitions: ToolDefinition[] = [
|
|
69
|
+
addEventToolDefinition,
|
|
70
|
+
queryEventsToolDefinition,
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Tool Executor Factory - Creates executors with database repository
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
export function createCalendarToolExecutors(
|
|
78
|
+
eventsRepository: IEventsRepository
|
|
79
|
+
): Record<string, ToolExecutor> {
|
|
80
|
+
const addEventExecutor: ToolExecutor = async (args) => {
|
|
81
|
+
const result = AddEventSchema.safeParse(args);
|
|
82
|
+
if (!result.success) {
|
|
83
|
+
throw new Error(`Validation Failed: ${result.error.message}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const input = result.data;
|
|
87
|
+
const id = crypto.randomUUID();
|
|
88
|
+
const now = new Date();
|
|
89
|
+
|
|
90
|
+
const event = await eventsRepository.create({
|
|
91
|
+
id,
|
|
92
|
+
title: input.title,
|
|
93
|
+
datetime: new Date(input.datetime),
|
|
94
|
+
endTime: input.endTime ? new Date(input.endTime) : undefined,
|
|
95
|
+
notes: input.notes,
|
|
96
|
+
tags: input.tags ?? [],
|
|
97
|
+
createdAt: now,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return JSON.stringify({
|
|
101
|
+
success: true,
|
|
102
|
+
event: {
|
|
103
|
+
id: event.id,
|
|
104
|
+
title: event.title,
|
|
105
|
+
datetime: event.datetime.toISOString(),
|
|
106
|
+
endTime: event.endTime?.toISOString(),
|
|
107
|
+
notes: event.notes,
|
|
108
|
+
tags: event.tags,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const queryEventsExecutor: ToolExecutor = async (args) => {
|
|
114
|
+
const result = QueryEventsSchema.safeParse(args);
|
|
115
|
+
if (!result.success) {
|
|
116
|
+
throw new Error(`Validation Failed: ${result.error.message}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const input = result.data;
|
|
120
|
+
const now = new Date();
|
|
121
|
+
let startDate: Date;
|
|
122
|
+
let endDate: Date;
|
|
123
|
+
|
|
124
|
+
switch (input.type) {
|
|
125
|
+
case "today":
|
|
126
|
+
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
127
|
+
endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
128
|
+
break;
|
|
129
|
+
case "tomorrow":
|
|
130
|
+
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
131
|
+
endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 2);
|
|
132
|
+
break;
|
|
133
|
+
case "week":
|
|
134
|
+
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
135
|
+
endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 7);
|
|
136
|
+
break;
|
|
137
|
+
case "date":
|
|
138
|
+
if (!input.date) {
|
|
139
|
+
throw new Error("Validation Failed: 'date' field is required for type 'date'");
|
|
140
|
+
}
|
|
141
|
+
startDate = new Date(input.date);
|
|
142
|
+
endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
|
|
143
|
+
break;
|
|
144
|
+
case "range":
|
|
145
|
+
if (!input.startDate || !input.endDate) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"Validation Failed: 'startDate' and 'endDate' are required for type 'range'"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
startDate = new Date(input.startDate);
|
|
151
|
+
endDate = new Date(input.endDate);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const events = await eventsRepository.getByDateRange(startDate, endDate);
|
|
156
|
+
|
|
157
|
+
return JSON.stringify({
|
|
158
|
+
success: true,
|
|
159
|
+
query: {
|
|
160
|
+
type: input.type,
|
|
161
|
+
startDate: startDate.toISOString(),
|
|
162
|
+
endDate: endDate.toISOString(),
|
|
163
|
+
},
|
|
164
|
+
events: events.map((e) => ({
|
|
165
|
+
id: e.id,
|
|
166
|
+
title: e.title,
|
|
167
|
+
datetime: e.datetime.toISOString(),
|
|
168
|
+
endTime: e.endTime?.toISOString(),
|
|
169
|
+
notes: e.notes,
|
|
170
|
+
tags: e.tags,
|
|
171
|
+
})),
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
add_calendar_event: addEventExecutor,
|
|
177
|
+
query_calendar_events: queryEventsExecutor,
|
|
178
|
+
};
|
|
179
|
+
}
|