feat: entity attributes and timeframe filter in search

This commit is contained in:
Manoj K 2025-06-16 19:23:13 +05:30
parent 7124253981
commit 33eae2619a
11 changed files with 697 additions and 44 deletions

View File

@ -45,7 +45,7 @@ const EnvironmentSchema = z.object({
AUTH_GOOGLE_CLIENT_ID: z.string().optional(), AUTH_GOOGLE_CLIENT_ID: z.string().optional(),
AUTH_GOOGLE_CLIENT_SECRET: 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
REDIS_HOST: z.string().default("localhost"), REDIS_HOST: z.string().default("localhost"),

View File

@ -6,7 +6,14 @@ import { json } from "@remix-run/node";
export const SearchBodyRequest = z.object({ export const SearchBodyRequest = z.object({
query: z.string(), query: z.string(),
spaceId: z.string().optional(), 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(); const searchService = new SearchService();
@ -23,6 +30,16 @@ const { action, loader } = createActionApiRoute(
const results = await searchService.search( const results = await searchService.search(
body.query, body.query,
authentication.userId, 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); return json(results);
}, },

View File

@ -7,7 +7,7 @@ export async function saveEntity(entity: EntityNode): Promise<string> {
ON CREATE SET ON CREATE SET
n.name = $name, n.name = $name,
n.type = $type, n.type = $type,
n.attributesJson = $attributesJson, n.attributes = $attributes,
n.nameEmbedding = $nameEmbedding, n.nameEmbedding = $nameEmbedding,
n.createdAt = $createdAt, n.createdAt = $createdAt,
n.userId = $userId, n.userId = $userId,
@ -15,7 +15,7 @@ export async function saveEntity(entity: EntityNode): Promise<string> {
ON MATCH SET ON MATCH SET
n.name = $name, n.name = $name,
n.type = $type, n.type = $type,
n.attributesJson = $attributesJson, n.attributes = $attributes,
n.nameEmbedding = $nameEmbedding, n.nameEmbedding = $nameEmbedding,
n.space = $space n.space = $space
RETURN n.uuid as uuid RETURN n.uuid as uuid
@ -25,7 +25,7 @@ export async function saveEntity(entity: EntityNode): Promise<string> {
uuid: entity.uuid, uuid: entity.uuid,
name: entity.name, name: entity.name,
type: entity.type, type: entity.type,
attributesJson: JSON.stringify(entity.attributes || {}), attributes: JSON.stringify(entity.attributes || {}),
nameEmbedding: entity.nameEmbedding, nameEmbedding: entity.nameEmbedding,
createdAt: entity.createdAt.toISOString(), createdAt: entity.createdAt.toISOString(),
userId: entity.userId, userId: entity.userId,
@ -50,7 +50,7 @@ export async function getEntity(uuid: string): Promise<EntityNode | null> {
uuid: entity.uuid, uuid: entity.uuid,
name: entity.name, name: entity.name,
type: entity.type, type: entity.type,
attributes: JSON.parse(entity.attributesJson || "{}"), attributes: JSON.parse(entity.attributes || "{}"),
nameEmbedding: entity.nameEmbedding, nameEmbedding: entity.nameEmbedding,
createdAt: new Date(entity.createdAt), createdAt: new Date(entity.createdAt),
userId: entity.userId, userId: entity.userId,
@ -81,7 +81,7 @@ export async function findSimilarEntities(params: {
uuid: entity.uuid, uuid: entity.uuid,
name: entity.name, name: entity.name,
type: entity.type, type: entity.type,
attributes: JSON.parse(entity.attributesJson || "{}"), attributes: JSON.parse(entity.attributes || "{}"),
nameEmbedding: entity.nameEmbedding, nameEmbedding: entity.nameEmbedding,
createdAt: new Date(entity.createdAt), createdAt: new Date(entity.createdAt),
userId: entity.userId, userId: entity.userId,

View File

@ -8,7 +8,7 @@ export async function saveEpisode(episode: EpisodicNode): Promise<string> {
e.content = $content, e.content = $content,
e.originalContent = $originalContent, e.originalContent = $originalContent,
e.contentEmbedding = $contentEmbedding, e.contentEmbedding = $contentEmbedding,
e.type = $type, e.metadata = $metadata,
e.source = $source, e.source = $source,
e.createdAt = $createdAt, e.createdAt = $createdAt,
e.validAt = $validAt, e.validAt = $validAt,
@ -20,7 +20,7 @@ export async function saveEpisode(episode: EpisodicNode): Promise<string> {
e.content = $content, e.content = $content,
e.contentEmbedding = $contentEmbedding, e.contentEmbedding = $contentEmbedding,
e.originalContent = $originalContent, e.originalContent = $originalContent,
e.type = $type, e.metadata = $metadata,
e.source = $source, e.source = $source,
e.validAt = $validAt, e.validAt = $validAt,
e.labels = $labels, e.labels = $labels,
@ -34,7 +34,7 @@ export async function saveEpisode(episode: EpisodicNode): Promise<string> {
content: episode.content, content: episode.content,
originalContent: episode.originalContent, originalContent: episode.originalContent,
source: episode.source, source: episode.source,
type: episode.type, metadata: JSON.stringify(episode.metadata || {}),
userId: episode.userId || null, userId: episode.userId || null,
labels: episode.labels || [], labels: episode.labels || [],
createdAt: episode.createdAt.toISOString(), createdAt: episode.createdAt.toISOString(),
@ -64,7 +64,7 @@ export async function getEpisode(uuid: string): Promise<EpisodicNode | null> {
content: episode.content, content: episode.content,
originalContent: episode.originalContent, originalContent: episode.originalContent,
contentEmbedding: episode.contentEmbedding, contentEmbedding: episode.contentEmbedding,
type: episode.type, metadata: JSON.parse(episode.metadata || "{}"),
source: episode.source, source: episode.source,
createdAt: new Date(episode.createdAt), createdAt: new Date(episode.createdAt),
validAt: new Date(episode.validAt), validAt: new Date(episode.validAt),
@ -118,7 +118,7 @@ export async function getRecentEpisodes(params: {
content: episode.content, content: episode.content,
originalContent: episode.originalContent, originalContent: episode.originalContent,
contentEmbedding: episode.contentEmbedding, contentEmbedding: episode.contentEmbedding,
type: episode.type, metadata: JSON.parse(episode.metadata || "{}"),
source: episode.source, source: episode.source,
createdAt: new Date(episode.createdAt), createdAt: new Date(episode.createdAt),
validAt: new Date(episode.validAt), validAt: new Date(episode.validAt),

View File

@ -21,7 +21,7 @@ export async function saveTriple(triple: Triple): Promise<string> {
n.createdAt = $createdAt, n.createdAt = $createdAt,
n.validAt = $validAt, n.validAt = $validAt,
n.invalidAt = $invalidAt, n.invalidAt = $invalidAt,
n.attributesJson = $attributesJson, n.attributes = $attributes,
n.userId = $userId, n.userId = $userId,
n.space = $space n.space = $space
ON MATCH SET ON MATCH SET
@ -29,7 +29,7 @@ export async function saveTriple(triple: Triple): Promise<string> {
n.factEmbedding = $factEmbedding, n.factEmbedding = $factEmbedding,
n.validAt = $validAt, n.validAt = $validAt,
n.invalidAt = $invalidAt, n.invalidAt = $invalidAt,
n.attributesJson = $attributesJson, n.attributes = $attributes,
n.space = $space n.space = $space
RETURN n.uuid as uuid RETURN n.uuid as uuid
`; `;
@ -43,7 +43,7 @@ export async function saveTriple(triple: Triple): Promise<string> {
invalidAt: triple.statement.invalidAt invalidAt: triple.statement.invalidAt
? triple.statement.invalidAt.toISOString() ? triple.statement.invalidAt.toISOString()
: null, : null,
attributesJson: JSON.stringify(triple.statement.attributes || {}), attributes: JSON.stringify(triple.statement.attributes || {}),
userId: triple.provenance.userId, userId: triple.provenance.userId,
space: triple.statement.space || null, space: triple.statement.space || null,
}; };
@ -273,7 +273,7 @@ export async function getTripleForStatement({
content: episodeProps.content, content: episodeProps.content,
originalContent: episodeProps.originalContent, originalContent: episodeProps.originalContent,
source: episodeProps.source, source: episodeProps.source,
type: episodeProps.type, metadata: episodeProps.metadata,
createdAt: new Date(episodeProps.createdAt), createdAt: new Date(episodeProps.createdAt),
validAt: new Date(episodeProps.validAt), validAt: new Date(episodeProps.validAt),
contentEmbedding: episodeProps.contentEmbedding, contentEmbedding: episodeProps.contentEmbedding,

View File

@ -11,7 +11,12 @@ import {
} from "@core/types"; } from "@core/types";
import { logger } from "./logger.service"; import { logger } from "./logger.service";
import crypto from "crypto"; import crypto from "crypto";
import { dedupeNodes, extractMessage, extractText } from "./prompts/nodes"; import {
dedupeNodes,
extractAttributes,
extractMessage,
extractText,
} from "./prompts/nodes";
import { import {
extractStatements, extractStatements,
resolveStatementPrompt, resolveStatementPrompt,
@ -31,7 +36,6 @@ import { normalizePrompt } from "./prompts";
// Default number of previous episodes to retrieve for context // Default number of previous episodes to retrieve for context
const DEFAULT_EPISODE_WINDOW = 5; const DEFAULT_EPISODE_WINDOW = 5;
const RELEVANT_SCHEMA_LIMIT = 10;
export class KnowledgeGraphService { export class KnowledgeGraphService {
async getEmbedding(text: string) { async getEmbedding(text: string) {
@ -60,6 +64,7 @@ export class KnowledgeGraphService {
limit: DEFAULT_EPISODE_WINDOW, limit: DEFAULT_EPISODE_WINDOW,
userId: params.userId, userId: params.userId,
source: params.source, source: params.source,
sessionId: params.sessionId,
}); });
const normalizedEpisodeBody = await this.normalizeEpisodeBody( const normalizedEpisodeBody = await this.normalizeEpisodeBody(
@ -73,7 +78,7 @@ export class KnowledgeGraphService {
content: normalizedEpisodeBody, content: normalizedEpisodeBody,
originalContent: params.episodeBody, originalContent: params.episodeBody,
source: params.source, source: params.source,
type: params.type || EpisodeType.Text, metadata: params.metadata || {},
createdAt: now, createdAt: now,
validAt: new Date(params.referenceTime), validAt: new Date(params.referenceTime),
labels: [], labels: [],
@ -106,8 +111,27 @@ export class KnowledgeGraphService {
const { resolvedStatements, invalidatedStatements } = const { resolvedStatements, invalidatedStatements } =
await this.resolveStatements(resolvedTriples, episode); 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 // Save triples sequentially to avoid parallel processing issues
for (const triple of resolvedStatements) { for (const triple of updatedTriples) {
await saveTriple(triple); await saveTriple(triple);
} }
@ -154,10 +178,9 @@ export class KnowledgeGraphService {
}; };
// Get the extract_json prompt from the prompt library // Get the extract_json prompt from the prompt library
const messages = const messages = episode.sessionId
episode.type === EpisodeType.Conversation ? extractMessage(context)
? extractMessage(context) : extractText(context);
: extractText(context);
let responseText = ""; let responseText = "";
@ -668,6 +691,100 @@ export class KnowledgeGraphService {
return { resolvedStatements, invalidatedStatements }; return { resolvedStatements, invalidatedStatements };
} }
/**
* Add attributes to entity nodes based on the resolved statements
*/
private async addAttributesToEntities(
triples: Triple[],
episode: EpisodicNode,
): Promise<Triple[]> {
// Collect all unique entities from the triples
const entityMap = new Map<string, EntityNode>();
// 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(/<output>([\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 * Normalize an episode by extracting entities and creating nodes and statements
*/ */

View File

@ -256,3 +256,56 @@ ${JSON.stringify(context.extracted_nodes, null, 2)}
}, },
]; ];
}; };
export const extractAttributes = (
context: Record<string, any>,
): 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:
<output>
{
"entities": [
{
"uuid": "entity-uuid",
"attributes": {
"attributeName1": "value1",
"attributeName2": "value2",
...
}
},
...
]
}
</output>`;
const userPrompt = `
<ENTITY_TYPES>
${JSON.stringify(context.entityTypes, null, 2)}
</ENTITY_TYPES>
<ENTITIES>
${JSON.stringify(context.entities, null, 2)}
</ENTITIES>
<EPISODE_CONTENT>
${context.episodeContent}
</EPISODE_CONTENT>
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 },
];
};

View File

@ -37,10 +37,13 @@ export class SearchService {
options: SearchOptions = {}, options: SearchOptions = {},
): Promise<{ episodes: string[]; facts: string[] }> { ): Promise<{ episodes: string[]; facts: string[] }> {
// Default options // Default options
const opts: Required<SearchOptions> = { const opts: Required<SearchOptions> = {
limit: options.limit || 10, limit: options.limit || 10,
maxBfsDepth: options.maxBfsDepth || 4, maxBfsDepth: options.maxBfsDepth || 4,
validAt: options.validAt || new Date(), validAt: options.validAt || new Date(),
startTime: options.startTime || null,
endTime: options.endTime || new Date(),
includeInvalidated: options.includeInvalidated || false, includeInvalidated: options.includeInvalidated || false,
entityTypes: options.entityTypes || [], entityTypes: options.entityTypes || [],
predicateTypes: options.predicateTypes || [], predicateTypes: options.predicateTypes || [],
@ -213,6 +216,8 @@ export interface SearchOptions {
limit?: number; limit?: number;
maxBfsDepth?: number; maxBfsDepth?: number;
validAt?: Date; validAt?: Date;
startTime?: Date | null;
endTime?: Date;
includeInvalidated?: boolean; includeInvalidated?: boolean;
entityTypes?: string[]; entityTypes?: string[];
predicateTypes?: string[]; predicateTypes?: string[];

View File

@ -16,14 +16,28 @@ export async function performBM25Search(
// Sanitize the query for Lucene syntax // Sanitize the query for Lucene syntax
const sanitizedQuery = sanitizeLuceneQuery(query); 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 // Use Neo4j's built-in fulltext search capabilities
const cypher = ` const cypher = `
CALL db.index.fulltext.queryNodes("statement_fact_index", $query) CALL db.index.fulltext.queryNodes("statement_fact_index", $query)
YIELD node AS s, score YIELD node AS s, score
WHERE WHERE
s.validAt <= $validAt (s.userId = $userId)
AND (s.invalidAt IS NULL OR s.invalidAt > $validAt) ${timeframeCondition}
AND (s.userId = $userId)
RETURN s, score RETURN s, score
ORDER BY score DESC ORDER BY score DESC
`; `;
@ -31,7 +45,8 @@ export async function performBM25Search(
const params = { const params = {
query: sanitizedQuery, query: sanitizedQuery,
userId, userId,
validAt: options.validAt.toISOString(), validAt: options.endTime.toISOString(),
...(options.startTime && { startTime: options.startTime.toISOString() }),
}; };
const records = await runQuery(cypher, params); const records = await runQuery(cypher, params);
@ -46,9 +61,9 @@ export async function performBM25Search(
* Sanitize a query string for Lucene syntax * Sanitize a query string for Lucene syntax
*/ */
export function sanitizeLuceneQuery(query: string): string { export function sanitizeLuceneQuery(query: string): string {
// Escape special characters: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ // Escape special characters: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
let sanitized = query.replace( let sanitized = query.replace(
/[+\-&|!(){}[\]^"~*?:\\]/g, /[+\-&|!(){}[\]^"~*?:\\\/]/g,
(match) => "\\" + match, (match) => "\\" + match,
); );
@ -71,16 +86,27 @@ export async function performVectorSearch(
options: Required<SearchOptions>, options: Required<SearchOptions>,
): Promise<StatementNode[]> { ): Promise<StatementNode[]> {
try { try {
// 1. Generate embedding for the query // Build the WHERE clause based on timeframe options
// const embedding = await this.getEmbedding(query); 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 = ` const cypher = `
MATCH (s:Statement) MATCH (s:Statement)
WHERE WHERE
s.validAt <= $validAt (s.userId = $userId)
AND (s.invalidAt IS NULL OR s.invalidAt > $validAt) ${timeframeCondition}
AND (s.userId = $userId)
WITH s, vector.similarity.cosine(s.factEmbedding, $embedding) AS score WITH s, vector.similarity.cosine(s.factEmbedding, $embedding) AS score
WHERE score > 0.7 WHERE score > 0.7
RETURN s, score RETURN s, score
@ -90,7 +116,8 @@ export async function performVectorSearch(
const params = { const params = {
embedding: query, embedding: query,
userId, userId,
validAt: options.validAt.toISOString(), validAt: options.endTime.toISOString(),
...(options.startTime && { startTime: options.startTime.toISOString() }),
}; };
const records = await runQuery(cypher, params); const records = await runQuery(cypher, params);
@ -120,9 +147,10 @@ export async function performBfsSearch(
const statements = await bfsTraversal( const statements = await bfsTraversal(
entity.uuid, entity.uuid,
options.maxBfsDepth, options.maxBfsDepth,
options.validAt, options.endTime,
userId, userId,
options.includeInvalidated, options.includeInvalidated,
options.startTime,
); );
allStatements.push(...statements); allStatements.push(...statements);
} }
@ -143,17 +171,31 @@ export async function bfsTraversal(
validAt: Date, validAt: Date,
userId: string, userId: string,
includeInvalidated: boolean, includeInvalidated: boolean,
startTime: Date | null,
): Promise<StatementNode[]> { ): Promise<StatementNode[]> {
try { 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 // 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 // This query implements BFS up to maxDepth and collects all statements along the way
const cypher = ` const cypher = `
MATCH (e:Entity {uuid: $startEntityId})<-[:HAS_SUBJECT|HAS_OBJECT|HAS_PREDICATE]-(s:Statement) MATCH (e:Entity {uuid: $startEntityId})<-[:HAS_SUBJECT|HAS_OBJECT|HAS_PREDICATE]-(s:Statement)
WHERE WHERE
s.validAt <= $validAt (s.userId = $userId)
AND (s.invalidAt IS NULL OR s.invalidAt > $validAt)
AND (s.userId = $userId)
AND ($includeInvalidated OR s.invalidAt IS NULL) AND ($includeInvalidated OR s.invalidAt IS NULL)
${timeframeCondition}
RETURN s as statement RETURN s as statement
`; `;
@ -163,6 +205,7 @@ export async function bfsTraversal(
validAt: validAt.toISOString(), validAt: validAt.toISOString(),
userId, userId,
includeInvalidated, includeInvalidated,
...(startTime && { startTime: startTime.toISOString() }),
}; };
const records = await runQuery(cypher, params); const records = await runQuery(cypher, params);

View File

@ -10,31 +10,106 @@ export const AppNames = {
[Apps.SOL]: "Sol", [Apps.SOL]: "Sol",
} as const; } 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 // General node types that are common across all apps
export const GENERAL_NODE_TYPES = { export const GENERAL_NODE_TYPES = {
PERSON: { PERSON: {
name: "Person", name: "Person",
description: "Represents an individual, like a team member or contact", 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: { APP: {
name: "App", name: "App",
description: "A software application or service that's integrated", description: "A software application or service that's integrated",
attributes: [],
}, },
PLACE: { PLACE: {
name: "Place", name: "Place",
description: "A physical location like an office, meeting room, or city", 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: { ORGANIZATION: {
name: "Organization", name: "Organization",
description: "A company, team, or any formal group of people", 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: { EVENT: {
name: "Event", name: "Event",
description: "A meeting, deadline, or any time-based occurrence", 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: { ALIAS: {
name: "Alias", name: "Alias",
description: "An alternative name or identifier for an entity", 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; } as const;
@ -45,70 +120,409 @@ export const APP_NODE_TYPES = {
name: "Sol Task", name: "Sol Task",
description: 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.", "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: { LIST: {
name: "Sol List", name: "Sol List",
description: 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.", "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: { PREFERENCE: {
name: "Sol Preference", name: "Sol Preference",
description: 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.", "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: { COMMAND: {
name: "Sol Command", name: "Sol Command",
description: 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: { AUTOMATION: {
name: "Sol Automation", name: "Sol Automation",
description: 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.", "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]: { [Apps.LINEAR]: {
ISSUE: { ISSUE: {
name: "Linear Issue", name: "Linear Issue",
description: "A task, bug report, or feature request tracked in Linear", 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: { PROJECT: {
name: "Linear Project", name: "Linear Project",
description: "A collection of related issues and work items in Linear", 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: { CYCLE: {
name: "Linear Cycle", name: "Linear Cycle",
description: "A time-boxed iteration of work in Linear", 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: { TEAM: {
name: "Linear Team", name: "Linear Team",
description: "A group of people working together in Linear", 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: { LABEL: {
name: "Linear Label", name: "Linear Label",
description: "A tag used to categorize and organize issues in Linear", 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]: { [Apps.SLACK]: {
CHANNEL: { CHANNEL: {
name: "Slack Channel", name: "Slack Channel",
description: "A dedicated space for team communication in Slack", 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: { THREAD: {
name: "Slack Thread", name: "Slack Thread",
description: "A focused conversation branch within a Slack channel", 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: { MESSAGE: {
name: "Slack Message", name: "Slack Message",
description: "A single communication sent in a Slack channel or thread", 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: { REACTION: {
name: "Slack Reaction", name: "Slack Reaction",
description: "An emoji response to a message in Slack", 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: { FILE: {
name: "Slack File", name: "Slack File",
description: "A document, image or other file shared in Slack", 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; } as const;
@ -161,3 +575,7 @@ export function getNodeTypesString(apps: Array<keyof typeof APP_NODE_TYPES>) {
nodeTypesString += `App-specific Node Types:\n${appSpecificTypesString}`; nodeTypesString += `App-specific Node Types:\n${appSpecificTypesString}`;
return nodeTypesString; return nodeTypesString;
} }
export function getNodeAttributesString(
apps: Array<keyof typeof APP_NODE_TYPES>,
) {}

View File

@ -12,7 +12,7 @@ export interface EpisodicNode {
content: string; content: string;
originalContent: string; originalContent: string;
contentEmbedding?: number[]; contentEmbedding?: number[];
type: string; metadata: Record<string, any>;
source: string; source: string;
createdAt: Date; createdAt: Date;
validAt: Date; validAt: Date;
@ -70,7 +70,7 @@ export type AddEpisodeParams = {
name: string; name: string;
episodeBody: string; episodeBody: string;
referenceTime: Date; referenceTime: Date;
type: EpisodeType; metadata: Record<string, any>;
source: string; source: string;
userId: string; userId: string;
spaceId?: string; spaceId?: string;