Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agent-docs/src/agents/agent-pulse/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function createTools(context: ToolContext) {
* Tool for starting a tutorial - adds action to state queue
*/
const startTutorialAtStep = tool({
description: "Start a specific tutorial for the user. This will validate the tutorial exists, modify user data state, and finally return information about the tutorial including its title, total steps, and description. The tutorial will be displayed to the user automatically. The step number should be between 1 and the total number of steps in the tutorial.",
description: "Start a specific tutorial for the user. You must call this function in order for the user to see the tutorial step content. The step number should be between 1 and the total number of steps in the tutorial.",
parameters: z.object({
tutorialId: z.string().describe("The exact ID of the tutorial to start"),
stepNumber: z.number().describe("The step number of the tutorial to start (1 to total available steps in the tutorial)")
Expand Down
19 changes: 8 additions & 11 deletions app/api/sessions/[sessionId]/messages/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { toISOString, getCurrentTimestamp } from "@/app/chat/utils/dateUtils";
import { getAgentPulseConfig } from "@/lib/env";
import { config } from "@/lib/config";
import { parseAndValidateJSON, SessionMessageRequestSchema } from "@/lib/validation/middleware";

// Constants
const DEFAULT_CONVERSATION_HISTORY_LIMIT = 10;
Expand Down Expand Up @@ -63,19 +64,15 @@ export async function POST(

const paramsData = await params;
const sessionId = paramsData.sessionId;
const body = await request.json();
const { message, processWithAgent = true } = body as {
message: Message;
processWithAgent?: boolean;
};

if (!message) {
return NextResponse.json(
{ error: "Message data is required" },
{ status: 400 }
);

const validation = await parseAndValidateJSON(request, SessionMessageRequestSchema);

if (!validation.success) {
return validation.response;
}

const { message, processWithAgent } = validation.data;

// Ensure timestamp is in ISO string format
if (message.timestamp) {
message.timestamp = toISOString(message.timestamp);
Expand Down
31 changes: 19 additions & 12 deletions app/api/sessions/[sessionId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { getKVValue, setKVValue, deleteKVValue } from '@/lib/kv-store';
import { Session, Message } from '@/app/chat/types';
import { Session, Message, SessionSchema } from '@/app/chat/types';
import { toISOString } from '@/app/chat/utils/dateUtils';
import { config } from '@/lib/config';
import { parseAndValidateJSON, SessionMessageOnlyRequestSchema } from '@/lib/validation/middleware';

/**
* GET /api/sessions/[sessionId] - Get a specific session
Expand Down Expand Up @@ -53,19 +54,25 @@ export async function PUT(

const paramsData = await params;
const sessionId = paramsData.sessionId;
const session = await request.json() as Session;
const sessionKey = `${userId}_${sessionId}`;

if (!session || session.sessionId !== sessionId) {
const validation = await parseAndValidateJSON(request, SessionSchema);
if (!validation.success) {
return validation.response;
}

const session = validation.data;

if (session.sessionId !== sessionId) {
return NextResponse.json(
{ error: 'Invalid session data or session ID mismatch' },
{ error: 'Session ID mismatch' },
{ status: 400 }
);
}

// Process any messages to ensure timestamps are in ISO string format
if (session.messages && session.messages.length > 0) {
session.messages = session.messages.map(message => {
session.messages = session.messages.map((message: Message) => {
if (message.timestamp) {
return {
...message,
Expand Down Expand Up @@ -192,15 +199,15 @@ export async function POST(
const paramsData = await params;
const sessionId = paramsData.sessionId;
const sessionKey = `${userId}_${sessionId}`;
const { message } = await request.json() as { message: Message };

if (!message) {
return NextResponse.json(
{ error: 'Message data is required' },
{ status: 400 }
);
const validation = await parseAndValidateJSON(request, SessionMessageOnlyRequestSchema);

if (!validation.success) {
return validation.response;
}

const { message } = validation.data;

// Get current session
const sessionResponse = await getKVValue<Session>(sessionKey, { storeName: config.defaultStoreName });
if (!sessionResponse.success || !sessionResponse.data) {
Expand Down Expand Up @@ -256,4 +263,4 @@ export async function POST(
{ status: 500 }
);
}
}
}
30 changes: 20 additions & 10 deletions app/api/sessions/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { getKVValue, setKVValue } from '@/lib/kv-store';
import { Session } from '@/app/chat/types';
import { Session, Message, SessionSchema } from '@/app/chat/types';
import { toISOString } from '@/app/chat/utils/dateUtils';
import { config } from '@/lib/config';
import { parseAndValidateJSON } from '@/lib/validation/middleware';

// Constants
const DEFAULT_SESSIONS_LIMIT = 10;
Expand All @@ -26,7 +27,17 @@ export async function GET(request: NextRequest) {
const cursor = Number.isFinite(parsedCursor) ? Math.max(parsedCursor, 0) : 0;

const response = await getKVValue<string[]>(userId, { storeName: config.defaultStoreName });
if (!response.success || !response.data?.length) {
if (!response.success) {
if (response.statusCode === 404) {
return NextResponse.json({ sessions: [], pagination: { cursor, nextCursor: null, hasMore: false, total: 0, limit } });
}
return NextResponse.json(
{ error: response.error || 'Failed to retrieve sessions' },
{ status: response.statusCode || 500 }
);
}

if (!response.data?.length) {
return NextResponse.json({ sessions: [], pagination: { cursor, nextCursor: null, hasMore: false, total: 0, limit } });
}

Expand Down Expand Up @@ -67,18 +78,17 @@ export async function POST(request: NextRequest) {
if (!userId) {
return NextResponse.json({ error: 'User ID not found' }, { status: 401 });
}
const session = await request.json() as Session;

if (!session || !session.sessionId) {
return NextResponse.json(
{ error: 'Invalid session data' },
{ status: 400 }
);
const validation = await parseAndValidateJSON(request, SessionSchema);
if (!validation.success) {
return validation.response;
}

const session = validation.data;

// Process any messages to ensure timestamps are in ISO string format
if (session.messages && session.messages.length > 0) {
session.messages = session.messages.map(message => {
session.messages = session.messages.map((message: Message) => {
if (message.timestamp) {
return {
...message,
Expand Down Expand Up @@ -136,4 +146,4 @@ export async function POST(request: NextRequest) {
{ status: 500 }
);
}
}
}
52 changes: 41 additions & 11 deletions app/api/tutorials/[id]/steps/[stepNumber]/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { join } from 'path';
import { join, resolve, sep } from 'path';
import { readFile } from 'fs/promises';
import matter from 'gray-matter';
import { validateTutorialId, validateStepNumber, createValidationError } from '@/lib/validation/middleware';

interface RouteParams {
params: Promise<{ id: string; stepNumber: string }>;
Expand All @@ -10,8 +11,19 @@ interface RouteParams {
export async function GET(request: NextRequest, { params }: RouteParams) {
try {
const { id, stepNumber } = await params;
const stepIndex = parseInt(stepNumber, 10);
if (Number.isNaN(stepIndex) || stepIndex < 1) {

const idValidation = validateTutorialId(id);
if (!idValidation.success) {
return createValidationError('Invalid tutorial ID', idValidation.errors || []);
}

const stepValidation = validateStepNumber(stepNumber);
if (!stepValidation.success) {
return createValidationError('Invalid step number', stepValidation.errors || []);
}

const stepIndex = stepValidation.data;
if (!stepIndex) {
return NextResponse.json(
{ success: false, error: 'Invalid step number' },
{ status: 400 }
Expand All @@ -28,6 +40,14 @@ export async function GET(request: NextRequest, { params }: RouteParams) {

// Filter out index; map to actual MDX files
const stepSlugs = pages.filter(p => p !== 'index');

if (stepIndex < 1 || stepIndex > stepSlugs.length) {
return NextResponse.json(
{ success: false, error: 'Step not found' },
{ status: 404 }
);
}

const slug = stepSlugs[stepIndex - 1];
if (!slug) {
return NextResponse.json(
Expand All @@ -47,13 +67,23 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
async function loadSnippet(desc: { path: string; lang?: string; from?: number; to?: number; title?: string }) {
const filePath = desc.path;
if (!filePath || !filePath.startsWith('/examples/')) return;
const absolutePath = join(repoRoot, `.${filePath}`);
const fileRaw = await readFile(absolutePath, 'utf-8');
const lines = fileRaw.split(/\r?\n/);
const startIdx = Math.max(0, (desc.from ? desc.from - 1 : 0));
const endIdx = Math.min(lines.length, desc.to ? desc.to : lines.length);
const content = lines.slice(startIdx, endIdx).join('\n');
snippets.push({ ...desc, content });

// Resolve against repo root and ensure containment within /examples
const resolvedPath = resolve(repoRoot, `.${filePath}`);
const examplesBase = resolve(repoRoot, 'examples');
const isContained = resolvedPath === examplesBase || resolvedPath.startsWith(examplesBase + sep);
if (!isContained) return;

try {
const fileRaw = await readFile(resolvedPath, 'utf-8');
const lines = fileRaw.split(/\r?\n/);
const startIdx = Math.max(0, (desc.from ? desc.from - 1 : 0));
const endIdx = Math.min(lines.length, desc.to ? desc.to : lines.length);
const content = lines.slice(startIdx, endIdx).join('\n');
snippets.push({ ...desc, content });
} catch (error) {
console.warn(`Failed to load snippet from ${filePath}:`, error);
}
}

// 1) Parse <CodeFromFiles snippets={[{...}, {...}]}/> blocks
Expand Down Expand Up @@ -145,4 +175,4 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
{ status: 500 }
);
}
}
}
6 changes: 5 additions & 1 deletion app/api/tutorials/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export async function GET(request: NextRequest) {
const results: Array<{ id: string; title: string; description?: string; totalSteps: number }> = [];

for (const entry of pages) {
if (entry.includes('..') || entry.includes('/') || entry.includes('\\')) {
continue;
}

const dirPath = join(tutorialRoot, entry);
const filePath = join(tutorialRoot, `${entry}.mdx`);
try {
Expand Down Expand Up @@ -70,4 +74,4 @@ export async function GET(request: NextRequest) {
{ status: 500 }
);
}
}
}
56 changes: 26 additions & 30 deletions app/api/users/tutorial-state/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { NextRequest, NextResponse } from 'next/server';
import { TutorialStateManager } from '@/lib/tutorial/state-manager';
import { setKVValue } from '@/lib/kv-store';
import { config } from '@/lib/config';
import {
parseAndValidateJSON,
TutorialProgressRequestSchema,
TutorialResetRequestSchema
} from '@/lib/validation/middleware';

/**
* GET /api/users/tutorial-state - Get current user's tutorial state
Expand Down Expand Up @@ -36,22 +43,12 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'User ID not found' }, { status: 401 });
}

let body;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
const validation = await parseAndValidateJSON(request, TutorialProgressRequestSchema);
if (!validation.success) {
return validation.response;
}

const { tutorialId, currentStep, totalSteps } = body;


if (!tutorialId || typeof currentStep !== 'number' || typeof totalSteps !== 'number') {
return NextResponse.json(
{ error: 'Invalid tutorial data. Required: tutorialId, currentStep, totalSteps' },
{ status: 400 }
);
}
const { tutorialId, currentStep, totalSteps } = validation.data;

await TutorialStateManager.updateTutorialProgress(
userId,
Expand Down Expand Up @@ -83,32 +80,31 @@ export async function DELETE(request: NextRequest) {
return NextResponse.json({ error: 'User ID not found' }, { status: 401 });
}

let body;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
const validation = await parseAndValidateJSON(request, TutorialResetRequestSchema);
if (!validation.success) {
return validation.response;
}
const { tutorialId } = body;

if (!tutorialId) {
return NextResponse.json(
{ error: 'tutorialId is required' },
{ status: 400 }
);
}
const { tutorialId } = validation.data;

const state = await TutorialStateManager.getUserTutorialState(userId);
if (!state.tutorials) {
state.tutorials = {};
}
delete state.tutorials[tutorialId];

// Save the updated state
const { setKVValue } = await import('@/lib/kv-store');
const { config } = await import('@/lib/config');

await setKVValue(`tutorial_state_${userId}`, state, {
const kvResponse = await setKVValue(`tutorial_state_${userId}`, state, {
storeName: config.defaultStoreName
});

if (!kvResponse.success) {
return NextResponse.json(
{ error: kvResponse.error || 'Failed to reset tutorial state' },
{ status: kvResponse.statusCode || 500 }
);
}

return NextResponse.json({
success: true,
message: 'Tutorial progress reset'
Expand Down
Loading