feat: implement AI checkpoint and task suggestion UI with approval flow
This commit is contained in:
@@ -248,7 +248,20 @@ function buildProjectTools(projectId: string): StructuredTool[] {
|
||||
const match = content.match(/\[[\s\S]*\]/);
|
||||
const jsonStr = match ? match[0] : '[]';
|
||||
try {
|
||||
JSON.parse(jsonStr);
|
||||
const parsed = JSON.parse(jsonStr) as Array<{ title: string; date: string }>;
|
||||
for (const s of parsed) {
|
||||
const ts = new Date(s.date).getTime();
|
||||
if (Number.isNaN(ts)) continue;
|
||||
db.insert(checkpoints).values({
|
||||
id: crypto.randomUUID(),
|
||||
projectId,
|
||||
title: s.title,
|
||||
date: ts,
|
||||
isAiSuggested: 1,
|
||||
isApproved: 0,
|
||||
createdAt: Date.now(),
|
||||
}).run();
|
||||
}
|
||||
return jsonStr;
|
||||
} catch {
|
||||
return '[]';
|
||||
@@ -263,7 +276,57 @@ function buildProjectTools(projectId: string): StructuredTool[] {
|
||||
},
|
||||
);
|
||||
|
||||
return [readProjectNotesTool, addTaskTool, getSummaryTool, suggestCheckpointsTool] as StructuredTool[];
|
||||
const suggestTasksTool = tool(
|
||||
async (_input: Record<string, never>) => {
|
||||
const contextData = buildProjectContext(projectId);
|
||||
const suggestionLlm = await getLLM();
|
||||
if (!suggestionLlm) return '[]';
|
||||
const result = await suggestionLlm.invoke([
|
||||
new SystemMessage(
|
||||
'You are a project planning assistant. Analyze the project data below and extract ' +
|
||||
'actionable tasks from the notes (e.g. "set up CI pipeline", "draft proposal for client").\n\n' +
|
||||
'Return ONLY a valid JSON array of objects with shape { "title": string, "description"?: string, "priority"?: "low"|"medium"|"high", "dueDate"?: string } ' +
|
||||
'where dueDate is ISO 8601 format (YYYY-MM-DD) if mentioned. Return [] if no actionable tasks are found.\n\n' +
|
||||
'Example: [{"title":"Set up CI pipeline","description":"Configure GitHub Actions for automated testing","priority":"high"},{"title":"Draft client proposal","dueDate":"2026-03-20"}]',
|
||||
),
|
||||
new HumanMessage(contextData),
|
||||
]);
|
||||
const content = typeof result.content === 'string' ? result.content.trim() : '[]';
|
||||
const match = content.match(/\[[\s\S]*\]/);
|
||||
const jsonStr = match ? match[0] : '[]';
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr) as Array<{ title: string; description?: string; priority?: string; dueDate?: string }>;
|
||||
for (const s of parsed) {
|
||||
db.insert(tasks).values({
|
||||
id: crypto.randomUUID(),
|
||||
projectId,
|
||||
title: s.title,
|
||||
description: s.description ?? null,
|
||||
status: 'todo',
|
||||
priority: s.priority ?? 'medium',
|
||||
assignee: null,
|
||||
dueDate: s.dueDate ? new Date(s.dueDate).getTime() : null,
|
||||
isAiSuggested: 1,
|
||||
isApproved: 0,
|
||||
createdAt: Date.now(),
|
||||
}).run();
|
||||
}
|
||||
return jsonStr;
|
||||
} catch {
|
||||
return '[]';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'suggest_tasks',
|
||||
description:
|
||||
'Analyzes project notes for actionable tasks and returns a JSON array of ' +
|
||||
'{ title: string, description?: string, priority?: string, dueDate?: string } suggested tasks. ' +
|
||||
'Use when the user asks for task suggestions.',
|
||||
schema: z.object({}),
|
||||
},
|
||||
);
|
||||
|
||||
return [readProjectNotesTool, addTaskTool, getSummaryTool, suggestCheckpointsTool, suggestTasksTool] as StructuredTool[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -379,8 +442,9 @@ You also have access to the following tools — use them proactively when approp
|
||||
- add_task: Create a new task in this project. Use when the user asks to add a task.
|
||||
- get_summary: Generate and save a 2-3 sentence project summary. Use when asked to summarize.
|
||||
- suggest_checkpoints: Extract date-based milestone suggestions from notes. Use for timeline/checkpoint questions.
|
||||
- suggest_tasks: Extract actionable task suggestions from notes. Use when the user asks for task suggestions.
|
||||
|
||||
When suggest_checkpoints returns a JSON array, present the checkpoints in a readable bullet-point format.` : '';
|
||||
When suggest_checkpoints or suggest_tasks returns a JSON array, present the items in a readable bullet-point format.` : '';
|
||||
|
||||
return `You are @ProjectAgent, an AI assistant specialized in a specific project within Adiuva.
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ const MIGRATION_SQL = `
|
||||
priority TEXT NOT NULL DEFAULT 'medium',
|
||||
assignee TEXT,
|
||||
due_date INTEGER,
|
||||
is_ai_suggested INTEGER NOT NULL DEFAULT 0,
|
||||
is_approved INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
@@ -79,6 +81,10 @@ export function initDb(): DbInstance {
|
||||
// Run non-destructive migrations on every start
|
||||
sqlite.exec(MIGRATION_SQL);
|
||||
|
||||
// Additive column migrations (SQLite has no ADD COLUMN IF NOT EXISTS)
|
||||
try { sqlite.exec('ALTER TABLE tasks ADD COLUMN is_ai_suggested INTEGER NOT NULL DEFAULT 0'); } catch { /* already exists */ }
|
||||
try { sqlite.exec('ALTER TABLE tasks ADD COLUMN is_approved INTEGER NOT NULL DEFAULT 1'); } catch { /* already exists */ }
|
||||
|
||||
dbInstance = drizzle(sqlite, { schema });
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export const tasks = sqliteTable('tasks', {
|
||||
priority: text('priority').notNull().default('medium'),
|
||||
assignee: text('assignee'),
|
||||
dueDate: integer('due_date', { mode: 'number' }),
|
||||
isAiSuggested: integer('is_ai_suggested', { mode: 'number' }).notNull().default(0),
|
||||
isApproved: integer('is_approved', { mode: 'number' }).notNull().default(1),
|
||||
createdAt: integer('created_at', { mode: 'number' }).notNull(),
|
||||
});
|
||||
|
||||
|
||||
@@ -221,6 +221,8 @@ const tasksRouter = router({
|
||||
priority: tasks.priority,
|
||||
assignee: tasks.assignee,
|
||||
dueDate: tasks.dueDate,
|
||||
isAiSuggested: tasks.isAiSuggested,
|
||||
isApproved: tasks.isApproved,
|
||||
createdAt: tasks.createdAt,
|
||||
projectName: projects.name,
|
||||
clientName: sql<string | null>`CASE WHEN ${clients.parentId} IS NOT NULL THEN ${parentClients.name} ELSE ${clients.name} END`,
|
||||
@@ -268,6 +270,8 @@ const tasksRouter = router({
|
||||
assignees: z.array(z.string()).optional(),
|
||||
dueDate: z.number().optional(),
|
||||
projectId: z.string().optional(),
|
||||
isAiSuggested: z.number().optional(),
|
||||
isApproved: z.number().optional(),
|
||||
}))
|
||||
.mutation(({ input }) => {
|
||||
const id = crypto.randomUUID();
|
||||
@@ -281,6 +285,8 @@ const tasksRouter = router({
|
||||
assignee: input.assignees?.length ? JSON.stringify(input.assignees) : null,
|
||||
dueDate: input.dueDate ?? null,
|
||||
projectId: input.projectId ?? null,
|
||||
isAiSuggested: input.isAiSuggested ?? 0,
|
||||
isApproved: input.isApproved ?? 1,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
return { id };
|
||||
@@ -296,6 +302,7 @@ const tasksRouter = router({
|
||||
assignees: z.array(z.string()).optional(),
|
||||
dueDate: z.number().optional(),
|
||||
projectId: z.string().optional(),
|
||||
isApproved: z.number().optional(),
|
||||
}))
|
||||
.mutation(({ input }) => {
|
||||
const set: Partial<{
|
||||
@@ -306,6 +313,7 @@ const tasksRouter = router({
|
||||
assignee: string | null;
|
||||
dueDate: number | null;
|
||||
projectId: string | null;
|
||||
isApproved: number;
|
||||
}> = {};
|
||||
if (input.title !== undefined) set.title = input.title;
|
||||
if (input.description !== undefined) set.description = input.description;
|
||||
@@ -313,6 +321,7 @@ const tasksRouter = router({
|
||||
if (input.priority !== undefined) set.priority = input.priority;
|
||||
if (input.assignees !== undefined) set.assignee = input.assignees.length ? JSON.stringify(input.assignees) : null;
|
||||
if (input.dueDate !== undefined) set.dueDate = input.dueDate;
|
||||
if (input.isApproved !== undefined) set.isApproved = input.isApproved;
|
||||
if (input.projectId !== undefined) set.projectId = input.projectId;
|
||||
if (Object.keys(set).length > 0) {
|
||||
getDb().update(tasks).set(set).where(eq(tasks.id, input.id)).run();
|
||||
|
||||
Reference in New Issue
Block a user