diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index ef50d4c..f3943ac 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -45,7 +45,7 @@ const EnvironmentSchema = z.object({ AUTH_GOOGLE_CLIENT_ID: z.string().optional(), AUTH_GOOGLE_CLIENT_SECRET: z.string().optional(), - ENABLE_EMAIL_LOGIN: z.coerce.boolean().default(false), + ENABLE_EMAIL_LOGIN: z.coerce.boolean().default(true), //Redis REDIS_HOST: z.string().default("localhost"), diff --git a/apps/webapp/app/routes/search.tsx b/apps/webapp/app/routes/search.tsx index f8cb7be..758fd9c 100644 --- a/apps/webapp/app/routes/search.tsx +++ b/apps/webapp/app/routes/search.tsx @@ -6,7 +6,14 @@ import { json } from "@remix-run/node"; export const SearchBodyRequest = z.object({ query: z.string(), spaceId: z.string().optional(), - sessionId: z.string().optional(), + startTime: z.string().optional(), + endTime: z.string().optional(), + limit: z.number().optional(), + maxBfsDepth: z.number().optional(), + includeInvalidated: z.boolean().optional(), + entityTypes: z.array(z.string()).optional(), + scoreThreshold: z.number().optional(), + minResults: z.number().optional(), }); const searchService = new SearchService(); @@ -23,6 +30,16 @@ const { action, loader } = createActionApiRoute( const results = await searchService.search( body.query, authentication.userId, + { + startTime: body.startTime ? new Date(body.startTime) : undefined, + endTime: body.endTime ? new Date(body.endTime) : undefined, + limit: body.limit, + maxBfsDepth: body.maxBfsDepth, + includeInvalidated: body.includeInvalidated, + entityTypes: body.entityTypes, + scoreThreshold: body.scoreThreshold, + minResults: body.minResults, + }, ); return json(results); }, diff --git a/apps/webapp/app/services/graphModels/entity.ts b/apps/webapp/app/services/graphModels/entity.ts index 975b2f8..a1445cc 100644 --- a/apps/webapp/app/services/graphModels/entity.ts +++ b/apps/webapp/app/services/graphModels/entity.ts @@ -7,7 +7,7 @@ export async function saveEntity(entity: EntityNode): Promise { ON CREATE SET n.name = $name, n.type = $type, - n.attributesJson = $attributesJson, + n.attributes = $attributes, n.nameEmbedding = $nameEmbedding, n.createdAt = $createdAt, n.userId = $userId, @@ -15,7 +15,7 @@ export async function saveEntity(entity: EntityNode): Promise { ON MATCH SET n.name = $name, n.type = $type, - n.attributesJson = $attributesJson, + n.attributes = $attributes, n.nameEmbedding = $nameEmbedding, n.space = $space RETURN n.uuid as uuid @@ -25,7 +25,7 @@ export async function saveEntity(entity: EntityNode): Promise { uuid: entity.uuid, name: entity.name, type: entity.type, - attributesJson: JSON.stringify(entity.attributes || {}), + attributes: JSON.stringify(entity.attributes || {}), nameEmbedding: entity.nameEmbedding, createdAt: entity.createdAt.toISOString(), userId: entity.userId, @@ -50,7 +50,7 @@ export async function getEntity(uuid: string): Promise { uuid: entity.uuid, name: entity.name, type: entity.type, - attributes: JSON.parse(entity.attributesJson || "{}"), + attributes: JSON.parse(entity.attributes || "{}"), nameEmbedding: entity.nameEmbedding, createdAt: new Date(entity.createdAt), userId: entity.userId, @@ -81,7 +81,7 @@ export async function findSimilarEntities(params: { uuid: entity.uuid, name: entity.name, type: entity.type, - attributes: JSON.parse(entity.attributesJson || "{}"), + attributes: JSON.parse(entity.attributes || "{}"), nameEmbedding: entity.nameEmbedding, createdAt: new Date(entity.createdAt), userId: entity.userId, diff --git a/apps/webapp/app/services/graphModels/episode.ts b/apps/webapp/app/services/graphModels/episode.ts index e1657a5..54b8734 100644 --- a/apps/webapp/app/services/graphModels/episode.ts +++ b/apps/webapp/app/services/graphModels/episode.ts @@ -8,7 +8,7 @@ export async function saveEpisode(episode: EpisodicNode): Promise { e.content = $content, e.originalContent = $originalContent, e.contentEmbedding = $contentEmbedding, - e.type = $type, + e.metadata = $metadata, e.source = $source, e.createdAt = $createdAt, e.validAt = $validAt, @@ -20,7 +20,7 @@ export async function saveEpisode(episode: EpisodicNode): Promise { e.content = $content, e.contentEmbedding = $contentEmbedding, e.originalContent = $originalContent, - e.type = $type, + e.metadata = $metadata, e.source = $source, e.validAt = $validAt, e.labels = $labels, @@ -34,7 +34,7 @@ export async function saveEpisode(episode: EpisodicNode): Promise { content: episode.content, originalContent: episode.originalContent, source: episode.source, - type: episode.type, + metadata: JSON.stringify(episode.metadata || {}), userId: episode.userId || null, labels: episode.labels || [], createdAt: episode.createdAt.toISOString(), @@ -64,7 +64,7 @@ export async function getEpisode(uuid: string): Promise { content: episode.content, originalContent: episode.originalContent, contentEmbedding: episode.contentEmbedding, - type: episode.type, + metadata: JSON.parse(episode.metadata || "{}"), source: episode.source, createdAt: new Date(episode.createdAt), validAt: new Date(episode.validAt), @@ -118,7 +118,7 @@ export async function getRecentEpisodes(params: { content: episode.content, originalContent: episode.originalContent, contentEmbedding: episode.contentEmbedding, - type: episode.type, + metadata: JSON.parse(episode.metadata || "{}"), source: episode.source, createdAt: new Date(episode.createdAt), validAt: new Date(episode.validAt), diff --git a/apps/webapp/app/services/graphModels/statement.ts b/apps/webapp/app/services/graphModels/statement.ts index b5c2dc4..f192e46 100644 --- a/apps/webapp/app/services/graphModels/statement.ts +++ b/apps/webapp/app/services/graphModels/statement.ts @@ -21,7 +21,7 @@ export async function saveTriple(triple: Triple): Promise { n.createdAt = $createdAt, n.validAt = $validAt, n.invalidAt = $invalidAt, - n.attributesJson = $attributesJson, + n.attributes = $attributes, n.userId = $userId, n.space = $space ON MATCH SET @@ -29,7 +29,7 @@ export async function saveTriple(triple: Triple): Promise { n.factEmbedding = $factEmbedding, n.validAt = $validAt, n.invalidAt = $invalidAt, - n.attributesJson = $attributesJson, + n.attributes = $attributes, n.space = $space RETURN n.uuid as uuid `; @@ -43,7 +43,7 @@ export async function saveTriple(triple: Triple): Promise { invalidAt: triple.statement.invalidAt ? triple.statement.invalidAt.toISOString() : null, - attributesJson: JSON.stringify(triple.statement.attributes || {}), + attributes: JSON.stringify(triple.statement.attributes || {}), userId: triple.provenance.userId, space: triple.statement.space || null, }; @@ -273,7 +273,7 @@ export async function getTripleForStatement({ content: episodeProps.content, originalContent: episodeProps.originalContent, source: episodeProps.source, - type: episodeProps.type, + metadata: episodeProps.metadata, createdAt: new Date(episodeProps.createdAt), validAt: new Date(episodeProps.validAt), contentEmbedding: episodeProps.contentEmbedding, diff --git a/apps/webapp/app/services/knowledgeGraph.server.ts b/apps/webapp/app/services/knowledgeGraph.server.ts index 3116164..2a075c1 100644 --- a/apps/webapp/app/services/knowledgeGraph.server.ts +++ b/apps/webapp/app/services/knowledgeGraph.server.ts @@ -11,7 +11,12 @@ import { } from "@core/types"; import { logger } from "./logger.service"; import crypto from "crypto"; -import { dedupeNodes, extractMessage, extractText } from "./prompts/nodes"; +import { + dedupeNodes, + extractAttributes, + extractMessage, + extractText, +} from "./prompts/nodes"; import { extractStatements, resolveStatementPrompt, @@ -31,7 +36,6 @@ import { normalizePrompt } from "./prompts"; // Default number of previous episodes to retrieve for context const DEFAULT_EPISODE_WINDOW = 5; -const RELEVANT_SCHEMA_LIMIT = 10; export class KnowledgeGraphService { async getEmbedding(text: string) { @@ -60,6 +64,7 @@ export class KnowledgeGraphService { limit: DEFAULT_EPISODE_WINDOW, userId: params.userId, source: params.source, + sessionId: params.sessionId, }); const normalizedEpisodeBody = await this.normalizeEpisodeBody( @@ -73,7 +78,7 @@ export class KnowledgeGraphService { content: normalizedEpisodeBody, originalContent: params.episodeBody, source: params.source, - type: params.type || EpisodeType.Text, + metadata: params.metadata || {}, createdAt: now, validAt: new Date(params.referenceTime), labels: [], @@ -106,8 +111,27 @@ export class KnowledgeGraphService { const { resolvedStatements, invalidatedStatements } = await this.resolveStatements(resolvedTriples, episode); + // Step 7: ADd attributes to entity nodes + const updatedTriples = await this.addAttributesToEntities( + resolvedStatements, + episode, + ); + + for (const triple of updatedTriples) { + const { subject, predicate, object, statement, provenance } = triple; + const safeTriple = { + subject: { ...subject, nameEmbedding: undefined }, + predicate: { ...predicate, nameEmbedding: undefined }, + object: { ...object, nameEmbedding: undefined }, + statement: { ...statement, factEmbedding: undefined }, + provenance, + }; + console.log("Triple (no embedding):", JSON.stringify(safeTriple)); + } + // console.log("Invalidated statements", invalidatedStatements); + // Save triples sequentially to avoid parallel processing issues - for (const triple of resolvedStatements) { + for (const triple of updatedTriples) { await saveTriple(triple); } @@ -154,10 +178,9 @@ export class KnowledgeGraphService { }; // Get the extract_json prompt from the prompt library - const messages = - episode.type === EpisodeType.Conversation - ? extractMessage(context) - : extractText(context); + const messages = episode.sessionId + ? extractMessage(context) + : extractText(context); let responseText = ""; @@ -668,6 +691,100 @@ export class KnowledgeGraphService { return { resolvedStatements, invalidatedStatements }; } + /** + * Add attributes to entity nodes based on the resolved statements + */ + private async addAttributesToEntities( + triples: Triple[], + episode: EpisodicNode, + ): Promise { + // Collect all unique entities from the triples + const entityMap = new Map(); + + // Add all subjects, predicates, and objects to the map + triples.forEach((triple) => { + if (triple.subject) { + entityMap.set(triple.subject.uuid, triple.subject); + } + if (triple.predicate) { + entityMap.set(triple.predicate.uuid, triple.predicate); + } + if (triple.object) { + entityMap.set(triple.object.uuid, triple.object); + } + }); + + // Convert the map to an array of entities + const entities = Array.from(entityMap.values()); + + if (entities.length === 0) { + return triples; // No entities to process + } + + // Get all app keys + const allAppEnumValues = Object.values(Apps); + + // Get all node types with their attribute definitions + const entityTypes = getNodeTypes(allAppEnumValues); + + // Prepare simplified context for the LLM + const context = { + episodeContent: episode.content, + entityTypes: entityTypes, + entities: entities.map((entity) => ({ + uuid: entity.uuid, + name: entity.name, + type: entity.type, + currentAttributes: entity.attributes || {}, + })), + }; + + console.log("entityTypes", JSON.stringify(entityTypes)); + console.log("entities", JSON.stringify(context.entities)); + + // Create a prompt for the LLM to extract attributes + const messages = extractAttributes(context); + + let responseText = ""; + + // Call the LLM to extract attributes + await makeModelCall( + false, + LLMModelEnum.GPT41, + messages as CoreMessage[], + (text) => { + responseText = text; + }, + ); + + try { + const outputMatch = responseText.match(/([\s\S]*?)<\/output>/); + if (outputMatch && outputMatch[1]) { + responseText = outputMatch[1].trim(); + } + // Parse the LLM response + const responseData = JSON.parse(responseText); + const updatedEntities = responseData.entities || []; + + // Update entity attributes and save them + for (const updatedEntity of updatedEntities) { + const entity = entityMap.get(updatedEntity.uuid); + if (entity) { + // Merge the existing attributes with the new ones + entity.attributes = { + ...updatedEntity.attributes, + }; + } + } + + logger.info(`Updated attributes for ${updatedEntities.length} entities`); + } catch (error) { + logger.error("Error processing entity attributes", { error }); + } + + return triples; + } + /** * Normalize an episode by extracting entities and creating nodes and statements */ diff --git a/apps/webapp/app/services/prompts/nodes.ts b/apps/webapp/app/services/prompts/nodes.ts index 9139e8d..6f6ff1f 100644 --- a/apps/webapp/app/services/prompts/nodes.ts +++ b/apps/webapp/app/services/prompts/nodes.ts @@ -256,3 +256,56 @@ ${JSON.stringify(context.extracted_nodes, null, 2)} }, ]; }; + +export const extractAttributes = ( + context: Record, +): CoreMessage[] => { + const sysPrompt = ` +You are an AI assistant that extracts and enhances entity attributes based on context. +Your task is to analyze entities and provide appropriate attribute values for each entity based on its type definition. + +For each entity: +1. Look at its type and identify the required and optional attributes from the entity type definitions +2. Check if the entity already has values for these attributes +3. For missing attributes, extract appropriate values from the context if possible +4. For existing attributes, enhance or correct them if needed based on the context +5. Give empty attributes object ({}) when there are no attributes to update +6. Only include attributes that you're updating - don't repeat existing attributes that don't need changes +7. I'll merge your new attributes with the current attributes, so only provide values that should be added or modified + +Provide your output in this structure: + +{ +"entities": [ +{ + "uuid": "entity-uuid", + "attributes": { + "attributeName1": "value1", + "attributeName2": "value2", + ... + } +}, +... +] +} +`; + + const userPrompt = ` + +${JSON.stringify(context.entityTypes, null, 2)} + + + +${JSON.stringify(context.entities, null, 2)} + + + +${context.episodeContent} + + +Based on the above information, please extract and enhance attributes for each entity according to its type definition. Return only the uuid and updated attributes for each entity.`; + return [ + { role: "system", content: sysPrompt }, + { role: "user", content: userPrompt }, + ]; +}; diff --git a/apps/webapp/app/services/search.server.ts b/apps/webapp/app/services/search.server.ts index 2a444fc..335762c 100644 --- a/apps/webapp/app/services/search.server.ts +++ b/apps/webapp/app/services/search.server.ts @@ -37,10 +37,13 @@ export class SearchService { options: SearchOptions = {}, ): Promise<{ episodes: string[]; facts: string[] }> { // Default options + const opts: Required = { limit: options.limit || 10, maxBfsDepth: options.maxBfsDepth || 4, validAt: options.validAt || new Date(), + startTime: options.startTime || null, + endTime: options.endTime || new Date(), includeInvalidated: options.includeInvalidated || false, entityTypes: options.entityTypes || [], predicateTypes: options.predicateTypes || [], @@ -213,6 +216,8 @@ export interface SearchOptions { limit?: number; maxBfsDepth?: number; validAt?: Date; + startTime?: Date | null; + endTime?: Date; includeInvalidated?: boolean; entityTypes?: string[]; predicateTypes?: string[]; diff --git a/apps/webapp/app/services/search/utils.ts b/apps/webapp/app/services/search/utils.ts index ec06fbb..9f2026e 100644 --- a/apps/webapp/app/services/search/utils.ts +++ b/apps/webapp/app/services/search/utils.ts @@ -16,14 +16,28 @@ export async function performBM25Search( // Sanitize the query for Lucene syntax const sanitizedQuery = sanitizeLuceneQuery(query); + // Build the WHERE clause based on timeframe options + let timeframeCondition = ` + AND s.validAt <= $validAt + AND (s.invalidAt IS NULL OR s.invalidAt > $validAt) + `; + + // If startTime is provided, add condition to filter by validAt >= startTime + if (options.startTime) { + timeframeCondition = ` + AND s.validAt <= $validAt + AND (s.invalidAt IS NULL OR s.invalidAt > $validAt) + AND s.validAt >= $startTime + `; + } + // Use Neo4j's built-in fulltext search capabilities const cypher = ` CALL db.index.fulltext.queryNodes("statement_fact_index", $query) YIELD node AS s, score WHERE - s.validAt <= $validAt - AND (s.invalidAt IS NULL OR s.invalidAt > $validAt) - AND (s.userId = $userId) + (s.userId = $userId) + ${timeframeCondition} RETURN s, score ORDER BY score DESC `; @@ -31,7 +45,8 @@ export async function performBM25Search( const params = { query: sanitizedQuery, userId, - validAt: options.validAt.toISOString(), + validAt: options.endTime.toISOString(), + ...(options.startTime && { startTime: options.startTime.toISOString() }), }; const records = await runQuery(cypher, params); @@ -46,9 +61,9 @@ export async function performBM25Search( * Sanitize a query string for Lucene syntax */ export function sanitizeLuceneQuery(query: string): string { - // Escape special characters: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ + // Escape special characters: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ / let sanitized = query.replace( - /[+\-&|!(){}[\]^"~*?:\\]/g, + /[+\-&|!(){}[\]^"~*?:\\\/]/g, (match) => "\\" + match, ); @@ -71,16 +86,27 @@ export async function performVectorSearch( options: Required, ): Promise { try { - // 1. Generate embedding for the query - // const embedding = await this.getEmbedding(query); + // Build the WHERE clause based on timeframe options + let timeframeCondition = ` + AND s.validAt <= $validAt + AND (s.invalidAt IS NULL OR s.invalidAt > $validAt) + `; - // 2. Search for similar statements using Neo4j vector search + // If startTime is provided, add condition to filter by validAt >= startTime + if (options.startTime) { + timeframeCondition = ` + AND s.validAt <= $validAt + AND (s.invalidAt IS NULL OR s.invalidAt > $validAt) + AND s.validAt >= $startTime + `; + } + + // 1. Search for similar statements using Neo4j vector search const cypher = ` MATCH (s:Statement) WHERE - s.validAt <= $validAt - AND (s.invalidAt IS NULL OR s.invalidAt > $validAt) - AND (s.userId = $userId) + (s.userId = $userId) + ${timeframeCondition} WITH s, vector.similarity.cosine(s.factEmbedding, $embedding) AS score WHERE score > 0.7 RETURN s, score @@ -90,7 +116,8 @@ export async function performVectorSearch( const params = { embedding: query, userId, - validAt: options.validAt.toISOString(), + validAt: options.endTime.toISOString(), + ...(options.startTime && { startTime: options.startTime.toISOString() }), }; const records = await runQuery(cypher, params); @@ -120,9 +147,10 @@ export async function performBfsSearch( const statements = await bfsTraversal( entity.uuid, options.maxBfsDepth, - options.validAt, + options.endTime, userId, options.includeInvalidated, + options.startTime, ); allStatements.push(...statements); } @@ -143,17 +171,31 @@ export async function bfsTraversal( validAt: Date, userId: string, includeInvalidated: boolean, + startTime: Date | null, ): Promise { try { + // Build the WHERE clause based on timeframe options + let timeframeCondition = ` + AND s.validAt <= $validAt + AND (s.invalidAt IS NULL OR s.invalidAt > $validAt) + `; + + // If startTime is provided, add condition to filter by validAt >= startTime + if (startTime) { + timeframeCondition = ` + AND s.validAt <= $validAt + AND (s.invalidAt IS NULL OR s.invalidAt > $validAt) + AND s.validAt >= $startTime + `; + } // Use Neo4j's built-in path finding capabilities for efficient BFS // This query implements BFS up to maxDepth and collects all statements along the way const cypher = ` MATCH (e:Entity {uuid: $startEntityId})<-[:HAS_SUBJECT|HAS_OBJECT|HAS_PREDICATE]-(s:Statement) WHERE - s.validAt <= $validAt - AND (s.invalidAt IS NULL OR s.invalidAt > $validAt) - AND (s.userId = $userId) + (s.userId = $userId) AND ($includeInvalidated OR s.invalidAt IS NULL) + ${timeframeCondition} RETURN s as statement `; @@ -163,6 +205,7 @@ export async function bfsTraversal( validAt: validAt.toISOString(), userId, includeInvalidated, + ...(startTime && { startTime: startTime.toISOString() }), }; const records = await runQuery(cypher, params); diff --git a/apps/webapp/app/utils/presets/nodes.ts b/apps/webapp/app/utils/presets/nodes.ts index 41c9d99..4d31477 100644 --- a/apps/webapp/app/utils/presets/nodes.ts +++ b/apps/webapp/app/utils/presets/nodes.ts @@ -10,31 +10,106 @@ export const AppNames = { [Apps.SOL]: "Sol", } as const; +// Define attribute structure +export interface NodeAttribute { + name: string; + description: string; + type?: "string" | "number" | "boolean" | "date" | "array"; + required?: boolean; +} + // General node types that are common across all apps export const GENERAL_NODE_TYPES = { PERSON: { name: "Person", description: "Represents an individual, like a team member or contact", + attributes: [ + { + name: "email", + description: "The email address of the person", + type: "string", + }, + { + name: "role", + description: "The role or position of the person", + type: "string", + }, + ], }, APP: { name: "App", description: "A software application or service that's integrated", + attributes: [], }, PLACE: { name: "Place", description: "A physical location like an office, meeting room, or city", + attributes: [ + { + name: "address", + description: "The address of the location", + type: "string", + }, + { + name: "coordinates", + description: "Geographic coordinates of the location", + type: "string", + }, + ], }, ORGANIZATION: { name: "Organization", description: "A company, team, or any formal group of people", + attributes: [ + { + name: "industry", + description: "The industry the organization operates in", + type: "string", + }, + { + name: "size", + description: "The size of the organization", + type: "string", + }, + ], }, EVENT: { name: "Event", description: "A meeting, deadline, or any time-based occurrence", + attributes: [ + { + name: "startTime", + description: "The start date and time of the event", + type: "date", + required: true, + }, + { + name: "endTime", + description: "The end date and time of the event", + type: "date", + }, + { + name: "location", + description: "The location of the event", + type: "string", + }, + ], }, ALIAS: { name: "Alias", description: "An alternative name or identifier for an entity", + attributes: [ + { + name: "originalName", + description: "The original name this is an alias for", + type: "string", + }, + { + name: "context", + description: "The context in which this alias is used", + type: "string", + }, + ], }, } as const; @@ -45,70 +120,409 @@ export const APP_NODE_TYPES = { name: "Sol Task", description: "An independent unit of work in Sol, such as a task, bug report, or feature request. Tasks can be associated with lists or linked as subtasks to other tasks.", + attributes: [ + { + name: "taskId", + description: "Unique identifier for the task", + type: "string", + required: true, + }, + { + name: "title", + description: "The title of the task", + type: "string", + required: true, + }, + { + name: "description", + description: "The description of the task", + type: "string", + }, + { + name: "status", + description: "The current status of the task", + type: "string", + }, + { + name: "dueDate", + description: "The due date of the task", + type: "date", + }, + { + name: "priority", + description: "The priority level of the task", + type: "string", + }, + ], }, LIST: { name: "Sol List", description: "A flexible container in Sol for organizing content such as tasks, text, or references. Lists are used for task tracking, information collections, or reference materials.", + attributes: [ + { + name: "listId", + description: "Unique identifier for the list", + type: "string", + required: true, + }, + { + name: "title", + description: "The title of the list", + type: "string", + required: true, + }, + { + name: "description", + description: "The description of the list", + type: "string", + }, + { + name: "itemCount", + description: "The number of items in the list", + type: "number", + }, + ], }, PREFERENCE: { name: "Sol Preference", description: "A user-stated intent, setting, or configuration in Sol, such as preferred formats, notification settings, timezones, or other customizations. Preferences reflect how a user wants the system to behave.", + attributes: [ + { + name: "key", + description: "The preference key or name", + type: "string", + required: true, + }, + { + name: "value", + description: "The preference value", + type: "string", + required: true, + }, + ], }, COMMAND: { name: "Sol Command", description: - "A user-issued command or trigger phrase, often starting with '/' or '@', that directs the system or an app to perform a specific action. Commands should always be extracted as distinct, important user actions.", + "A user-issued command or trigger phrase, often starting with '/', that directs the system or an app to perform a specific action. Commands should always be extracted as distinct, important user actions.", + attributes: [ + { + name: "commandId", + description: "Unique identifier for the command", + type: "string", + required: true, + }, + { + name: "commandName", + description: "The name of the command", + type: "string", + required: true, + }, + ], }, AUTOMATION: { name: "Sol Automation", description: "A workflow or rule in Sol that automatically performs actions based on specific conditions or triggers, such as recurring tasks, reminders, or integrations with other systems.", + attributes: [ + { + name: "automationId", + description: "Unique identifier for the automation", + type: "string", + required: true, + }, + { + name: "trigger", + description: "The event that triggers this automation", + type: "string", + required: true, + }, + { + name: "action", + description: "The action performed by this automation", + type: "string", + required: true, + }, + ], }, }, [Apps.LINEAR]: { ISSUE: { name: "Linear Issue", description: "A task, bug report, or feature request tracked in Linear", + attributes: [ + { + name: "issueId", + description: "Unique identifier for the issue", + type: "string", + required: true, + }, + { + name: "title", + description: "The title of the issue", + type: "string", + required: true, + }, + { + name: "status", + description: "The current status of the issue", + type: "string", + }, + { + name: "priority", + description: "The priority level of the issue", + type: "number", + }, + { + name: "assignee", + description: "The person assigned to the issue", + type: "string", + }, + ], }, PROJECT: { name: "Linear Project", description: "A collection of related issues and work items in Linear", + attributes: [ + { + name: "projectId", + description: "Unique identifier for the project", + type: "string", + required: true, + }, + { + name: "name", + description: "The name of the project", + type: "string", + required: true, + }, + { + name: "status", + description: "The current status of the project", + type: "string", + }, + { + name: "startDate", + description: "The start date of the project", + type: "date", + }, + { + name: "targetDate", + description: "The target completion date of the project", + type: "date", + }, + ], }, CYCLE: { name: "Linear Cycle", description: "A time-boxed iteration of work in Linear", + attributes: [ + { + name: "cycleId", + description: "Unique identifier for the cycle", + type: "string", + required: true, + }, + { + name: "name", + description: "The name of the cycle", + type: "string", + required: true, + }, + { + name: "startDate", + description: "The start date of the cycle", + type: "date", + required: true, + }, + { + name: "endDate", + description: "The end date of the cycle", + type: "date", + required: true, + }, + ], }, TEAM: { name: "Linear Team", description: "A group of people working together in Linear", + attributes: [ + { + name: "teamId", + description: "Unique identifier for the team", + type: "string", + required: true, + }, + { + name: "name", + description: "The name of the team", + type: "string", + required: true, + }, + { + name: "key", + description: "The team's key or shorthand", + type: "string", + }, + { + name: "memberCount", + description: "Number of members in the team", + type: "number", + }, + ], }, LABEL: { name: "Linear Label", description: "A tag used to categorize and organize issues in Linear", + attributes: [ + { + name: "labelId", + description: "Unique identifier for the label", + type: "string", + required: true, + }, + { + name: "name", + description: "The name of the label", + type: "string", + required: true, + }, + { + name: "color", + description: "The color of the label", + type: "string", + }, + ], }, }, [Apps.SLACK]: { CHANNEL: { name: "Slack Channel", description: "A dedicated space for team communication in Slack", + attributes: [ + { + name: "channelId", + description: "Unique identifier for the channel", + type: "string", + required: true, + }, + { + name: "name", + description: "The name of the channel", + type: "string", + required: true, + }, + { + name: "isPrivate", + description: "Whether the channel is private", + type: "boolean", + }, + { + name: "memberCount", + description: "The number of members in the channel", + type: "number", + }, + ], }, THREAD: { name: "Slack Thread", description: "A focused conversation branch within a Slack channel", + attributes: [ + { + name: "threadId", + description: "Unique identifier for the thread", + type: "string", + required: true, + }, + { + name: "parentMessageId", + description: "ID of the parent message", + type: "string", + required: true, + }, + { + name: "replyCount", + description: "Number of replies in the thread", + type: "number", + }, + ], }, MESSAGE: { name: "Slack Message", description: "A single communication sent in a Slack channel or thread", + attributes: [ + { + name: "messageId", + description: "Unique identifier for the message", + type: "string", + required: true, + }, + { + name: "content", + description: "The content of the message", + type: "string", + required: true, + }, + { + name: "timestamp", + description: "When the message was sent", + type: "date", + required: true, + }, + { + name: "reactions", + description: "Reactions to the message", + type: "array", + }, + ], }, REACTION: { name: "Slack Reaction", description: "An emoji response to a message in Slack", + attributes: [ + { + name: "emoji", + description: "The emoji used in the reaction", + type: "string", + required: true, + }, + { + name: "count", + description: "Number of users who reacted with this emoji", + type: "number", + required: true, + }, + ], }, FILE: { name: "Slack File", description: "A document, image or other file shared in Slack", + attributes: [ + { + name: "fileId", + description: "Unique identifier for the file", + type: "string", + required: true, + }, + { + name: "name", + description: "The name of the file", + type: "string", + required: true, + }, + { + name: "type", + description: "The file type or format", + type: "string", + }, + { + name: "size", + description: "The size of the file in bytes", + type: "number", + }, + ], }, }, } as const; @@ -161,3 +575,7 @@ export function getNodeTypesString(apps: Array) { nodeTypesString += `App-specific Node Types:\n${appSpecificTypesString}`; return nodeTypesString; } + +export function getNodeAttributesString( + apps: Array, +) {} diff --git a/packages/types/src/graph/graph.entity.ts b/packages/types/src/graph/graph.entity.ts index 4b838d0..8ffefe4 100644 --- a/packages/types/src/graph/graph.entity.ts +++ b/packages/types/src/graph/graph.entity.ts @@ -12,7 +12,7 @@ export interface EpisodicNode { content: string; originalContent: string; contentEmbedding?: number[]; - type: string; + metadata: Record; source: string; createdAt: Date; validAt: Date; @@ -70,7 +70,7 @@ export type AddEpisodeParams = { name: string; episodeBody: string; referenceTime: Date; - type: EpisodeType; + metadata: Record; source: string; userId: string; spaceId?: string;