mirror of
https://github.com/larchanka/manbot.git
synced 2026-05-22 09:45:04 +00:00
P11-01: Create Time Parser Service
- Implement TimeParserService that converts natural language time expressions to cron expressions - Uses OllamaAdapter and ModelRouter to leverage LLM capabilities - Validates cron expressions using node-cron's validate() function - Handles both one-time and recurring reminders - Includes comprehensive error handling and JSON parsing - Exports both class-based and convenience function interfaces Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
Mikhail Larchanka
parent
50c7525982
commit
76a5072f8d
117
_board/_BOARD.md
117
_board/_BOARD.md
@@ -2,8 +2,125 @@
|
||||
|
||||
## To Do
|
||||
|
||||
### P11-02 Add Time Parser Tests
|
||||
- tags: [todo, reminder-system, phase-1, testing]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Create unit tests for the time parser service.
|
||||
|
||||
Source: P11-02_TIME_PARSER_TESTS.md
|
||||
```
|
||||
|
||||
### P11-03 Update Cron Manager Event Payload
|
||||
- tags: [todo, reminder-system, phase-1]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Modify CronManager.runJob() to emit structured reminder data in event.cron.completed.
|
||||
|
||||
Source: P11-03_CRON_EVENT_PAYLOAD.md
|
||||
```
|
||||
|
||||
### P11-04 Add Cron Manager Integration Tests
|
||||
- tags: [todo, reminder-system, phase-1, testing]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Create integration tests for cron manager reminder functionality.
|
||||
|
||||
Source: P11-04_CRON_INTEGRATION_TESTS.md
|
||||
```
|
||||
|
||||
### P11-05 Handle Cron Events in Orchestrator
|
||||
- tags: [todo, reminder-system, phase-2]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Add handler for event.cron.completed to route reminders to Telegram.
|
||||
|
||||
Source: P11-05_ORCHESTRATOR_CRON_HANDLER.md
|
||||
```
|
||||
|
||||
### P11-06 Update Planner Prompt with Reminder Capability
|
||||
- tags: [todo, reminder-system, phase-3]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Add cron-manager service and schedule_reminder capability to planner prompt.
|
||||
|
||||
Source: P11-06_PLANNER_REMINDER_CAPABILITY.md
|
||||
```
|
||||
|
||||
### P11-07 Add Planner Example for Reminders
|
||||
- tags: [todo, reminder-system, phase-3]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Add few-shot example showing how to plan a reminder request.
|
||||
|
||||
Source: P11-07_PLANNER_REMINDER_EXAMPLE.md
|
||||
```
|
||||
|
||||
### P11-08 Add Schedule Reminder Handler to Executor
|
||||
- tags: [todo, reminder-system, phase-4]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Add handler for schedule_reminder node type in the executor.
|
||||
|
||||
Source: P11-08_EXECUTOR_REMINDER_HANDLER.md
|
||||
```
|
||||
|
||||
### P11-09 Add List Reminders Command
|
||||
- tags: [todo, reminder-system, phase-5]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Add /reminders command to list active reminders for the user.
|
||||
|
||||
Source: P11-09_LIST_REMINDERS_COMMAND.md
|
||||
```
|
||||
|
||||
### P11-10 Add Cancel Reminder Command
|
||||
- tags: [todo, reminder-system, phase-5]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Add /cancel_reminder command to remove a specific reminder.
|
||||
|
||||
Source: P11-10_CANCEL_REMINDER_COMMAND.md
|
||||
```
|
||||
|
||||
### P11-11 Update Help Command
|
||||
- tags: [todo, reminder-system, phase-5]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Update /help command to document reminder functionality.
|
||||
|
||||
Source: P11-11_UPDATE_HELP_COMMAND.md
|
||||
```
|
||||
|
||||
### P11-12 Manual End-to-End Testing
|
||||
- tags: [todo, reminder-system, phase-6, testing]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Perform manual testing of the complete reminder flow.
|
||||
|
||||
Source: P11-12_E2E_TESTING.md
|
||||
```
|
||||
|
||||
### P11-13 Update README
|
||||
- tags: [todo, reminder-system, phase-6, documentation]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Document the reminder feature in the README.
|
||||
|
||||
Source: P11-13_UPDATE_README.md
|
||||
```
|
||||
|
||||
## In Progress
|
||||
|
||||
### P11-01 Create Time Parser Service
|
||||
- tags: [in-progress, reminder-system, phase-1]
|
||||
- defaultExpanded: false
|
||||
```md
|
||||
Create a service that converts natural language time expressions into cron expressions using the LLM.
|
||||
|
||||
Source: P11-01_TIME_PARSER_SERVICE.md
|
||||
```
|
||||
|
||||
## Done
|
||||
### P10-06 Model Selection Verification
|
||||
- tags: [done]
|
||||
|
||||
154
src/services/time-parser.ts
Normal file
154
src/services/time-parser.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Time Parser Service: converts natural language time expressions into cron expressions using the LLM.
|
||||
* P11-01: _board/TASKS/P11-01_TIME_PARSER_SERVICE.md
|
||||
*/
|
||||
|
||||
import cron from "node-cron";
|
||||
import { OllamaAdapter } from "./ollama-adapter.js";
|
||||
import { ModelRouter } from "./model-router.js";
|
||||
|
||||
const SYSTEM_PROMPT = `You are a time expression parser. Your task is to convert natural language time expressions into cron expressions.
|
||||
|
||||
Rules:
|
||||
1. For recurring reminders (e.g., "every day at 9am", "every Monday", "every week"), output a standard cron expression.
|
||||
2. For one-time reminders (e.g., "in 5 minutes", "tomorrow at 3pm", "next Monday at 9am"), calculate the exact date and time, then output a cron expression for that specific time.
|
||||
3. Cron format: "minute hour day month dayOfWeek"
|
||||
- minute: 0-59
|
||||
- hour: 0-23 (24-hour format)
|
||||
- day: 1-31
|
||||
- month: 1-12
|
||||
- dayOfWeek: 0-7 (0 or 7 = Sunday, 1 = Monday, ..., 6 = Saturday)
|
||||
4. Use * for "every" values (e.g., "every day" = "* * * * *")
|
||||
5. For one-time reminders, use specific values for all fields (e.g., "15 14 17 2 *" = Feb 17 at 2:15 PM)
|
||||
|
||||
Output your response as a JSON object with this exact format:
|
||||
{
|
||||
"cronExpr": "<cron expression>",
|
||||
"isRecurring": <true or false>,
|
||||
"description": "<human-readable description of when this will trigger>"
|
||||
}
|
||||
|
||||
Examples:
|
||||
- Input: "in 5 minutes" (assuming current time is 14:30)
|
||||
Output: {"cronExpr": "35 14 <day> <month> *", "isRecurring": false, "description": "In 5 minutes (at 14:35)"}
|
||||
|
||||
- Input: "every day at 9am"
|
||||
Output: {"cronExpr": "0 9 * * *", "isRecurring": true, "description": "Every day at 9:00 AM"}
|
||||
|
||||
- Input: "every Monday at 2pm"
|
||||
Output: {"cronExpr": "0 14 * * 1", "isRecurring": true, "description": "Every Monday at 2:00 PM"}
|
||||
|
||||
- Input: "tomorrow at 3pm" (assuming today is Feb 17)
|
||||
Output: {"cronExpr": "0 15 18 2 *", "isRecurring": false, "description": "Tomorrow at 3:00 PM (Feb 18)"}
|
||||
|
||||
Note: For one-time reminders, calculate the exact date and time based on the current date/time provided in the user message. Replace <day> and <month> placeholders with actual numeric values.
|
||||
|
||||
Important: Always output valid JSON. The cron expression must be valid according to node-cron format.`;
|
||||
|
||||
export interface ParseTimeExpressionResult {
|
||||
cronExpr: string;
|
||||
isRecurring: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export class TimeParserService {
|
||||
private readonly ollama: OllamaAdapter;
|
||||
private readonly modelRouter: ModelRouter;
|
||||
|
||||
constructor(options?: { ollama?: OllamaAdapter; modelRouter?: ModelRouter }) {
|
||||
this.ollama = options?.ollama ?? new OllamaAdapter();
|
||||
this.modelRouter = options?.modelRouter ?? new ModelRouter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a natural language time expression into a cron expression.
|
||||
* @param input Natural language time expression (e.g., "in 5 minutes", "every day at 9am")
|
||||
* @returns Promise resolving to cron expression, recurrence flag, and description
|
||||
* @throws Error if the input cannot be parsed or the generated cron expression is invalid
|
||||
*/
|
||||
async parseTimeExpression(input: string): Promise<ParseTimeExpressionResult> {
|
||||
if (!input || typeof input !== "string" || input.trim().length === 0) {
|
||||
throw new Error("Invalid input: time expression must be a non-empty string");
|
||||
}
|
||||
|
||||
const prompt = `Parse this time expression into a cron expression: "${input.trim()}"\n\nCurrent date and time: ${new Date().toISOString()}`;
|
||||
|
||||
try {
|
||||
// Use "small" model for this task as it's relatively straightforward
|
||||
const model = this.modelRouter.getModel("small");
|
||||
const result = await this.ollama.chat(
|
||||
[
|
||||
{ role: "system", content: SYSTEM_PROMPT },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
model,
|
||||
);
|
||||
|
||||
const responseText = (result.message.content || "").trim();
|
||||
if (!responseText) {
|
||||
throw new Error("LLM returned empty response");
|
||||
}
|
||||
|
||||
// Try to extract JSON from the response (may be wrapped in markdown code blocks)
|
||||
let jsonText = responseText;
|
||||
const jsonMatch = responseText.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/);
|
||||
if (jsonMatch && jsonMatch[1]) {
|
||||
jsonText = jsonMatch[1];
|
||||
} else {
|
||||
// Try to find JSON object in the response
|
||||
const braceMatch = responseText.match(/\{[\s\S]*\}/);
|
||||
if (braceMatch && braceMatch[0]) {
|
||||
jsonText = braceMatch[0];
|
||||
}
|
||||
}
|
||||
|
||||
let parsed: ParseTimeExpressionResult;
|
||||
try {
|
||||
parsed = JSON.parse(jsonText) as ParseTimeExpressionResult;
|
||||
} catch (parseError) {
|
||||
throw new Error(
|
||||
`Failed to parse LLM response as JSON. Response: ${responseText.substring(0, 200)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (typeof parsed.cronExpr !== "string") {
|
||||
throw new Error(`Invalid response: missing or invalid cronExpr field`);
|
||||
}
|
||||
if (typeof parsed.isRecurring !== "boolean") {
|
||||
throw new Error(`Invalid response: missing or invalid isRecurring field`);
|
||||
}
|
||||
if (typeof parsed.description !== "string") {
|
||||
throw new Error(`Invalid response: missing or invalid description field`);
|
||||
}
|
||||
|
||||
// Validate cron expression using node-cron
|
||||
const isValid = cron.validate(parsed.cronExpr);
|
||||
if (!isValid) {
|
||||
throw new Error(
|
||||
`Generated cron expression is invalid: "${parsed.cronExpr}". LLM response: ${responseText.substring(0, 200)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (message.includes("Invalid") || message.includes("Failed to parse")) {
|
||||
throw new Error(`Failed to parse time expression "${input}": ${message}`);
|
||||
}
|
||||
// Re-throw network/timeout errors as-is
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to parse a time expression.
|
||||
* Creates a new TimeParserService instance and calls parseTimeExpression.
|
||||
*/
|
||||
export async function parseTimeExpression(
|
||||
input: string,
|
||||
): Promise<ParseTimeExpressionResult> {
|
||||
const parser = new TimeParserService();
|
||||
return parser.parseTimeExpression(input);
|
||||
}
|
||||
Reference in New Issue
Block a user