diff --git a/apps/web/src/app/api/[username]/agents/[agentSlug]/conversation/[conversationId]/route.ts b/apps/web/src/app/api/[username]/agents/[agentSlug]/conversation/[conversationId]/route.ts new file mode 100644 index 0000000..f90e974 --- /dev/null +++ b/apps/web/src/app/api/[username]/agents/[agentSlug]/conversation/[conversationId]/route.ts @@ -0,0 +1,778 @@ +/** + * Agent Conversation Endpoint (Username/Slug-based version) + * + * POST: Send a message and stream the AI response + * GET: Retrieve conversation history + * DELETE: Delete a conversation + * + * This endpoint uses username and agent slug for public-friendly URLs. + * Example: /api/ajax/agents/tpmjs-discord/conversation/my-conv-1 + * + * Authentication: Supports both session auth and TPMJS API key auth. + * Requires 'agent:chat' scope for API key access. + */ + +import { Prisma, prisma } from '@tpmjs/db'; +import type { AIProvider } from '@tpmjs/types/agent'; +import { SendMessageSchema } from '@tpmjs/types/agent'; +import type { LanguageModel, ModelMessage } from 'ai'; +import { type NextRequest, NextResponse } from 'next/server'; +import { decryptApiKey } from '@/lib/crypto/api-keys'; +import { authenticateRequest, hasScope } from '~/lib/api-keys/middleware'; +import { trackUsage } from '~/lib/api-keys/usage'; +import { checkRateLimit, type RateLimitConfig } from '~/lib/rate-limit'; + +/** + * Rate limit for chat messages: 30 requests per minute + * This is stricter than default because chat involves expensive LLM calls + */ +const CHAT_RATE_LIMIT: RateLimitConfig = { + limit: 30, + windowSeconds: 60, +}; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 300; // 5 minutes for long agentic runs + +type RouteContext = { + params: Promise<{ username: string; agentSlug: string; conversationId: string }>; +}; + +/** + * Get AI provider SDK based on provider type + */ +async function getProviderModel( + provider: AIProvider, + modelId: string, + apiKey: string +): Promise { + switch (provider) { + case 'OPENAI': { + const { createOpenAI } = await import('@ai-sdk/openai'); + return createOpenAI({ apiKey })(modelId); + } + case 'ANTHROPIC': { + const { createAnthropic } = await import('@ai-sdk/anthropic'); + return createAnthropic({ apiKey })(modelId); + } + case 'GOOGLE': { + const { createGoogleGenerativeAI } = await import('@ai-sdk/google'); + return createGoogleGenerativeAI({ apiKey })(modelId); + } + case 'GROQ': { + const { createGroq } = await import('@ai-sdk/groq'); + return createGroq({ apiKey })(modelId); + } + case 'MISTRAL': { + const { createMistral } = await import('@ai-sdk/mistral'); + return createMistral({ apiKey })(modelId); + } + default: + throw new Error(`Unsupported provider: ${provider}`); + } +} + +/** + * POST /api/[username]/agents/[agentSlug]/conversation/[conversationId] + * Send a message and stream the AI response via SSE + */ +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex streaming logic required +export async function POST(request: NextRequest, context: RouteContext): Promise { + const startTime = Date.now(); + + // Authenticate request (supports both session and API key) + const authResult = await authenticateRequest(); + + if (!authResult.authenticated) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // Check scope for API key auth + if (authResult.authenticated && !authResult.isSessionAuth) { + if (!hasScope(authResult, 'agent:chat')) { + return NextResponse.json( + { error: 'API key does not have agent:chat scope' }, + { status: 403 } + ); + } + } + + // Check rate limit (after auth so we can track by user if needed) + const rateLimitResponse = checkRateLimit(request, CHAT_RATE_LIMIT); + if (rateLimitResponse) { + return rateLimitResponse; + } + + const { username, agentSlug, conversationId } = await context.params; + + try { + const body = await request.json(); + const parsed = SendMessageSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { success: false, error: 'Invalid request', details: parsed.error.flatten() }, + { status: 400 } + ); + } + + // Fetch agent by username and slug with all tool relations + const { fetchAgentByUsernameAndUidWithTools, buildAgentTools } = await import( + '@/lib/agents/build-tools' + ); + const agent = await fetchAgentByUsernameAndUidWithTools(username, agentSlug); + + if (!agent) { + return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 }); + } + + // Owner-only enforcement: Only the agent owner can chat with the agent + if (authResult.userId !== agent.userId) { + return NextResponse.json( + { + success: false, + error: + 'Fork this agent to use it. Only the agent owner can chat with agents. ' + + 'Visit the agent page to fork it to your account.', + }, + { status: 403 } + ); + } + + // Map provider to expected key name format + const providerKeyNames: Record = { + OPENAI: 'OPENAI_API_KEY', + ANTHROPIC: 'ANTHROPIC_API_KEY', + GOOGLE: 'GOOGLE_API_KEY', + GROQ: 'GROQ_API_KEY', + MISTRAL: 'MISTRAL_API_KEY', + }; + const keyName = providerKeyNames[agent.provider]; + + if (!keyName) { + return NextResponse.json( + { success: false, error: `Unsupported provider: ${agent.provider}` }, + { status: 400 } + ); + } + + // Get user's API key for this provider + const userApiKey = await prisma.userApiKey.findUnique({ + where: { + userId_keyName: { + userId: agent.userId, + keyName, + }, + }, + }); + + if (!userApiKey) { + return NextResponse.json( + { + success: false, + error: `No API key configured for ${agent.provider}. Please add your API key in settings.`, + }, + { status: 400 } + ); + } + + // Decrypt the API key + const apiKey = decryptApiKey(userApiKey.encryptedKey, userApiKey.keyIv); + + // Get or create conversation + let conversation = await prisma.conversation.findUnique({ + where: { + agentId_slug: { + agentId: agent.id, + slug: conversationId, + }, + }, + }); + + if (!conversation) { + conversation = await prisma.conversation.create({ + data: { + agentId: agent.id, + slug: conversationId, + title: parsed.data.message.slice(0, 100), + }, + }); + } + + // Fetch recent messages for context + const recentMessages = await prisma.message.findMany({ + where: { conversationId: conversation.id }, + orderBy: { createdAt: 'desc' }, + take: agent.maxMessagesInContext, + }); + + // Reverse to get chronological order + recentMessages.reverse(); + + // Save user message + await prisma.message.create({ + data: { + conversationId: conversation.id, + role: 'USER', + content: parsed.data.message, + }, + }); + + // Build AI SDK messages from conversation history + const { streamText, stepCountIs } = await import('ai'); + + const messages: ModelMessage[] = []; + + // Add system prompt if defined + if (agent.systemPrompt) { + messages.push({ + role: 'system', + content: agent.systemPrompt, + }); + } + + // Add conversation history - properly format for AI SDK + for (const msg of recentMessages) { + if (msg.role === 'USER') { + messages.push({ role: 'user', content: msg.content }); + } else if (msg.role === 'ASSISTANT') { + // For assistant messages with tool calls, include ToolCallParts in content + if (msg.toolCalls && Array.isArray(msg.toolCalls) && msg.toolCalls.length > 0) { + const toolCallParts = ( + msg.toolCalls as Array<{ toolCallId: string; toolName: string; args: unknown }> + ).map((tc) => ({ + type: 'tool-call' as const, + toolCallId: tc.toolCallId, + toolName: tc.toolName, + input: tc.args, + })); + // Content includes text (if any) plus tool call parts + const content: Array< + | { type: 'text'; text: string } + | { type: 'tool-call'; toolCallId: string; toolName: string; input: unknown } + > = []; + if (msg.content) { + content.push({ type: 'text', text: msg.content }); + } + content.push(...toolCallParts); + messages.push({ + role: 'assistant', + content, + }); + } else { + messages.push({ + role: 'assistant', + content: msg.content, + }); + } + } else if (msg.role === 'TOOL') { + // Tool results use the 'tool' role with content array + messages.push({ + role: 'tool', + content: [ + { + type: 'tool-result' as const, + toolCallId: msg.toolCallId || '', + toolName: msg.toolName || '', + output: { + type: 'json' as const, + value: msg.toolResult, + }, + }, + ], + }); + } + } + + // Add new user message + messages.push({ role: 'user', content: parsed.data.message }); + + // Build tools from agent configuration + const tools = buildAgentTools(agent); + + // Get the provider model + const model = await getProviderModel(agent.provider, agent.modelId, apiKey); + + // Create SSE stream + const stream = new ReadableStream({ + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex streaming logic + async start(controller) { + const encoder = new TextEncoder(); + + const sendEvent = (event: string, data: unknown) => { + const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; + controller.enqueue(encoder.encode(message)); + }; + + try { + const startTime = Date.now(); + let fullContent = ''; + // Accumulate tool calls with their input args + const toolCallsMap: Map = + new Map(); + // Collect tool results to save AFTER assistant message (for correct chronological order) + const pendingToolResults: Array<{ + toolCallId: string; + toolName: string; + output: unknown; + }> = []; + let inputTokens = 0; + let outputTokens = 0; + + // Stream the response with agentic loop control + const result = await streamText({ + model, + messages, + tools, + stopWhen: stepCountIs(agent.maxToolCallsPerTurn), + onChunk: async ({ chunk }) => { + // Stream tool calls as they come in and capture their inputs + if (chunk.type === 'tool-call') { + const input = 'args' in chunk ? chunk.args : chunk.input; + + // Log tool call for debugging + console.log('[Agent] Tool call initiated:', { + toolName: chunk.toolName, + toolCallId: chunk.toolCallId, + input: JSON.stringify(input).slice(0, 500), + }); + + // Store tool call with input for later persistence + toolCallsMap.set(chunk.toolCallId, { + toolCallId: chunk.toolCallId, + toolName: chunk.toolName, + args: input, + }); + sendEvent('tool_call', { + toolCallId: chunk.toolCallId, + toolName: chunk.toolName, + input, + }); + } + }, + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex callback + onStepFinish: async ({ toolCalls, toolResults, usage }) => { + // Capture tool calls from step finish (backup in case onChunk missed any) + if (toolCalls && Array.isArray(toolCalls)) { + for (const tc of toolCalls) { + if (!toolCallsMap.has(tc.toolCallId)) { + // Use 'input' from DynamicToolCall or fall back to type assertion for typed calls + const args = + 'input' in tc ? tc.input : 'args' in tc ? (tc as { args: unknown }).args : {}; + toolCallsMap.set(tc.toolCallId, { + toolCallId: tc.toolCallId, + toolName: tc.toolName, + args, + }); + } + } + } + + // Send tool results via SSE but don't save yet (save after assistant message for correct order) + if (toolResults && toolResults.length > 0) { + for (const tr of toolResults) { + // Check if this is an error result (AI SDK wraps errors in the output) + const isError = + tr.output && typeof tr.output === 'object' && 'error' in tr.output; + + // Log tool results for debugging + if (isError) { + console.error('[Agent] Tool execution failed:', { + toolName: tr.toolName, + toolCallId: tr.toolCallId, + error: tr.output, + }); + } else { + console.log('[Agent] Tool execution success:', { + toolName: tr.toolName, + toolCallId: tr.toolCallId, + outputPreview: JSON.stringify(tr.output).slice(0, 200), + }); + } + + sendEvent('tool_result', { + toolCallId: tr.toolCallId, + output: tr.output, + isError, + }); + + // Collect tool results to save after assistant message + pendingToolResults.push({ + toolCallId: tr.toolCallId, + toolName: tr.toolName, + output: tr.output, + }); + } + } + + // Track token usage + if (usage) { + inputTokens += usage.inputTokens ?? 0; + outputTokens += usage.outputTokens ?? 0; + } + }, + }); + + // Stream text chunks + for await (const chunk of result.textStream) { + fullContent += chunk; + sendEvent('chunk', { type: 'text', text: chunk }); + } + + // Get final response data + const finalUsage = await result.usage; + + // Convert tool calls map to array for storage + const allToolCalls = Array.from(toolCallsMap.values()); + + // Update token counts from final usage + if (finalUsage) { + inputTokens = finalUsage.inputTokens ?? inputTokens; + outputTokens = finalUsage.outputTokens ?? outputTokens; + } + + // Save assistant message FIRST (so it has earlier createdAt than tool results) + const assistantMessage = await prisma.message.create({ + data: { + conversationId: conversation.id, + role: 'ASSISTANT', + content: fullContent, + // Cast to Prisma-compatible JSON type + toolCalls: + allToolCalls.length > 0 + ? (allToolCalls as unknown as Prisma.InputJsonValue) + : Prisma.JsonNull, + inputTokens, + outputTokens, + }, + }); + + // Now save TOOL messages (after assistant, for correct chronological order) + for (const tr of pendingToolResults) { + await prisma.message.create({ + data: { + conversationId: conversation.id, + role: 'TOOL', + content: JSON.stringify(tr.output), + toolCallId: tr.toolCallId, + toolName: tr.toolName, + toolResult: tr.output as object, + }, + }); + } + + // Update conversation timestamp + await prisma.conversation.update({ + where: { id: conversation.id }, + data: { updatedAt: new Date() }, + }); + + const executionTimeMs = Date.now() - startTime; + + // Send token usage + sendEvent('tokens', { + inputTokens, + outputTokens, + totalTokens: inputTokens + outputTokens, + }); + + // Send completion event + sendEvent('complete', { + messageId: assistantMessage.id, + conversationId: conversation.id, + executionTimeMs, + }); + + // Track usage + if (authResult.userId) { + trackUsage({ + apiKeyId: authResult.apiKeyId ?? undefined, + userId: authResult.userId, + endpoint: `/api/${username}/agents/${agentSlug}/conversation/${conversationId}`, + method: 'POST', + statusCode: 200, + latencyMs: executionTimeMs, + resourceType: 'agent', + resourceId: agent.id, + tokensIn: inputTokens, + tokensOut: outputTokens, + model: agent.modelId, + }); + } + } catch (error) { + // Log detailed error for debugging + console.error('[Agent] Conversation stream error:', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + agentId: agent.id, + conversationId: conversation.id, + }); + sendEvent('error', { + message: error instanceof Error ? error.message : 'Unknown error', + }); + + // Track error + if (authResult.userId) { + trackUsage({ + apiKeyId: authResult.apiKeyId ?? undefined, + userId: authResult.userId, + endpoint: `/api/${username}/agents/${agentSlug}/conversation/${conversationId}`, + method: 'POST', + statusCode: 500, + latencyMs: Date.now() - startTime, + resourceType: 'agent', + resourceId: agent.id, + errorCode: 'STREAM_ERROR', + errorMessage: error instanceof Error ? error.message : 'Unknown error', + }); + } + } finally { + controller.close(); + } + }, + }); + + return new NextResponse(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }, + }); + } catch (error) { + console.error('Failed to process message:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to process message', + }, + { status: 500 } + ); + } +} + +/** + * GET /api/[username]/agents/[agentSlug]/conversation/[conversationId] + * Retrieve conversation history with pagination + */ +export async function GET(request: NextRequest, context: RouteContext): Promise { + const { username, agentSlug, conversationId } = await context.params; + const { searchParams } = new URL(request.url); + + const format = searchParams.get('format'); + const limit = Math.min(Number.parseInt(searchParams.get('limit') || '50', 10), 100); + const before = searchParams.get('before'); + const after = searchParams.get('after'); + + try { + // Fetch agent by username and slug + const agent = await prisma.agent.findFirst({ + where: { + uid: agentSlug, + user: { username }, + }, + select: { + id: true, + uid: true, + name: true, + provider: true, + modelId: true, + executorType: true, + executorConfig: true, + }, + }); + + if (!agent) { + return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 }); + } + + // Fetch conversation + const conversation = await prisma.conversation.findUnique({ + where: { + agentId_slug: { + agentId: agent.id, + slug: conversationId, + }, + }, + }); + + if (!conversation) { + return NextResponse.json( + { success: false, error: 'Conversation not found' }, + { status: 404 } + ); + } + + // For JSON format, return all messages with full details + if (format === 'json') { + const allMessages = await prisma.message.findMany({ + where: { conversationId: conversation.id }, + orderBy: { createdAt: 'asc' }, + }); + + const response = { + _meta: { + exportedAt: new Date().toISOString(), + format: 'tpmjs-conversation-log', + version: '1.0', + }, + agent: { + id: agent.id, + uid: agent.uid, + name: agent.name, + provider: agent.provider, + modelId: agent.modelId, + executorType: agent.executorType, + executorConfig: agent.executorConfig, + }, + conversation: { + id: conversation.id, + slug: conversation.slug, + title: conversation.title, + createdAt: conversation.createdAt, + updatedAt: conversation.updatedAt, + messageCount: allMessages.length, + }, + messages: allMessages.map((m) => ({ + id: m.id, + role: m.role, + content: m.content, + toolCalls: m.toolCalls, + toolCallId: m.toolCallId, + toolName: m.toolName, + toolResult: m.toolResult, + inputTokens: m.inputTokens, + outputTokens: m.outputTokens, + createdAt: m.createdAt, + })), + stats: { + totalMessages: allMessages.length, + userMessages: allMessages.filter((m) => m.role === 'USER').length, + assistantMessages: allMessages.filter((m) => m.role === 'ASSISTANT').length, + toolResults: allMessages.filter((m) => m.role === 'TOOL').length, + totalInputTokens: allMessages.reduce((sum, m) => sum + (m.inputTokens || 0), 0), + totalOutputTokens: allMessages.reduce((sum, m) => sum + (m.outputTokens || 0), 0), + }, + }; + + // Return pretty-printed JSON with proper content type + return new NextResponse(JSON.stringify(response, null, 2), { + headers: { + 'Content-Type': 'application/json', + 'Content-Disposition': `inline; filename="${conversation.slug}-logs.json"`, + }, + }); + } + + // Standard paginated response + const whereClause: { + conversationId: string; + createdAt?: { lt?: Date; gt?: Date }; + } = { conversationId: conversation.id }; + + if (before) { + whereClause.createdAt = { lt: new Date(before) }; + } else if (after) { + whereClause.createdAt = { gt: new Date(after) }; + } + + // Determine fetch order: + // - Default (no cursor) or "before": Fetch desc (newest first), then reverse for chronological order + // - "after": Fetch asc (oldest first) to get messages after the cursor + const shouldFetchDesc = !after; + + // Fetch messages + const messages = await prisma.message.findMany({ + where: whereClause, + orderBy: { createdAt: shouldFetchDesc ? 'desc' : 'asc' }, + take: limit + 1, + }); + + const hasMore = messages.length > limit; + let paginatedMessages = hasMore ? messages.slice(0, limit) : messages; + + // Reverse if we fetched in desc order to maintain chronological order + if (shouldFetchDesc) { + paginatedMessages = paginatedMessages.reverse(); + } + + const mappedMessages = paginatedMessages.map((m) => ({ + id: m.id, + role: m.role, + content: m.content, + toolCalls: m.toolCalls, + toolCallId: m.toolCallId, + toolName: m.toolName, + toolResult: m.toolResult, + inputTokens: m.inputTokens, + outputTokens: m.outputTokens, + createdAt: m.createdAt, + })); + + return NextResponse.json({ + success: true, + data: { + id: conversation.id, + slug: conversation.slug, + title: conversation.title, + createdAt: conversation.createdAt, + updatedAt: conversation.updatedAt, + messages: mappedMessages, + }, + pagination: { + limit, + hasMore, + ...(before && { before }), + ...(after && { after }), + }, + }); + } catch (error) { + console.error('Failed to fetch conversation:', error); + return NextResponse.json( + { success: false, error: 'Failed to fetch conversation' }, + { status: 500 } + ); + } +} + +/** + * DELETE /api/[username]/agents/[agentSlug]/conversation/[conversationId] + * Delete a conversation + */ +export async function DELETE(_request: NextRequest, context: RouteContext): Promise { + const { username, agentSlug, conversationId } = await context.params; + + try { + // Fetch agent by username and slug + const agent = await prisma.agent.findFirst({ + where: { + uid: agentSlug, + user: { username }, + }, + select: { id: true }, + }); + + if (!agent) { + return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 }); + } + + // Delete conversation (messages cascade) + await prisma.conversation.deleteMany({ + where: { + agentId: agent.id, + slug: conversationId, + }, + }); + + return NextResponse.json({ + success: true, + data: { deleted: true }, + }); + } catch (error) { + console.error('Failed to delete conversation:', error); + return NextResponse.json( + { success: false, error: 'Failed to delete conversation' }, + { status: 500 } + ); + } +} diff --git a/apps/web/src/app/api/[username]/agents/[agentSlug]/conversations/route.ts b/apps/web/src/app/api/[username]/agents/[agentSlug]/conversations/route.ts new file mode 100644 index 0000000..5261e80 --- /dev/null +++ b/apps/web/src/app/api/[username]/agents/[agentSlug]/conversations/route.ts @@ -0,0 +1,84 @@ +/** + * Agent Conversations List Endpoint (Username/Slug-based version) + * + * GET: List all conversations for an agent + * + * Example: /api/ajax/agents/tpmjs-discord/conversations + */ + +import { prisma } from '@tpmjs/db'; +import { type NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 30; + +type RouteContext = { + params: Promise<{ username: string; agentSlug: string }>; +}; + +/** + * GET /api/[username]/agents/[agentSlug]/conversations + * List all conversations for an agent + */ +export async function GET(request: NextRequest, context: RouteContext): Promise { + const { username, agentSlug } = await context.params; + const { searchParams } = new URL(request.url); + + const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20', 10), 100); + const offset = Number.parseInt(searchParams.get('offset') || '0', 10); + + try { + // Fetch agent by username and slug + const agent = await prisma.agent.findFirst({ + where: { + uid: agentSlug, + user: { username }, + }, + select: { id: true }, + }); + + if (!agent) { + return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 }); + } + + // Fetch conversations with message count + const conversations = await prisma.conversation.findMany({ + where: { agentId: agent.id }, + orderBy: { updatedAt: 'desc' }, + take: limit + 1, + skip: offset, + include: { + _count: { + select: { messages: true }, + }, + }, + }); + + const hasMore = conversations.length > limit; + const data = hasMore ? conversations.slice(0, limit) : conversations; + + return NextResponse.json({ + success: true, + data: data.map((c) => ({ + id: c.id, + slug: c.slug, + title: c.title, + messageCount: c._count.messages, + createdAt: c.createdAt, + updatedAt: c.updatedAt, + })), + pagination: { + limit, + offset, + hasMore, + }, + }); + } catch (error) { + console.error('Failed to fetch conversations:', error); + return NextResponse.json( + { success: false, error: 'Failed to fetch conversations' }, + { status: 500 } + ); + } +} diff --git a/apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts b/apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts index 33b5ba7..f40ffba 100644 --- a/apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts +++ b/apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts @@ -114,11 +114,9 @@ export async function POST(request: NextRequest, context: RouteContext): Promise ); } - // Fetch agent with all tool relations using agent ID or UID - const { fetchAgentByIdOrUidWithTools, buildAgentTools } = await import( - '@/lib/agents/build-tools' - ); - const agent = await fetchAgentByIdOrUidWithTools(agentId); + // Fetch agent with all tool relations using agent ID + const { fetchAgentWithTools, buildAgentTools } = await import('@/lib/agents/build-tools'); + const agent = await fetchAgentWithTools(agentId); if (!agent) { return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 }); @@ -571,11 +569,9 @@ export async function GET(request: NextRequest, context: RouteContext): Promise< const after = searchParams.get('after'); try { - // Fetch agent by ID or UID with more details for JSON format - const agent = await prisma.agent.findFirst({ - where: { - OR: [{ id: agentId }, { uid: agentId }], - }, + // Fetch agent by ID with more details for JSON format + const agent = await prisma.agent.findUnique({ + where: { id: agentId }, select: { id: true, uid: true, @@ -748,11 +744,9 @@ export async function DELETE(_request: NextRequest, context: RouteContext): Prom const { id: agentId, conversationId } = await context.params; try { - // Fetch agent by ID or UID - const agent = await prisma.agent.findFirst({ - where: { - OR: [{ id: agentId }, { uid: agentId }], - }, + // Fetch agent by ID + const agent = await prisma.agent.findUnique({ + where: { id: agentId }, select: { id: true }, }); diff --git a/apps/web/src/app/api/agents/[id]/route.ts b/apps/web/src/app/api/agents/[id]/route.ts index 28faf82..605804c 100644 --- a/apps/web/src/app/api/agents/[id]/route.ts +++ b/apps/web/src/app/api/agents/[id]/route.ts @@ -36,6 +36,11 @@ export async function GET(_request: NextRequest, context: RouteContext) { const agent = await prisma.agent.findUnique({ where: { id }, include: { + user: { + select: { + username: true, + }, + }, collections: { include: { collection: { diff --git a/apps/web/src/app/dashboard/agents/[id]/page.tsx b/apps/web/src/app/dashboard/agents/[id]/page.tsx index 73a8c99..cbf8bc2 100644 --- a/apps/web/src/app/dashboard/agents/[id]/page.tsx +++ b/apps/web/src/app/dashboard/agents/[id]/page.tsx @@ -43,6 +43,9 @@ interface Agent { collectionCount: number; createdAt: string; updatedAt: string; + user: { + username: string; + }; } interface AgentTool { @@ -111,8 +114,9 @@ function ApiDocsSection({ agent, agentTools }: { agent: Agent; agentTools: Agent const [activeSection, setActiveSection] = useState('send'); const [activeLang, setActiveLang] = useState('curl'); const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'https://tpmjs.com'; - const endpoint = `${baseUrl}/api/agents/${agent.uid}/conversation/my-conv-1`; - const listEndpoint = `${baseUrl}/api/agents/${agent.uid}/conversations`; + const username = agent.user.username; + const endpoint = `${baseUrl}/api/${username}/agents/${agent.uid}/conversation/my-conv-1`; + const listEndpoint = `${baseUrl}/api/${username}/agents/${agent.uid}/conversations`; const toolPackages = agentTools.map((t) => t.tool.npmPackageName).join(' ') || '@tpmjs/hello'; const sendExamples: Record = { @@ -237,8 +241,8 @@ conv = resp.json() # conv['data']['messages']`,

API Reference

{isSend - ? `POST /api/agents/${agent.uid}/conversation/:id` - : `GET /api/agents/${agent.uid}/conversations`} + ? `POST /api/${username}/agents/${agent.uid}/conversation/:id` + : `GET /api/${username}/agents/${agent.uid}/conversations`}