mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-07 11:02:11 +02:00
Initial open-source release
This commit is contained in:
commit
1a42152e6f
1199 changed files with 257054 additions and 0 deletions
213
packages/cli/src/demo-full.ts
Normal file
213
packages/cli/src/demo-full.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import { resolveKloConfigReference } from '@klo/context/core';
|
||||
import {
|
||||
createMemoryFlowLiveBuffer,
|
||||
ingestReportToMemoryFlowReplay,
|
||||
runLocalIngest,
|
||||
type IngestReportSnapshot,
|
||||
type LocalIngestResult,
|
||||
type MemoryFlowReplayInput,
|
||||
type RunLocalIngestOptions,
|
||||
} from '@klo/context/ingest';
|
||||
import { loadKloProject, type KloLocalProject } from '@klo/context/project';
|
||||
import { runLocalScan, type LocalScanRunResult } from '@klo/context/scan';
|
||||
import { DEMO_ADAPTER, DEMO_CONNECTION_ID, DEMO_FULL_JOB_ID, ensureDemoProject } from './demo-assets.js';
|
||||
import { runDemoScan } from './demo-scan.js';
|
||||
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
|
||||
import { formatNextStepLines } from './next-steps.js';
|
||||
|
||||
interface DemoFullOptions {
|
||||
projectDir: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
runLocalScan?: typeof runLocalScan;
|
||||
runLocalIngest?: typeof runLocalIngest;
|
||||
onMemoryFlowChange?: (snapshot: MemoryFlowReplayInput) => void;
|
||||
}
|
||||
|
||||
export interface DemoFullResult {
|
||||
project: KloLocalProject;
|
||||
scan: LocalScanRunResult;
|
||||
ingest: LocalIngestResult;
|
||||
report: IngestReportSnapshot;
|
||||
replay: MemoryFlowReplayInput;
|
||||
}
|
||||
|
||||
type FullDemoCredentialStatus =
|
||||
| { status: 'ready' }
|
||||
| { status: 'missing-anthropic-key' }
|
||||
| { status: 'unsupported-provider'; provider: string };
|
||||
|
||||
async function ensureDemoProjectForReuse(projectDir: string): Promise<void> {
|
||||
await ensureDemoProject({ projectDir, force: false }).catch((error) => {
|
||||
if (error instanceof Error && error.message.includes('Demo project already exists')) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function savedCounts(report: IngestReportSnapshot): { wikiCount: number; slCount: number } {
|
||||
const actions = report.body.workUnits.flatMap((workUnit) => workUnit.actions);
|
||||
return {
|
||||
wikiCount: actions.filter((action) => action.target === 'wiki').length,
|
||||
slCount: actions.filter((action) => action.target === 'sl').length,
|
||||
};
|
||||
}
|
||||
|
||||
export function fullDemoCredentialStatus(
|
||||
project: KloLocalProject,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): FullDemoCredentialStatus {
|
||||
const llm = project.config.llm;
|
||||
if (llm.provider.backend === 'none') {
|
||||
return { status: 'unsupported-provider', provider: llm.provider.backend };
|
||||
}
|
||||
|
||||
if (llm.provider.backend === 'anthropic' && !resolveKloConfigReference(llm.provider.anthropic?.api_key, env)) {
|
||||
return { status: 'missing-anthropic-key' };
|
||||
}
|
||||
|
||||
return { status: 'ready' };
|
||||
}
|
||||
|
||||
export function assertFullDemoCredentials(project: KloLocalProject, env: NodeJS.ProcessEnv = process.env): void {
|
||||
const llm = project.config.llm;
|
||||
const status = fullDemoCredentialStatus(project, env);
|
||||
if (status.status === 'ready') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.status === 'unsupported-provider') {
|
||||
throw new Error(
|
||||
'klo setup demo --mode full requires llm.provider.backend: anthropic, vertex, or gateway. Run `klo setup demo init --force --no-input` to recreate the demo config, or run `klo setup demo --mode seeded --no-input` without credentials.',
|
||||
);
|
||||
}
|
||||
|
||||
if (llm.provider.backend === 'anthropic') {
|
||||
throw new Error(
|
||||
'klo setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `klo setup demo --mode full --no-input`, or run `klo setup demo --mode seeded --no-input` without credentials.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function buildFullDemoReplay(report: IngestReportSnapshot): MemoryFlowReplayInput {
|
||||
return ingestReportToMemoryFlowReplay(report, { provenanceRowCount: report.body.provenanceRows.length });
|
||||
}
|
||||
|
||||
function initialFullReplay(projectDir: string): MemoryFlowReplayInput {
|
||||
return {
|
||||
runId: DEMO_FULL_JOB_ID,
|
||||
connectionId: DEMO_CONNECTION_ID,
|
||||
adapter: DEMO_ADAPTER,
|
||||
status: 'running',
|
||||
sourceDir: `${projectDir}/raw-sources/${DEMO_CONNECTION_ID}/${DEMO_ADAPTER}`,
|
||||
syncId: 'pending',
|
||||
errors: [],
|
||||
events: [],
|
||||
plannedWorkUnits: [],
|
||||
details: { actions: [], provenance: [], transcripts: [] },
|
||||
};
|
||||
}
|
||||
|
||||
export async function runDemoFull(options: DemoFullOptions): Promise<DemoFullResult> {
|
||||
await ensureDemoProjectForReuse(options.projectDir);
|
||||
const project = await loadKloProject({ projectDir: options.projectDir });
|
||||
assertFullDemoCredentials(project, options.env);
|
||||
|
||||
const { result: scan } = await runDemoScan({
|
||||
projectDir: project.projectDir,
|
||||
jobId: 'demo-full-scan',
|
||||
...(options.runLocalScan ? { runLocalScan: options.runLocalScan } : {}),
|
||||
});
|
||||
|
||||
const memoryFlow = options.onMemoryFlowChange
|
||||
? createMemoryFlowLiveBuffer(initialFullReplay(project.projectDir), { onChange: options.onMemoryFlowChange })
|
||||
: undefined;
|
||||
const executeLocalIngest = options.runLocalIngest ?? runLocalIngest;
|
||||
const ingest = await executeLocalIngest({
|
||||
project,
|
||||
adapters: createKloCliLocalIngestAdapters(project),
|
||||
adapter: DEMO_ADAPTER,
|
||||
connectionId: DEMO_CONNECTION_ID,
|
||||
trigger: 'manual_resync',
|
||||
jobId: DEMO_FULL_JOB_ID,
|
||||
...(memoryFlow ? { memoryFlow } : {}),
|
||||
} satisfies RunLocalIngestOptions);
|
||||
|
||||
return {
|
||||
project,
|
||||
scan,
|
||||
ingest,
|
||||
report: ingest.report,
|
||||
replay: buildFullDemoReplay(ingest.report),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatFullDemoSummary(report: IngestReportSnapshot): string {
|
||||
const counts = savedCounts(report);
|
||||
return [
|
||||
'Full demo ingest: done',
|
||||
`Report: ${report.id}`,
|
||||
`Run: ${report.runId}`,
|
||||
`Job: ${report.jobId}`,
|
||||
`Sync: ${report.body.syncId}`,
|
||||
`Saved memory: ${counts.wikiCount} wiki, ${counts.slCount} semantic layer`,
|
||||
`Provenance rows: ${report.body.provenanceRows.length}`,
|
||||
'Next: klo setup demo inspect',
|
||||
' Shows the files, semantic-layer sources, and memory KLO just produced.',
|
||||
'Next: klo setup demo replay',
|
||||
' Replays the same visual story without calling the LLM again.',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const ADAPTER_PREFIXES = ['live_database_', 'metabase_', 'looker_', 'lookml_', 'metricflow_', 'notion_', 'historic_sql_', 'dbt_descriptions_'];
|
||||
|
||||
function humanizeUnitKeyForReport(unitKey: string): string {
|
||||
let key = unitKey.replace(/-/g, '_');
|
||||
for (const prefix of ADAPTER_PREFIXES) {
|
||||
if (key.startsWith(prefix)) { key = key.slice(prefix.length); break; }
|
||||
}
|
||||
return key.replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
export function formatCleanDemoSummary(report: IngestReportSnapshot, projectDir: string): string {
|
||||
const counts = savedCounts(report);
|
||||
const workUnits = report.body.workUnits;
|
||||
const conflictCount = report.body.conflictsResolved.length;
|
||||
const areasAnalyzed = workUnits.filter((wu) => wu.actions.length > 0).length;
|
||||
|
||||
const lines: string[] = ['', '★ KLO finished ingesting your data', ''];
|
||||
|
||||
if (areasAnalyzed > 0) {
|
||||
lines.push(` ✓ Analyzed ${areasAnalyzed} business area${areasAnalyzed === 1 ? '' : 's'}`);
|
||||
}
|
||||
if (!report.body.reconciliationSkipped) {
|
||||
lines.push(` ✓ Reconciled — ${conflictCount > 0 ? `${conflictCount} conflict${conflictCount === 1 ? '' : 's'} resolved` : 'no conflicts'}`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
if (counts.slCount > 0 || counts.wikiCount > 0) {
|
||||
lines.push(' KLO created:');
|
||||
if (counts.slCount > 0) lines.push(` 📊 ${counts.slCount} query definition${counts.slCount === 1 ? '' : 's'} — so agents can write accurate SQL for your data`);
|
||||
if (counts.wikiCount > 0) lines.push(` 📝 ${counts.wikiCount} knowledge page${counts.wikiCount === 1 ? '' : 's'} — so agents understand your business context`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const memoryFlow = report.body.memoryFlow;
|
||||
if (memoryFlow) {
|
||||
for (const detail of memoryFlow.details.actions) {
|
||||
if (!detail.summary) continue;
|
||||
const icon = detail.target === 'sl' ? '📊' : '📝';
|
||||
lines.push(` ${icon} ${detail.summary}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push(' What to do next:');
|
||||
lines.push(...formatNextStepLines());
|
||||
lines.push('');
|
||||
lines.push(` Your KLO project files are at: ${projectDir}`);
|
||||
lines.push('');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue