mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 21:38:27 +00:00
568 lines
16 KiB
TypeScript
568 lines
16 KiB
TypeScript
import type {
|
|
Triple,
|
|
EntityNode,
|
|
EpisodicNode,
|
|
StatementNode,
|
|
} from "@core/types";
|
|
import crypto from "crypto";
|
|
|
|
export interface OnboardingQuestion {
|
|
id: string;
|
|
title: string;
|
|
description?: string;
|
|
type: "single-select" | "multi-select" | "text";
|
|
options?: OnboardingOption[];
|
|
placeholder?: string;
|
|
required?: boolean;
|
|
}
|
|
|
|
export interface OnboardingOption {
|
|
id: string;
|
|
label: string;
|
|
value: string;
|
|
}
|
|
|
|
export interface OnboardingAnswer {
|
|
questionId: string;
|
|
value: string | string[];
|
|
}
|
|
|
|
// Onboarding questions in order
|
|
export const ONBOARDING_QUESTIONS: OnboardingQuestion[] = [
|
|
{
|
|
id: "role",
|
|
title: "What best describes you?",
|
|
description: 'Role / identity → anchors the "user" node',
|
|
type: "single-select",
|
|
options: [
|
|
{ id: "developer", label: "Developer", value: "Developer" },
|
|
{ id: "designer", label: "Designer", value: "Designer" },
|
|
{
|
|
id: "product-manager",
|
|
label: "Product Manager",
|
|
value: "Product Manager",
|
|
},
|
|
{
|
|
id: "engineering-manager",
|
|
label: "Engineering Manager",
|
|
value: "Engineering Manager",
|
|
},
|
|
{
|
|
id: "founder",
|
|
label: "Founder / Executive",
|
|
value: "Founder / Executive",
|
|
},
|
|
{ id: "other", label: "Other", value: "Other" },
|
|
],
|
|
required: true,
|
|
},
|
|
{
|
|
id: "goal",
|
|
title: "What's your primary goal with CORE?",
|
|
description: 'Motivation → drives the "objective" branch of graph',
|
|
type: "single-select",
|
|
options: [
|
|
{
|
|
id: "personal-memory",
|
|
label: "Build a personal memory system",
|
|
value: "Build a personal memory system",
|
|
},
|
|
{
|
|
id: "team-knowledge",
|
|
label: "Manage team/project knowledge",
|
|
value: "Manage team/project knowledge",
|
|
},
|
|
{
|
|
id: "automate-workflows",
|
|
label: "Automate workflows across tools",
|
|
value: "Automate workflows across tools",
|
|
},
|
|
{
|
|
id: "ai-assistant",
|
|
label: "Power an AI assistant / agent with context",
|
|
value: "Power an AI assistant / agent with context",
|
|
},
|
|
{
|
|
id: "explore-core",
|
|
label: "Explore core",
|
|
value: "Explore core",
|
|
},
|
|
],
|
|
required: true,
|
|
},
|
|
{
|
|
id: "tools",
|
|
title: "Which tools do you care about most?",
|
|
description: "Context → lets you connect integration nodes live",
|
|
type: "multi-select",
|
|
options: [
|
|
{ id: "claude", label: "Claude", value: "Claude" },
|
|
{ id: "claude-code", label: "Claude Code", value: "Claude Code" },
|
|
{ id: "cursor", label: "Cursor", value: "Cursor" },
|
|
{ id: "windsurf", label: "Windsurf", value: "Windsurf" },
|
|
{ id: "zed", label: "Zed", value: "Zed" },
|
|
{ id: "github", label: "GitHub", value: "GitHub" },
|
|
{ id: "slack", label: "Slack", value: "Slack" },
|
|
{ id: "notion", label: "Notion", value: "Notion" },
|
|
{ id: "obsidian", label: "Obsidian", value: "Obsidian" },
|
|
{ id: "gmail", label: "Gmail", value: "Gmail" },
|
|
{ id: "linear", label: "Linear", value: "Linear" },
|
|
{
|
|
id: "figma",
|
|
label: "Figma",
|
|
value: "Figma",
|
|
},
|
|
],
|
|
required: true,
|
|
},
|
|
];
|
|
|
|
// Helper function to create entity nodes (client-side, no embeddings)
|
|
function createEntity(
|
|
name: string,
|
|
type: string,
|
|
userId: string,
|
|
space?: string,
|
|
): EntityNode {
|
|
return {
|
|
uuid: crypto.randomUUID(),
|
|
name,
|
|
type,
|
|
attributes: {},
|
|
nameEmbedding: [], // Empty placeholder for client-side preview
|
|
typeEmbedding: [], // Empty placeholder for client-side preview
|
|
createdAt: new Date(),
|
|
userId,
|
|
space,
|
|
};
|
|
}
|
|
|
|
// Helper function to create episodic node (client-side, no embeddings)
|
|
function createEpisode(
|
|
content: string,
|
|
userId: string,
|
|
space?: string,
|
|
): EpisodicNode {
|
|
return {
|
|
uuid: crypto.randomUUID(),
|
|
content,
|
|
originalContent: content,
|
|
contentEmbedding: [], // Empty placeholder for client-side preview
|
|
metadata: { source: "onboarding" },
|
|
source: "onboarding",
|
|
createdAt: new Date(),
|
|
validAt: new Date(),
|
|
labels: ["onboarding"],
|
|
userId,
|
|
space,
|
|
};
|
|
}
|
|
|
|
// Helper function to create statement node (client-side, no embeddings)
|
|
function createStatement(
|
|
fact: string,
|
|
userId: string,
|
|
space?: string,
|
|
): StatementNode {
|
|
return {
|
|
uuid: crypto.randomUUID(),
|
|
fact,
|
|
factEmbedding: [], // Empty placeholder for client-side preview
|
|
createdAt: new Date(),
|
|
validAt: new Date(),
|
|
invalidAt: null,
|
|
attributes: {},
|
|
userId,
|
|
space,
|
|
};
|
|
}
|
|
|
|
// Create triplet from onboarding answer using reified knowledge graph structure (client-side, no embeddings)
|
|
export function createOnboardingTriplet(
|
|
username: string,
|
|
questionId: string,
|
|
answer: string | string[],
|
|
userId: string,
|
|
space?: string,
|
|
): Triple[] {
|
|
const triplets: Triple[] = [];
|
|
|
|
// Convert array answers to individual triplets
|
|
const answers = Array.isArray(answer) ? answer : [answer];
|
|
|
|
for (const singleAnswer of answers) {
|
|
// Get the statement mapping for this question type
|
|
const { predicateType, objectType, factTemplate } =
|
|
getStatementMapping(questionId);
|
|
|
|
// Create the statement fact (e.g., "Manoj uses GitHub")
|
|
const fact = factTemplate(username, singleAnswer);
|
|
|
|
// Create entities following CORE's reified structure (client-side preview only)
|
|
const subject = createEntity(username, "Person", userId, space);
|
|
const predicate = createEntity(
|
|
predicateType.toLowerCase().replace("_", " "), // "uses tool" instead of "USES_TOOL"
|
|
"Predicate", // Use "Predicate" type instead of "Relationship"
|
|
userId,
|
|
space,
|
|
);
|
|
const object = createEntity(singleAnswer, objectType, userId, space);
|
|
|
|
// Create statement node as first-class object (client-side preview only)
|
|
const statement = createStatement(fact, userId, space);
|
|
|
|
// Create provenance episode (client-side preview only)
|
|
const provenance = createEpisode(
|
|
`Onboarding question: ${questionId} - Answer: ${singleAnswer}`,
|
|
userId,
|
|
space,
|
|
);
|
|
|
|
// Create the reified triple structure (no embeddings for client preview)
|
|
triplets.push({
|
|
statement,
|
|
subject,
|
|
predicate,
|
|
object,
|
|
provenance,
|
|
});
|
|
}
|
|
|
|
return triplets;
|
|
}
|
|
|
|
// Create initial identity statement for preview using reified knowledge graph structure
|
|
export function createInitialIdentityStatement(displayName: string): any {
|
|
const timestamp = Date.now();
|
|
const now = new Date().toISOString();
|
|
|
|
// Create the identity statement: "I'm [DisplayName]" using reified structure
|
|
const fact = `I'm ${displayName}`;
|
|
|
|
return {
|
|
// Statement node (center)
|
|
statementNode: {
|
|
uuid: `identity-statement-${timestamp}`,
|
|
name: fact,
|
|
labels: ["Statement"],
|
|
attributes: {
|
|
nodeType: "Statement",
|
|
type: "Statement",
|
|
fact: fact,
|
|
source: "onboarding",
|
|
validAt: now,
|
|
},
|
|
createdAt: now,
|
|
},
|
|
// Subject entity ("I")
|
|
subjectNode: {
|
|
uuid: `pronoun-${timestamp}`,
|
|
name: "I",
|
|
labels: ["Entity"],
|
|
attributes: {
|
|
nodeType: "Entity",
|
|
type: "Pronoun",
|
|
source: "onboarding",
|
|
},
|
|
createdAt: now,
|
|
},
|
|
// Predicate entity ("am")
|
|
predicateNode: {
|
|
uuid: `predicate-identity-${timestamp}`,
|
|
name: "am",
|
|
labels: ["Entity"],
|
|
attributes: {
|
|
nodeType: "Entity",
|
|
type: "Predicate",
|
|
source: "onboarding",
|
|
},
|
|
createdAt: now,
|
|
},
|
|
// Object entity (DisplayName)
|
|
objectNode: {
|
|
uuid: `user-${timestamp}`,
|
|
name: displayName,
|
|
labels: ["Entity"],
|
|
attributes: {
|
|
nodeType: "Entity",
|
|
type: "Person",
|
|
source: "onboarding",
|
|
},
|
|
createdAt: now,
|
|
},
|
|
// Edges connecting statement to subject, predicate, object
|
|
edges: {
|
|
hasSubject: {
|
|
uuid: `identity-has-subject-${timestamp}`,
|
|
type: "HAS_SUBJECT",
|
|
source_node_uuid: `identity-statement-${timestamp}`,
|
|
target_node_uuid: `pronoun-${timestamp}`,
|
|
createdAt: now,
|
|
},
|
|
hasPredicate: {
|
|
uuid: `identity-has-predicate-${timestamp}`,
|
|
type: "HAS_PREDICATE",
|
|
source_node_uuid: `identity-statement-${timestamp}`,
|
|
target_node_uuid: `predicate-identity-${timestamp}`,
|
|
createdAt: now,
|
|
},
|
|
hasObject: {
|
|
uuid: `identity-has-object-${timestamp}`,
|
|
type: "HAS_OBJECT",
|
|
source_node_uuid: `identity-statement-${timestamp}`,
|
|
target_node_uuid: `user-${timestamp}`,
|
|
createdAt: now,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
// Create progressive episode content as user answers questions
|
|
export function createProgressiveEpisode(
|
|
username: string,
|
|
answers: OnboardingAnswer[],
|
|
): string {
|
|
// Start with identity
|
|
let episodeContent = `I'm ${username}.`;
|
|
|
|
// Build episode progressively based on answers
|
|
for (const answer of answers) {
|
|
const values = Array.isArray(answer.value) ? answer.value : [answer.value];
|
|
|
|
switch (answer.questionId) {
|
|
case "role":
|
|
episodeContent += ` I'm a ${values[0]}.`;
|
|
break;
|
|
|
|
case "goal":
|
|
episodeContent += ` My primary goal with CORE is to ${values[0].toLowerCase()}.`;
|
|
break;
|
|
|
|
case "tools":
|
|
if (values.length === 1) {
|
|
episodeContent += ` I use ${values[0]}.`;
|
|
} else if (values.length === 2) {
|
|
episodeContent += ` I use ${values[0]} and ${values[1]}.`;
|
|
} else {
|
|
// Create a copy to avoid mutating the original array
|
|
const toolsCopy = [...values];
|
|
const lastTool = toolsCopy.pop();
|
|
episodeContent += ` I use ${toolsCopy.join(", ")}, and ${lastTool}.`;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return episodeContent;
|
|
}
|
|
|
|
// Create preview statements for real-time visualization (reified structure)
|
|
// Including episode hierarchy: Episode → Statements → Entities
|
|
export function createPreviewStatements(
|
|
username: string,
|
|
answers: OnboardingAnswer[],
|
|
): { episode: any; statements: any[] } {
|
|
const allStatements: any[] = [];
|
|
const now = new Date().toISOString();
|
|
const baseTimestamp = Date.now();
|
|
|
|
// Create the cumulative episode content
|
|
const episodeContent = createProgressiveEpisode(username, answers);
|
|
|
|
// Create episode node that contains all statements
|
|
const episode = {
|
|
uuid: `onboarding-episode-${baseTimestamp}`,
|
|
name: username,
|
|
content: episodeContent,
|
|
labels: ["Episode"],
|
|
attributes: {
|
|
nodeType: "Episode",
|
|
type: "Episode",
|
|
source: "onboarding",
|
|
content: episodeContent,
|
|
validAt: now,
|
|
},
|
|
createdAt: now,
|
|
};
|
|
|
|
// Create user entity that will be the subject of all statements
|
|
const userEntityId = `user-${baseTimestamp}`;
|
|
|
|
for (let i = 0; i < answers.length; i++) {
|
|
const answer = answers[i];
|
|
const values = Array.isArray(answer.value) ? answer.value : [answer.value];
|
|
|
|
for (let j = 0; j < values.length; j++) {
|
|
const value = values[j];
|
|
const uniqueId = `${baseTimestamp}-${i}-${j}`;
|
|
|
|
// Get the relationship mapping for this question
|
|
const { predicateType, objectType, factTemplate } = getStatementMapping(
|
|
answer.questionId,
|
|
);
|
|
|
|
// Create the statement fact (e.g., "Manoj uses GitHub")
|
|
const fact = factTemplate(username, value);
|
|
|
|
// Create statement visualization as a reified structure
|
|
const statement = {
|
|
// Statement node (center)
|
|
statementNode: {
|
|
uuid: `statement-${uniqueId}`,
|
|
name: fact,
|
|
labels: ["Statement"],
|
|
attributes: {
|
|
nodeType: "Statement",
|
|
type: "Statement",
|
|
fact: fact,
|
|
source: "onboarding",
|
|
validAt: now,
|
|
},
|
|
createdAt: now,
|
|
},
|
|
// Subject entity (user)
|
|
subjectNode: {
|
|
uuid: userEntityId,
|
|
name: username,
|
|
labels: ["Entity"],
|
|
attributes: {
|
|
nodeType: "Entity",
|
|
type: "Person",
|
|
source: "onboarding",
|
|
},
|
|
createdAt: now,
|
|
},
|
|
// Predicate entity (relationship type)
|
|
predicateNode: {
|
|
uuid: `predicate-${predicateType}-${uniqueId}`,
|
|
name: predicateType.toLowerCase().replace("_", " "),
|
|
labels: ["Entity"],
|
|
attributes: {
|
|
nodeType: "Entity",
|
|
type: "Predicate",
|
|
source: "onboarding",
|
|
},
|
|
createdAt: now,
|
|
},
|
|
// Object entity (the thing being related to)
|
|
objectNode: {
|
|
uuid: `object-${uniqueId}`,
|
|
name: value,
|
|
labels: ["Entity"],
|
|
attributes: {
|
|
nodeType: "Entity",
|
|
type: objectType,
|
|
source: "onboarding",
|
|
},
|
|
createdAt: now,
|
|
},
|
|
// Edges connecting statement to subject, predicate, object
|
|
edges: {
|
|
hasSubject: {
|
|
uuid: `has-subject-${uniqueId}`,
|
|
type: "HAS_SUBJECT",
|
|
source_node_uuid: `statement-${uniqueId}`,
|
|
target_node_uuid: userEntityId,
|
|
createdAt: now,
|
|
},
|
|
hasPredicate: {
|
|
uuid: `has-predicate-${uniqueId}`,
|
|
type: "HAS_PREDICATE",
|
|
source_node_uuid: `statement-${uniqueId}`,
|
|
target_node_uuid: `predicate-${predicateType}-${uniqueId}`,
|
|
createdAt: now,
|
|
},
|
|
hasObject: {
|
|
uuid: `has-object-${uniqueId}`,
|
|
type: "HAS_OBJECT",
|
|
source_node_uuid: `statement-${uniqueId}`,
|
|
target_node_uuid: `object-${uniqueId}`,
|
|
createdAt: now,
|
|
},
|
|
// Provenance connection: Episode → Statement
|
|
hasProvenance: {
|
|
uuid: `provenance-${uniqueId}`,
|
|
type: "HAS_PROVENANCE",
|
|
source_node_uuid: `statement-${uniqueId}`,
|
|
target_node_uuid: episode.uuid,
|
|
createdAt: now,
|
|
},
|
|
},
|
|
};
|
|
|
|
allStatements.push(statement);
|
|
}
|
|
}
|
|
|
|
return { episode, statements: allStatements };
|
|
}
|
|
|
|
// Helper function to map question types to statement templates with natural English phrasing
|
|
function getStatementMapping(questionId: string): {
|
|
predicateType: string;
|
|
objectType: string;
|
|
factTemplate: (subject: string, object: string) => string;
|
|
} {
|
|
switch (questionId) {
|
|
case "role":
|
|
return {
|
|
predicateType: "IS_A",
|
|
objectType: "Role",
|
|
factTemplate: (subject, object) =>
|
|
`${subject} is a ${object.toLowerCase()}`,
|
|
};
|
|
case "goal":
|
|
return {
|
|
predicateType: "WANTS_TO",
|
|
objectType: "Goal",
|
|
factTemplate: (subject, object) =>
|
|
`${subject} wants to ${object.toLowerCase()}`,
|
|
};
|
|
case "tools":
|
|
return {
|
|
predicateType: "USES",
|
|
objectType: "Tool",
|
|
factTemplate: (subject, object) => `${subject} uses ${object}`,
|
|
};
|
|
default:
|
|
return {
|
|
predicateType: "HAS",
|
|
objectType: "Attribute",
|
|
factTemplate: (subject, object) => `${subject} has ${object}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Create main onboarding episode (client-side preview, no embeddings)
|
|
export function createOnboardingEpisode(
|
|
username: string,
|
|
answers: OnboardingAnswer[],
|
|
userId: string,
|
|
space?: string,
|
|
): EpisodicNode {
|
|
// Generate progressive episode content
|
|
const episodeContent = createProgressiveEpisode(username, answers);
|
|
|
|
// Create the main onboarding episode for client preview
|
|
const episode: EpisodicNode = {
|
|
uuid: crypto.randomUUID(),
|
|
content: episodeContent,
|
|
originalContent: episodeContent, // Same as content for onboarding
|
|
contentEmbedding: [], // Empty placeholder for client-side preview
|
|
source: "onboarding",
|
|
metadata: {
|
|
completedAt: new Date().toISOString(),
|
|
questionCount: answers.length,
|
|
answersData: answers, // Store original answers for reference
|
|
},
|
|
createdAt: new Date(),
|
|
validAt: new Date(),
|
|
labels: ["onboarding", "user-profile"],
|
|
userId,
|
|
space,
|
|
sessionId: crypto.randomUUID(), // Generate unique session for onboarding
|
|
};
|
|
|
|
return episode;
|
|
}
|