mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 09:58:28 +00:00
Feat: generate space summary by topics
This commit is contained in:
parent
e89e7c1024
commit
43c3482351
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, ReactNode } from "react";
|
import { useState, useEffect, type ReactNode } from "react";
|
||||||
import { useFetcher } from "@remix-run/react";
|
import { useFetcher } from "@remix-run/react";
|
||||||
import { AlertCircle, Loader2 } from "lucide-react";
|
import { AlertCircle, Loader2 } from "lucide-react";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export const SearchBodyRequest = z.object({
|
|||||||
entityTypes: z.array(z.string()).optional(),
|
entityTypes: z.array(z.string()).optional(),
|
||||||
scoreThreshold: z.number().optional(),
|
scoreThreshold: z.number().optional(),
|
||||||
minResults: z.number().optional(),
|
minResults: z.number().optional(),
|
||||||
|
adaptiveFiltering: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchService = new SearchService();
|
const searchService = new SearchService();
|
||||||
@ -45,6 +46,7 @@ const { action, loader } = createHybridActionApiRoute(
|
|||||||
scoreThreshold: body.scoreThreshold,
|
scoreThreshold: body.scoreThreshold,
|
||||||
minResults: body.minResults,
|
minResults: body.minResults,
|
||||||
spaceIds: body.spaceIds,
|
spaceIds: body.spaceIds,
|
||||||
|
adaptiveFiltering: body.adaptiveFiltering,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return json(results);
|
return json(results);
|
||||||
|
|||||||
249
apps/webapp/app/routes/api.v1.spaces.$spaceId.summary.ts
Normal file
249
apps/webapp/app/routes/api.v1.spaces.$spaceId.summary.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
createHybridActionApiRoute,
|
||||||
|
createHybridLoaderApiRoute,
|
||||||
|
} from "~/services/routeBuilders/apiBuilder.server";
|
||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { apiCors } from "~/utils/apiCors";
|
||||||
|
import { triggerSpaceSummary } from "~/trigger/spaces/space-summary";
|
||||||
|
import { SpaceService } from "~/services/space.server";
|
||||||
|
import { addToQueue } from "~/lib/ingest.server";
|
||||||
|
import { EpisodeType ,type DocumentNode } from "@core/types";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
import { saveDocument } from "~/services/graphModels/document";
|
||||||
|
import { logger } from "~/services/logger.service";
|
||||||
|
import { prisma } from "~/db.server";
|
||||||
|
|
||||||
|
const spaceService = new SpaceService();
|
||||||
|
|
||||||
|
// Schema for space ID parameter
|
||||||
|
const SpaceParamsSchema = z.object({
|
||||||
|
spaceId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { action } = createHybridActionApiRoute(
|
||||||
|
{
|
||||||
|
params: SpaceParamsSchema,
|
||||||
|
allowJWT: true,
|
||||||
|
authorization: {
|
||||||
|
action: "manage",
|
||||||
|
},
|
||||||
|
corsStrategy: "all",
|
||||||
|
},
|
||||||
|
async ({ authentication, params, request }) => {
|
||||||
|
const userId = authentication.userId;
|
||||||
|
const { spaceId } = params;
|
||||||
|
|
||||||
|
if (request.method === "PUT") {
|
||||||
|
try {
|
||||||
|
// Get the markdown content from request body
|
||||||
|
const markdownContent = await request.text();
|
||||||
|
|
||||||
|
if (!markdownContent || markdownContent.trim().length === 0) {
|
||||||
|
return json({ error: "Empty summary content provided" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get space details
|
||||||
|
const space = await spaceService.getSpace(spaceId, userId);
|
||||||
|
if (!space) {
|
||||||
|
return json({ error: "Space not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create updated summary document
|
||||||
|
const documentUuid = await createUpdatedSummaryDocument(
|
||||||
|
spaceId,
|
||||||
|
userId,
|
||||||
|
space.name,
|
||||||
|
markdownContent
|
||||||
|
);
|
||||||
|
|
||||||
|
// Queue document for ingestion
|
||||||
|
await queueSummaryDocumentIngestion(
|
||||||
|
documentUuid,
|
||||||
|
spaceId,
|
||||||
|
userId,
|
||||||
|
markdownContent
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Updated space summary document ${documentUuid} for space ${spaceId}`);
|
||||||
|
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
summary: {
|
||||||
|
documentId: documentUuid,
|
||||||
|
spaceId,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error updating space summary for ${spaceId}:`, error as Record<string, unknown>);
|
||||||
|
return json(
|
||||||
|
{ error: "Failed to update space summary" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === "POST") {
|
||||||
|
try {
|
||||||
|
// Get space details first
|
||||||
|
const space = await spaceService.getSpace(spaceId, userId);
|
||||||
|
if (!space) {
|
||||||
|
return json({ error: "Space not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get workspace for user
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
where: { id: userId },
|
||||||
|
include: { Workspace: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user?.Workspace?.id) {
|
||||||
|
return json(
|
||||||
|
{ error: "Workspace not found" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger space summary generation using existing infrastructure
|
||||||
|
const result = await triggerSpaceSummary({
|
||||||
|
userId,
|
||||||
|
workspaceId: user.Workspace.id,
|
||||||
|
spaceId,
|
||||||
|
triggerSource: "manual",
|
||||||
|
});
|
||||||
|
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
summary: {
|
||||||
|
taskId: result.id,
|
||||||
|
spaceId,
|
||||||
|
triggeredAt: new Date().toISOString(),
|
||||||
|
status: "processing",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error generating space summary for ${spaceId}:`, error as Record<string, unknown>);
|
||||||
|
return json(
|
||||||
|
{ error: "Failed to generate space summary" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({ error: "Method not allowed" }, { status: 405 });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const loader = createHybridLoaderApiRoute(
|
||||||
|
{
|
||||||
|
allowJWT: true,
|
||||||
|
params: SpaceParamsSchema,
|
||||||
|
corsStrategy: "all",
|
||||||
|
findResource: async () => 1,
|
||||||
|
},
|
||||||
|
async ({ authentication, request, params }) => {
|
||||||
|
if (request.method.toUpperCase() === "OPTIONS") {
|
||||||
|
return apiCors(request, json({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get space details
|
||||||
|
const space = await spaceService.getSpace(
|
||||||
|
params.spaceId,
|
||||||
|
authentication.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!space) {
|
||||||
|
return json({ error: "Space not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return current space summary information
|
||||||
|
return json({
|
||||||
|
space: {
|
||||||
|
id: space.uuid,
|
||||||
|
name: space.name,
|
||||||
|
description: space.description,
|
||||||
|
summary: space.summary,
|
||||||
|
themes: space.themes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error fetching space summary for ${params.spaceId}:`, error as Record<string, unknown>);
|
||||||
|
return json(
|
||||||
|
{ error: "Failed to fetch space summary" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an updated summary document
|
||||||
|
*/
|
||||||
|
async function createUpdatedSummaryDocument(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
spaceName: string,
|
||||||
|
markdownContent: string
|
||||||
|
): Promise<string> {
|
||||||
|
const documentUuid = crypto.randomUUID();
|
||||||
|
const contentHash = crypto.createHash('sha256').update(markdownContent).digest('hex');
|
||||||
|
|
||||||
|
const document: DocumentNode = {
|
||||||
|
uuid: documentUuid,
|
||||||
|
title: `${spaceName} - Space Summary (Updated)`,
|
||||||
|
originalContent: markdownContent,
|
||||||
|
metadata: {
|
||||||
|
documentType: "space_summary",
|
||||||
|
spaceId,
|
||||||
|
spaceName,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
updateSource: "manual",
|
||||||
|
},
|
||||||
|
source: "space",
|
||||||
|
userId,
|
||||||
|
createdAt: new Date(),
|
||||||
|
validAt: new Date(),
|
||||||
|
totalChunks: 1,
|
||||||
|
sessionId: spaceId,
|
||||||
|
version: 1, // TODO: Implement proper versioning
|
||||||
|
contentHash,
|
||||||
|
previousVersionUuid: undefined, // TODO: Link to previous version
|
||||||
|
chunkHashes: [contentHash],
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveDocument(document);
|
||||||
|
return documentUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue the updated summary document for ingestion
|
||||||
|
*/
|
||||||
|
async function queueSummaryDocumentIngestion(
|
||||||
|
documentUuid: string,
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
markdownContent: string
|
||||||
|
): Promise<void> {
|
||||||
|
const ingestBody = {
|
||||||
|
episodeBody: markdownContent,
|
||||||
|
referenceTime: new Date().toISOString(),
|
||||||
|
metadata: {
|
||||||
|
documentType: "space_summary",
|
||||||
|
documentUuid,
|
||||||
|
spaceId,
|
||||||
|
updateSource: "manual",
|
||||||
|
},
|
||||||
|
source: "space",
|
||||||
|
spaceId,
|
||||||
|
sessionId: spaceId,
|
||||||
|
type: EpisodeType.DOCUMENT,
|
||||||
|
};
|
||||||
|
|
||||||
|
await addToQueue(ingestBody, userId);
|
||||||
|
|
||||||
|
logger.info(`Queued updated space summary document ${documentUuid} for ingestion`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { action, loader };
|
||||||
@ -60,7 +60,7 @@ async function createMcpServer(
|
|||||||
const { name, arguments: args } = request.params;
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
// Handle memory tools
|
// Handle memory tools
|
||||||
if (name.startsWith("memory_") || name.startsWith("get_user_profile")) {
|
if (name.startsWith("memory_") || name.startsWith("get_")) {
|
||||||
return await callMemoryTool(name, args, userId, source);
|
return await callMemoryTool(name, args, userId, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export class SearchService {
|
|||||||
query: string,
|
query: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
options: SearchOptions = {},
|
options: SearchOptions = {},
|
||||||
): Promise<{ episodes: string[]; facts: { fact: string; validAt: Date }[] }> {
|
): Promise<{ episodes: string[]; facts: { fact: string; validAt: Date; invalidAt: Date | null; relevantScore: number }[] }> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
// Default options
|
// Default options
|
||||||
|
|
||||||
@ -52,6 +52,7 @@ export class SearchService {
|
|||||||
scoreThreshold: options.scoreThreshold || 0.7,
|
scoreThreshold: options.scoreThreshold || 0.7,
|
||||||
minResults: options.minResults || 10,
|
minResults: options.minResults || 10,
|
||||||
spaceIds: options.spaceIds || [],
|
spaceIds: options.spaceIds || [],
|
||||||
|
adaptiveFiltering: options.adaptiveFiltering || false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryVector = await this.getEmbedding(query);
|
const queryVector = await this.getEmbedding(query);
|
||||||
@ -79,27 +80,29 @@ export class SearchService {
|
|||||||
// const filteredResults = rankedStatements;
|
// const filteredResults = rankedStatements;
|
||||||
|
|
||||||
// 3. Return top results
|
// 3. Return top results
|
||||||
const episodes = await getEpisodesByStatements(filteredResults);
|
const episodes = await getEpisodesByStatements(filteredResults.map((item) => item.statement));
|
||||||
|
|
||||||
// Log recall asynchronously (don't await to avoid blocking response)
|
// Log recall asynchronously (don't await to avoid blocking response)
|
||||||
const responseTime = Date.now() - startTime;
|
const responseTime = Date.now() - startTime;
|
||||||
this.logRecallAsync(
|
this.logRecallAsync(
|
||||||
query,
|
query,
|
||||||
userId,
|
userId,
|
||||||
filteredResults,
|
filteredResults.map((item) => item.statement),
|
||||||
opts,
|
opts,
|
||||||
responseTime,
|
responseTime,
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
logger.error("Failed to log recall event:", error);
|
logger.error("Failed to log recall event:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateRecallCount(userId, episodes, filteredResults);
|
this.updateRecallCount(userId, episodes, filteredResults.map((item) => item.statement));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
episodes: episodes.map((episode) => episode.originalContent),
|
episodes: episodes.map((episode) => episode.originalContent),
|
||||||
facts: filteredResults.map((statement) => ({
|
facts: filteredResults.map((statement) => ({
|
||||||
fact: statement.fact,
|
fact: statement.statement.fact,
|
||||||
validAt: statement.validAt,
|
validAt: statement.statement.validAt,
|
||||||
|
invalidAt: statement.statement.invalidAt || null,
|
||||||
|
relevantScore: statement.score,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -111,11 +114,8 @@ export class SearchService {
|
|||||||
private applyAdaptiveFiltering(
|
private applyAdaptiveFiltering(
|
||||||
results: StatementNode[],
|
results: StatementNode[],
|
||||||
options: Required<SearchOptions>,
|
options: Required<SearchOptions>,
|
||||||
): StatementNode[] {
|
): { statement: StatementNode, score: number }[] {
|
||||||
if (results.length === 0) return [];
|
if (results.length === 0) return [];
|
||||||
if (results.length <= 5) {
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isRRF = false;
|
let isRRF = false;
|
||||||
// Extract scores from results
|
// Extract scores from results
|
||||||
@ -141,14 +141,18 @@ export class SearchService {
|
|||||||
score = (result as any).cohereScore;
|
score = (result as any).cohereScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { result, score };
|
return { statement: result, score };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!options.adaptiveFiltering || results.length <= 5) {
|
||||||
|
return scoredResults;
|
||||||
|
}
|
||||||
|
|
||||||
const hasScores = scoredResults.some((item) => item.score > 0);
|
const hasScores = scoredResults.some((item) => item.score > 0);
|
||||||
// If no scores are available, return the original results
|
// If no scores are available, return the original results
|
||||||
if (!hasScores) {
|
if (!hasScores) {
|
||||||
logger.info("No scores found in results, skipping adaptive filtering");
|
logger.info("No scores found in results, skipping adaptive filtering");
|
||||||
return options.limit > 0 ? results.slice(0, options.limit) : results;
|
return options.limit > 0 ? results.slice(0, options.limit).map((item) => ({ statement: item, score: 0 })) : results.map((item) => ({ statement: item, score: 0 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by score (descending)
|
// Sort by score (descending)
|
||||||
@ -197,15 +201,15 @@ export class SearchService {
|
|||||||
// Filter out low-quality results
|
// Filter out low-quality results
|
||||||
const filteredResults = scoredResults
|
const filteredResults = scoredResults
|
||||||
.filter((item) => item.score >= threshold)
|
.filter((item) => item.score >= threshold)
|
||||||
.map((item) => item.result);
|
.map((item) => ({ statement: item.statement, score: item.score }));
|
||||||
|
|
||||||
// Apply limit if specified
|
// Apply limit if specified
|
||||||
const limitedResults =
|
const limitedResults =
|
||||||
options.limit > 0
|
options.limit > 0
|
||||||
? filteredResults.slice(
|
? filteredResults.slice(
|
||||||
0,
|
0,
|
||||||
Math.min(filteredResults.length, options.limit),
|
Math.min(filteredResults.length, options.limit),
|
||||||
)
|
)
|
||||||
: filteredResults;
|
: filteredResults;
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -367,4 +371,5 @@ export interface SearchOptions {
|
|||||||
scoreThreshold?: number;
|
scoreThreshold?: number;
|
||||||
minResults?: number;
|
minResults?: number;
|
||||||
spaceIds?: string[]; // Filter results by specific spaces
|
spaceIds?: string[]; // Filter results by specific spaces
|
||||||
|
adaptiveFiltering?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -463,7 +463,7 @@ async function getStatementsToAnalyze(
|
|||||||
// For new space: analyze all statements (or recent ones)
|
// For new space: analyze all statements (or recent ones)
|
||||||
query = `
|
query = `
|
||||||
MATCH (s:Statement)
|
MATCH (s:Statement)
|
||||||
WHERE s.userId = $userId AND s.invalidAt IS NULL
|
WHERE s.userId = $userId
|
||||||
MATCH (s)-[:HAS_SUBJECT]->(subj:Entity)
|
MATCH (s)-[:HAS_SUBJECT]->(subj:Entity)
|
||||||
MATCH (s)-[:HAS_PREDICATE]->(pred:Entity)
|
MATCH (s)-[:HAS_PREDICATE]->(pred:Entity)
|
||||||
MATCH (s)-[:HAS_OBJECT]->(obj:Entity)
|
MATCH (s)-[:HAS_OBJECT]->(obj:Entity)
|
||||||
@ -476,7 +476,6 @@ async function getStatementsToAnalyze(
|
|||||||
query = `
|
query = `
|
||||||
UNWIND $episodeIds AS episodeId
|
UNWIND $episodeIds AS episodeId
|
||||||
MATCH (e:Episode {uuid: episodeId, userId: $userId})-[:HAS_PROVENANCE]->(s:Statement)
|
MATCH (e:Episode {uuid: episodeId, userId: $userId})-[:HAS_PROVENANCE]->(s:Statement)
|
||||||
WHERE s.invalidAt IS NULL
|
|
||||||
MATCH (s)-[:HAS_SUBJECT]->(subj:Entity),
|
MATCH (s)-[:HAS_SUBJECT]->(subj:Entity),
|
||||||
(s)-[:HAS_PREDICATE]->(pred:Entity),
|
(s)-[:HAS_PREDICATE]->(pred:Entity),
|
||||||
(s)-[:HAS_OBJECT]->(obj:Entity)
|
(s)-[:HAS_OBJECT]->(obj:Entity)
|
||||||
|
|||||||
@ -19,6 +19,7 @@ interface SpacePatternPayload {
|
|||||||
triggerSource?:
|
triggerSource?:
|
||||||
| "summary_complete"
|
| "summary_complete"
|
||||||
| "manual"
|
| "manual"
|
||||||
|
| "assignment"
|
||||||
| "scheduled"
|
| "scheduled"
|
||||||
| "new_space"
|
| "new_space"
|
||||||
| "growth_threshold"
|
| "growth_threshold"
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import type { CoreMessage } from "ai";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { triggerSpacePattern } from "./space-pattern";
|
import { triggerSpacePattern } from "./space-pattern";
|
||||||
import { getSpace, updateSpace } from "../utils/space-utils";
|
import { getSpace, updateSpace } from "../utils/space-utils";
|
||||||
|
import { addToQueue } from "~/lib/ingest.server";
|
||||||
|
import { EpisodeType } from "@core/types";
|
||||||
|
import { getSpaceStatementCount } from "~/services/graphModels/space";
|
||||||
|
|
||||||
interface SpaceSummaryPayload {
|
interface SpaceSummaryPayload {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -16,14 +19,15 @@ interface SpaceSummaryPayload {
|
|||||||
triggerSource?: "assignment" | "manual" | "scheduled";
|
triggerSource?: "assignment" | "manual" | "scheduled";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpaceStatementData {
|
interface SpaceEpisodeData {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
fact: string;
|
content: string;
|
||||||
subject: string;
|
originalContent: string;
|
||||||
predicate: string;
|
source: string;
|
||||||
object: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
validAt: Date;
|
validAt: Date;
|
||||||
|
metadata: any;
|
||||||
|
sessionId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpaceSummaryData {
|
interface SpaceSummaryData {
|
||||||
@ -48,8 +52,8 @@ const SummaryResultSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
maxStatementsForSummary: 200, // Limit statements for performance
|
maxEpisodesForSummary: 20, // Limit episodes for performance
|
||||||
minStatementsForSummary: 3, // Minimum statements to generate summary
|
minEpisodesForSummary: 1, // Minimum episodes to generate summary
|
||||||
summaryPromptTokenLimit: 4000, // Approximate token limit for prompt
|
summaryPromptTokenLimit: 4000, // Approximate token limit for prompt
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -105,11 +109,14 @@ export const spaceSummaryTask = task({
|
|||||||
triggerSource,
|
triggerSource,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ingest summary as document if it exists and continue with patterns
|
||||||
if (!summaryResult.isIncremental && summaryResult.statementCount > 0) {
|
if (!summaryResult.isIncremental && summaryResult.statementCount > 0) {
|
||||||
await triggerSpacePattern({
|
await processSpaceSummarySequentially({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
spaceId,
|
spaceId,
|
||||||
|
spaceName: summaryResult.spaceName,
|
||||||
|
summaryContent: summaryResult.summary,
|
||||||
triggerSource: "summary_complete",
|
triggerSource: "summary_complete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -125,23 +132,25 @@ export const spaceSummaryTask = task({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Update status to error if summary generation fails
|
// No summary generated - this could be due to insufficient episodes or no new episodes
|
||||||
await updateSpaceStatus(spaceId, SPACE_STATUS.ERROR, {
|
// This is not an error state, so update status to ready
|
||||||
|
await updateSpaceStatus(spaceId, SPACE_STATUS.READY, {
|
||||||
userId,
|
userId,
|
||||||
operation: "space-summary",
|
operation: "space-summary",
|
||||||
metadata: {
|
metadata: {
|
||||||
triggerSource,
|
triggerSource,
|
||||||
phase: "failed_summary",
|
phase: "no_summary_needed",
|
||||||
error: "Failed to generate summary",
|
reason: "Insufficient episodes or no new episodes to summarize",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.warn(`Failed to generate summary for space ${spaceId}`);
|
logger.info(`No summary generated for space ${spaceId} - insufficient or no new episodes`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: true,
|
||||||
spaceId,
|
spaceId,
|
||||||
triggerSource,
|
triggerSource,
|
||||||
error: "Failed to generate summary",
|
summary: null,
|
||||||
|
reason: "No episodes to summarize",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -189,45 +198,45 @@ async function generateSpaceSummary(
|
|||||||
const existingSummary = await getExistingSummary(spaceId);
|
const existingSummary = await getExistingSummary(spaceId);
|
||||||
const isIncremental = existingSummary !== null;
|
const isIncremental = existingSummary !== null;
|
||||||
|
|
||||||
// 3. Get statements (all or new ones based on existing summary)
|
// 3. Get episodes (all or new ones based on existing summary)
|
||||||
const statements = await getSpaceStatements(
|
const episodes = await getSpaceEpisodes(
|
||||||
spaceId,
|
spaceId,
|
||||||
userId,
|
userId,
|
||||||
isIncremental ? existingSummary?.lastUpdated : undefined,
|
isIncremental ? existingSummary?.lastUpdated : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle case where no new statements exist for incremental update
|
// Handle case where no new episodes exist for incremental update
|
||||||
if (isIncremental && statements.length === 0) {
|
if (isIncremental && episodes.length === 0) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`No new statements found for space ${spaceId}, skipping summary update`,
|
`No new episodes found for space ${spaceId}, skipping summary update`,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check minimum statement requirement for new summaries only
|
// Check minimum episode requirement for new summaries only
|
||||||
if (!isIncremental && statements.length < CONFIG.minStatementsForSummary) {
|
if (!isIncremental && episodes.length < CONFIG.minEpisodesForSummary) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Space ${spaceId} has insufficient statements (${statements.length}) for new summary`,
|
`Space ${spaceId} has insufficient episodes (${episodes.length}) for new summary`,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Process statements using unified approach
|
// 4. Process episodes using unified approach
|
||||||
let summaryResult;
|
let summaryResult;
|
||||||
|
|
||||||
if (statements.length > CONFIG.maxStatementsForSummary) {
|
if (episodes.length > CONFIG.maxEpisodesForSummary) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Large space detected (${statements.length} statements). Processing in batches.`,
|
`Large space detected (${episodes.length} episodes). Processing in batches.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Process in batches, each building on previous result
|
// Process in batches, each building on previous result
|
||||||
const batches: SpaceStatementData[][] = [];
|
const batches: SpaceEpisodeData[][] = [];
|
||||||
for (
|
for (
|
||||||
let i = 0;
|
let i = 0;
|
||||||
i < statements.length;
|
i < episodes.length;
|
||||||
i += CONFIG.maxStatementsForSummary
|
i += CONFIG.maxEpisodesForSummary
|
||||||
) {
|
) {
|
||||||
batches.push(statements.slice(i, i + CONFIG.maxStatementsForSummary));
|
batches.push(episodes.slice(i, i + CONFIG.maxEpisodesForSummary));
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentSummary = existingSummary?.summary || null;
|
let currentSummary = existingSummary?.summary || null;
|
||||||
@ -236,7 +245,7 @@ async function generateSpaceSummary(
|
|||||||
|
|
||||||
for (const [batchIndex, batch] of batches.entries()) {
|
for (const [batchIndex, batch] of batches.entries()) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Processing batch ${batchIndex + 1}/${batches.length} with ${batch.length} statements`,
|
`Processing batch ${batchIndex + 1}/${batches.length} with ${batch.length} episodes`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const batchResult = await generateUnifiedSummary(
|
const batchResult = await generateUnifiedSummary(
|
||||||
@ -270,14 +279,14 @@ async function generateSpaceSummary(
|
|||||||
: null;
|
: null;
|
||||||
} else {
|
} else {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Processing ${statements.length} statements with unified approach`,
|
`Processing ${episodes.length} episodes with unified approach`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use unified approach for smaller spaces
|
// Use unified approach for smaller spaces
|
||||||
summaryResult = await generateUnifiedSummary(
|
summaryResult = await generateUnifiedSummary(
|
||||||
space.name,
|
space.name,
|
||||||
space.description as string,
|
space.description as string,
|
||||||
statements,
|
episodes,
|
||||||
existingSummary?.summary || null,
|
existingSummary?.summary || null,
|
||||||
existingSummary?.themes || [],
|
existingSummary?.themes || [],
|
||||||
);
|
);
|
||||||
@ -288,13 +297,14 @@ async function generateSpaceSummary(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the actual current statement count from Neo4j
|
||||||
|
const currentStatementCount = await getSpaceStatementCount(spaceId, userId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
spaceId: space.uuid,
|
spaceId: space.uuid,
|
||||||
spaceName: space.name,
|
spaceName: space.name,
|
||||||
spaceDescription: space.description as string,
|
spaceDescription: space.description as string,
|
||||||
statementCount: existingSummary?.statementCount
|
statementCount: currentStatementCount,
|
||||||
? existingSummary?.statementCount + statements.length
|
|
||||||
: statements.length,
|
|
||||||
summary: summaryResult.summary,
|
summary: summaryResult.summary,
|
||||||
keyEntities: summaryResult.keyEntities || [],
|
keyEntities: summaryResult.keyEntities || [],
|
||||||
themes: summaryResult.themes,
|
themes: summaryResult.themes,
|
||||||
@ -314,7 +324,7 @@ async function generateSpaceSummary(
|
|||||||
async function generateUnifiedSummary(
|
async function generateUnifiedSummary(
|
||||||
spaceName: string,
|
spaceName: string,
|
||||||
spaceDescription: string | undefined,
|
spaceDescription: string | undefined,
|
||||||
statements: SpaceStatementData[],
|
episodes: SpaceEpisodeData[],
|
||||||
previousSummary: string | null = null,
|
previousSummary: string | null = null,
|
||||||
previousThemes: string[] = [],
|
previousThemes: string[] = [],
|
||||||
): Promise<{
|
): Promise<{
|
||||||
@ -327,7 +337,7 @@ async function generateUnifiedSummary(
|
|||||||
const prompt = createUnifiedSummaryPrompt(
|
const prompt = createUnifiedSummaryPrompt(
|
||||||
spaceName,
|
spaceName,
|
||||||
spaceDescription,
|
spaceDescription,
|
||||||
statements,
|
episodes,
|
||||||
previousSummary,
|
previousSummary,
|
||||||
previousThemes,
|
previousThemes,
|
||||||
);
|
);
|
||||||
@ -350,70 +360,78 @@ async function generateUnifiedSummary(
|
|||||||
function createUnifiedSummaryPrompt(
|
function createUnifiedSummaryPrompt(
|
||||||
spaceName: string,
|
spaceName: string,
|
||||||
spaceDescription: string | undefined,
|
spaceDescription: string | undefined,
|
||||||
statements: SpaceStatementData[],
|
episodes: SpaceEpisodeData[],
|
||||||
previousSummary: string | null,
|
previousSummary: string | null,
|
||||||
previousThemes: string[],
|
previousThemes: string[],
|
||||||
): CoreMessage[] {
|
): CoreMessage[] {
|
||||||
// If there are no statements and no previous summary, we cannot generate a meaningful summary
|
// If there are no episodes and no previous summary, we cannot generate a meaningful summary
|
||||||
if (statements.length === 0 && previousSummary === null) {
|
if (episodes.length === 0 && previousSummary === null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Cannot generate summary without statements or existing summary",
|
"Cannot generate summary without episodes or existing summary",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statementsText = statements
|
const episodesText = episodes
|
||||||
.map(
|
.map(
|
||||||
(stmt) =>
|
(episode) =>
|
||||||
`- ${stmt.fact} (${stmt.subject} → ${stmt.predicate} → ${stmt.object})`,
|
`- ${episode.content} (Source: ${episode.source}, Session: ${episode.sessionId || 'N/A'})`,
|
||||||
)
|
)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
const entityFrequency = new Map<string, number>();
|
// Extract key entities and themes from episode content
|
||||||
statements.forEach((stmt) => {
|
const contentWords = episodes
|
||||||
[stmt.subject, stmt.object].forEach((entity) => {
|
.map(ep => ep.content.toLowerCase())
|
||||||
entityFrequency.set(entity, (entityFrequency.get(entity) || 0) + 1);
|
.join(' ')
|
||||||
});
|
.split(/\s+/)
|
||||||
|
.filter(word => word.length > 3);
|
||||||
|
|
||||||
|
const wordFrequency = new Map<string, number>();
|
||||||
|
contentWords.forEach((word) => {
|
||||||
|
wordFrequency.set(word, (wordFrequency.get(word) || 0) + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
const topEntities = Array.from(entityFrequency.entries())
|
const topEntities = Array.from(wordFrequency.entries())
|
||||||
.sort(([, a], [, b]) => b - a)
|
.sort(([, a], [, b]) => b - a)
|
||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map(([entity]) => entity);
|
.map(([word]) => word);
|
||||||
|
|
||||||
const isUpdate = previousSummary !== null;
|
const isUpdate = previousSummary !== null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
content: `You are an expert at analyzing and summarizing structured knowledge within semantic spaces. Your task is to ${isUpdate ? "update an existing summary by integrating new statements" : "create a comprehensive summary of statements"}.
|
content: `You are an expert at analyzing and summarizing structured knowledge within semantic spaces. Your task is to ${isUpdate ? "update an existing summary by integrating new episodes" : "create a comprehensive summary of episodes"}.
|
||||||
|
|
||||||
CRITICAL RULES:
|
CRITICAL RULES:
|
||||||
1. Base your summary ONLY on insights derived from the actual facts/statements provided
|
1. Base your summary ONLY on insights derived from the actual content/episodes provided
|
||||||
2. Use the space description only as contextual guidance, never copy or paraphrase it
|
2. Use the space description only as contextual guidance, never copy or paraphrase it
|
||||||
3. Write in a factual, neutral tone - avoid promotional language ("pivotal", "invaluable", "cutting-edge")
|
3. Write in a factual, neutral tone - avoid promotional language ("pivotal", "invaluable", "cutting-edge")
|
||||||
4. Be specific and concrete - reference actual entities, relationships, and patterns found in the data
|
4. Be specific and concrete - reference actual content, patterns, and themes found in the episodes
|
||||||
5. If statements are insufficient for meaningful insights, state that more data is needed
|
5. If episodes are insufficient for meaningful insights, state that more data is needed
|
||||||
|
|
||||||
INSTRUCTIONS:
|
INSTRUCTIONS:
|
||||||
${
|
${
|
||||||
isUpdate
|
isUpdate
|
||||||
? `1. Review the existing summary and themes carefully
|
? `1. Review the existing summary and themes carefully
|
||||||
2. Analyze the new statements for patterns and insights
|
2. Analyze the new episodes for patterns and insights
|
||||||
3. Identify connecting points between existing knowledge and new statements
|
3. Identify connecting points between existing knowledge and new episodes
|
||||||
4. Update the summary to seamlessly integrate new information while preserving valuable existing insights
|
4. Update the summary to seamlessly integrate new information while preserving valuable existing insights
|
||||||
5. Evolve themes by adding new ones or refining existing ones based on connections found`
|
5. Evolve themes by adding new ones or refining existing ones based on connections found
|
||||||
: `1. Analyze the semantic content and relationships within the statements
|
6. Update the markdown summary to reflect the enhanced themes and new insights`
|
||||||
2. Identify the main themes and patterns across all statements
|
: `1. Analyze the semantic content and relationships within the episodes
|
||||||
3. Create a coherent summary that captures the essence of this knowledge domain`
|
2. Identify the main themes and patterns across all episodes (themes must have at least 3 supporting episodes)
|
||||||
|
3. Create a coherent summary that captures the essence of this knowledge domain
|
||||||
|
4. Generate a well-structured markdown summary organized by the identified themes`
|
||||||
}
|
}
|
||||||
6. Assess your confidence in the ${isUpdate ? "updated" : ""} summary quality (0.0-1.0)
|
${isUpdate ? '7' : '6'}. Assess your confidence in the ${isUpdate ? "updated" : ""} summary quality (0.0-1.0)
|
||||||
|
|
||||||
THEME IDENTIFICATION RULES:
|
THEME IDENTIFICATION RULES:
|
||||||
- A theme must be supported by AT LEAST 5 related statements to be considered valid
|
- A theme must be supported by AT LEAST 3 related episodes to be considered valid
|
||||||
- Themes should represent substantial, meaningful patterns rather than minor occurrences
|
- Themes should represent substantial, meaningful patterns rather than minor occurrences
|
||||||
- Each theme must capture a distinct semantic domain or conceptual area
|
- Each theme must capture a distinct semantic domain or conceptual area
|
||||||
- Only identify themes that have sufficient evidence in the data
|
- Only identify themes that have sufficient evidence in the data
|
||||||
- If fewer than 5 statements support a potential theme, do not include it
|
- If fewer than 3 episodes support a potential theme, do not include it
|
||||||
|
- Themes will be used to organize the markdown summary into logical sections
|
||||||
|
|
||||||
${
|
${
|
||||||
isUpdate
|
isUpdate
|
||||||
@ -427,7 +445,7 @@ ${
|
|||||||
}
|
}
|
||||||
|
|
||||||
RESPONSE FORMAT:
|
RESPONSE FORMAT:
|
||||||
Provide your response inside <output></output> tags with valid JSON. The summary should be formatted as HTML for better presentation.
|
Provide your response inside <output></output> tags with valid JSON. Include both HTML summary and markdown format.
|
||||||
|
|
||||||
<output>
|
<output>
|
||||||
{
|
{
|
||||||
@ -450,14 +468,14 @@ ${
|
|||||||
isUpdate
|
isUpdate
|
||||||
? `- Preserve valuable insights from existing summary
|
? `- Preserve valuable insights from existing summary
|
||||||
- Integrate new information by highlighting connections
|
- Integrate new information by highlighting connections
|
||||||
- Themes should evolve naturally, don't replace wholesale
|
- Themes should evolve naturally, don't replace wholesale
|
||||||
- The updated summary should read as a coherent whole
|
- The updated summary should read as a coherent whole
|
||||||
- Make the summary user-friendly and explain what value this space provides`
|
- Make the summary user-friendly and explain what value this space provides`
|
||||||
: `- Report only what the statements actually reveal - be specific and concrete
|
: `- Report only what the episodes actually reveal - be specific and concrete
|
||||||
- Cite actual entities and relationships found in the data
|
- Cite actual content and patterns found in the episodes
|
||||||
- Avoid generic descriptions that could apply to any space
|
- Avoid generic descriptions that could apply to any space
|
||||||
- Use neutral, factual language - no "comprehensive", "robust", "cutting-edge" etc.
|
- Use neutral, factual language - no "comprehensive", "robust", "cutting-edge" etc.
|
||||||
- Themes must be backed by at least 5 supporting statements with clear evidence
|
- Themes must be backed by at least 3 supporting episodes with clear evidence
|
||||||
- Better to have fewer, well-supported themes than many weak ones
|
- Better to have fewer, well-supported themes than many weak ones
|
||||||
- Confidence should reflect actual data quality and coverage, not aspirational goals`
|
- Confidence should reflect actual data quality and coverage, not aspirational goals`
|
||||||
}`,
|
}`,
|
||||||
@ -476,22 +494,22 @@ ${previousSummary}
|
|||||||
EXISTING THEMES:
|
EXISTING THEMES:
|
||||||
${previousThemes.join(", ")}
|
${previousThemes.join(", ")}
|
||||||
|
|
||||||
NEW STATEMENTS TO INTEGRATE (${statements.length} statements):`
|
NEW EPISODES TO INTEGRATE (${episodes.length} episodes):`
|
||||||
: `STATEMENTS IN THIS SPACE (${statements.length} statements):`
|
: `EPISODES IN THIS SPACE (${episodes.length} episodes):`
|
||||||
}
|
}
|
||||||
${statementsText}
|
${episodesText}
|
||||||
|
|
||||||
${
|
${
|
||||||
statements.length > 0
|
episodes.length > 0
|
||||||
? `TOP ENTITIES BY FREQUENCY:
|
? `TOP WORDS BY FREQUENCY:
|
||||||
${topEntities.join(", ")}`
|
${topEntities.join(", ")}`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
${
|
${
|
||||||
isUpdate
|
isUpdate
|
||||||
? "Please identify connections between the existing summary and new statements, then update the summary to integrate the new insights coherently. Remember: only summarize insights from the actual statements, not the space description."
|
? "Please identify connections between the existing summary and new episodes, then update the summary to integrate the new insights coherently. Remember: only summarize insights from the actual episode content, not the space description."
|
||||||
: "Please analyze the statements and provide a comprehensive summary that captures insights derived from the facts provided. Use the description only as context. If there are too few statements to generate meaningful insights, indicate that more data is needed rather than falling back on the description."
|
: "Please analyze the episodes and provide a comprehensive summary that captures insights derived from the episode content provided. Use the description only as context. If there are too few episodes to generate meaningful insights, indicate that more data is needed rather than falling back on the description."
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -510,8 +528,8 @@ async function getExistingSummary(spaceId: string): Promise<{
|
|||||||
return {
|
return {
|
||||||
summary: existingSummary.summary,
|
summary: existingSummary.summary,
|
||||||
themes: existingSummary.themes,
|
themes: existingSummary.themes,
|
||||||
lastUpdated: existingSummary.lastPatternTrigger as Date,
|
lastUpdated: existingSummary.lastPatternTrigger || new Date(),
|
||||||
statementCount: existingSummary.statementCount as number,
|
statementCount: existingSummary.statementCount || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,44 +542,46 @@ async function getExistingSummary(spaceId: string): Promise<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSpaceStatements(
|
async function getSpaceEpisodes(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
sinceDate?: Date,
|
sinceDate?: Date,
|
||||||
): Promise<SpaceStatementData[]> {
|
): Promise<SpaceEpisodeData[]> {
|
||||||
// Build query with optional date filter for incremental updates
|
// Build query to get distinct episodes that have statements in the space
|
||||||
let whereClause =
|
let whereClause =
|
||||||
"s.userId = $userId AND s.spaceIds IS NOT NULL AND $spaceId IN s.spaceIds AND s.invalidAt IS NULL";
|
"s.spaceIds IS NOT NULL AND $spaceId IN s.spaceIds AND s.invalidAt IS NULL";
|
||||||
const params: any = { spaceId, userId };
|
const params: any = { spaceId, userId };
|
||||||
|
|
||||||
|
// Store the sinceDate condition separately to apply after e is defined
|
||||||
|
let dateCondition = "";
|
||||||
if (sinceDate) {
|
if (sinceDate) {
|
||||||
whereClause += " AND s.createdAt > $sinceDate";
|
dateCondition = "e.createdAt > $sinceDate";
|
||||||
params.sinceDate = sinceDate.toISOString();
|
params.sinceDate = sinceDate.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
MATCH (s:Statement)
|
MATCH (s:Statement{userId: $userId})
|
||||||
WHERE ${whereClause}
|
WHERE ${whereClause}
|
||||||
MATCH (s)-[:HAS_SUBJECT]->(subj:Entity)
|
OPTIONAL MATCH (e:Episode{userId: $userId})-[:HAS_PROVENANCE]->(s)
|
||||||
MATCH (s)-[:HAS_PREDICATE]->(pred:Entity)
|
WITH e
|
||||||
MATCH (s)-[:HAS_OBJECT]->(obj:Entity)
|
WHERE e IS NOT NULL ${dateCondition ? `AND ${dateCondition}` : ''}
|
||||||
RETURN s, subj.name as subject, pred.name as predicate, obj.name as object
|
RETURN DISTINCT e
|
||||||
ORDER BY s.createdAt DESC
|
ORDER BY e.createdAt DESC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await runQuery(query, params);
|
const result = await runQuery(query, params);
|
||||||
|
|
||||||
return result.map((record) => {
|
return result.map((record) => {
|
||||||
const statement = record.get("s").properties;
|
const episode = record.get("e").properties;
|
||||||
return {
|
return {
|
||||||
uuid: statement.uuid,
|
uuid: episode.uuid,
|
||||||
fact: statement.fact,
|
content: episode.content,
|
||||||
subject: record.get("subject"),
|
originalContent: episode.originalContent,
|
||||||
predicate: record.get("predicate"),
|
source: episode.source,
|
||||||
object: record.get("object"),
|
createdAt: new Date(episode.createdAt),
|
||||||
createdAt: new Date(statement.createdAt),
|
validAt: new Date(episode.validAt),
|
||||||
validAt: new Date(statement.validAt),
|
metadata: JSON.parse(episode.metadata || "{}"),
|
||||||
invalidAt: new Date(statement.invalidAt),
|
sessionId: episode.sessionId,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -663,6 +683,85 @@ async function storeSummary(summaryData: SpaceSummaryData): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process space summary sequentially: ingest document then trigger patterns
|
||||||
|
*/
|
||||||
|
async function processSpaceSummarySequentially({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
spaceId,
|
||||||
|
spaceName,
|
||||||
|
summaryContent,
|
||||||
|
triggerSource,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
spaceId: string;
|
||||||
|
spaceName: string;
|
||||||
|
summaryContent: string;
|
||||||
|
triggerSource:
|
||||||
|
| "summary_complete"
|
||||||
|
| "manual"
|
||||||
|
| "assignment"
|
||||||
|
| "scheduled"
|
||||||
|
| "new_space"
|
||||||
|
| "growth_threshold"
|
||||||
|
| "ingestion_complete";
|
||||||
|
}): Promise<void> {
|
||||||
|
// Step 1: Ingest summary as document synchronously
|
||||||
|
await ingestSpaceSummaryDocument(
|
||||||
|
spaceId,
|
||||||
|
userId,
|
||||||
|
spaceName,
|
||||||
|
summaryContent
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Successfully ingested space summary document for space ${spaceId}`);
|
||||||
|
|
||||||
|
// Step 2: Now trigger space patterns (patterns will have access to the ingested summary)
|
||||||
|
await triggerSpacePattern({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
spaceId,
|
||||||
|
triggerSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Sequential processing completed for space ${spaceId}: summary ingested → patterns triggered`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ingest space summary as document synchronously
|
||||||
|
*/
|
||||||
|
async function ingestSpaceSummaryDocument(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
spaceName: string,
|
||||||
|
summaryContent: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Create the ingest body
|
||||||
|
const ingestBody = {
|
||||||
|
episodeBody: summaryContent,
|
||||||
|
referenceTime: new Date().toISOString(),
|
||||||
|
metadata: {
|
||||||
|
documentType: "space_summary",
|
||||||
|
spaceId,
|
||||||
|
spaceName,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
source: "space",
|
||||||
|
spaceId,
|
||||||
|
sessionId: spaceId,
|
||||||
|
type: EpisodeType.DOCUMENT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to queue
|
||||||
|
await addToQueue(ingestBody, userId);
|
||||||
|
|
||||||
|
logger.info(`Queued space summary for synchronous ingestion`);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to trigger the task
|
// Helper function to trigger the task
|
||||||
export async function triggerSpaceSummary(payload: SpaceSummaryPayload) {
|
export async function triggerSpaceSummary(payload: SpaceSummaryPayload) {
|
||||||
return await spaceSummaryTask.trigger(payload, {
|
return await spaceSummaryTask.trigger(payload, {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user