mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
* feat(analytics): add PostHog product analytics * fix(posthog): fix workspace group via URL params, type errors, and clean up comments * fix(posthog): address PR review - fix pre-tx event, auth_method, paused executions, enterprise cancellation, settings double-fire * chore(posthog): remove unused identifyServerPerson * fix(posthog): isolate processQueuedResumes errors, simplify settings posthog deps * fix(posthog): correctly classify SSO auth_method, fix phantom empty-string workspace groups * fix(posthog): remove usePostHog from memo'd TemplateCard, fix copilot chat phantom workspace group * fix(posthog): eliminate all remaining phantom empty-string workspace groups * fix(posthog): fix cancel route phantom group, remove redundant workspaceId shadow in catch block * fix(posthog): use ids.length for block_removed guard to handle container blocks with descendants * chore(posthog): remove unused removedBlockTypes variable * fix(posthog): remove phantom $set person properties from subscription events * fix(posthog): add passedKnowledgeBaseName to knowledge_base_opened effect deps * fix(posthog): capture currentWorkflowId synchronously before async import to avoid stale closure * fix(posthog): add typed captureEvent wrapper for React components, deduplicate copilot_panel_opened * feat(posthog): add task_created and task_message_sent events, remove copilot_panel_opened * feat(posthog): track task_renamed, task_deleted, task_marked_read, task_marked_unread * feat(analytics): expand posthog event coverage with source tracking and lifecycle events * fix(analytics): flush posthog events on SIGTERM before ECS task termination * fix(analytics): fix posthog in useCallback deps and fire block events for bulk operations
176 lines
5.6 KiB
TypeScript
176 lines
5.6 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
||
import { type NextRequest, NextResponse } from 'next/server'
|
||
import { z } from 'zod'
|
||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||
import { getSession } from '@/lib/auth'
|
||
import { PlatformEvents } from '@/lib/core/telemetry'
|
||
import { generateRequestId } from '@/lib/core/utils/request'
|
||
import {
|
||
createKnowledgeBase,
|
||
getKnowledgeBases,
|
||
KnowledgeBaseConflictError,
|
||
type KnowledgeBaseScope,
|
||
} from '@/lib/knowledge/service'
|
||
import { captureServerEvent } from '@/lib/posthog/server'
|
||
|
||
const logger = createLogger('KnowledgeBaseAPI')
|
||
|
||
/**
|
||
* Schema for creating a knowledge base
|
||
*
|
||
* Chunking config units:
|
||
* - maxSize: tokens (1 token ≈ 4 characters)
|
||
* - minSize: characters
|
||
* - overlap: tokens (1 token ≈ 4 characters)
|
||
*/
|
||
const CreateKnowledgeBaseSchema = z.object({
|
||
name: z.string().min(1, 'Name is required'),
|
||
description: z.string().optional(),
|
||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
||
embeddingModel: z.literal('text-embedding-3-small').default('text-embedding-3-small'),
|
||
embeddingDimension: z.literal(1536).default(1536),
|
||
chunkingConfig: z
|
||
.object({
|
||
/** Maximum chunk size in tokens (1 token ≈ 4 characters) */
|
||
maxSize: z.number().min(100).max(4000).default(1024),
|
||
/** Minimum chunk size in characters */
|
||
minSize: z.number().min(1).max(2000).default(100),
|
||
/** Overlap between chunks in tokens (1 token ≈ 4 characters) */
|
||
overlap: z.number().min(0).max(500).default(200),
|
||
})
|
||
.default({
|
||
maxSize: 1024,
|
||
minSize: 100,
|
||
overlap: 200,
|
||
})
|
||
.refine(
|
||
(data) => {
|
||
// Convert maxSize from tokens to characters for comparison (1 token ≈ 4 chars)
|
||
const maxSizeInChars = data.maxSize * 4
|
||
return data.minSize < maxSizeInChars
|
||
},
|
||
{
|
||
message: 'Min chunk size (characters) must be less than max chunk size (tokens × 4)',
|
||
}
|
||
),
|
||
})
|
||
|
||
export async function GET(req: NextRequest) {
|
||
const requestId = generateRequestId()
|
||
|
||
try {
|
||
const session = await getSession()
|
||
if (!session?.user?.id) {
|
||
logger.warn(`[${requestId}] Unauthorized knowledge base access attempt`)
|
||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||
}
|
||
|
||
const { searchParams } = new URL(req.url)
|
||
const workspaceId = searchParams.get('workspaceId')
|
||
const scope = (searchParams.get('scope') ?? 'active') as KnowledgeBaseScope
|
||
if (!['active', 'archived', 'all'].includes(scope)) {
|
||
return NextResponse.json({ error: 'Invalid scope' }, { status: 400 })
|
||
}
|
||
|
||
const knowledgeBasesWithCounts = await getKnowledgeBases(session.user.id, workspaceId, scope)
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: knowledgeBasesWithCounts,
|
||
})
|
||
} catch (error) {
|
||
logger.error(`[${requestId}] Error fetching knowledge bases`, error)
|
||
return NextResponse.json({ error: 'Failed to fetch knowledge bases' }, { status: 500 })
|
||
}
|
||
}
|
||
|
||
export async function POST(req: NextRequest) {
|
||
const requestId = generateRequestId()
|
||
|
||
try {
|
||
const session = await getSession()
|
||
if (!session?.user?.id) {
|
||
logger.warn(`[${requestId}] Unauthorized knowledge base creation attempt`)
|
||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||
}
|
||
|
||
const body = await req.json()
|
||
|
||
try {
|
||
const validatedData = CreateKnowledgeBaseSchema.parse(body)
|
||
|
||
const createData = {
|
||
...validatedData,
|
||
userId: session.user.id,
|
||
}
|
||
|
||
const newKnowledgeBase = await createKnowledgeBase(createData, requestId)
|
||
|
||
try {
|
||
PlatformEvents.knowledgeBaseCreated({
|
||
knowledgeBaseId: newKnowledgeBase.id,
|
||
name: validatedData.name,
|
||
workspaceId: validatedData.workspaceId,
|
||
})
|
||
} catch {
|
||
// Telemetry should not fail the operation
|
||
}
|
||
|
||
captureServerEvent(
|
||
session.user.id,
|
||
'knowledge_base_created',
|
||
{
|
||
knowledge_base_id: newKnowledgeBase.id,
|
||
workspace_id: validatedData.workspaceId,
|
||
name: validatedData.name,
|
||
},
|
||
{
|
||
groups: { workspace: validatedData.workspaceId },
|
||
setOnce: { first_kb_created_at: new Date().toISOString() },
|
||
}
|
||
)
|
||
|
||
logger.info(
|
||
`[${requestId}] Knowledge base created: ${newKnowledgeBase.id} for user ${session.user.id}`
|
||
)
|
||
|
||
recordAudit({
|
||
workspaceId: validatedData.workspaceId,
|
||
actorId: session.user.id,
|
||
actorName: session.user.name,
|
||
actorEmail: session.user.email,
|
||
action: AuditAction.KNOWLEDGE_BASE_CREATED,
|
||
resourceType: AuditResourceType.KNOWLEDGE_BASE,
|
||
resourceId: newKnowledgeBase.id,
|
||
resourceName: validatedData.name,
|
||
description: `Created knowledge base "${validatedData.name}"`,
|
||
metadata: { name: validatedData.name },
|
||
request: req,
|
||
})
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: newKnowledgeBase,
|
||
})
|
||
} catch (validationError) {
|
||
if (validationError instanceof z.ZodError) {
|
||
logger.warn(`[${requestId}] Invalid knowledge base data`, {
|
||
errors: validationError.errors,
|
||
})
|
||
return NextResponse.json(
|
||
{ error: 'Invalid request data', details: validationError.errors },
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
throw validationError
|
||
}
|
||
} catch (error) {
|
||
if (error instanceof KnowledgeBaseConflictError) {
|
||
return NextResponse.json({ error: error.message }, { status: 409 })
|
||
}
|
||
|
||
logger.error(`[${requestId}] Error creating knowledge base`, error)
|
||
return NextResponse.json({ error: 'Failed to create knowledge base' }, { status: 500 })
|
||
}
|
||
}
|