feat(ai): optimize tool loops, fix deployments, and integrate new onboarding flow
This commit is contained in:
@@ -1,228 +0,0 @@
|
||||
/**
|
||||
* Backend Extraction Module
|
||||
*
|
||||
* Runs extraction as a pure backend job, not in chat.
|
||||
* Called when Collector phase completes.
|
||||
*/
|
||||
|
||||
import { getAdminDb } from '@/lib/firebase/admin';
|
||||
import { GeminiLlmClient } from '@/lib/ai/gemini-client';
|
||||
import { BACKEND_EXTRACTOR_SYSTEM_PROMPT } from '@/lib/ai/prompts/extractor';
|
||||
import { writeKnowledgeChunksForItem } from '@/lib/server/vector-memory';
|
||||
import type { ExtractionOutput, ExtractedInsight } from '@/lib/types/extraction-output';
|
||||
import type { PhaseHandoff } from '@/lib/types/phase-handoff';
|
||||
import { z } from 'zod';
|
||||
|
||||
const ExtractionOutputSchema = z.object({
|
||||
insights: z.array(z.object({
|
||||
id: z.string(),
|
||||
type: z.enum(["problem", "user", "feature", "constraint", "opportunity", "other"]),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
sourceText: z.string(),
|
||||
sourceKnowledgeItemId: z.string(),
|
||||
importance: z.enum(["primary", "supporting"]),
|
||||
confidence: z.number().min(0).max(1),
|
||||
})),
|
||||
problems: z.array(z.string()),
|
||||
targetUsers: z.array(z.string()),
|
||||
features: z.array(z.string()),
|
||||
constraints: z.array(z.string()),
|
||||
opportunities: z.array(z.string()),
|
||||
uncertainties: z.array(z.string()),
|
||||
missingInformation: z.array(z.string()),
|
||||
overallConfidence: z.number().min(0).max(1),
|
||||
});
|
||||
|
||||
export async function runBackendExtractionForProject(projectId: string): Promise<void> {
|
||||
console.log(`[Backend Extractor] Starting extraction for project ${projectId}`);
|
||||
|
||||
const adminDb = getAdminDb();
|
||||
|
||||
try {
|
||||
// 1. Load project
|
||||
const projectDoc = await adminDb.collection('projects').doc(projectId).get();
|
||||
if (!projectDoc.exists) {
|
||||
throw new Error(`Project ${projectId} not found`);
|
||||
}
|
||||
|
||||
const projectData = projectDoc.data();
|
||||
|
||||
// 2. Load knowledge items
|
||||
const knowledgeSnapshot = await adminDb
|
||||
.collection('knowledge_items')
|
||||
.where('projectId', '==', projectId)
|
||||
.where('sourceType', '==', 'imported_document')
|
||||
.get();
|
||||
|
||||
if (knowledgeSnapshot.empty) {
|
||||
console.log(`[Backend Extractor] No documents to extract for project ${projectId} - creating empty handoff`);
|
||||
|
||||
// Create a minimal extraction handoff even with no documents
|
||||
const emptyHandoff: PhaseHandoff = {
|
||||
phase: 'extraction',
|
||||
readyForNextPhase: false, // Not ready - no materials to extract from
|
||||
confidence: 0,
|
||||
confirmed: {
|
||||
problems: [],
|
||||
targetUsers: [],
|
||||
features: [],
|
||||
constraints: [],
|
||||
opportunities: [],
|
||||
},
|
||||
uncertain: {},
|
||||
missing: ['No documents uploaded - need product requirements, specs, or notes'],
|
||||
questionsForUser: [
|
||||
'You haven\'t uploaded any documents yet. Do you have any product specs, requirements, or notes to share?',
|
||||
],
|
||||
sourceEvidence: [],
|
||||
version: 'extraction_v1',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await adminDb.collection('projects').doc(projectId).update({
|
||||
'phaseData.phaseHandoffs.extraction': emptyHandoff,
|
||||
currentPhase: 'extraction_review',
|
||||
phaseStatus: 'in_progress',
|
||||
'phaseData.extractionCompletedAt': new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
console.log(`[Backend Extractor] Set phase to extraction_review with empty handoff`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Backend Extractor] Found ${knowledgeSnapshot.size} documents to process`);
|
||||
|
||||
const llm = new GeminiLlmClient();
|
||||
const allExtractionOutputs: ExtractionOutput[] = [];
|
||||
const processedKnowledgeItemIds: string[] = [];
|
||||
|
||||
// 3. Process each document
|
||||
for (const knowledgeDoc of knowledgeSnapshot.docs) {
|
||||
const knowledgeData = knowledgeDoc.data();
|
||||
const knowledgeItemId = knowledgeDoc.id;
|
||||
|
||||
try {
|
||||
console.log(`[Backend Extractor] Processing document: ${knowledgeData.title || knowledgeItemId}`);
|
||||
|
||||
// Call LLM with structured extraction + thinking mode
|
||||
const extraction = await llm.structuredCall<ExtractionOutput>({
|
||||
model: 'gemini',
|
||||
systemPrompt: BACKEND_EXTRACTOR_SYSTEM_PROMPT,
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: `Document Title: ${knowledgeData.title || 'Untitled'}\nSource Type: ${knowledgeData.sourceType}\n\nContent:\n${knowledgeData.content}`,
|
||||
}],
|
||||
schema: ExtractionOutputSchema as any,
|
||||
temperature: 1.0, // Gemini 3 default (changed from 0.3)
|
||||
thinking_config: {
|
||||
thinking_level: 'high', // Enable deep reasoning for document analysis
|
||||
include_thoughts: false, // Don't include thought tokens in output (saves cost)
|
||||
},
|
||||
});
|
||||
|
||||
// Add knowledgeItemId to each insight
|
||||
extraction.insights.forEach(insight => {
|
||||
insight.sourceKnowledgeItemId = knowledgeItemId;
|
||||
});
|
||||
|
||||
allExtractionOutputs.push(extraction);
|
||||
processedKnowledgeItemIds.push(knowledgeItemId);
|
||||
|
||||
// 4. Persist extraction to chat_extractions
|
||||
await adminDb.collection('chat_extractions').add({
|
||||
projectId,
|
||||
knowledgeItemId,
|
||||
data: extraction,
|
||||
overallConfidence: extraction.overallConfidence,
|
||||
overallCompletion: extraction.overallConfidence > 0.7 ? 0.9 : 0.6,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
console.log(`[Backend Extractor] Extracted ${extraction.insights.length} insights from ${knowledgeData.title || knowledgeItemId}`);
|
||||
|
||||
// 5. Write vector chunks for primary insights
|
||||
const primaryInsights = extraction.insights.filter(i => i.importance === 'primary');
|
||||
for (const insight of primaryInsights) {
|
||||
try {
|
||||
// Create a knowledge chunk for this insight
|
||||
await writeKnowledgeChunksForItem({
|
||||
id: knowledgeItemId,
|
||||
projectId,
|
||||
content: `${insight.title}\n\n${insight.description}\n\nSource: ${insight.sourceText}`,
|
||||
sourceMeta: {
|
||||
sourceType: 'extracted_insight',
|
||||
importance: 'primary',
|
||||
},
|
||||
});
|
||||
} catch (chunkError) {
|
||||
console.error(`[Backend Extractor] Failed to write chunk for insight ${insight.id}:`, chunkError);
|
||||
// Continue processing other insights
|
||||
}
|
||||
}
|
||||
|
||||
} catch (docError) {
|
||||
console.error(`[Backend Extractor] Failed to process document ${knowledgeItemId}:`, docError);
|
||||
// Continue with next document
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Build extraction PhaseHandoff
|
||||
// Flatten all extracted items (they're already strings, not objects)
|
||||
const allProblems = [...new Set(allExtractionOutputs.flatMap(e => e.problems))];
|
||||
const allUsers = [...new Set(allExtractionOutputs.flatMap(e => e.targetUsers))];
|
||||
const allFeatures = [...new Set(allExtractionOutputs.flatMap(e => e.features))];
|
||||
const allConstraints = [...new Set(allExtractionOutputs.flatMap(e => e.constraints))];
|
||||
const allOpportunities = [...new Set(allExtractionOutputs.flatMap(e => e.opportunities))];
|
||||
const allUncertainties = [...new Set(allExtractionOutputs.flatMap(e => e.uncertainties))];
|
||||
const allMissing = [...new Set(allExtractionOutputs.flatMap(e => e.missingInformation))];
|
||||
|
||||
const avgConfidence = allExtractionOutputs.length > 0
|
||||
? allExtractionOutputs.reduce((sum, e) => sum + e.overallConfidence, 0) / allExtractionOutputs.length
|
||||
: 0;
|
||||
|
||||
const readyForNextPhase = allProblems.length > 0 && allFeatures.length > 0 && avgConfidence > 0.5;
|
||||
|
||||
const extractionHandoff: PhaseHandoff = {
|
||||
phase: 'extraction',
|
||||
readyForNextPhase,
|
||||
confidence: avgConfidence,
|
||||
confirmed: {
|
||||
problems: allProblems,
|
||||
targetUsers: allUsers,
|
||||
features: allFeatures,
|
||||
constraints: allConstraints,
|
||||
opportunities: allOpportunities,
|
||||
},
|
||||
uncertain: {},
|
||||
missing: allMissing,
|
||||
questionsForUser: allUncertainties,
|
||||
sourceEvidence: processedKnowledgeItemIds,
|
||||
version: 'extraction_v1',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 7. Persist handoff and update phase
|
||||
await adminDb.collection('projects').doc(projectId).update({
|
||||
'phaseData.phaseHandoffs.extraction': extractionHandoff,
|
||||
currentPhase: 'extraction_review',
|
||||
phaseStatus: 'in_progress',
|
||||
'phaseData.extractionCompletedAt': new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
console.log(`[Backend Extractor] ✅ Extraction complete for project ${projectId}`);
|
||||
console.log(`[Backend Extractor] - Problems: ${allProblems.length}`);
|
||||
console.log(`[Backend Extractor] - Users: ${allUsers.length}`);
|
||||
console.log(`[Backend Extractor] - Features: ${allFeatures.length}`);
|
||||
console.log(`[Backend Extractor] - Confidence: ${(avgConfidence * 100).toFixed(1)}%`);
|
||||
console.log(`[Backend Extractor] - Ready for next phase: ${readyForNextPhase}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Backend Extractor] Fatal error during extraction:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { getAdminDb } from '@/lib/firebase/admin';
|
||||
import { FieldValue } from 'firebase-admin/firestore';
|
||||
import type { ChatExtractionRecord } from '@/lib/types/chat-extraction';
|
||||
|
||||
const COLLECTION = 'chat_extractions';
|
||||
|
||||
interface CreateChatExtractionInput<TData> {
|
||||
projectId: string;
|
||||
knowledgeItemId: string;
|
||||
data: TData;
|
||||
overallCompletion: number;
|
||||
overallConfidence: number;
|
||||
}
|
||||
|
||||
export async function createChatExtraction<TData>(
|
||||
input: CreateChatExtractionInput<TData>,
|
||||
): Promise<ChatExtractionRecord<TData>> {
|
||||
const adminDb = getAdminDb();
|
||||
const docRef = adminDb.collection(COLLECTION).doc();
|
||||
|
||||
const payload = {
|
||||
id: docRef.id,
|
||||
projectId: input.projectId,
|
||||
knowledgeItemId: input.knowledgeItemId,
|
||||
data: input.data,
|
||||
overallCompletion: input.overallCompletion,
|
||||
overallConfidence: input.overallConfidence,
|
||||
createdAt: FieldValue.serverTimestamp(),
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
};
|
||||
|
||||
await docRef.set(payload);
|
||||
const snapshot = await docRef.get();
|
||||
return snapshot.data() as ChatExtractionRecord<TData>;
|
||||
}
|
||||
|
||||
export async function listChatExtractions<TData>(
|
||||
projectId: string,
|
||||
): Promise<ChatExtractionRecord<TData>[]> {
|
||||
const adminDb = getAdminDb();
|
||||
const querySnapshot = await adminDb
|
||||
.collection(COLLECTION)
|
||||
.where('projectId', '==', projectId)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.get();
|
||||
|
||||
return querySnapshot.docs.map(
|
||||
(doc) => doc.data() as ChatExtractionRecord<TData>,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getChatExtraction<TData>(
|
||||
extractionId: string,
|
||||
): Promise<ChatExtractionRecord<TData> | null> {
|
||||
const adminDb = getAdminDb();
|
||||
const docRef = adminDb.collection(COLLECTION).doc(extractionId);
|
||||
const snapshot = await docRef.get();
|
||||
if (!snapshot.exists) {
|
||||
return null;
|
||||
}
|
||||
return snapshot.data() as ChatExtractionRecord<TData>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* Chat Mode Resolution Logic
|
||||
*
|
||||
* Determines which chat mode (collector, extraction_review, vision, mvp, marketing, general)
|
||||
* should be active based on project state stored in Postgres.
|
||||
*/
|
||||
|
||||
import { query } from '@/lib/db-postgres';
|
||||
import type { ChatMode } from '@/lib/ai/chat-modes';
|
||||
|
||||
/**
|
||||
* Resolve the appropriate chat mode for a project using Postgres (fs_projects).
|
||||
*/
|
||||
export async function resolveChatMode(projectId: string): Promise<ChatMode> {
|
||||
try {
|
||||
const rows = await query<{ data: any }>(
|
||||
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
|
||||
[projectId]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.warn(`[Chat Mode Resolver] Project ${projectId} not found`);
|
||||
return 'collector_mode';
|
||||
}
|
||||
|
||||
const projectData = rows[0].data ?? {};
|
||||
const phaseData = (projectData.phaseData ?? {}) as Record<string, any>;
|
||||
const currentPhase: string = projectData.currentPhase ?? 'collector';
|
||||
|
||||
// Explicit phase overrides
|
||||
if (currentPhase === 'extraction_review' || currentPhase === 'analyzed') return 'extraction_review_mode';
|
||||
if (currentPhase === 'vision') return 'vision_mode';
|
||||
if (currentPhase === 'mvp') return 'mvp_mode';
|
||||
if (currentPhase === 'marketing') return 'marketing_mode';
|
||||
|
||||
// Derive from phase artifacts
|
||||
if (!phaseData.canonicalProductModel) return 'collector_mode';
|
||||
if (!phaseData.mvpPlan) return 'vision_mode';
|
||||
if (!phaseData.marketingPlan) return 'mvp_mode';
|
||||
if (phaseData.marketingPlan) return 'marketing_mode';
|
||||
|
||||
return 'general_chat_mode';
|
||||
} catch (error) {
|
||||
console.error('[Chat Mode Resolver] Failed to resolve mode:', error);
|
||||
return 'collector_mode';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarise knowledge items for context building.
|
||||
* Uses Postgres fs_knowledge_items if available, otherwise returns empty.
|
||||
*/
|
||||
export async function summarizeKnowledgeItems(projectId: string): Promise<{
|
||||
totalCount: number;
|
||||
bySourceType: Record<string, number>;
|
||||
recentTitles: string[];
|
||||
}> {
|
||||
try {
|
||||
const rows = await query<{ data: any }>(
|
||||
`SELECT data FROM fs_knowledge_items WHERE project_id = $1 ORDER BY created_at DESC LIMIT 20`,
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const bySourceType: Record<string, number> = {};
|
||||
const recentTitles: string[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const d = row.data ?? {};
|
||||
const sourceType = d.sourceType ?? 'unknown';
|
||||
bySourceType[sourceType] = (bySourceType[sourceType] ?? 0) + 1;
|
||||
if (d.title && recentTitles.length < 5) recentTitles.push(d.title);
|
||||
}
|
||||
|
||||
return { totalCount: rows.length, bySourceType, recentTitles };
|
||||
} catch {
|
||||
// Table may not exist for older deployments — return empty
|
||||
return { totalCount: 0, bySourceType: {}, recentTitles: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarise extractions for context building.
|
||||
* Returns empty defaults — extractions not yet migrated to Postgres.
|
||||
*/
|
||||
export async function summarizeExtractions(projectId: string): Promise<{
|
||||
totalCount: number;
|
||||
avgConfidence: number;
|
||||
avgCompletion: number;
|
||||
}> {
|
||||
return { totalCount: 0, avgConfidence: 0, avgCompletion: 0 };
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
/**
|
||||
* Persistent dev-server configuration store.
|
||||
* Closes BETA_LAUNCH_PLAN P6.B1.
|
||||
*
|
||||
* When `dev_server_start` succeeds, the MCP tool should call
|
||||
* `upsertDevServerConfig` so the project page can auto-resume the
|
||||
* server on next mount without requiring the user to re-type the
|
||||
* command (see P6.B2 for the auto-resume hook).
|
||||
*
|
||||
* Schema:
|
||||
* fs_project_dev_servers
|
||||
* project_id UUID PK → fs_projects.id
|
||||
* command TEXT NOT NULL e.g. "cd myapp && npm run dev"
|
||||
* port INT NOT NULL e.g. 3000
|
||||
* framework TEXT e.g. "nextjs", "vite", "express"
|
||||
* preview_url TEXT last known *.preview.vibnai.com URL
|
||||
* last_started_at TIMESTAMPTZ
|
||||
* status TEXT CHECK IN ('running','stopped','crashed')
|
||||
* updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
*/
|
||||
|
||||
import { query } from "@/lib/db-postgres";
|
||||
import { log } from "@/lib/server/logger";
|
||||
|
||||
let tableReady = false;
|
||||
async function ensureTable() {
|
||||
if (tableReady) return;
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS fs_project_dev_servers (
|
||||
project_id TEXT PRIMARY KEY,
|
||||
command TEXT NOT NULL,
|
||||
port INT NOT NULL,
|
||||
framework TEXT,
|
||||
preview_url TEXT,
|
||||
last_started_at TIMESTAMPTZ,
|
||||
status TEXT NOT NULL DEFAULT 'stopped'
|
||||
CHECK (status IN ('running', 'stopped', 'crashed')),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
tableReady = true;
|
||||
}
|
||||
|
||||
export interface DevServerConfig {
|
||||
projectId: string;
|
||||
command: string;
|
||||
port: number;
|
||||
framework?: string;
|
||||
previewUrl?: string;
|
||||
status: "running" | "stopped" | "crashed";
|
||||
}
|
||||
|
||||
/** Called by the MCP dev_server_start handler after a successful start. */
|
||||
export async function upsertDevServerConfig(
|
||||
cfg: DevServerConfig,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await ensureTable();
|
||||
await query(
|
||||
`INSERT INTO fs_project_dev_servers
|
||||
(project_id, command, port, framework, preview_url, last_started_at, status, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW(), $6, NOW())
|
||||
ON CONFLICT (project_id) DO UPDATE SET
|
||||
command = EXCLUDED.command,
|
||||
port = EXCLUDED.port,
|
||||
framework = COALESCE(EXCLUDED.framework, fs_project_dev_servers.framework),
|
||||
preview_url = COALESCE(EXCLUDED.preview_url, fs_project_dev_servers.preview_url),
|
||||
last_started_at = NOW(),
|
||||
status = EXCLUDED.status,
|
||||
updated_at = NOW()`,
|
||||
[
|
||||
cfg.projectId,
|
||||
cfg.command,
|
||||
cfg.port,
|
||||
cfg.framework ?? null,
|
||||
cfg.previewUrl ?? null,
|
||||
cfg.status,
|
||||
],
|
||||
);
|
||||
} catch (err) {
|
||||
log.warn("dev-server-state: upsert failed (non-fatal)", {
|
||||
projectId: cfg.projectId,
|
||||
err: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Update just the status (e.g. on stop / crash). */
|
||||
export async function setDevServerStatus(
|
||||
projectId: string,
|
||||
status: "running" | "stopped" | "crashed",
|
||||
): Promise<void> {
|
||||
try {
|
||||
await ensureTable();
|
||||
await query(
|
||||
`UPDATE fs_project_dev_servers
|
||||
SET status = $2, updated_at = NOW()
|
||||
WHERE project_id = $1`,
|
||||
[projectId, status],
|
||||
);
|
||||
} catch (err) {
|
||||
log.warn("dev-server-state: status update failed (non-fatal)", {
|
||||
projectId,
|
||||
err: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the last-known dev server config for a project, or null. */
|
||||
export async function getDevServerConfig(
|
||||
projectId: string,
|
||||
): Promise<DevServerConfig | null> {
|
||||
try {
|
||||
await ensureTable();
|
||||
const rows = await query<{
|
||||
project_id: string;
|
||||
command: string;
|
||||
port: number;
|
||||
framework: string | null;
|
||||
preview_url: string | null;
|
||||
status: string;
|
||||
}>(
|
||||
`SELECT project_id, command, port, framework, preview_url, status
|
||||
FROM fs_project_dev_servers WHERE project_id = $1`,
|
||||
[projectId],
|
||||
);
|
||||
if (!rows[0]) return null;
|
||||
const r = rows[0];
|
||||
return {
|
||||
projectId: r.project_id,
|
||||
command: r.command,
|
||||
port: r.port,
|
||||
framework: r.framework ?? undefined,
|
||||
previewUrl: r.preview_url ?? undefined,
|
||||
status: r.status as "running" | "stopped" | "crashed",
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/**
|
||||
* Coolify connectivity checks for ops / uptime monitors.
|
||||
*
|
||||
* - HTTP API (token) — provisioning via REST
|
||||
* - SSH → Docker on host — required for shell.exec / dev containers
|
||||
*/
|
||||
|
||||
import { listServers } from '@/lib/coolify';
|
||||
import { runOnCoolifyHost } from '@/lib/coolify-ssh';
|
||||
|
||||
export interface CoolifyInfraHealthReport {
|
||||
checkedAt: string;
|
||||
ssh: {
|
||||
configured: boolean;
|
||||
missingEnvVars: string[];
|
||||
reachable?: boolean;
|
||||
latencyMs?: number;
|
||||
dockerDaemonOk?: boolean;
|
||||
/** docker Server.Version when daemon responds */
|
||||
dockerVersion?: string;
|
||||
error?: string;
|
||||
};
|
||||
api: {
|
||||
configured: boolean;
|
||||
reachable?: boolean;
|
||||
latencyMs?: number;
|
||||
serverCount?: number;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function getCoolifySshConfigGap(): {
|
||||
configured: boolean;
|
||||
missingEnvVars: string[];
|
||||
} {
|
||||
const missing: string[] = [];
|
||||
if (!process.env.COOLIFY_SSH_HOST?.trim()) missing.push('COOLIFY_SSH_HOST');
|
||||
if (!process.env.COOLIFY_SSH_PRIVATE_KEY_B64?.trim()) {
|
||||
missing.push('COOLIFY_SSH_PRIVATE_KEY_B64');
|
||||
}
|
||||
return { configured: missing.length === 0, missingEnvVars: missing };
|
||||
}
|
||||
|
||||
/** True when SSH is wired and `docker` responds on the Coolify host. */
|
||||
export async function runCoolifyInfraHealthProbe(): Promise<CoolifyInfraHealthReport> {
|
||||
const checkedAt = new Date().toISOString();
|
||||
const sshGap = getCoolifySshConfigGap();
|
||||
|
||||
const report: CoolifyInfraHealthReport = {
|
||||
checkedAt,
|
||||
ssh: {
|
||||
configured: sshGap.configured,
|
||||
missingEnvVars: sshGap.missingEnvVars,
|
||||
},
|
||||
api: {
|
||||
configured: !!process.env.COOLIFY_API_TOKEN?.trim(),
|
||||
},
|
||||
};
|
||||
|
||||
if (report.api.configured) {
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
const servers = await listServers();
|
||||
report.api.reachable = true;
|
||||
report.api.latencyMs = Date.now() - t0;
|
||||
report.api.serverCount = Array.isArray(servers) ? servers.length : 0;
|
||||
} catch (e) {
|
||||
report.api.reachable = false;
|
||||
report.api.latencyMs = Date.now() - t0;
|
||||
report.api.error = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
} else {
|
||||
report.api.error = 'COOLIFY_API_TOKEN is not set';
|
||||
}
|
||||
|
||||
if (!sshGap.configured) {
|
||||
report.ssh.error =
|
||||
`Missing: ${sshGap.missingEnvVars.join(', ')} — dev containers need SSH to the Docker host (see lib/coolify-ssh.ts).`;
|
||||
return report;
|
||||
}
|
||||
|
||||
const tSsh = Date.now();
|
||||
try {
|
||||
const res = await runOnCoolifyHost(`docker info --format '{{.ServerVersion}}'`, {
|
||||
timeoutMs: 15_000,
|
||||
maxBytes: 8192,
|
||||
});
|
||||
report.ssh.latencyMs = Date.now() - tSsh;
|
||||
report.ssh.reachable = true;
|
||||
const ver = res.stdout.trim().split(/\s+/)[0]?.trim() ?? '';
|
||||
if (res.code === 0 && ver.length > 0) {
|
||||
report.ssh.dockerDaemonOk = true;
|
||||
report.ssh.dockerVersion = ver;
|
||||
} else {
|
||||
report.ssh.dockerDaemonOk = false;
|
||||
report.ssh.error = `docker probe exit ${res.code}: ${(res.stderr || res.stdout || '(empty)').slice(0, 600)}`;
|
||||
}
|
||||
} catch (e) {
|
||||
report.ssh.latencyMs = Date.now() - tSsh;
|
||||
report.ssh.reachable = false;
|
||||
report.ssh.dockerDaemonOk = false;
|
||||
report.ssh.error = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
export function isCoolifyInfraOperational(report: CoolifyInfraHealthReport): boolean {
|
||||
return (
|
||||
report.ssh.configured &&
|
||||
report.ssh.reachable === true &&
|
||||
report.ssh.dockerDaemonOk === true &&
|
||||
report.api.configured &&
|
||||
report.api.reachable === true
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { listChatExtractions } from '@/lib/server/chat-extraction';
|
||||
import { clamp, nowIso, persistPhaseArtifacts, uniqueStrings, toStage } from '@/lib/server/projects';
|
||||
import type { CanonicalProductModel } from '@/lib/types/product-model';
|
||||
import type { ChatExtractionRecord } from '@/lib/types/chat-extraction';
|
||||
|
||||
const average = (numbers: number[]) =>
|
||||
numbers.length ? numbers.reduce((sum, value) => sum + value, 0) / numbers.length : 0;
|
||||
|
||||
export async function buildCanonicalProductModel(projectId: string): Promise<CanonicalProductModel> {
|
||||
const extractions = await listChatExtractions(projectId);
|
||||
if (!extractions.length) {
|
||||
throw new Error('No chat extractions found for project');
|
||||
}
|
||||
|
||||
const completionAvg = average(
|
||||
extractions.map(
|
||||
(record) =>
|
||||
(record.data as any)?.summary_scores?.overall_completion ?? record.overallCompletion ?? 0,
|
||||
),
|
||||
);
|
||||
const confidenceAvg = average(
|
||||
extractions.map(
|
||||
(record) =>
|
||||
(record.data as any)?.summary_scores?.overall_confidence ?? record.overallConfidence ?? 0,
|
||||
),
|
||||
);
|
||||
|
||||
const canonical = mapExtractionToCanonical(
|
||||
projectId,
|
||||
pickHighestConfidence(extractions as any),
|
||||
completionAvg,
|
||||
confidenceAvg,
|
||||
);
|
||||
|
||||
await persistPhaseArtifacts(projectId, (phaseData, phaseScores, phaseHistory) => {
|
||||
phaseData.canonicalProductModel = canonical;
|
||||
phaseScores.vision = {
|
||||
overallCompletion: canonical.overallCompletion,
|
||||
overallConfidence: canonical.overallConfidence,
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
phaseHistory.push({ phase: 'vision', status: 'completed', timestamp: nowIso() });
|
||||
return { phaseData, phaseScores, phaseHistory, nextPhase: 'vision_ready' };
|
||||
});
|
||||
|
||||
return canonical;
|
||||
}
|
||||
|
||||
function pickHighestConfidence(records: ChatExtractionRecord[]) {
|
||||
return records.reduce((best, record) =>
|
||||
record.overallConfidence > best.overallConfidence ? record : best,
|
||||
);
|
||||
}
|
||||
|
||||
function mapExtractionToCanonical(
|
||||
projectId: string,
|
||||
record: ChatExtractionRecord,
|
||||
completionAvg: number,
|
||||
confidenceAvg: number,
|
||||
): CanonicalProductModel {
|
||||
const data = record.data;
|
||||
|
||||
const coreFeatures = data.solution_and_features.core_features.map(
|
||||
(feature) => feature.name || feature.description,
|
||||
);
|
||||
const niceToHaveFeatures = data.solution_and_features.nice_to_have_features.map(
|
||||
(feature) => feature.name || feature.description,
|
||||
);
|
||||
|
||||
return {
|
||||
projectId,
|
||||
workingTitle: data.project_summary.working_title ?? null,
|
||||
oneLiner: data.project_summary.one_liner ?? null,
|
||||
problem: data.product_vision.problem_statement.description ?? null,
|
||||
targetUser: data.target_users.primary_segment.description ?? null,
|
||||
desiredOutcome: data.product_vision.target_outcome.description ?? null,
|
||||
coreSolution: data.solution_and_features.core_solution.description ?? null,
|
||||
coreFeatures: uniqueStrings(coreFeatures),
|
||||
niceToHaveFeatures: uniqueStrings(niceToHaveFeatures),
|
||||
marketCategory: data.market_and_competition.market_category.description ?? null,
|
||||
competitors: uniqueStrings(
|
||||
data.market_and_competition.competitors.map((competitor) => competitor.name),
|
||||
),
|
||||
techStack: uniqueStrings(
|
||||
data.tech_and_constraints.stack_mentions.map((item) => item.description),
|
||||
),
|
||||
constraints: uniqueStrings(
|
||||
data.tech_and_constraints.constraints.map((constraint) => constraint.description),
|
||||
),
|
||||
currentStage: toStage(data.project_summary.stage),
|
||||
shortTermGoals: uniqueStrings(
|
||||
data.goals_and_success.short_term_goals.map((goal) => goal.description),
|
||||
),
|
||||
longTermGoals: uniqueStrings(
|
||||
data.goals_and_success.long_term_goals.map((goal) => goal.description),
|
||||
),
|
||||
overallCompletion: clamp(completionAvg),
|
||||
overallConfidence: clamp(confidenceAvg),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,453 +0,0 @@
|
||||
/**
|
||||
* Server-side helpers for AlloyDB vector memory operations
|
||||
*
|
||||
* Handles CRUD operations on knowledge_chunks and semantic search.
|
||||
*/
|
||||
|
||||
import { getAlloyDbClient, executeQuery, getPooledClient } from '@/lib/db/alloydb';
|
||||
import type {
|
||||
KnowledgeChunk,
|
||||
KnowledgeChunkRow,
|
||||
KnowledgeChunkSearchResult,
|
||||
VectorSearchOptions,
|
||||
CreateKnowledgeChunkInput,
|
||||
BatchCreateKnowledgeChunksInput,
|
||||
} from '@/lib/types/vector-memory';
|
||||
|
||||
/**
|
||||
* Convert database row (snake_case) to TypeScript object (camelCase)
|
||||
*/
|
||||
function rowToKnowledgeChunk(row: KnowledgeChunkRow): KnowledgeChunk {
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.project_id,
|
||||
knowledgeItemId: row.knowledge_item_id,
|
||||
chunkIndex: row.chunk_index,
|
||||
content: row.content,
|
||||
sourceType: row.source_type,
|
||||
importance: row.importance,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve relevant knowledge chunks using vector similarity search
|
||||
*
|
||||
* @param projectId - Firestore project ID to filter by
|
||||
* @param queryEmbedding - Vector embedding of the query (e.g., user's question)
|
||||
* @param options - Search options (limit, filters, etc.)
|
||||
* @returns Array of chunks ordered by similarity (most relevant first)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const embedding = await embedText("What's the MVP scope?");
|
||||
* const chunks = await retrieveRelevantChunks('proj123', embedding, { limit: 10, minSimilarity: 0.7 });
|
||||
* ```
|
||||
*/
|
||||
export async function retrieveRelevantChunks(
|
||||
projectId: string,
|
||||
queryEmbedding: number[],
|
||||
options: VectorSearchOptions = {}
|
||||
): Promise<KnowledgeChunkSearchResult[]> {
|
||||
const {
|
||||
limit = 10,
|
||||
minSimilarity,
|
||||
sourceTypes,
|
||||
importanceLevels,
|
||||
} = options;
|
||||
|
||||
try {
|
||||
// Build the query with optional filters
|
||||
let queryText = `
|
||||
SELECT
|
||||
id,
|
||||
project_id,
|
||||
knowledge_item_id,
|
||||
chunk_index,
|
||||
content,
|
||||
source_type,
|
||||
importance,
|
||||
created_at,
|
||||
updated_at,
|
||||
1 - (embedding <=> $1::vector) AS similarity
|
||||
FROM knowledge_chunks
|
||||
WHERE project_id = $2
|
||||
`;
|
||||
|
||||
const params: any[] = [JSON.stringify(queryEmbedding), projectId];
|
||||
let paramIndex = 3;
|
||||
|
||||
// Filter by source types
|
||||
if (sourceTypes && sourceTypes.length > 0) {
|
||||
queryText += ` AND source_type = ANY($${paramIndex})`;
|
||||
params.push(sourceTypes);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Filter by importance levels
|
||||
if (importanceLevels && importanceLevels.length > 0) {
|
||||
queryText += ` AND importance = ANY($${paramIndex})`;
|
||||
params.push(importanceLevels);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Filter by minimum similarity
|
||||
if (minSimilarity !== undefined) {
|
||||
queryText += ` AND (1 - (embedding <=> $1::vector)) >= $${paramIndex}`;
|
||||
params.push(minSimilarity);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Order by similarity and limit
|
||||
queryText += ` ORDER BY embedding <=> $1::vector LIMIT $${paramIndex}`;
|
||||
params.push(limit);
|
||||
|
||||
const result = await executeQuery<KnowledgeChunkRow & { similarity: number }>(
|
||||
queryText,
|
||||
params
|
||||
);
|
||||
|
||||
return result.rows.map((row) => ({
|
||||
...rowToKnowledgeChunk(row),
|
||||
similarity: row.similarity,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[Vector Memory] Failed to retrieve relevant chunks:', error);
|
||||
throw new Error(
|
||||
`Failed to retrieve chunks: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single knowledge chunk
|
||||
*/
|
||||
export async function createKnowledgeChunk(
|
||||
input: CreateKnowledgeChunkInput
|
||||
): Promise<KnowledgeChunk> {
|
||||
const {
|
||||
projectId,
|
||||
knowledgeItemId,
|
||||
chunkIndex,
|
||||
content,
|
||||
embedding,
|
||||
sourceType = null,
|
||||
importance = null,
|
||||
} = input;
|
||||
|
||||
try {
|
||||
const queryText = `
|
||||
INSERT INTO knowledge_chunks (
|
||||
project_id,
|
||||
knowledge_item_id,
|
||||
chunk_index,
|
||||
content,
|
||||
embedding,
|
||||
source_type,
|
||||
importance
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5::vector, $6, $7)
|
||||
RETURNING
|
||||
id,
|
||||
project_id,
|
||||
knowledge_item_id,
|
||||
chunk_index,
|
||||
content,
|
||||
source_type,
|
||||
importance,
|
||||
created_at,
|
||||
updated_at
|
||||
`;
|
||||
|
||||
const result = await executeQuery<KnowledgeChunkRow>(queryText, [
|
||||
projectId,
|
||||
knowledgeItemId,
|
||||
chunkIndex,
|
||||
content,
|
||||
JSON.stringify(embedding),
|
||||
sourceType,
|
||||
importance,
|
||||
]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Failed to insert knowledge chunk');
|
||||
}
|
||||
|
||||
return rowToKnowledgeChunk(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('[Vector Memory] Failed to create knowledge chunk:', error);
|
||||
throw new Error(
|
||||
`Failed to create chunk: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch create multiple knowledge chunks efficiently
|
||||
*
|
||||
* Uses a transaction to ensure atomicity.
|
||||
*/
|
||||
export async function batchCreateKnowledgeChunks(
|
||||
input: BatchCreateKnowledgeChunksInput
|
||||
): Promise<KnowledgeChunk[]> {
|
||||
const { projectId, knowledgeItemId, chunks } = input;
|
||||
|
||||
if (chunks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const client = await getPooledClient();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const createdChunks: KnowledgeChunk[] = [];
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const queryText = `
|
||||
INSERT INTO knowledge_chunks (
|
||||
project_id,
|
||||
knowledge_item_id,
|
||||
chunk_index,
|
||||
content,
|
||||
embedding,
|
||||
source_type,
|
||||
importance
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5::vector, $6, $7)
|
||||
RETURNING
|
||||
id,
|
||||
project_id,
|
||||
knowledge_item_id,
|
||||
chunk_index,
|
||||
content,
|
||||
source_type,
|
||||
importance,
|
||||
created_at,
|
||||
updated_at
|
||||
`;
|
||||
|
||||
const result = await client.query<KnowledgeChunkRow>(queryText, [
|
||||
projectId,
|
||||
knowledgeItemId,
|
||||
chunk.chunkIndex,
|
||||
chunk.content,
|
||||
JSON.stringify(chunk.embedding),
|
||||
chunk.sourceType ?? null,
|
||||
chunk.importance ?? null,
|
||||
]);
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
createdChunks.push(rowToKnowledgeChunk(result.rows[0]));
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
console.log(
|
||||
`[Vector Memory] Batch created ${createdChunks.length} chunks for knowledge_item ${knowledgeItemId}`
|
||||
);
|
||||
|
||||
return createdChunks;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('[Vector Memory] Failed to batch create chunks:', error);
|
||||
throw new Error(
|
||||
`Failed to batch create chunks: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all chunks for a specific knowledge_item
|
||||
*
|
||||
* Used when regenerating chunks or removing a knowledge_item.
|
||||
*/
|
||||
export async function deleteChunksForKnowledgeItem(
|
||||
knowledgeItemId: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
const queryText = `
|
||||
DELETE FROM knowledge_chunks
|
||||
WHERE knowledge_item_id = $1
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const result = await executeQuery(queryText, [knowledgeItemId]);
|
||||
|
||||
console.log(
|
||||
`[Vector Memory] Deleted ${result.rowCount ?? 0} chunks for knowledge_item ${knowledgeItemId}`
|
||||
);
|
||||
|
||||
return result.rowCount ?? 0;
|
||||
} catch (error) {
|
||||
console.error('[Vector Memory] Failed to delete chunks:', error);
|
||||
throw new Error(
|
||||
`Failed to delete chunks: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all chunks for a specific project
|
||||
*
|
||||
* Used when cleaning up or resetting a project.
|
||||
*/
|
||||
export async function deleteChunksForProject(projectId: string): Promise<number> {
|
||||
try {
|
||||
const queryText = `
|
||||
DELETE FROM knowledge_chunks
|
||||
WHERE project_id = $1
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const result = await executeQuery(queryText, [projectId]);
|
||||
|
||||
console.log(
|
||||
`[Vector Memory] Deleted ${result.rowCount ?? 0} chunks for project ${projectId}`
|
||||
);
|
||||
|
||||
return result.rowCount ?? 0;
|
||||
} catch (error) {
|
||||
console.error('[Vector Memory] Failed to delete project chunks:', error);
|
||||
throw new Error(
|
||||
`Failed to delete project chunks: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chunk count for a knowledge_item
|
||||
*/
|
||||
export async function getChunkCountForKnowledgeItem(
|
||||
knowledgeItemId: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
const result = await executeQuery<{ count: string }>(
|
||||
'SELECT COUNT(*) as count FROM knowledge_chunks WHERE knowledge_item_id = $1',
|
||||
[knowledgeItemId]
|
||||
);
|
||||
|
||||
return parseInt(result.rows[0]?.count ?? '0', 10);
|
||||
} catch (error) {
|
||||
console.error('[Vector Memory] Failed to get chunk count:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chunk count for a project
|
||||
*/
|
||||
export async function getChunkCountForProject(projectId: string): Promise<number> {
|
||||
try {
|
||||
const result = await executeQuery<{ count: string }>(
|
||||
'SELECT COUNT(*) as count FROM knowledge_chunks WHERE project_id = $1',
|
||||
[projectId]
|
||||
);
|
||||
|
||||
return parseInt(result.rows[0]?.count ?? '0', 10);
|
||||
} catch (error) {
|
||||
console.error('[Vector Memory] Failed to get project chunk count:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate knowledge_chunks for a single knowledge_item
|
||||
*
|
||||
* This is the main pipeline that:
|
||||
* 1. Chunks the knowledge_item.content
|
||||
* 2. Generates embeddings for each chunk
|
||||
* 3. Deletes existing chunks for this item
|
||||
* 4. Inserts new chunks into AlloyDB
|
||||
*
|
||||
* @param knowledgeItem - The knowledge item to process
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const knowledgeItem = await getKnowledgeItem(projectId, itemId);
|
||||
* await writeKnowledgeChunksForItem(knowledgeItem);
|
||||
* ```
|
||||
*/
|
||||
export async function writeKnowledgeChunksForItem(
|
||||
knowledgeItem: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
content: string;
|
||||
sourceMeta?: { sourceType?: string; importance?: 'primary' | 'supporting' | 'irrelevant' };
|
||||
}
|
||||
): Promise<void> {
|
||||
const { chunkText } = await import('@/lib/ai/chunking');
|
||||
const { embedTextBatch } = await import('@/lib/ai/embeddings');
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`[Vector Memory] Starting chunking pipeline for knowledge_item ${knowledgeItem.id}`
|
||||
);
|
||||
|
||||
// Step 1: Chunk the content
|
||||
const textChunks = chunkText(knowledgeItem.content, {
|
||||
maxTokens: 800,
|
||||
overlapChars: 200,
|
||||
preserveParagraphs: true,
|
||||
});
|
||||
|
||||
if (textChunks.length === 0) {
|
||||
console.warn(
|
||||
`[Vector Memory] No chunks generated for knowledge_item ${knowledgeItem.id} - content may be empty`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Vector Memory] Generated ${textChunks.length} chunks for knowledge_item ${knowledgeItem.id}`
|
||||
);
|
||||
|
||||
// Step 2: Generate embeddings for all chunks
|
||||
const chunkTexts = textChunks.map((chunk) => chunk.text);
|
||||
const embeddings = await embedTextBatch(chunkTexts, {
|
||||
delayMs: 50, // Small delay to avoid rate limiting
|
||||
skipEmpty: true,
|
||||
});
|
||||
|
||||
if (embeddings.length !== textChunks.length) {
|
||||
throw new Error(
|
||||
`Embedding count mismatch: got ${embeddings.length}, expected ${textChunks.length}`
|
||||
);
|
||||
}
|
||||
|
||||
// Step 3: Delete existing chunks for this knowledge_item
|
||||
await deleteChunksForKnowledgeItem(knowledgeItem.id);
|
||||
|
||||
// Step 4: Insert new chunks
|
||||
const chunksToInsert = textChunks.map((chunk, index) => ({
|
||||
chunkIndex: chunk.index,
|
||||
content: chunk.text,
|
||||
embedding: embeddings[index],
|
||||
sourceType: knowledgeItem.sourceMeta?.sourceType ?? null,
|
||||
importance: knowledgeItem.sourceMeta?.importance ?? null,
|
||||
}));
|
||||
|
||||
await batchCreateKnowledgeChunks({
|
||||
projectId: knowledgeItem.projectId,
|
||||
knowledgeItemId: knowledgeItem.id,
|
||||
chunks: chunksToInsert,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[Vector Memory] Successfully processed ${chunksToInsert.length} chunks for knowledge_item ${knowledgeItem.id}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[Vector Memory] Failed to write chunks for knowledge_item ${knowledgeItem.id}:`,
|
||||
error
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to write chunks: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user