import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { type NextRequest, NextResponse } from 'next/server' import { searchToolContract } from '@/lib/api/contracts/tools/search' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { SEARCH_TOOL_COST } from '@/lib/billing/constants' import { env } from '@/lib/core/config/env' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { executeTool } from '@/tools' const logger = createLogger('search') export const maxDuration = 60 export const dynamic = 'force-dynamic' export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateId() try { const { searchParams: urlParams } = new URL(request.url) const workflowId = urlParams.get('workflowId') || undefined const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { const errorMessage = workflowId ? 'Workflow not found' : authResult.error || 'Unauthorized' const statusCode = workflowId ? 404 : 401 return NextResponse.json({ success: false, error: errorMessage }, { status: statusCode }) } const userId = authResult.userId logger.info(`[${requestId}] Authenticated search request via ${authResult.authType}`, { userId, }) const parsed = await parseRequest(searchToolContract, request, {}) if (!parsed.success) return parsed.response const validated = parsed.data.body const exaApiKey = env.EXA_API_KEY if (!exaApiKey) { logger.error(`[${requestId}] No Exa API key available`) return NextResponse.json( { success: false, error: 'Search service not configured' }, { status: 503 } ) } logger.info(`[${requestId}] Executing search`, { userId, query: validated.query, }) const result = await executeTool('exa_search', { query: validated.query, type: 'auto', useAutoprompt: true, highlights: true, apiKey: exaApiKey, }) if (!result.success) { logger.error(`[${requestId}] Search failed`, { userId, error: result.error, }) return NextResponse.json( { success: false, error: result.error || 'Search failed', }, { status: 500 } ) } const results = (result.output.results || []).map((r: any, index: number) => ({ title: r.title || '', link: r.url || '', snippet: Array.isArray(r.highlights) ? r.highlights.join(' ... ') : '', date: r.publishedDate || undefined, position: index + 1, })) const cost = { input: 0, output: 0, total: SEARCH_TOOL_COST, tokens: { input: 0, output: 0, total: 0, }, model: 'search-exa', pricing: { input: 0, cachedInput: 0, output: 0, updatedAt: new Date().toISOString(), }, } logger.info(`[${requestId}] Search completed`, { userId, resultCount: results.length, cost: cost.total, }) return NextResponse.json({ results, query: validated.query, totalResults: results.length, source: 'exa', cost, }) } catch (error: any) { logger.error(`[${requestId}] Search failed`, { error: error.message, stack: error.stack, }) return NextResponse.json( { success: false, error: error.message || 'Search failed', }, { status: 500 } ) } })