feat: add AI catalog description generation
This commit is contained in:
@@ -2,7 +2,10 @@ import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
CatalogConflictError,
|
||||
CatalogConnectorError,
|
||||
DescriptionGenerationRunActiveError,
|
||||
type CatalogColumn,
|
||||
type CatalogDescriptionConsolidationCounts,
|
||||
type CatalogDescriptionTarget,
|
||||
type CatalogDatabaseMetadataDeleteTarget,
|
||||
type CatalogMetadataDeleteCounts,
|
||||
type CatalogRelationship,
|
||||
@@ -19,6 +22,10 @@ import {
|
||||
type DatabaseTestResult,
|
||||
type ObservedCatalogTable,
|
||||
type ObservedSchemaSnapshot,
|
||||
type DescriptionGenerationEvent,
|
||||
type DescriptionGenerationRun,
|
||||
type DescriptionGenerationRunUpdate,
|
||||
type DescriptionGenerationScope,
|
||||
type TableSyncRepositoryResult,
|
||||
type WorkspaceDatabase,
|
||||
} from "./types.js";
|
||||
@@ -33,6 +40,8 @@ export class MemoryCatalogRepository implements CatalogRepository {
|
||||
private readonly tables = new Map<string, CatalogTable>();
|
||||
private readonly columns = new Map<string, CatalogColumn>();
|
||||
private readonly relationships = new Map<string, CatalogRelationship>();
|
||||
private readonly descriptionGenerationRuns = new Map<string, DescriptionGenerationRun>();
|
||||
private readonly descriptionGenerationEvents = new Map<string, DescriptionGenerationEvent[]>();
|
||||
private readonly syncRuns = new Map<string, CatalogSyncRun>();
|
||||
private readonly syncEvents = new Map<string, CatalogSyncEvent[]>();
|
||||
|
||||
@@ -122,6 +131,11 @@ export class MemoryCatalogRepository implements CatalogRepository {
|
||||
for (const [relationshipId, relationship] of this.relationships) {
|
||||
if (relationship.databaseId === id) this.relationships.delete(relationshipId);
|
||||
}
|
||||
for (const [runId, run] of this.descriptionGenerationRuns) {
|
||||
if (run.databaseId !== id) continue;
|
||||
this.descriptionGenerationRuns.delete(runId);
|
||||
this.descriptionGenerationEvents.delete(runId);
|
||||
}
|
||||
return this.records.delete(id);
|
||||
}
|
||||
async listTables(databaseId: string): Promise<CatalogTable[]> {
|
||||
@@ -196,6 +210,166 @@ export class MemoryCatalogRepository implements CatalogRepository {
|
||||
return structuredClone(updated);
|
||||
}
|
||||
|
||||
async consolidateGeneratedDescriptions(
|
||||
databaseId: string,
|
||||
target: CatalogDescriptionTarget,
|
||||
targetIds: readonly string[],
|
||||
): Promise<CatalogDescriptionConsolidationCounts | undefined> {
|
||||
const selectedTargetIds = [...new Set(targetIds)];
|
||||
if (!this.records.has(databaseId) || selectedTargetIds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
if (target === "tables") {
|
||||
const targets = selectedTargetIds.map((id) => this.tables.get(id));
|
||||
if (targets.some((table) => !table || table.databaseId !== databaseId)) return undefined;
|
||||
const copied = targets.filter((table) => Boolean(table!.generatedDescription?.trim()));
|
||||
for (const table of copied) {
|
||||
this.tables.set(table!.id, {
|
||||
...table!,
|
||||
description: table!.generatedDescription,
|
||||
version: table!.version + 1,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return { copied: copied.length, skipped: targets.length - copied.length };
|
||||
}
|
||||
|
||||
const targets = selectedTargetIds.map((id) => this.columns.get(id));
|
||||
if (targets.some((column) => (
|
||||
!column || this.tables.get(column.tableId)?.databaseId !== databaseId
|
||||
))) return undefined;
|
||||
const copied = targets.filter((column) => Boolean(column!.generatedDescription?.trim()));
|
||||
for (const column of copied) {
|
||||
this.columns.set(column!.id, {
|
||||
...column!,
|
||||
description: column!.generatedDescription,
|
||||
version: column!.version + 1,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return { copied: copied.length, skipped: targets.length - copied.length };
|
||||
}
|
||||
|
||||
async createDescriptionGenerationRun(
|
||||
databaseId: string,
|
||||
scope: DescriptionGenerationScope,
|
||||
modelId: string,
|
||||
language: DescriptionGenerationRun["language"],
|
||||
total: number,
|
||||
): Promise<DescriptionGenerationRun> {
|
||||
if ([...this.descriptionGenerationRuns.values()].some((run) => (
|
||||
run.status === "queued" || run.status === "running"
|
||||
))) {
|
||||
throw new DescriptionGenerationRunActiveError("A description generation run is already active");
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
const run: DescriptionGenerationRun = {
|
||||
id: randomUUID(),
|
||||
databaseId,
|
||||
scope,
|
||||
modelId,
|
||||
language,
|
||||
status: "queued",
|
||||
total,
|
||||
processed: 0,
|
||||
generated: 0,
|
||||
nonGeneratable: 0,
|
||||
failed: 0,
|
||||
createdAt: now,
|
||||
startedAt: null,
|
||||
updatedAt: now,
|
||||
finishedAt: null,
|
||||
errorSummary: null,
|
||||
};
|
||||
this.descriptionGenerationRuns.set(run.id, run);
|
||||
return structuredClone(run);
|
||||
}
|
||||
|
||||
async getDescriptionGenerationRun(runId: string): Promise<DescriptionGenerationRun | undefined> {
|
||||
const run = this.descriptionGenerationRuns.get(runId);
|
||||
return run ? structuredClone(run) : undefined;
|
||||
}
|
||||
|
||||
async listDescriptionGenerationRuns(limit = 50): Promise<DescriptionGenerationRun[]> {
|
||||
return [...this.descriptionGenerationRuns.values()]
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id))
|
||||
.slice(0, limit)
|
||||
.map((run) => structuredClone(run));
|
||||
}
|
||||
|
||||
async getActiveDescriptionGenerationRun(): Promise<DescriptionGenerationRun | undefined> {
|
||||
const run = [...this.descriptionGenerationRuns.values()]
|
||||
.filter((candidate) => candidate.status === "queued" || candidate.status === "running")
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
|
||||
return run ? structuredClone(run) : undefined;
|
||||
}
|
||||
|
||||
async interruptActiveDescriptionGenerationRuns(
|
||||
errorSummary: string,
|
||||
): Promise<DescriptionGenerationRun[]> {
|
||||
const interrupted: DescriptionGenerationRun[] = [];
|
||||
for (const run of this.descriptionGenerationRuns.values()) {
|
||||
if (run.status !== "queued" && run.status !== "running") continue;
|
||||
const now = new Date().toISOString();
|
||||
const updated: DescriptionGenerationRun = {
|
||||
...run,
|
||||
status: "interrupted",
|
||||
updatedAt: now,
|
||||
finishedAt: now,
|
||||
errorSummary,
|
||||
};
|
||||
this.descriptionGenerationRuns.set(run.id, updated);
|
||||
interrupted.push(structuredClone(updated));
|
||||
}
|
||||
return interrupted;
|
||||
}
|
||||
|
||||
async updateDescriptionGenerationRun(
|
||||
runId: string,
|
||||
update: DescriptionGenerationRunUpdate,
|
||||
): Promise<DescriptionGenerationRun | undefined> {
|
||||
const current = this.descriptionGenerationRuns.get(runId);
|
||||
if (!current) return undefined;
|
||||
const updated = {
|
||||
...current,
|
||||
...structuredClone(update),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
this.descriptionGenerationRuns.set(runId, updated);
|
||||
return structuredClone(updated);
|
||||
}
|
||||
|
||||
async appendDescriptionGenerationEvent(
|
||||
runId: string,
|
||||
level: DescriptionGenerationEvent["level"],
|
||||
message: string,
|
||||
): Promise<DescriptionGenerationEvent> {
|
||||
if (!this.descriptionGenerationRuns.has(runId)) {
|
||||
throw new CatalogConflictError("Description Generation Run does not exist");
|
||||
}
|
||||
const events = this.descriptionGenerationEvents.get(runId) ?? [];
|
||||
const event: DescriptionGenerationEvent = {
|
||||
runId,
|
||||
sequence: events.length + 1,
|
||||
level,
|
||||
message,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
events.push(event);
|
||||
this.descriptionGenerationEvents.set(runId, events);
|
||||
return structuredClone(event);
|
||||
}
|
||||
|
||||
async listDescriptionGenerationEvents(
|
||||
runId: string,
|
||||
afterSequence = 0,
|
||||
): Promise<DescriptionGenerationEvent[]> {
|
||||
return (this.descriptionGenerationEvents.get(runId) ?? [])
|
||||
.filter((event) => event.sequence > afterSequence)
|
||||
.map((event) => structuredClone(event));
|
||||
}
|
||||
|
||||
async listRelationships(databaseId: string): Promise<CatalogRelationship[]> {
|
||||
return [...this.relationships.values()].filter((relationship) => relationship.databaseId === databaseId)
|
||||
.sort((a, b) => `${a.sourceTableName}.${a.constraintName}`.localeCompare(`${b.sourceTableName}.${b.constraintName}`))
|
||||
|
||||
Reference in New Issue
Block a user