mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 09:29:38 +02:00
Remove native classes from TS runtime
This commit is contained in:
parent
952daf325d
commit
dca2786828
79 changed files with 7622 additions and 6703 deletions
|
|
@ -14,13 +14,14 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
|
||||
import {
|
||||
FlowProcessor,
|
||||
ConsumerSpec,
|
||||
ProducerSpec,
|
||||
makeFlowProcessor,
|
||||
makeConsumerSpec,
|
||||
makeProducerSpec,
|
||||
makeFlowProcessorProgram,
|
||||
errorMessage,
|
||||
type ProcessorConfig,
|
||||
type FlowContext,
|
||||
type FlowProcessorRuntime,
|
||||
type ToolRequest,
|
||||
type ToolResponse,
|
||||
type EffectConfigHandler,
|
||||
|
|
@ -281,41 +282,35 @@ const onMcpToolRequest = Effect.fn("McpToolService.onRequest")(function* (
|
|||
});
|
||||
|
||||
export const makeMcpToolSpecs = (): ReadonlyArray<Spec<McpToolRuntime>> => [
|
||||
new ConsumerSpec<ToolRequest, McpToolHandlerError, McpToolRuntime>(
|
||||
makeConsumerSpec<ToolRequest, McpToolHandlerError, McpToolRuntime>(
|
||||
"mcp-tool-request",
|
||||
onMcpToolRequest,
|
||||
),
|
||||
new ProducerSpec<ToolResponse>("mcp-tool-response"),
|
||||
makeProducerSpec<ToolResponse>("mcp-tool-response"),
|
||||
];
|
||||
|
||||
export const makeMcpToolConfigHandlers = (): ReadonlyArray<
|
||||
EffectConfigHandler<never, McpToolRuntime>
|
||||
> => [onMcpConfig];
|
||||
|
||||
export class McpToolService extends FlowProcessor<McpToolRuntime> {
|
||||
private readonly runtime = Effect.runSync(makeMcpToolRuntime);
|
||||
export type McpToolService = FlowProcessorRuntime<McpToolRuntime>;
|
||||
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
|
||||
for (const spec of makeMcpToolSpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
|
||||
this.registerConfigHandler((config, version) =>
|
||||
Effect.runPromise(onMcpConfig(config, version).pipe(
|
||||
Effect.provideService(McpToolRuntime, this.runtime),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
override startEffect() {
|
||||
return super.startEffect().pipe(
|
||||
Effect.provideService(McpToolRuntime, this.runtime),
|
||||
);
|
||||
}
|
||||
export function makeMcpToolService(config: ProcessorConfig): McpToolService {
|
||||
const runtime = Effect.runSync(makeMcpToolRuntime);
|
||||
const service = makeFlowProcessor(config, {
|
||||
specifications: makeMcpToolSpecs(),
|
||||
provide: (effect) => effect.pipe(Effect.provideService(McpToolRuntime, runtime)),
|
||||
});
|
||||
service.registerConfigHandler((pushedConfig, version) =>
|
||||
Effect.runPromise(onMcpConfig(pushedConfig, version).pipe(
|
||||
Effect.provideService(McpToolRuntime, runtime),
|
||||
)),
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
||||
export const McpToolService = makeMcpToolService;
|
||||
|
||||
export const program = makeFlowProcessorProgram<ProcessorConfig, never, McpToolRuntime>({
|
||||
id: "mcp-tool",
|
||||
specs: () => makeMcpToolSpecs(),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ReAct agent -- barrel exports
|
||||
|
||||
export { AgentService } from "./service.js";
|
||||
export { StreamingReActParser } from "./parser.js";
|
||||
export { makeStreamingReActParser, type StreamingReActParser } from "./parser.js";
|
||||
export { buildReActPrompt } from "./prompt.js";
|
||||
export {
|
||||
createKnowledgeQueryTool,
|
||||
|
|
|
|||
|
|
@ -22,57 +22,75 @@ const MARKERS = [
|
|||
// Longest marker prefix for partial-match detection
|
||||
const MAX_MARKER_LEN = Math.max(...MARKERS.map((m) => m.prefix.length));
|
||||
|
||||
export class StreamingReActParser {
|
||||
private state: ReActState = "initial";
|
||||
private buffer = "";
|
||||
private onThought: (text: string) => void;
|
||||
private onAction: (name: string) => void;
|
||||
private onActionInput: (input: string) => void;
|
||||
private onFinalAnswer: (text: string) => void;
|
||||
export interface StreamingReActParser {
|
||||
readonly feed: (text: string) => void;
|
||||
readonly flush: () => void;
|
||||
}
|
||||
|
||||
constructor(
|
||||
onThought: (text: string) => void,
|
||||
onAction: (name: string) => void,
|
||||
onActionInput: (input: string) => void,
|
||||
onFinalAnswer: (text: string) => void,
|
||||
) {
|
||||
this.onThought = onThought;
|
||||
this.onAction = onAction;
|
||||
this.onActionInput = onActionInput;
|
||||
this.onFinalAnswer = onFinalAnswer;
|
||||
}
|
||||
export function makeStreamingReActParser(
|
||||
onThought: (text: string) => void,
|
||||
onAction: (name: string) => void,
|
||||
onActionInput: (input: string) => void,
|
||||
onFinalAnswer: (text: string) => void,
|
||||
): StreamingReActParser {
|
||||
let state: ReActState = "initial";
|
||||
let buffer = "";
|
||||
|
||||
/**
|
||||
* Feed a chunk of LLM output text into the parser.
|
||||
* Accumulates in a buffer and processes complete lines.
|
||||
*/
|
||||
feed(text: string): void {
|
||||
this.buffer += text;
|
||||
this.processBuffer(false);
|
||||
}
|
||||
const emitContent = (content: string): void => {
|
||||
if (content.length === 0) return;
|
||||
|
||||
/**
|
||||
* Flush any remaining buffered content at the end of output.
|
||||
*/
|
||||
flush(): void {
|
||||
this.processBuffer(true);
|
||||
// Emit any remaining buffer content in the current state
|
||||
if (this.buffer.trim().length > 0) {
|
||||
this.emitContent(this.buffer);
|
||||
this.buffer = "";
|
||||
switch (state) {
|
||||
case "thought":
|
||||
onThought(content);
|
||||
break;
|
||||
case "action":
|
||||
onAction(content);
|
||||
break;
|
||||
case "action_input":
|
||||
onActionInput(content);
|
||||
break;
|
||||
case "final_answer":
|
||||
onFinalAnswer(content);
|
||||
break;
|
||||
case "initial":
|
||||
// Content before any marker -- treat as thought
|
||||
state = "thought";
|
||||
onThought(content);
|
||||
break;
|
||||
case "complete":
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private processBuffer(isFinal: boolean): void {
|
||||
const processLine = (line: string): void => {
|
||||
const trimmed = line.trimStart();
|
||||
|
||||
// Check if this line starts a new section
|
||||
for (const marker of MARKERS) {
|
||||
if (trimmed.startsWith(marker.prefix)) {
|
||||
const content = trimmed.slice(marker.prefix.length).trim();
|
||||
state = marker.state;
|
||||
emitContent(content);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, this is continuation content for the current state
|
||||
if (trimmed.length > 0) {
|
||||
emitContent(trimmed);
|
||||
}
|
||||
};
|
||||
|
||||
const processBuffer = (isFinal: boolean): void => {
|
||||
// Process complete lines (terminated by newline)
|
||||
while (true) {
|
||||
const newlineIdx = this.buffer.indexOf("\n");
|
||||
const newlineIdx = buffer.indexOf("\n");
|
||||
if (newlineIdx === -1) {
|
||||
// No complete line yet.
|
||||
// If not final, check for partial marker match at the end and wait.
|
||||
if (!isFinal) {
|
||||
// If the remaining buffer could be the start of a marker, wait for more input.
|
||||
const trimmed = this.buffer.trimStart();
|
||||
const trimmed = buffer.trimStart();
|
||||
if (trimmed.length > 0 && trimmed.length < MAX_MARKER_LEN) {
|
||||
const couldBeMarker = MARKERS.some((m) =>
|
||||
m.prefix.startsWith(trimmed),
|
||||
|
|
@ -86,54 +104,29 @@ export class StreamingReActParser {
|
|||
break;
|
||||
}
|
||||
|
||||
const line = this.buffer.slice(0, newlineIdx);
|
||||
this.buffer = this.buffer.slice(newlineIdx + 1);
|
||||
this.processLine(line);
|
||||
const line = buffer.slice(0, newlineIdx);
|
||||
buffer = buffer.slice(newlineIdx + 1);
|
||||
processLine(line);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private processLine(line: string): void {
|
||||
const trimmed = line.trimStart();
|
||||
/**
|
||||
* Feed a chunk of LLM output text into the parser.
|
||||
* Accumulates in a buffer and processes complete lines.
|
||||
*/
|
||||
const feed = (text: string): void => {
|
||||
buffer += text;
|
||||
processBuffer(false);
|
||||
};
|
||||
|
||||
// Check if this line starts a new section
|
||||
for (const marker of MARKERS) {
|
||||
if (trimmed.startsWith(marker.prefix)) {
|
||||
const content = trimmed.slice(marker.prefix.length).trim();
|
||||
this.state = marker.state;
|
||||
this.emitContent(content);
|
||||
return;
|
||||
}
|
||||
const flush = (): void => {
|
||||
processBuffer(true);
|
||||
// Emit any remaining buffer content in the current state
|
||||
if (buffer.trim().length > 0) {
|
||||
emitContent(buffer);
|
||||
buffer = "";
|
||||
}
|
||||
};
|
||||
|
||||
// Otherwise, this is continuation content for the current state
|
||||
if (trimmed.length > 0) {
|
||||
this.emitContent(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
private emitContent(content: string): void {
|
||||
if (content.length === 0) return;
|
||||
|
||||
switch (this.state) {
|
||||
case "thought":
|
||||
this.onThought(content);
|
||||
break;
|
||||
case "action":
|
||||
this.onAction(content);
|
||||
break;
|
||||
case "action_input":
|
||||
this.onActionInput(content);
|
||||
break;
|
||||
case "final_answer":
|
||||
this.onFinalAnswer(content);
|
||||
break;
|
||||
case "initial":
|
||||
// Content before any marker -- treat as thought
|
||||
this.state = "thought";
|
||||
this.onThought(content);
|
||||
break;
|
||||
case "complete":
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { feed, flush };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,15 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
FlowProcessor,
|
||||
ConsumerSpec,
|
||||
ProducerSpec,
|
||||
RequestResponseSpec,
|
||||
makeFlowProcessor,
|
||||
makeConsumerSpec,
|
||||
makeProducerSpec,
|
||||
makeRequestResponseSpec,
|
||||
makeFlowProcessorProgram,
|
||||
errorMessage,
|
||||
type ProcessorConfig,
|
||||
type FlowContext,
|
||||
type FlowProcessorRuntime,
|
||||
type AgentRequest,
|
||||
type AgentResponse,
|
||||
type TextCompletionRequest,
|
||||
|
|
@ -488,32 +489,32 @@ const onAgentRequest = Effect.fn("AgentService.onRequest")(function* (
|
|||
});
|
||||
|
||||
export const makeAgentSpecs = (): ReadonlyArray<Spec<AgentRuntime>> => [
|
||||
new ConsumerSpec<AgentRequest, AgentHandlerError, AgentRuntime>(
|
||||
makeConsumerSpec<AgentRequest, AgentHandlerError, AgentRuntime>(
|
||||
"agent-request",
|
||||
onAgentRequest,
|
||||
),
|
||||
new ProducerSpec<AgentResponse>("agent-response"),
|
||||
new RequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
|
||||
makeProducerSpec<AgentResponse>("agent-response"),
|
||||
makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
|
||||
"llm",
|
||||
"text-completion-request",
|
||||
"text-completion-response",
|
||||
),
|
||||
new RequestResponseSpec<GraphRagRequest, GraphRagResponse>(
|
||||
makeRequestResponseSpec<GraphRagRequest, GraphRagResponse>(
|
||||
"graph-rag",
|
||||
"graph-rag-request",
|
||||
"graph-rag-response",
|
||||
),
|
||||
new RequestResponseSpec<DocumentRagRequest, DocumentRagResponse>(
|
||||
makeRequestResponseSpec<DocumentRagRequest, DocumentRagResponse>(
|
||||
"doc-rag",
|
||||
"document-rag-request",
|
||||
"document-rag-response",
|
||||
),
|
||||
new RequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
|
||||
makeRequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
|
||||
"triples",
|
||||
"triples-request",
|
||||
"triples-response",
|
||||
),
|
||||
new RequestResponseSpec<ToolRequest, ToolResponse>(
|
||||
makeRequestResponseSpec<ToolRequest, ToolResponse>(
|
||||
"mcp-tool",
|
||||
"mcp-tool-request",
|
||||
"mcp-tool-response",
|
||||
|
|
@ -524,32 +525,25 @@ export const makeAgentConfigHandlers = (): ReadonlyArray<
|
|||
EffectConfigHandler<never, AgentRuntime>
|
||||
> => [onToolsConfig];
|
||||
|
||||
export class AgentService extends FlowProcessor<AgentRuntime> {
|
||||
private readonly runtime = Effect.runSync(makeAgentRuntime);
|
||||
export type AgentService = FlowProcessorRuntime<AgentRuntime>;
|
||||
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
|
||||
for (const spec of makeAgentSpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
|
||||
this.registerConfigHandler((config, version) =>
|
||||
Effect.runPromise(onToolsConfig(config, version).pipe(
|
||||
Effect.provideService(AgentRuntime, this.runtime),
|
||||
)),
|
||||
);
|
||||
|
||||
console.log("[AgentService] Service initialized");
|
||||
}
|
||||
|
||||
override startEffect() {
|
||||
return super.startEffect().pipe(
|
||||
Effect.provideService(AgentRuntime, this.runtime),
|
||||
);
|
||||
}
|
||||
export function makeAgentService(config: ProcessorConfig): AgentService {
|
||||
const runtime = Effect.runSync(makeAgentRuntime);
|
||||
const service = makeFlowProcessor(config, {
|
||||
specifications: makeAgentSpecs(),
|
||||
provide: (effect) => effect.pipe(Effect.provideService(AgentRuntime, runtime)),
|
||||
});
|
||||
service.registerConfigHandler((pushedConfig, version) =>
|
||||
Effect.runPromise(onToolsConfig(pushedConfig, version).pipe(
|
||||
Effect.provideService(AgentRuntime, runtime),
|
||||
)),
|
||||
);
|
||||
console.log("[AgentService] Service initialized");
|
||||
return service;
|
||||
}
|
||||
|
||||
export const AgentService = makeAgentService;
|
||||
|
||||
/**
|
||||
* Simple line-based parser for ReAct LLM output.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
FlowProcessor,
|
||||
ConsumerSpec,
|
||||
ProducerSpec,
|
||||
ParameterSpec,
|
||||
makeFlowProcessor,
|
||||
makeConsumerSpec,
|
||||
makeProducerSpec,
|
||||
makeParameterSpec,
|
||||
type ProcessorConfig,
|
||||
type FlowProcessorRuntime,
|
||||
type FlowContext,
|
||||
type FlowResourceNotFoundError,
|
||||
type MessagingDeliveryError,
|
||||
|
|
@ -74,28 +75,28 @@ const onChunkMessage = Effect.fn("ChunkingService.onMessage")(function* (
|
|||
export const makeChunkingSpecs = (): ReadonlyArray<
|
||||
Spec<never>
|
||||
> => [
|
||||
new ConsumerSpec<TextDocument, FlowResourceNotFoundError | MessagingDeliveryError>(
|
||||
makeConsumerSpec<TextDocument, FlowResourceNotFoundError | MessagingDeliveryError>(
|
||||
"chunk-input",
|
||||
onChunkMessage,
|
||||
),
|
||||
new ProducerSpec<Chunk>("chunk-output"),
|
||||
new ProducerSpec<Triples>("chunk-triples"),
|
||||
new ParameterSpec("chunk-size"),
|
||||
new ParameterSpec("chunk-overlap"),
|
||||
makeProducerSpec<Chunk>("chunk-output"),
|
||||
makeProducerSpec<Triples>("chunk-triples"),
|
||||
makeParameterSpec("chunk-size"),
|
||||
makeParameterSpec("chunk-overlap"),
|
||||
];
|
||||
|
||||
export class ChunkingService extends FlowProcessor {
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
export type ChunkingService = FlowProcessorRuntime;
|
||||
|
||||
for (const spec of makeChunkingSpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
|
||||
console.log("[ChunkingService] Service initialized");
|
||||
}
|
||||
export function makeChunkingService(config: ProcessorConfig): ChunkingService {
|
||||
const service = makeFlowProcessor(config, {
|
||||
specifications: makeChunkingSpecs(),
|
||||
});
|
||||
console.log("[ChunkingService] Service initialized");
|
||||
return service;
|
||||
}
|
||||
|
||||
export const ChunkingService = makeChunkingService;
|
||||
|
||||
export const program = makeFlowProcessorProgram({
|
||||
id: "chunking",
|
||||
specs: () => makeChunkingSpecs(),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -11,8 +11,9 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
AsyncProcessor,
|
||||
makeAsyncProcessor,
|
||||
type ProcessorConfig,
|
||||
type AsyncProcessorRuntime,
|
||||
topics,
|
||||
type KnowledgeRequest,
|
||||
type KnowledgeResponse,
|
||||
|
|
@ -20,7 +21,7 @@ import {
|
|||
type Term,
|
||||
} from "@trustgraph/base";
|
||||
import { makeProcessorProgram } from "@trustgraph/base";
|
||||
import type { BackendProducer, BackendConsumer, Message } from "@trustgraph/base";
|
||||
import type { Message } from "@trustgraph/base";
|
||||
import { Effect } from "effect";
|
||||
import { ensureDirectory, joinPath, readTextFile, writeTextFile } from "../runtime/effect-files.js";
|
||||
|
||||
|
|
@ -39,394 +40,461 @@ interface DocumentEmbeddingsCore {
|
|||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export class KnowledgeCoreService extends AsyncProcessor {
|
||||
/** Keyed by `${user}:${id}` */
|
||||
private cores = new Map<string, KnowledgeCore>();
|
||||
private deCores = new Map<string, DocumentEmbeddingsCore[]>();
|
||||
private readonly dataDir: string;
|
||||
private readonly persistPath: string;
|
||||
export type KnowledgeCoreService = AsyncProcessorRuntime & Record<string, any>;
|
||||
|
||||
private consumer: BackendConsumer<KnowledgeRequest> | null = null;
|
||||
private responseProducer: BackendProducer<KnowledgeResponse> | null = null;
|
||||
export function makeKnowledgeCoreService(config: KnowledgeCoreServiceConfig): KnowledgeCoreService {
|
||||
const service = makeAsyncProcessor(config, {
|
||||
run: async () => {
|
||||
await service.run();
|
||||
},
|
||||
}) as KnowledgeCoreService;
|
||||
const baseStop = service.stop;
|
||||
service.cores = new Map<string, KnowledgeCore>();
|
||||
service.deCores = new Map<string, DocumentEmbeddingsCore[]>();
|
||||
service.consumer = null;
|
||||
service.responseProducer = null;
|
||||
const dataDir = config.dataDir ?? process.env.KNOWLEDGE_DATA_DIR ?? "./data/knowledge";
|
||||
service.dataDir = dataDir;
|
||||
service.persistPath = joinPath(dataDir, "knowledge-state.json");
|
||||
Object.assign(service, {
|
||||
|
||||
constructor(config: KnowledgeCoreServiceConfig) {
|
||||
super(config);
|
||||
const dataDir = config.dataDir ?? process.env.KNOWLEDGE_DATA_DIR ?? "./data/knowledge";
|
||||
this.dataDir = dataDir;
|
||||
this.persistPath = joinPath(dataDir, "knowledge-state.json");
|
||||
}
|
||||
|
||||
private coreKey(user: string, id: string): string {
|
||||
return `${user}:${id}`;
|
||||
}
|
||||
coreKey: function(this: KnowledgeCoreService, user: string, id: string): string {
|
||||
return `${user}:${id}`;
|
||||
|
||||
protected override async run(): Promise<void> {
|
||||
await ensureDirectory(this.dataDir);
|
||||
// Load persisted state
|
||||
await this.loadFromDisk();
|
||||
},
|
||||
|
||||
// Create producer
|
||||
this.responseProducer = await this.pubsub.createProducer<KnowledgeResponse>({
|
||||
topic: topics.knowledgeResponse,
|
||||
});
|
||||
|
||||
// Create consumer
|
||||
this.consumer = await this.pubsub.createConsumer<KnowledgeRequest>({
|
||||
topic: topics.knowledgeRequest,
|
||||
subscription: `${this.config.id}-knowledge-request`,
|
||||
});
|
||||
|
||||
console.log(`[KnowledgeCoreService] Listening on ${topics.knowledgeRequest}`);
|
||||
run: async function(this: KnowledgeCoreService): Promise<void> {
|
||||
await ensureDirectory(this.dataDir);
|
||||
// Load persisted state
|
||||
await this.loadFromDisk();
|
||||
|
||||
// Main consume loop
|
||||
while (this.running) {
|
||||
try {
|
||||
const msg = await this.consumer.receive(2000);
|
||||
if (msg === null) continue;
|
||||
|
||||
await this.handleMessage(msg);
|
||||
await this.consumer.acknowledge(msg);
|
||||
} catch (err) {
|
||||
if (!this.running) break;
|
||||
console.error("[KnowledgeCoreService] Error in consume loop:", err);
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMessage(msg: Message<KnowledgeRequest>): Promise<void> {
|
||||
const request = msg.value();
|
||||
const props = msg.properties();
|
||||
const requestId = props.id;
|
||||
|
||||
if (requestId === undefined || requestId.length === 0) {
|
||||
console.warn("[KnowledgeCoreService] Received request without id, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.handleOperation(request, requestId);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await this.responseProducer!.send(
|
||||
{ error: { type: "knowledge-error", message } },
|
||||
{ id: requestId },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleOperation(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
switch (request.operation) {
|
||||
case "list-kg-cores":
|
||||
return this.listKgCores(request, requestId);
|
||||
case "get-kg-core":
|
||||
return this.getKgCore(request, requestId);
|
||||
case "delete-kg-core":
|
||||
return this.deleteKgCore(request, requestId);
|
||||
case "put-kg-core":
|
||||
return this.putKgCore(request, requestId);
|
||||
case "load-kg-core":
|
||||
return this.loadKgCore(request, requestId);
|
||||
case "unload-kg-core":
|
||||
return this.unloadKgCore(request, requestId);
|
||||
case "list-de-cores":
|
||||
return this.listDeCores(request, requestId);
|
||||
case "get-de-core":
|
||||
return this.getDeCore(request, requestId);
|
||||
case "delete-de-core":
|
||||
return this.deleteDeCore(request, requestId);
|
||||
case "put-de-core":
|
||||
return this.putDeCore(request, requestId);
|
||||
case "load-de-core":
|
||||
return this.loadDeCore(request, requestId);
|
||||
default:
|
||||
throw new Error(`Unknown knowledge operation: ${request.operation as string}`);
|
||||
}
|
||||
}
|
||||
|
||||
private requestRecord(request: KnowledgeRequest): Record<string, unknown> {
|
||||
return request as Record<string, unknown>;
|
||||
}
|
||||
|
||||
private graphEmbeddings(request: KnowledgeRequest): { entity: Term; vectors: number[][] }[] {
|
||||
const req = this.requestRecord(request);
|
||||
const value = request.graphEmbeddings ?? req["graph-embeddings"];
|
||||
return Array.isArray(value) ? value as { entity: Term; vectors: number[][] }[] : [];
|
||||
}
|
||||
|
||||
private documentEmbeddings(request: KnowledgeRequest): DocumentEmbeddingsCore | undefined {
|
||||
const req = this.requestRecord(request);
|
||||
const value = request.documentEmbeddings ?? req["document-embeddings"];
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
||||
return value as DocumentEmbeddingsCore;
|
||||
}
|
||||
|
||||
private async listKgCores(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const prefix = user.length > 0 ? `${user}:` : "";
|
||||
|
||||
const ids: string[] = [];
|
||||
for (const key of this.cores.keys()) {
|
||||
if (prefix.length === 0 || key.startsWith(prefix)) {
|
||||
// Extract the ID portion after the user prefix
|
||||
const id = key.slice(prefix.length);
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
await this.responseProducer!.send({ ids }, { id: requestId });
|
||||
}
|
||||
|
||||
private async getKgCore(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
|
||||
const core = this.cores.get(key);
|
||||
if (core === undefined) {
|
||||
throw new Error(`Knowledge core not found: ${key}`);
|
||||
}
|
||||
|
||||
// Send triples and embeddings in batches
|
||||
const BATCH_SIZE = 100;
|
||||
|
||||
// Send triples in batches
|
||||
for (let i = 0; i < core.triples.length; i += BATCH_SIZE) {
|
||||
const batch = core.triples.slice(i, i + BATCH_SIZE);
|
||||
const isLast = i + BATCH_SIZE >= core.triples.length && core.graphEmbeddings.length === 0;
|
||||
|
||||
await this.responseProducer!.send(
|
||||
{ triples: batch, eos: isLast },
|
||||
{ id: requestId },
|
||||
);
|
||||
}
|
||||
|
||||
// Send graph embeddings in batches
|
||||
for (let i = 0; i < core.graphEmbeddings.length; i += BATCH_SIZE) {
|
||||
const batch = core.graphEmbeddings.slice(i, i + BATCH_SIZE);
|
||||
const isLast = i + BATCH_SIZE >= core.graphEmbeddings.length;
|
||||
|
||||
await this.responseProducer!.send(
|
||||
{ graphEmbeddings: batch, "graph-embeddings": batch, eos: isLast } as KnowledgeResponse,
|
||||
{ id: requestId },
|
||||
);
|
||||
}
|
||||
|
||||
// If core was empty, send a final eos
|
||||
if (core.triples.length === 0 && core.graphEmbeddings.length === 0) {
|
||||
await this.responseProducer!.send({ eos: true }, { id: requestId });
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteKgCore(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
|
||||
this.cores.delete(key);
|
||||
await this.persist();
|
||||
|
||||
console.log(`[KnowledgeCoreService] Deleted core: ${key}`);
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
}
|
||||
|
||||
private async putKgCore(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
|
||||
let core = this.cores.get(key);
|
||||
if (core === undefined) {
|
||||
core = { triples: [], graphEmbeddings: [] };
|
||||
this.cores.set(key, core);
|
||||
}
|
||||
|
||||
// Append triples if provided
|
||||
if (request.triples !== undefined && request.triples.length > 0) {
|
||||
core.triples.push(...request.triples);
|
||||
}
|
||||
|
||||
// Append graph embeddings if provided
|
||||
const graphEmbeddings = this.graphEmbeddings(request);
|
||||
if (graphEmbeddings.length > 0) {
|
||||
core.graphEmbeddings.push(...graphEmbeddings);
|
||||
}
|
||||
|
||||
await this.persist();
|
||||
|
||||
console.log(
|
||||
`[KnowledgeCoreService] Updated core ${key}: triples=${core.triples.length}, embeddings=${core.graphEmbeddings.length}`,
|
||||
);
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
}
|
||||
|
||||
private async loadKgCore(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
|
||||
const core = this.cores.get(key);
|
||||
if (core === undefined) {
|
||||
throw new Error(`Knowledge core not found: ${key}`);
|
||||
}
|
||||
|
||||
if (core.triples.length > 0) {
|
||||
const producer = await this.pubsub.createProducer<unknown>({ topic: "tg.flow.triples" });
|
||||
try {
|
||||
await producer.send({
|
||||
metadata: {
|
||||
id: coreId,
|
||||
root: coreId,
|
||||
user,
|
||||
collection: request.collection ?? "default",
|
||||
},
|
||||
triples: core.triples,
|
||||
// Create producer
|
||||
this.responseProducer = await this.pubsub.createProducer<KnowledgeResponse>({
|
||||
topic: topics.knowledgeResponse,
|
||||
});
|
||||
} finally {
|
||||
await producer.close();
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[KnowledgeCoreService] Loaded core ${key} (triples=${core.triples.length}, embeddings=${core.graphEmbeddings.length})`,
|
||||
);
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
}
|
||||
// Create consumer
|
||||
this.consumer = await this.pubsub.createConsumer<KnowledgeRequest>({
|
||||
topic: topics.knowledgeRequest,
|
||||
subscription: `${this.config.id}-knowledge-request`,
|
||||
});
|
||||
|
||||
private async unloadKgCore(_request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
}
|
||||
console.log(`[KnowledgeCoreService] Listening on ${topics.knowledgeRequest}`);
|
||||
|
||||
private async listDeCores(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const prefix = user.length > 0 ? `${user}:` : "";
|
||||
const ids = [...this.deCores.keys()]
|
||||
.filter((key) => prefix.length === 0 || key.startsWith(prefix))
|
||||
.map((key) => key.slice(prefix.length));
|
||||
await this.responseProducer!.send({ ids }, { id: requestId });
|
||||
}
|
||||
// Main consume loop
|
||||
while (this.running) {
|
||||
try {
|
||||
const msg = await this.consumer.receive(2000);
|
||||
if (msg === null) continue;
|
||||
|
||||
private async getDeCore(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
const core = this.deCores.get(key);
|
||||
if (core === undefined) throw new Error(`Document embeddings core not found: ${key}`);
|
||||
|
||||
for (let i = 0; i < core.length; i++) {
|
||||
const isLast = i === core.length - 1;
|
||||
await this.responseProducer!.send(
|
||||
{
|
||||
documentEmbeddings: core[i],
|
||||
"document-embeddings": core[i],
|
||||
eos: isLast,
|
||||
} as KnowledgeResponse,
|
||||
{ id: requestId },
|
||||
);
|
||||
}
|
||||
if (core.length === 0) {
|
||||
await this.responseProducer!.send({ eos: true }, { id: requestId });
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteDeCore(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
this.deCores.delete(this.coreKey(user, coreId));
|
||||
await this.persist();
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
}
|
||||
|
||||
private async putDeCore(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
const item = this.documentEmbeddings(request);
|
||||
if (item === undefined) throw new Error("put-de-core requires document-embeddings");
|
||||
const core = this.deCores.get(key) ?? [];
|
||||
core.push(item);
|
||||
this.deCores.set(key, core);
|
||||
await this.persist();
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
}
|
||||
|
||||
private async loadDeCore(request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
if (!this.deCores.has(key)) throw new Error(`Document embeddings core not found: ${key}`);
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
}
|
||||
|
||||
// ---------- Persistence ----------
|
||||
|
||||
private async persist(): Promise<void> {
|
||||
try {
|
||||
// Serialize Map to object
|
||||
const data: {
|
||||
kg: Record<string, KnowledgeCore>;
|
||||
de: Record<string, DocumentEmbeddingsCore[]>;
|
||||
} = { kg: {}, de: {} };
|
||||
for (const [key, core] of this.cores) {
|
||||
data.kg[key] = core;
|
||||
}
|
||||
for (const [key, core] of this.deCores) {
|
||||
data.de[key] = core;
|
||||
}
|
||||
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
await writeTextFile(this.persistPath, json);
|
||||
} catch (err) {
|
||||
console.error("[KnowledgeCoreService] Failed to persist state:", err);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadFromDisk(): Promise<void> {
|
||||
try {
|
||||
const raw = await readTextFile(this.persistPath);
|
||||
const parsed = JSON.parse(raw) as Record<string, KnowledgeCore> | {
|
||||
kg?: Record<string, KnowledgeCore>;
|
||||
de?: Record<string, DocumentEmbeddingsCore[]>;
|
||||
};
|
||||
|
||||
this.cores.clear();
|
||||
this.deCores.clear();
|
||||
const kg = "kg" in parsed && parsed.kg !== undefined ? parsed.kg : parsed as Record<string, KnowledgeCore>;
|
||||
for (const [key, core] of Object.entries(kg)) {
|
||||
this.cores.set(key, core);
|
||||
}
|
||||
if ("de" in parsed && parsed.de !== undefined) {
|
||||
for (const [key, core] of Object.entries(parsed.de)) {
|
||||
this.deCores.set(key, core);
|
||||
await this.handleMessage(msg);
|
||||
await this.consumer.acknowledge(msg);
|
||||
} catch (err) {
|
||||
if (!this.running) break;
|
||||
console.error("[KnowledgeCoreService] Error in consume loop:", err);
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[KnowledgeCoreService] Loaded persisted state (kg=${this.cores.size}, de=${this.deCores.size})`);
|
||||
} catch {
|
||||
console.log("[KnowledgeCoreService] No persisted state found, starting fresh");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
if (this.consumer !== null) {
|
||||
await this.consumer.close();
|
||||
this.consumer = null;
|
||||
}
|
||||
if (this.responseProducer !== null) {
|
||||
await this.responseProducer.close();
|
||||
this.responseProducer = null;
|
||||
}
|
||||
await super.stop();
|
||||
}
|
||||
|
||||
|
||||
handleMessage: async function(this: KnowledgeCoreService, msg: Message<KnowledgeRequest>): Promise<void> {
|
||||
const request = msg.value();
|
||||
const props = msg.properties();
|
||||
const requestId = props.id;
|
||||
|
||||
if (requestId === undefined || requestId.length === 0) {
|
||||
console.warn("[KnowledgeCoreService] Received request without id, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.handleOperation(request, requestId);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await this.responseProducer!.send(
|
||||
{ error: { type: "knowledge-error", message } },
|
||||
{ id: requestId },
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
handleOperation: async function(this: KnowledgeCoreService, request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
switch (request.operation) {
|
||||
case "list-kg-cores":
|
||||
return this.listKgCores(request, requestId);
|
||||
case "get-kg-core":
|
||||
return this.getKgCore(request, requestId);
|
||||
case "delete-kg-core":
|
||||
return this.deleteKgCore(request, requestId);
|
||||
case "put-kg-core":
|
||||
return this.putKgCore(request, requestId);
|
||||
case "load-kg-core":
|
||||
return this.loadKgCore(request, requestId);
|
||||
case "unload-kg-core":
|
||||
return this.unloadKgCore(request, requestId);
|
||||
case "list-de-cores":
|
||||
return this.listDeCores(request, requestId);
|
||||
case "get-de-core":
|
||||
return this.getDeCore(request, requestId);
|
||||
case "delete-de-core":
|
||||
return this.deleteDeCore(request, requestId);
|
||||
case "put-de-core":
|
||||
return this.putDeCore(request, requestId);
|
||||
case "load-de-core":
|
||||
return this.loadDeCore(request, requestId);
|
||||
default:
|
||||
throw new Error(`Unknown knowledge operation: ${request.operation as string}`);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
requestRecord: function(this: KnowledgeCoreService, request: KnowledgeRequest): Record<string, unknown> {
|
||||
return request as Record<string, unknown>;
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
graphEmbeddings: function(this: KnowledgeCoreService, request: KnowledgeRequest): { entity: Term; vectors: number[][] }[] {
|
||||
const req = this.requestRecord(request);
|
||||
const value = request.graphEmbeddings ?? req["graph-embeddings"];
|
||||
return Array.isArray(value) ? value as { entity: Term; vectors: number[][] }[] : [];
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
documentEmbeddings: function(this: KnowledgeCoreService, request: KnowledgeRequest): DocumentEmbeddingsCore | undefined {
|
||||
const req = this.requestRecord(request);
|
||||
const value = request.documentEmbeddings ?? req["document-embeddings"];
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
||||
return value as DocumentEmbeddingsCore;
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
listKgCores: async function(this: KnowledgeCoreService, request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const prefix = user.length > 0 ? `${user}:` : "";
|
||||
|
||||
const ids: string[] = [];
|
||||
for (const key of (this.cores as Map<string, KnowledgeCore>).keys()) {
|
||||
if (prefix.length === 0 || key.startsWith(prefix)) {
|
||||
// Extract the ID portion after the user prefix
|
||||
const id = key.slice(prefix.length);
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
await this.responseProducer!.send({ ids }, { id: requestId });
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
getKgCore: async function(this: KnowledgeCoreService, request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
|
||||
const core = this.cores.get(key);
|
||||
if (core === undefined) {
|
||||
throw new Error(`Knowledge core not found: ${key}`);
|
||||
}
|
||||
|
||||
// Send triples and embeddings in batches
|
||||
const BATCH_SIZE = 100;
|
||||
|
||||
// Send triples in batches
|
||||
for (let i = 0; i < core.triples.length; i += BATCH_SIZE) {
|
||||
const batch = core.triples.slice(i, i + BATCH_SIZE);
|
||||
const isLast = i + BATCH_SIZE >= core.triples.length && core.graphEmbeddings.length === 0;
|
||||
|
||||
await this.responseProducer!.send(
|
||||
{ triples: batch, eos: isLast },
|
||||
{ id: requestId },
|
||||
);
|
||||
}
|
||||
|
||||
// Send graph embeddings in batches
|
||||
for (let i = 0; i < core.graphEmbeddings.length; i += BATCH_SIZE) {
|
||||
const batch = core.graphEmbeddings.slice(i, i + BATCH_SIZE);
|
||||
const isLast = i + BATCH_SIZE >= core.graphEmbeddings.length;
|
||||
|
||||
await this.responseProducer!.send(
|
||||
{ graphEmbeddings: batch, "graph-embeddings": batch, eos: isLast } as KnowledgeResponse,
|
||||
{ id: requestId },
|
||||
);
|
||||
}
|
||||
|
||||
// If core was empty, send a final eos
|
||||
if (core.triples.length === 0 && core.graphEmbeddings.length === 0) {
|
||||
await this.responseProducer!.send({ eos: true }, { id: requestId });
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
deleteKgCore: async function(this: KnowledgeCoreService, request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
|
||||
this.cores.delete(key);
|
||||
await this.persist();
|
||||
|
||||
console.log(`[KnowledgeCoreService] Deleted core: ${key}`);
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
putKgCore: async function(this: KnowledgeCoreService, request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
|
||||
let core = this.cores.get(key);
|
||||
if (core === undefined) {
|
||||
core = { triples: [], graphEmbeddings: [] };
|
||||
this.cores.set(key, core);
|
||||
}
|
||||
|
||||
// Append triples if provided
|
||||
if (request.triples !== undefined && request.triples.length > 0) {
|
||||
core.triples.push(...request.triples);
|
||||
}
|
||||
|
||||
// Append graph embeddings if provided
|
||||
const graphEmbeddings = this.graphEmbeddings(request);
|
||||
if (graphEmbeddings.length > 0) {
|
||||
core.graphEmbeddings.push(...graphEmbeddings);
|
||||
}
|
||||
|
||||
await this.persist();
|
||||
|
||||
console.log(
|
||||
`[KnowledgeCoreService] Updated core ${key}: triples=${core.triples.length}, embeddings=${core.graphEmbeddings.length}`,
|
||||
);
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
loadKgCore: async function(this: KnowledgeCoreService, request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
|
||||
const core = this.cores.get(key);
|
||||
if (core === undefined) {
|
||||
throw new Error(`Knowledge core not found: ${key}`);
|
||||
}
|
||||
|
||||
if (core.triples.length > 0) {
|
||||
const producer = await this.pubsub.createProducer<unknown>({ topic: "tg.flow.triples" });
|
||||
try {
|
||||
await producer.send({
|
||||
metadata: {
|
||||
id: coreId,
|
||||
root: coreId,
|
||||
user,
|
||||
collection: request.collection ?? "default",
|
||||
},
|
||||
triples: core.triples,
|
||||
});
|
||||
} finally {
|
||||
await producer.close();
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[KnowledgeCoreService] Loaded core ${key} (triples=${core.triples.length}, embeddings=${core.graphEmbeddings.length})`,
|
||||
);
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
unloadKgCore: async function(this: KnowledgeCoreService, _request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
listDeCores: async function(this: KnowledgeCoreService, request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const prefix = user.length > 0 ? `${user}:` : "";
|
||||
const ids = [...this.deCores.keys()]
|
||||
.filter((key) => prefix.length === 0 || key.startsWith(prefix))
|
||||
.map((key) => key.slice(prefix.length));
|
||||
await this.responseProducer!.send({ ids }, { id: requestId });
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
getDeCore: async function(this: KnowledgeCoreService, request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
const core = this.deCores.get(key);
|
||||
if (core === undefined) throw new Error(`Document embeddings core not found: ${key}`);
|
||||
|
||||
for (let i = 0; i < core.length; i++) {
|
||||
const isLast = i === core.length - 1;
|
||||
await this.responseProducer!.send(
|
||||
{
|
||||
documentEmbeddings: core[i],
|
||||
"document-embeddings": core[i],
|
||||
eos: isLast,
|
||||
} as KnowledgeResponse,
|
||||
{ id: requestId },
|
||||
);
|
||||
}
|
||||
if (core.length === 0) {
|
||||
await this.responseProducer!.send({ eos: true }, { id: requestId });
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
deleteDeCore: async function(this: KnowledgeCoreService, request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
this.deCores.delete(this.coreKey(user, coreId));
|
||||
await this.persist();
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
putDeCore: async function(this: KnowledgeCoreService, request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
const item = this.documentEmbeddings(request);
|
||||
if (item === undefined) throw new Error("put-de-core requires document-embeddings");
|
||||
const core = this.deCores.get(key) ?? [];
|
||||
core.push(item);
|
||||
this.deCores.set(key, core);
|
||||
await this.persist();
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
loadDeCore: async function(this: KnowledgeCoreService, request: KnowledgeRequest, requestId: string): Promise<void> {
|
||||
const user = request.user ?? "";
|
||||
const coreId = request.id ?? "";
|
||||
const key = this.coreKey(user, coreId);
|
||||
if (!(this.deCores as Map<string, DocumentEmbeddingsCore[]>).has(key)) throw new Error(`Document embeddings core not found: ${key}`);
|
||||
await this.responseProducer!.send({}, { id: requestId });
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
// ---------- Persistence ----------
|
||||
|
||||
persist: async function(this: KnowledgeCoreService): Promise<void> {
|
||||
try {
|
||||
// Serialize Map to object
|
||||
const data: {
|
||||
kg: Record<string, KnowledgeCore>;
|
||||
de: Record<string, DocumentEmbeddingsCore[]>;
|
||||
} = { kg: {}, de: {} };
|
||||
for (const [key, core] of this.cores) {
|
||||
data.kg[key] = core;
|
||||
}
|
||||
for (const [key, core] of this.deCores) {
|
||||
data.de[key] = core;
|
||||
}
|
||||
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
await writeTextFile(this.persistPath, json);
|
||||
} catch (err) {
|
||||
console.error("[KnowledgeCoreService] Failed to persist state:", err);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
loadFromDisk: async function(this: KnowledgeCoreService): Promise<void> {
|
||||
try {
|
||||
const raw = await readTextFile(this.persistPath);
|
||||
const parsed = JSON.parse(raw) as Record<string, KnowledgeCore> | {
|
||||
kg?: Record<string, KnowledgeCore>;
|
||||
de?: Record<string, DocumentEmbeddingsCore[]>;
|
||||
};
|
||||
|
||||
this.cores.clear();
|
||||
this.deCores.clear();
|
||||
const kg = "kg" in parsed && parsed.kg !== undefined ? parsed.kg : parsed as Record<string, KnowledgeCore>;
|
||||
for (const [key, core] of Object.entries(kg)) {
|
||||
this.cores.set(key, core);
|
||||
}
|
||||
if ("de" in parsed && parsed.de !== undefined) {
|
||||
for (const [key, core] of Object.entries(parsed.de)) {
|
||||
this.deCores.set(key, core);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[KnowledgeCoreService] Loaded persisted state (kg=${this.cores.size}, de=${this.deCores.size})`);
|
||||
} catch {
|
||||
console.log("[KnowledgeCoreService] No persisted state found, starting fresh");
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
stop: async function(this: KnowledgeCoreService): Promise<void> {
|
||||
if (this.consumer !== null) {
|
||||
await this.consumer.close();
|
||||
this.consumer = null;
|
||||
}
|
||||
if (this.responseProducer !== null) {
|
||||
await this.responseProducer.close();
|
||||
this.responseProducer = null;
|
||||
}
|
||||
await baseStop();
|
||||
|
||||
}
|
||||
});
|
||||
return service;
|
||||
}
|
||||
|
||||
export const KnowledgeCoreService = makeKnowledgeCoreService;
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const program = makeProcessorProgram({
|
||||
id: "knowledge-svc",
|
||||
make: (config) => new KnowledgeCoreService(config),
|
||||
make: (config) => makeKnowledgeCoreService(config),
|
||||
});
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@
|
|||
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
|
||||
import type { TextItem } from "pdfjs-dist/types/src/display/api.js";
|
||||
import {
|
||||
FlowProcessor,
|
||||
ConsumerSpec,
|
||||
ProducerSpec,
|
||||
RequestResponseSpec,
|
||||
makeFlowProcessor,
|
||||
makeConsumerSpec,
|
||||
makeProducerSpec,
|
||||
makeRequestResponseSpec,
|
||||
type ProcessorConfig,
|
||||
type FlowProcessorRuntime,
|
||||
type FlowContext,
|
||||
type FlowResourceNotFoundError,
|
||||
type Document,
|
||||
|
|
@ -209,28 +210,28 @@ const onPdfDecodeMessage = Effect.fn("PdfDecoderService.onMessage")(function* (
|
|||
});
|
||||
|
||||
export const makePdfDecoderSpecs = (): ReadonlyArray<Spec<never>> => [
|
||||
new ConsumerSpec<Document, PdfDecoderHandlerError>("decode-input", onPdfDecodeMessage),
|
||||
new ProducerSpec<TextDocument>("decode-output"),
|
||||
new ProducerSpec<Triples>("decode-triples"),
|
||||
new RequestResponseSpec<LibrarianRequest, LibrarianResponse>(
|
||||
makeConsumerSpec<Document, PdfDecoderHandlerError>("decode-input", onPdfDecodeMessage),
|
||||
makeProducerSpec<TextDocument>("decode-output"),
|
||||
makeProducerSpec<Triples>("decode-triples"),
|
||||
makeRequestResponseSpec<LibrarianRequest, LibrarianResponse>(
|
||||
"librarian-client",
|
||||
"librarian-request",
|
||||
"librarian-response",
|
||||
),
|
||||
];
|
||||
|
||||
export class PdfDecoderService extends FlowProcessor {
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
export type PdfDecoderService = FlowProcessorRuntime;
|
||||
|
||||
for (const spec of makePdfDecoderSpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
|
||||
console.log("[PdfDecoder] Service initialized");
|
||||
}
|
||||
export function makePdfDecoderService(config: ProcessorConfig): PdfDecoderService {
|
||||
const service = makeFlowProcessor(config, {
|
||||
specifications: makePdfDecoderSpecs(),
|
||||
});
|
||||
console.log("[PdfDecoder] Service initialized");
|
||||
return service;
|
||||
}
|
||||
|
||||
export const PdfDecoderService = makePdfDecoderService;
|
||||
|
||||
function iriTerm(iri: string): Term {
|
||||
return { type: "IRI", iri };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import { Effect, Layer } from "effect";
|
|||
import * as S from "effect/Schema";
|
||||
import {
|
||||
Embeddings,
|
||||
EmbeddingsService,
|
||||
embeddingsError,
|
||||
makeEmbeddingsService,
|
||||
makeEmbeddingsSpecs,
|
||||
type EmbeddingsServiceShape,
|
||||
type ProcessorConfig,
|
||||
|
|
@ -84,25 +84,18 @@ export function OllamaEmbeddingsLive(config: OllamaEmbeddingsConfig): Layer.Laye
|
|||
);
|
||||
}
|
||||
|
||||
export class OllamaEmbeddingsProcessor extends EmbeddingsService {
|
||||
private readonly embeddings: EmbeddingsServiceShape;
|
||||
export type OllamaEmbeddingsProcessor = ReturnType<typeof makeOllamaEmbeddingsProcessor>;
|
||||
|
||||
constructor(config: OllamaEmbeddingsConfig) {
|
||||
super(config);
|
||||
this.embeddings = makeOllamaEmbeddings(config);
|
||||
|
||||
console.log(
|
||||
`[OllamaEmbeddings] Initialized (host=${config.ollamaHost ?? process.env.OLLAMA_URL ?? process.env.OLLAMA_HOST ?? "http://localhost:11434"}, model=${config.model ?? "mxbai-embed-large"})`,
|
||||
);
|
||||
}
|
||||
|
||||
override startEffect() {
|
||||
return super.startEffect().pipe(
|
||||
Effect.provideService(Embeddings, Embeddings.of(this.embeddings)),
|
||||
);
|
||||
}
|
||||
export function makeOllamaEmbeddingsProcessor(config: OllamaEmbeddingsConfig) {
|
||||
const embeddings = makeOllamaEmbeddings(config);
|
||||
console.log(
|
||||
`[OllamaEmbeddings] Initialized (host=${config.ollamaHost ?? process.env.OLLAMA_URL ?? process.env.OLLAMA_HOST ?? "http://localhost:11434"}, model=${config.model ?? "mxbai-embed-large"})`,
|
||||
);
|
||||
return makeEmbeddingsService(config, embeddings);
|
||||
}
|
||||
|
||||
export const OllamaEmbeddingsProcessor = makeOllamaEmbeddingsProcessor;
|
||||
|
||||
export const program = makeFlowProcessorProgram<OllamaEmbeddingsConfig, never, Embeddings>({
|
||||
id: "embeddings",
|
||||
specs: () => makeEmbeddingsSpecs(),
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
FlowProcessor,
|
||||
ConsumerSpec,
|
||||
ProducerSpec,
|
||||
RequestResponseSpec,
|
||||
makeFlowProcessor,
|
||||
makeConsumerSpec,
|
||||
makeProducerSpec,
|
||||
makeRequestResponseSpec,
|
||||
makeFlowProcessorProgram,
|
||||
type ProcessorConfig,
|
||||
type FlowProcessorRuntime,
|
||||
type FlowContext,
|
||||
type Chunk,
|
||||
type Triples,
|
||||
|
|
@ -264,36 +265,36 @@ const onKnowledgeExtractMessage = Effect.fn("KnowledgeExtractService.onMessage")
|
|||
});
|
||||
|
||||
export const makeKnowledgeExtractSpecs = (): ReadonlyArray<Spec<never>> => [
|
||||
new ConsumerSpec<Chunk, KnowledgeExtractHandlerError>(
|
||||
makeConsumerSpec<Chunk, KnowledgeExtractHandlerError>(
|
||||
"extract-input",
|
||||
onKnowledgeExtractMessage,
|
||||
),
|
||||
new ProducerSpec<Triples>("extract-triples"),
|
||||
new ProducerSpec<EntityContexts>("extract-entity-contexts"),
|
||||
new RequestResponseSpec<PromptRequest, PromptResponse>(
|
||||
makeProducerSpec<Triples>("extract-triples"),
|
||||
makeProducerSpec<EntityContexts>("extract-entity-contexts"),
|
||||
makeRequestResponseSpec<PromptRequest, PromptResponse>(
|
||||
"prompt-client",
|
||||
"prompt-request",
|
||||
"prompt-response",
|
||||
),
|
||||
new RequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
|
||||
makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
|
||||
"llm-client",
|
||||
"text-completion-request",
|
||||
"text-completion-response",
|
||||
),
|
||||
];
|
||||
|
||||
export class KnowledgeExtractService extends FlowProcessor {
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
export type KnowledgeExtractService = FlowProcessorRuntime;
|
||||
|
||||
for (const spec of makeKnowledgeExtractSpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
|
||||
console.log("[KnowledgeExtract] Service initialized");
|
||||
}
|
||||
export function makeKnowledgeExtractService(config: ProcessorConfig): KnowledgeExtractService {
|
||||
const service = makeFlowProcessor(config, {
|
||||
specifications: makeKnowledgeExtractSpecs(),
|
||||
});
|
||||
console.log("[KnowledgeExtract] Service initialized");
|
||||
return service;
|
||||
}
|
||||
|
||||
export const KnowledgeExtractService = makeKnowledgeExtractService;
|
||||
|
||||
// ---------- Helpers ----------
|
||||
|
||||
function toEntityIri(name: string): Term {
|
||||
|
|
|
|||
|
|
@ -15,19 +15,16 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
AsyncProcessor,
|
||||
makeAsyncProcessor,
|
||||
type ProcessorConfig,
|
||||
type AsyncProcessorRuntime,
|
||||
topics,
|
||||
RequestResponse,
|
||||
makeRequestResponse,
|
||||
type ConfigRequest,
|
||||
type ConfigResponse,
|
||||
} from "@trustgraph/base";
|
||||
import { makeProcessorProgram } from "@trustgraph/base";
|
||||
import type {
|
||||
BackendProducer,
|
||||
BackendConsumer,
|
||||
Message,
|
||||
} from "@trustgraph/base";
|
||||
import type { Message } from "@trustgraph/base";
|
||||
import { Effect } from "effect";
|
||||
|
||||
// ---------- Internal state types ----------
|
||||
|
|
@ -136,451 +133,496 @@ const DEFAULT_BLUEPRINT: Blueprint = {
|
|||
|
||||
// ---------- Service ----------
|
||||
|
||||
export class FlowManagerService extends AsyncProcessor {
|
||||
private flows = new Map<string, FlowInstance>();
|
||||
private blueprints = new Map<string, Blueprint>();
|
||||
export type FlowManagerService = AsyncProcessorRuntime & Record<string, any>;
|
||||
|
||||
private consumer: BackendConsumer<Record<string, unknown>> | null = null;
|
||||
private responseProducer: BackendProducer<Record<string, unknown>> | null = null;
|
||||
private configClient: RequestResponse<ConfigRequest, ConfigResponse> | null = null;
|
||||
export function makeFlowManagerService(config: ProcessorConfig): FlowManagerService {
|
||||
const service = makeAsyncProcessor(config, {
|
||||
run: async () => {
|
||||
await service.run();
|
||||
},
|
||||
}) as FlowManagerService;
|
||||
const baseStop = service.stop;
|
||||
service.flows = new Map<string, FlowInstance>();
|
||||
service.blueprints = new Map<string, Blueprint>();
|
||||
service.consumer = null;
|
||||
service.responseProducer = null;
|
||||
service.configClient = null;
|
||||
service.blueprints.set("default", DEFAULT_BLUEPRINT);
|
||||
Object.assign(service, {
|
||||
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
this.blueprints.set("default", DEFAULT_BLUEPRINT);
|
||||
}
|
||||
|
||||
protected override async run(): Promise<void> {
|
||||
// Create config client for pushing flow configs to the config service
|
||||
this.configClient = new RequestResponse<ConfigRequest, ConfigResponse>({
|
||||
pubsub: this.pubsub,
|
||||
requestTopic: topics.configRequest,
|
||||
responseTopic: topics.configResponse,
|
||||
subscription: `${this.config.id}-config-client`,
|
||||
});
|
||||
await this.configClient.start();
|
||||
await this.ensureDefaultBlueprint();
|
||||
await this.refreshBlueprintsFromConfig();
|
||||
run: async function(this: FlowManagerService): Promise<void> {
|
||||
// Create config client for pushing flow configs to the config service
|
||||
this.configClient = makeRequestResponse<ConfigRequest, ConfigResponse>({
|
||||
pubsub: this.pubsub,
|
||||
requestTopic: topics.configRequest,
|
||||
responseTopic: topics.configResponse,
|
||||
subscription: `${this.config.id}-config-client`,
|
||||
});
|
||||
await this.configClient.start();
|
||||
await this.ensureDefaultBlueprint();
|
||||
await this.refreshBlueprintsFromConfig();
|
||||
|
||||
// Create producer for flow-response topic
|
||||
this.responseProducer = await this.pubsub.createProducer<Record<string, unknown>>({
|
||||
topic: topics.flowResponse,
|
||||
});
|
||||
// Create producer for flow-response topic
|
||||
this.responseProducer = await this.pubsub.createProducer<Record<string, unknown>>({
|
||||
topic: topics.flowResponse,
|
||||
});
|
||||
|
||||
// Create consumer for flow-request topic
|
||||
this.consumer = await this.pubsub.createConsumer<Record<string, unknown>>({
|
||||
topic: topics.flowRequest,
|
||||
subscription: `${this.config.id}-flow-request`,
|
||||
});
|
||||
// Create consumer for flow-request topic
|
||||
this.consumer = await this.pubsub.createConsumer<Record<string, unknown>>({
|
||||
topic: topics.flowRequest,
|
||||
subscription: `${this.config.id}-flow-request`,
|
||||
});
|
||||
|
||||
console.log(`[FlowManager] Listening on ${topics.flowRequest}`);
|
||||
console.log(`[FlowManager] Listening on ${topics.flowRequest}`);
|
||||
|
||||
// Main consume loop (same pattern as ConfigService)
|
||||
while (this.running) {
|
||||
try {
|
||||
const msg = await this.consumer.receive(2000);
|
||||
if (msg === null) continue;
|
||||
// Main consume loop (same pattern as ConfigService)
|
||||
while (this.running) {
|
||||
try {
|
||||
const msg = await this.consumer.receive(2000);
|
||||
if (msg === null) continue;
|
||||
|
||||
await this.handleMessage(msg);
|
||||
await this.consumer.acknowledge(msg);
|
||||
} catch (err) {
|
||||
if (!this.running) break;
|
||||
console.error("[FlowManager] Error in consume loop:", err);
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.handleMessage(msg);
|
||||
await this.consumer.acknowledge(msg);
|
||||
} catch (err) {
|
||||
if (!this.running) break;
|
||||
console.error("[FlowManager] Error in consume loop:", err);
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMessage(
|
||||
msg: Message<Record<string, unknown>>,
|
||||
): Promise<void> {
|
||||
const request = msg.value();
|
||||
const props = msg.properties();
|
||||
const requestId = props.id;
|
||||
|
||||
if (requestId === undefined || requestId.length === 0) {
|
||||
console.warn("[FlowManager] Received request without id, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.handleOperation(request);
|
||||
await this.responseProducer!.send(response, { id: requestId });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await this.responseProducer!.send(
|
||||
{
|
||||
error: { type: "flow-error", message },
|
||||
},
|
||||
{ id: requestId },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async configRequest(request: ConfigRequest): Promise<ConfigResponse> {
|
||||
if (this.configClient === null) throw new Error("Config client not started");
|
||||
return this.configClient.request(request);
|
||||
}
|
||||
|
||||
private async ensureDefaultBlueprint(): Promise<void> {
|
||||
const response = await this.configRequest({
|
||||
operation: "getvalues",
|
||||
type: "flow-blueprint",
|
||||
});
|
||||
if (configValues(response).some((value) => value.key === "default")) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.configRequest({
|
||||
operation: "put",
|
||||
keys: ["flow-blueprint"],
|
||||
values: {
|
||||
default: JSON.stringify(DEFAULT_BLUEPRINT),
|
||||
},
|
||||
});
|
||||
}
|
||||
handleMessage: async function(this: FlowManagerService, msg: Message<Record<string, unknown>>): Promise<void> {
|
||||
const request = msg.value();
|
||||
const props = msg.properties();
|
||||
const requestId = props.id;
|
||||
|
||||
private async refreshBlueprintsFromConfig(): Promise<void> {
|
||||
const response = await this.configRequest({
|
||||
operation: "getvalues",
|
||||
type: "flow-blueprint",
|
||||
});
|
||||
const next = new Map<string, Blueprint>();
|
||||
if (requestId === undefined || requestId.length === 0) {
|
||||
console.warn("[FlowManager] Received request without id, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of configValues(response)) {
|
||||
const parsed = parseConfigRecord(item.value);
|
||||
if (parsed === undefined) continue;
|
||||
next.set(item.key, parsed as Blueprint);
|
||||
}
|
||||
try {
|
||||
const response = await this.handleOperation(request);
|
||||
await this.responseProducer!.send(response, { id: requestId });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await this.responseProducer!.send(
|
||||
{
|
||||
error: { type: "flow-error", message },
|
||||
},
|
||||
{ id: requestId },
|
||||
);
|
||||
}
|
||||
|
||||
if (!next.has("default")) {
|
||||
next.set("default", DEFAULT_BLUEPRINT);
|
||||
}
|
||||
this.blueprints = next;
|
||||
}
|
||||
},
|
||||
|
||||
private async refreshFlowsFromConfig(): Promise<void> {
|
||||
const response = await this.configRequest({
|
||||
operation: "getvalues",
|
||||
type: "flow",
|
||||
});
|
||||
const next = new Map<string, FlowInstance>();
|
||||
|
||||
for (const item of configValues(response)) {
|
||||
const parsed = parseConfigRecord(item.value);
|
||||
if (parsed === undefined) continue;
|
||||
const parameters = isRecord(parsed.parameters) ? parsed.parameters : {};
|
||||
next.set(item.key, {
|
||||
id: item.key,
|
||||
blueprintName: optionalString(parsed["blueprint-name"]) ?? optionalString(parsed.blueprintName) ?? "default",
|
||||
description: optionalString(parsed.description) ?? "",
|
||||
parameters,
|
||||
status: "running",
|
||||
});
|
||||
}
|
||||
|
||||
if (next.size === 0) {
|
||||
const flowsResponse = await this.configRequest({
|
||||
operation: "getvalues",
|
||||
type: "flows",
|
||||
});
|
||||
for (const item of configValues(flowsResponse)) {
|
||||
next.set(item.key, {
|
||||
id: item.key,
|
||||
blueprintName: "default",
|
||||
description: "",
|
||||
parameters: {},
|
||||
configRequest: async function(this: FlowManagerService, request: ConfigRequest): Promise<ConfigResponse> {
|
||||
if (this.configClient === null) throw new Error("Config client not started");
|
||||
return this.configClient.request(request);
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
ensureDefaultBlueprint: async function(this: FlowManagerService): Promise<void> {
|
||||
const response = await this.configRequest({
|
||||
operation: "getvalues",
|
||||
type: "flow-blueprint",
|
||||
});
|
||||
if (configValues(response).some((value) => value.key === "default")) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.configRequest({
|
||||
operation: "put",
|
||||
keys: ["flow-blueprint"],
|
||||
values: {
|
||||
default: JSON.stringify(DEFAULT_BLUEPRINT),
|
||||
},
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
refreshBlueprintsFromConfig: async function(this: FlowManagerService): Promise<void> {
|
||||
const response = await this.configRequest({
|
||||
operation: "getvalues",
|
||||
type: "flow-blueprint",
|
||||
});
|
||||
const next = new Map<string, Blueprint>();
|
||||
|
||||
for (const item of configValues(response)) {
|
||||
const parsed = parseConfigRecord(item.value);
|
||||
if (parsed === undefined) continue;
|
||||
next.set(item.key, parsed as Blueprint);
|
||||
}
|
||||
|
||||
if (!next.has("default")) {
|
||||
next.set("default", DEFAULT_BLUEPRINT);
|
||||
}
|
||||
this.blueprints = next;
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
refreshFlowsFromConfig: async function(this: FlowManagerService): Promise<void> {
|
||||
const response = await this.configRequest({
|
||||
operation: "getvalues",
|
||||
type: "flow",
|
||||
});
|
||||
const next = new Map<string, FlowInstance>();
|
||||
|
||||
for (const item of configValues(response)) {
|
||||
const parsed = parseConfigRecord(item.value);
|
||||
if (parsed === undefined) continue;
|
||||
const parameters = isRecord(parsed.parameters) ? parsed.parameters : {};
|
||||
next.set(item.key, {
|
||||
id: item.key,
|
||||
blueprintName: optionalString(parsed["blueprint-name"]) ?? optionalString(parsed.blueprintName) ?? "default",
|
||||
description: optionalString(parsed.description) ?? "",
|
||||
parameters,
|
||||
status: "running",
|
||||
});
|
||||
}
|
||||
|
||||
if (next.size === 0) {
|
||||
const flowsResponse = await this.configRequest({
|
||||
operation: "getvalues",
|
||||
type: "flows",
|
||||
});
|
||||
for (const item of configValues(flowsResponse)) {
|
||||
next.set(item.key, {
|
||||
id: item.key,
|
||||
blueprintName: "default",
|
||||
description: "",
|
||||
parameters: {},
|
||||
status: "running",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.flows = next;
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
handleOperation: async function(this: FlowManagerService, request: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
const op = request.operation as string;
|
||||
await this.refreshBlueprintsFromConfig();
|
||||
await this.refreshFlowsFromConfig();
|
||||
|
||||
switch (op) {
|
||||
case "list-blueprints":
|
||||
return this.handleListBlueprints();
|
||||
|
||||
case "put-blueprint":
|
||||
return await this.handlePutBlueprint(request);
|
||||
|
||||
case "get-blueprint":
|
||||
return this.handleGetBlueprint(request);
|
||||
|
||||
case "delete-blueprint":
|
||||
return this.handleDeleteBlueprint(request);
|
||||
|
||||
case "list-flows":
|
||||
return this.handleListFlows();
|
||||
|
||||
case "get-flow":
|
||||
return this.handleGetFlow(request);
|
||||
|
||||
case "start-flow":
|
||||
return await this.handleStartFlow(request);
|
||||
|
||||
case "stop-flow":
|
||||
return await this.handleStopFlow(request);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown flow operation: ${op}`);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
// ---------- Blueprint operations ----------
|
||||
|
||||
handleListBlueprints: function(this: FlowManagerService): Record<string, unknown> {
|
||||
return {
|
||||
"blueprint-names": [...this.blueprints.keys()],
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
handleGetBlueprint: function(this: FlowManagerService, request: Record<string, unknown>): Record<string, unknown> {
|
||||
const name = request["blueprint-name"] as string | undefined;
|
||||
if (name === undefined || name.length === 0) {
|
||||
throw new Error("Missing blueprint-name");
|
||||
}
|
||||
|
||||
const blueprint = this.blueprints.get(name);
|
||||
if (blueprint === undefined) {
|
||||
throw new Error(`Blueprint not found: ${name}`);
|
||||
}
|
||||
|
||||
return {
|
||||
"blueprint-definition": JSON.stringify(blueprint),
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
handlePutBlueprint: async function(this: FlowManagerService, request: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
const name = request["blueprint-name"] as string | undefined;
|
||||
if (name === undefined || name.length === 0) {
|
||||
throw new Error("Missing blueprint-name");
|
||||
}
|
||||
const rawDefinition = request["blueprint-definition"];
|
||||
if (rawDefinition === undefined) {
|
||||
throw new Error("Missing blueprint-definition");
|
||||
}
|
||||
const definition = typeof rawDefinition === "string"
|
||||
? rawDefinition
|
||||
: JSON.stringify(rawDefinition);
|
||||
|
||||
await this.configRequest({
|
||||
operation: "put",
|
||||
keys: ["flow-blueprint"],
|
||||
values: { [name]: definition },
|
||||
});
|
||||
await this.refreshBlueprintsFromConfig();
|
||||
return {};
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
handleDeleteBlueprint: async function(this: FlowManagerService, request: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
const name = request["blueprint-name"] as string | undefined;
|
||||
if (name === undefined || name.length === 0) {
|
||||
throw new Error("Missing blueprint-name");
|
||||
}
|
||||
|
||||
if (name === "default") {
|
||||
throw new Error("Cannot delete the default blueprint");
|
||||
}
|
||||
|
||||
await this.configRequest({
|
||||
operation: "delete",
|
||||
keys: ["flow-blueprint", name],
|
||||
});
|
||||
this.blueprints.delete(name);
|
||||
|
||||
return {};
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
// ---------- Flow operations ----------
|
||||
|
||||
handleListFlows: function(this: FlowManagerService): Record<string, unknown> {
|
||||
return {
|
||||
"flow-ids": [...this.flows.keys()],
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
handleGetFlow: function(this: FlowManagerService, request: Record<string, unknown>): Record<string, unknown> {
|
||||
const id = request["flow-id"] as string | undefined;
|
||||
if (id === undefined || id.length === 0) {
|
||||
throw new Error("Missing flow-id");
|
||||
}
|
||||
|
||||
const inst = this.flows.get(id);
|
||||
if (inst === undefined) {
|
||||
throw new Error(`Flow not found: ${id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
flow: JSON.stringify({
|
||||
"blueprint-name": inst.blueprintName,
|
||||
description: inst.description,
|
||||
parameters: inst.parameters,
|
||||
}),
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
handleStartFlow: async function(this: FlowManagerService, request: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
const id = request["flow-id"] as string | undefined;
|
||||
const blueprintName = (request["blueprint-name"] as string) ?? "default";
|
||||
const description = (request["description"] as string) ?? "";
|
||||
const parameters = (request["parameters"] as Record<string, unknown>) ?? {};
|
||||
|
||||
if (id === undefined || id.length === 0) {
|
||||
throw new Error("Missing flow-id");
|
||||
}
|
||||
|
||||
if ((this.flows as Map<string, FlowInstance>).has(id)) {
|
||||
throw new Error(`Flow already exists: ${id}`);
|
||||
}
|
||||
|
||||
const blueprint = this.blueprints.get(blueprintName);
|
||||
if (blueprint === undefined) {
|
||||
throw new Error(`Blueprint not found: ${blueprintName}`);
|
||||
}
|
||||
|
||||
// Create the flow instance
|
||||
const inst: FlowInstance = {
|
||||
id,
|
||||
blueprintName,
|
||||
description,
|
||||
parameters,
|
||||
status: "running",
|
||||
};
|
||||
this.flows.set(id, inst);
|
||||
|
||||
console.log(
|
||||
`[FlowManager] Started flow "${id}" with blueprint "${blueprintName}"`,
|
||||
);
|
||||
|
||||
// Push updated flows config to the config service
|
||||
await this.pushFlowsConfig();
|
||||
|
||||
return {};
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
handleStopFlow: async function(this: FlowManagerService, request: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
const id = request["flow-id"] as string | undefined;
|
||||
if (id === undefined || id.length === 0) {
|
||||
throw new Error("Missing flow-id");
|
||||
}
|
||||
|
||||
const inst = this.flows.get(id);
|
||||
if (inst === undefined) {
|
||||
throw new Error(`Flow not found: ${id}`);
|
||||
}
|
||||
|
||||
this.flows.delete(id);
|
||||
|
||||
console.log(`[FlowManager] Stopped flow "${id}"`);
|
||||
|
||||
await this.deleteFlowConfig(id);
|
||||
|
||||
// Push updated flows config (without the removed flow)
|
||||
await this.pushFlowsConfig();
|
||||
|
||||
return {};
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
// ---------- Config push ----------
|
||||
|
||||
/**
|
||||
* Build the flows config object from all running flows and push it
|
||||
* to the config service via a PUT operation.
|
||||
*/
|
||||
pushFlowsConfig: async function(this: FlowManagerService): Promise<void> {
|
||||
if (this.configClient === null) return;
|
||||
|
||||
const flowsConfig: Record<string, { topics: Record<string, string> }> = {};
|
||||
const flowRecords: Record<string, string> = {};
|
||||
for (const [id, inst] of this.flows) {
|
||||
const blueprint = this.blueprints.get(inst.blueprintName);
|
||||
if (blueprint !== undefined) {
|
||||
flowsConfig[id] = { topics: blueprint.topics };
|
||||
flowRecords[id] = JSON.stringify({
|
||||
"blueprint-name": inst.blueprintName,
|
||||
description: inst.description,
|
||||
parameters: inst.parameters,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.configClient.request({
|
||||
operation: "put",
|
||||
keys: ["flows"],
|
||||
values: flowsConfig,
|
||||
});
|
||||
await this.configClient.request({
|
||||
operation: "put",
|
||||
keys: ["flow"],
|
||||
values: flowRecords,
|
||||
});
|
||||
console.log(
|
||||
`[FlowManager] Pushed flows config (${this.flows.size} active flows)`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[FlowManager] Failed to push flows config:", err);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
deleteFlowConfig: async function(this: FlowManagerService, id: string): Promise<void> {
|
||||
if (this.configClient === null) return;
|
||||
await this.configClient.request({
|
||||
operation: "delete",
|
||||
keys: ["flows", id],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.flows = next;
|
||||
}
|
||||
|
||||
private async handleOperation(
|
||||
request: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const op = request.operation as string;
|
||||
await this.refreshBlueprintsFromConfig();
|
||||
await this.refreshFlowsFromConfig();
|
||||
|
||||
switch (op) {
|
||||
case "list-blueprints":
|
||||
return this.handleListBlueprints();
|
||||
|
||||
case "put-blueprint":
|
||||
return await this.handlePutBlueprint(request);
|
||||
|
||||
case "get-blueprint":
|
||||
return this.handleGetBlueprint(request);
|
||||
|
||||
case "delete-blueprint":
|
||||
return this.handleDeleteBlueprint(request);
|
||||
|
||||
case "list-flows":
|
||||
return this.handleListFlows();
|
||||
|
||||
case "get-flow":
|
||||
return this.handleGetFlow(request);
|
||||
|
||||
case "start-flow":
|
||||
return await this.handleStartFlow(request);
|
||||
|
||||
case "stop-flow":
|
||||
return await this.handleStopFlow(request);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown flow operation: ${op}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Blueprint operations ----------
|
||||
|
||||
private handleListBlueprints(): Record<string, unknown> {
|
||||
return {
|
||||
"blueprint-names": [...this.blueprints.keys()],
|
||||
};
|
||||
}
|
||||
|
||||
private handleGetBlueprint(
|
||||
request: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const name = request["blueprint-name"] as string | undefined;
|
||||
if (name === undefined || name.length === 0) {
|
||||
throw new Error("Missing blueprint-name");
|
||||
}
|
||||
|
||||
const blueprint = this.blueprints.get(name);
|
||||
if (blueprint === undefined) {
|
||||
throw new Error(`Blueprint not found: ${name}`);
|
||||
}
|
||||
|
||||
return {
|
||||
"blueprint-definition": JSON.stringify(blueprint),
|
||||
};
|
||||
}
|
||||
|
||||
private async handlePutBlueprint(
|
||||
request: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const name = request["blueprint-name"] as string | undefined;
|
||||
if (name === undefined || name.length === 0) {
|
||||
throw new Error("Missing blueprint-name");
|
||||
}
|
||||
const rawDefinition = request["blueprint-definition"];
|
||||
if (rawDefinition === undefined) {
|
||||
throw new Error("Missing blueprint-definition");
|
||||
}
|
||||
const definition = typeof rawDefinition === "string"
|
||||
? rawDefinition
|
||||
: JSON.stringify(rawDefinition);
|
||||
|
||||
await this.configRequest({
|
||||
operation: "put",
|
||||
keys: ["flow-blueprint"],
|
||||
values: { [name]: definition },
|
||||
});
|
||||
await this.refreshBlueprintsFromConfig();
|
||||
return {};
|
||||
}
|
||||
|
||||
private async handleDeleteBlueprint(
|
||||
request: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const name = request["blueprint-name"] as string | undefined;
|
||||
if (name === undefined || name.length === 0) {
|
||||
throw new Error("Missing blueprint-name");
|
||||
}
|
||||
|
||||
if (name === "default") {
|
||||
throw new Error("Cannot delete the default blueprint");
|
||||
}
|
||||
|
||||
await this.configRequest({
|
||||
operation: "delete",
|
||||
keys: ["flow-blueprint", name],
|
||||
});
|
||||
this.blueprints.delete(name);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// ---------- Flow operations ----------
|
||||
|
||||
private handleListFlows(): Record<string, unknown> {
|
||||
return {
|
||||
"flow-ids": [...this.flows.keys()],
|
||||
};
|
||||
}
|
||||
|
||||
private handleGetFlow(
|
||||
request: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const id = request["flow-id"] as string | undefined;
|
||||
if (id === undefined || id.length === 0) {
|
||||
throw new Error("Missing flow-id");
|
||||
}
|
||||
|
||||
const inst = this.flows.get(id);
|
||||
if (inst === undefined) {
|
||||
throw new Error(`Flow not found: ${id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
flow: JSON.stringify({
|
||||
"blueprint-name": inst.blueprintName,
|
||||
description: inst.description,
|
||||
parameters: inst.parameters,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private async handleStartFlow(
|
||||
request: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const id = request["flow-id"] as string | undefined;
|
||||
const blueprintName = (request["blueprint-name"] as string) ?? "default";
|
||||
const description = (request["description"] as string) ?? "";
|
||||
const parameters = (request["parameters"] as Record<string, unknown>) ?? {};
|
||||
|
||||
if (id === undefined || id.length === 0) {
|
||||
throw new Error("Missing flow-id");
|
||||
}
|
||||
|
||||
if (this.flows.has(id)) {
|
||||
throw new Error(`Flow already exists: ${id}`);
|
||||
}
|
||||
|
||||
const blueprint = this.blueprints.get(blueprintName);
|
||||
if (blueprint === undefined) {
|
||||
throw new Error(`Blueprint not found: ${blueprintName}`);
|
||||
}
|
||||
|
||||
// Create the flow instance
|
||||
const inst: FlowInstance = {
|
||||
id,
|
||||
blueprintName,
|
||||
description,
|
||||
parameters,
|
||||
status: "running",
|
||||
};
|
||||
this.flows.set(id, inst);
|
||||
|
||||
console.log(
|
||||
`[FlowManager] Started flow "${id}" with blueprint "${blueprintName}"`,
|
||||
);
|
||||
|
||||
// Push updated flows config to the config service
|
||||
await this.pushFlowsConfig();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private async handleStopFlow(
|
||||
request: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const id = request["flow-id"] as string | undefined;
|
||||
if (id === undefined || id.length === 0) {
|
||||
throw new Error("Missing flow-id");
|
||||
}
|
||||
|
||||
const inst = this.flows.get(id);
|
||||
if (inst === undefined) {
|
||||
throw new Error(`Flow not found: ${id}`);
|
||||
}
|
||||
|
||||
this.flows.delete(id);
|
||||
|
||||
console.log(`[FlowManager] Stopped flow "${id}"`);
|
||||
|
||||
await this.deleteFlowConfig(id);
|
||||
|
||||
// Push updated flows config (without the removed flow)
|
||||
await this.pushFlowsConfig();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// ---------- Config push ----------
|
||||
|
||||
/**
|
||||
* Build the flows config object from all running flows and push it
|
||||
* to the config service via a PUT operation.
|
||||
*/
|
||||
private async pushFlowsConfig(): Promise<void> {
|
||||
if (this.configClient === null) return;
|
||||
|
||||
const flowsConfig: Record<string, { topics: Record<string, string> }> = {};
|
||||
const flowRecords: Record<string, string> = {};
|
||||
for (const [id, inst] of this.flows) {
|
||||
const blueprint = this.blueprints.get(inst.blueprintName);
|
||||
if (blueprint !== undefined) {
|
||||
flowsConfig[id] = { topics: blueprint.topics };
|
||||
flowRecords[id] = JSON.stringify({
|
||||
"blueprint-name": inst.blueprintName,
|
||||
description: inst.description,
|
||||
parameters: inst.parameters,
|
||||
await this.configClient.request({
|
||||
operation: "delete",
|
||||
keys: ["flow", id],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.configClient.request({
|
||||
operation: "put",
|
||||
keys: ["flows"],
|
||||
values: flowsConfig,
|
||||
});
|
||||
await this.configClient.request({
|
||||
operation: "put",
|
||||
keys: ["flow"],
|
||||
values: flowRecords,
|
||||
});
|
||||
console.log(
|
||||
`[FlowManager] Pushed flows config (${this.flows.size} active flows)`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[FlowManager] Failed to push flows config:", err);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
private async deleteFlowConfig(id: string): Promise<void> {
|
||||
if (this.configClient === null) return;
|
||||
await this.configClient.request({
|
||||
operation: "delete",
|
||||
keys: ["flows", id],
|
||||
});
|
||||
await this.configClient.request({
|
||||
operation: "delete",
|
||||
keys: ["flow", id],
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Lifecycle ----------
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
if (this.consumer !== null) {
|
||||
await this.consumer.close();
|
||||
this.consumer = null;
|
||||
}
|
||||
if (this.responseProducer !== null) {
|
||||
await this.responseProducer.close();
|
||||
this.responseProducer = null;
|
||||
}
|
||||
if (this.configClient !== null) {
|
||||
await this.configClient.stop();
|
||||
this.configClient = null;
|
||||
}
|
||||
await super.stop();
|
||||
}
|
||||
// ---------- Lifecycle ----------
|
||||
|
||||
stop: async function(this: FlowManagerService): Promise<void> {
|
||||
if (this.consumer !== null) {
|
||||
await this.consumer.close();
|
||||
this.consumer = null;
|
||||
}
|
||||
if (this.responseProducer !== null) {
|
||||
await this.responseProducer.close();
|
||||
this.responseProducer = null;
|
||||
}
|
||||
if (this.configClient !== null) {
|
||||
await this.configClient.stop();
|
||||
this.configClient = null;
|
||||
}
|
||||
await baseStop();
|
||||
|
||||
}
|
||||
});
|
||||
return service;
|
||||
}
|
||||
|
||||
export const FlowManagerService = makeFlowManagerService;
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const program = makeProcessorProgram({
|
||||
id: "flow-manager",
|
||||
make: (config) => new FlowManagerService(config),
|
||||
make: (config) => makeFlowManagerService(config),
|
||||
});
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* Python reference: trustgraph-flow/trustgraph/gateway/dispatch/manager.py
|
||||
*/
|
||||
|
||||
import { NatsBackend, RequestResponse, type PubSubBackend } from "@trustgraph/base";
|
||||
import { makeNatsBackend, makeRequestResponse, type PubSubBackend, type RequestResponse } from "@trustgraph/base";
|
||||
import type { GatewayConfig } from "../server.js";
|
||||
import { translateRequest, translateResponse } from "./serialize.js";
|
||||
|
||||
|
|
@ -66,38 +66,75 @@ function topicName(name: string): string {
|
|||
|
||||
// ---------- Manager ----------
|
||||
|
||||
export class DispatcherManager {
|
||||
private readonly pubsub: PubSubBackend;
|
||||
private requestors = new Map<string, Promise<RequestResponse<unknown, unknown>>>();
|
||||
export interface DispatcherManager {
|
||||
readonly start: () => Promise<void>;
|
||||
readonly stop: () => Promise<void>;
|
||||
readonly dispatchGlobalService: (
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
) => Promise<unknown>;
|
||||
readonly dispatchGlobalServiceStreaming: (
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
responder: Responder,
|
||||
) => Promise<void>;
|
||||
readonly dispatchFlowService: (
|
||||
flow: string,
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
) => Promise<unknown>;
|
||||
readonly dispatchFlowServiceStreaming: (
|
||||
flow: string,
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
responder: Responder,
|
||||
) => Promise<void>;
|
||||
readonly publishToTopic: (
|
||||
topic: string,
|
||||
message: unknown,
|
||||
id?: string,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
constructor(config: GatewayConfig) {
|
||||
this.pubsub = new NatsBackend(config.natsUrl ?? "nats://localhost:4222");
|
||||
}
|
||||
export const dispatcherManagerFlowServiceNames = (): readonly string[] => [
|
||||
...FLOW_SERVICES.keys(),
|
||||
];
|
||||
|
||||
async start(): Promise<void> {
|
||||
export const dispatcherManagerGlobalServiceNames = (): readonly string[] => [
|
||||
...GLOBAL_SERVICES.keys(),
|
||||
];
|
||||
|
||||
export const dispatcherManagerIsStreamingService = (kind: string): boolean =>
|
||||
STREAMING_SERVICES.has(kind);
|
||||
|
||||
export function makeDispatcherManager(config: GatewayConfig): DispatcherManager {
|
||||
const pubsub: PubSubBackend = makeNatsBackend(config.natsUrl ?? "nats://localhost:4222");
|
||||
const requestors = new Map<string, Promise<RequestResponse<unknown, unknown>>>();
|
||||
|
||||
const start = async (): Promise<void> => {
|
||||
// Requestors are created on demand when first accessed
|
||||
}
|
||||
};
|
||||
|
||||
async stop(): Promise<void> {
|
||||
for (const pending of this.requestors.values()) {
|
||||
const stop = async (): Promise<void> => {
|
||||
for (const pending of requestors.values()) {
|
||||
const rr = await pending;
|
||||
await rr.stop();
|
||||
}
|
||||
await this.pubsub.close();
|
||||
}
|
||||
await pubsub.close();
|
||||
};
|
||||
|
||||
// ---------- Internal helpers ----------
|
||||
|
||||
private getRequestor(
|
||||
const getRequestor = (
|
||||
requestTopic: string,
|
||||
responseTopic: string,
|
||||
key: string,
|
||||
): Promise<RequestResponse<unknown, unknown>> {
|
||||
let pending = this.requestors.get(key);
|
||||
): Promise<RequestResponse<unknown, unknown>> => {
|
||||
let pending = requestors.get(key);
|
||||
if (pending === undefined) {
|
||||
pending = (async () => {
|
||||
const rr = new RequestResponse({
|
||||
pubsub: this.pubsub,
|
||||
const rr = makeRequestResponse({
|
||||
pubsub,
|
||||
requestTopic,
|
||||
responseTopic,
|
||||
subscription: `gateway-${key}`,
|
||||
|
|
@ -105,14 +142,14 @@ export class DispatcherManager {
|
|||
await rr.start();
|
||||
return rr;
|
||||
})();
|
||||
this.requestors.set(key, pending);
|
||||
requestors.set(key, pending);
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
};
|
||||
|
||||
private resolveGlobalTopics(
|
||||
const resolveGlobalTopics = (
|
||||
kind: string,
|
||||
): { requestTopic: string; responseTopic: string } {
|
||||
): { requestTopic: string; responseTopic: string } => {
|
||||
const entry = GLOBAL_SERVICES.get(kind);
|
||||
if (entry !== undefined) {
|
||||
return {
|
||||
|
|
@ -125,11 +162,11 @@ export class DispatcherManager {
|
|||
requestTopic: topicName(`${kind}-request`),
|
||||
responseTopic: topicName(`${kind}-response`),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
private resolveFlowTopics(
|
||||
const resolveFlowTopics = (
|
||||
kind: string,
|
||||
): { requestTopic: string; responseTopic: string } {
|
||||
): { requestTopic: string; responseTopic: string } => {
|
||||
const entry = FLOW_SERVICES.get(kind);
|
||||
if (entry !== undefined) {
|
||||
return {
|
||||
|
|
@ -142,13 +179,13 @@ export class DispatcherManager {
|
|||
requestTopic: topicName(`${kind}-request`),
|
||||
responseTopic: topicName(`${kind}-response`),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine whether a response is the final one in a streaming sequence.
|
||||
* Checks for various end-of-stream markers used by different services.
|
||||
*/
|
||||
private isComplete(response: unknown): boolean {
|
||||
const isComplete = (response: unknown): boolean => {
|
||||
if (typeof response !== "object" || response === null) return true;
|
||||
const res = response as Record<string, unknown>;
|
||||
return (
|
||||
|
|
@ -162,50 +199,50 @@ export class DispatcherManager {
|
|||
// error responses are always final
|
||||
(res.error !== undefined && res.error !== null)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- Global service dispatch ----------
|
||||
|
||||
async dispatchGlobalService(
|
||||
const dispatchGlobalService = async (
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const { requestTopic, responseTopic } = this.resolveGlobalTopics(kind);
|
||||
const rr = await this.getRequestor(requestTopic, responseTopic, `global:${kind}`);
|
||||
): Promise<unknown> => {
|
||||
const { requestTopic, responseTopic } = resolveGlobalTopics(kind);
|
||||
const rr = await getRequestor(requestTopic, responseTopic, `global:${kind}`);
|
||||
|
||||
const translated = translateRequest(kind, request);
|
||||
const response = await rr.request(translated);
|
||||
return translateResponse(kind, response);
|
||||
}
|
||||
};
|
||||
|
||||
async dispatchGlobalServiceStreaming(
|
||||
const dispatchGlobalServiceStreaming = async (
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
responder: Responder,
|
||||
): Promise<void> {
|
||||
const { requestTopic, responseTopic } = this.resolveGlobalTopics(kind);
|
||||
const rr = await this.getRequestor(requestTopic, responseTopic, `global:${kind}`);
|
||||
): Promise<void> => {
|
||||
const { requestTopic, responseTopic } = resolveGlobalTopics(kind);
|
||||
const rr = await getRequestor(requestTopic, responseTopic, `global:${kind}`);
|
||||
const translated = translateRequest(kind, request);
|
||||
|
||||
await rr.request(translated, {
|
||||
recipient: async (response) => {
|
||||
const translatedRes = translateResponse(kind, response);
|
||||
const complete = this.isComplete(translatedRes);
|
||||
const complete = isComplete(translatedRes);
|
||||
await responder(translatedRes, complete);
|
||||
return complete;
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- Flow-scoped service dispatch ----------
|
||||
|
||||
async dispatchFlowService(
|
||||
const dispatchFlowService = async (
|
||||
flow: string,
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const { requestTopic, responseTopic } = this.resolveFlowTopics(kind);
|
||||
const rr = await this.getRequestor(
|
||||
): Promise<unknown> => {
|
||||
const { requestTopic, responseTopic } = resolveFlowTopics(kind);
|
||||
const rr = await getRequestor(
|
||||
requestTopic,
|
||||
responseTopic,
|
||||
`flow:${flow}:${kind}`,
|
||||
|
|
@ -214,16 +251,16 @@ export class DispatcherManager {
|
|||
const translated = translateRequest(kind, request);
|
||||
const response = await rr.request(translated);
|
||||
return translateResponse(kind, response);
|
||||
}
|
||||
};
|
||||
|
||||
async dispatchFlowServiceStreaming(
|
||||
const dispatchFlowServiceStreaming = async (
|
||||
flow: string,
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
responder: Responder,
|
||||
): Promise<void> {
|
||||
const { requestTopic, responseTopic } = this.resolveFlowTopics(kind);
|
||||
const rr = await this.getRequestor(
|
||||
): Promise<void> => {
|
||||
const { requestTopic, responseTopic } = resolveFlowTopics(kind);
|
||||
const rr = await getRequestor(
|
||||
requestTopic,
|
||||
responseTopic,
|
||||
`flow:${flow}:${kind}`,
|
||||
|
|
@ -233,12 +270,12 @@ export class DispatcherManager {
|
|||
await rr.request(translated, {
|
||||
recipient: async (response) => {
|
||||
const translatedRes = translateResponse(kind, response);
|
||||
const complete = this.isComplete(translatedRes);
|
||||
const complete = isComplete(translatedRes);
|
||||
await responder(translatedRes, complete);
|
||||
return complete;
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- Fire-and-forget publish ----------
|
||||
|
||||
|
|
@ -246,24 +283,20 @@ export class DispatcherManager {
|
|||
* Publish a single message to an arbitrary topic (no request/response).
|
||||
* Used for injecting documents into the processing pipeline.
|
||||
*/
|
||||
async publishToTopic(topic: string, message: unknown, id?: string): Promise<void> {
|
||||
const producer = await this.pubsub.createProducer<unknown>({ topic });
|
||||
const publishToTopic = async (topic: string, message: unknown, id?: string): Promise<void> => {
|
||||
const producer = await pubsub.createProducer<unknown>({ topic });
|
||||
const messageId = id ?? `pub-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
await producer.send(message, { id: messageId });
|
||||
await producer.close();
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- Static introspection ----------
|
||||
|
||||
static get flowServiceNames(): readonly string[] {
|
||||
return [...FLOW_SERVICES.keys()];
|
||||
}
|
||||
|
||||
static get globalServiceNames(): readonly string[] {
|
||||
return [...GLOBAL_SERVICES.keys()];
|
||||
}
|
||||
|
||||
static isStreamingService(kind: string): boolean {
|
||||
return STREAMING_SERVICES.has(kind);
|
||||
}
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
dispatchGlobalService,
|
||||
dispatchGlobalServiceStreaming,
|
||||
dispatchFlowService,
|
||||
dispatchFlowServiceStreaming,
|
||||
publishToTopic,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
export { createGateway, run, type GatewayConfig } from "./server.js";
|
||||
export { DispatcherManager } from "./dispatch/manager.js";
|
||||
export {
|
||||
dispatcherManagerFlowServiceNames,
|
||||
dispatcherManagerGlobalServiceNames,
|
||||
dispatcherManagerIsStreamingService,
|
||||
makeDispatcherManager,
|
||||
type DispatcherManager,
|
||||
} from "./dispatch/manager.js";
|
||||
export {
|
||||
clientTermToInternal,
|
||||
clientTripleToInternal,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import * as O from "effect/Option";
|
|||
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
|
||||
import * as EffectSocket from "effect/unstable/socket/Socket";
|
||||
import { optionalStringConfig, registry, toTgError } from "@trustgraph/base";
|
||||
import { DispatcherManager } from "./dispatch/manager.js";
|
||||
import { makeDispatcherManager } from "./dispatch/manager.js";
|
||||
import { makeGatewayRpcServer } from "./rpc-server.js";
|
||||
|
||||
export interface GatewayConfig {
|
||||
|
|
@ -28,7 +28,7 @@ export async function createGateway(config: GatewayConfig) {
|
|||
const app = Fastify({ logger: true });
|
||||
await app.register(websocketPlugin);
|
||||
|
||||
const dispatcher = new DispatcherManager(config);
|
||||
const dispatcher = makeDispatcherManager(config);
|
||||
await dispatcher.start();
|
||||
const rpcScope = await Effect.runPromise(Scope.make());
|
||||
const rpcServer = await Effect.runPromise(
|
||||
|
|
|
|||
|
|
@ -4,39 +4,43 @@ export { createGateway, type GatewayConfig } from "./gateway/index.js";
|
|||
export { OpenAIProcessor } from "./model/text-completion/openai.js";
|
||||
export { ClaudeProcessor } from "./model/text-completion/claude.js";
|
||||
export {
|
||||
GraphRag,
|
||||
GraphRagEngine,
|
||||
GraphRagLive,
|
||||
makeGraphRag,
|
||||
makeGraphRagEngine,
|
||||
normalizeGraphRagConfig,
|
||||
stringToTerm,
|
||||
termToString,
|
||||
type GraphRag,
|
||||
type GraphRagConfig,
|
||||
type GraphRagClients,
|
||||
type GraphRagEngineShape,
|
||||
type GraphRagQueryOptions,
|
||||
} from "./retrieval/graph-rag.js";
|
||||
export {
|
||||
DocumentRag,
|
||||
DocumentRagEngine,
|
||||
DocumentRagLive,
|
||||
makeDocumentRag,
|
||||
makeDocumentRagEngine,
|
||||
type DocumentRag,
|
||||
type DocumentRagClients,
|
||||
type DocumentRagEngineShape,
|
||||
type DocumentRagQueryOptions,
|
||||
} from "./retrieval/document-rag.js";
|
||||
export { FalkorDBTriplesStore, type FalkorDBConfig } from "./storage/triples/falkordb.js";
|
||||
export { FalkorDBTriplesQuery, type FalkorDBQueryConfig } from "./query/triples/falkordb.js";
|
||||
export { makeFalkorDBTriplesStore, type FalkorDBTriplesStore, type FalkorDBConfig } from "./storage/triples/falkordb.js";
|
||||
export { makeFalkorDBTriplesQuery, type FalkorDBTriplesQuery, type FalkorDBQueryConfig } from "./query/triples/falkordb.js";
|
||||
|
||||
// Qdrant embeddings storage
|
||||
export {
|
||||
QdrantDocEmbeddingsStore,
|
||||
makeQdrantDocEmbeddingsStore,
|
||||
type QdrantDocEmbeddingsStore,
|
||||
type QdrantDocEmbeddingsConfig,
|
||||
type DocEmbeddingsMessage,
|
||||
type DocEmbeddingChunk,
|
||||
} from "./storage/embeddings/qdrant-doc.js";
|
||||
export {
|
||||
QdrantGraphEmbeddingsStore,
|
||||
makeQdrantGraphEmbeddingsStore,
|
||||
type QdrantGraphEmbeddingsStore,
|
||||
type QdrantGraphEmbeddingsConfig,
|
||||
type GraphEmbeddingsMessage,
|
||||
type GraphEmbeddingEntity,
|
||||
|
|
@ -44,13 +48,15 @@ export {
|
|||
|
||||
// Qdrant embeddings query
|
||||
export {
|
||||
QdrantDocEmbeddingsQuery,
|
||||
makeQdrantDocEmbeddingsQuery,
|
||||
type QdrantDocEmbeddingsQuery,
|
||||
type QdrantDocQueryConfig,
|
||||
type ChunkMatch,
|
||||
type DocEmbeddingsQueryRequest,
|
||||
} from "./query/embeddings/qdrant-doc.js";
|
||||
export {
|
||||
QdrantGraphEmbeddingsQuery,
|
||||
makeQdrantGraphEmbeddingsQuery,
|
||||
type QdrantGraphEmbeddingsQuery,
|
||||
type QdrantGraphQueryConfig,
|
||||
type EntityMatch,
|
||||
type GraphEmbeddingsQueryRequest,
|
||||
|
|
@ -81,7 +87,7 @@ export { filterToolsByGroupAndState, getNextState } from "./agent/tool-filter.js
|
|||
|
||||
// Librarian service
|
||||
export { LibrarianService, type LibrarianServiceConfig } from "./librarian/service.js";
|
||||
export { CollectionManager, type CollectionEntry } from "./librarian/collection-manager.js";
|
||||
export { makeCollectionManager, type CollectionEntry, type CollectionManager } from "./librarian/collection-manager.js";
|
||||
|
||||
// Chunking service
|
||||
export { recursiveSplit } from "./chunking/recursive-splitter.js";
|
||||
|
|
|
|||
|
|
@ -14,60 +14,66 @@ export interface CollectionEntry {
|
|||
tags: string[];
|
||||
}
|
||||
|
||||
export class CollectionManager {
|
||||
/** keyed by `${user}:${collection}` */
|
||||
private collections = new Map<string, CollectionEntry>();
|
||||
|
||||
private key(user: string, collection: string): string {
|
||||
return `${user}:${collection}`;
|
||||
}
|
||||
|
||||
listCollections(user: string): CollectionEntry[] {
|
||||
const result: CollectionEntry[] = [];
|
||||
for (const entry of this.collections.values()) {
|
||||
if (entry.user === user) {
|
||||
result.push(entry);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getCollection(user: string, collection: string): CollectionEntry | undefined {
|
||||
return this.collections.get(this.key(user, collection));
|
||||
}
|
||||
|
||||
updateCollection(
|
||||
export interface CollectionManager {
|
||||
readonly listCollections: (user: string) => CollectionEntry[];
|
||||
readonly getCollection: (user: string, collection: string) => CollectionEntry | undefined;
|
||||
readonly updateCollection: (
|
||||
user: string,
|
||||
collection: string,
|
||||
name: string,
|
||||
description: string,
|
||||
tags: string[],
|
||||
): CollectionEntry {
|
||||
const entry: CollectionEntry = { user, collection, name, description, tags };
|
||||
this.collections.set(this.key(user, collection), entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
deleteCollection(user: string, collection: string): boolean {
|
||||
return this.collections.delete(this.key(user, collection));
|
||||
}
|
||||
|
||||
ensureCollectionExists(user: string, collection: string): CollectionEntry {
|
||||
const existing = this.getCollection(user, collection);
|
||||
if (existing !== undefined) return existing;
|
||||
return this.updateCollection(user, collection, collection, "", []);
|
||||
}
|
||||
|
||||
/** Serialize to a plain array for JSON persistence. */
|
||||
toJSON(): CollectionEntry[] {
|
||||
return [...this.collections.values()];
|
||||
}
|
||||
|
||||
/** Restore from a serialized array. */
|
||||
loadFromJSON(entries: CollectionEntry[]): void {
|
||||
this.collections.clear();
|
||||
for (const entry of entries) {
|
||||
this.collections.set(this.key(entry.user, entry.collection), entry);
|
||||
}
|
||||
}
|
||||
) => CollectionEntry;
|
||||
readonly deleteCollection: (user: string, collection: string) => boolean;
|
||||
readonly ensureCollectionExists: (user: string, collection: string) => CollectionEntry;
|
||||
readonly toJSON: () => CollectionEntry[];
|
||||
readonly loadFromJSON: (entries: CollectionEntry[]) => void;
|
||||
}
|
||||
|
||||
export function makeCollectionManager(): CollectionManager {
|
||||
/** keyed by `${user}:${collection}` */
|
||||
const collections = new Map<string, CollectionEntry>();
|
||||
|
||||
const key = (user: string, collection: string): string => `${user}:${collection}`;
|
||||
|
||||
const updateCollection = (
|
||||
user: string,
|
||||
collection: string,
|
||||
name: string,
|
||||
description: string,
|
||||
tags: string[],
|
||||
): CollectionEntry => {
|
||||
const entry: CollectionEntry = { user, collection, name, description, tags };
|
||||
collections.set(key(user, collection), entry);
|
||||
return entry;
|
||||
};
|
||||
|
||||
return {
|
||||
listCollections: (user) => {
|
||||
const result: CollectionEntry[] = [];
|
||||
for (const entry of collections.values()) {
|
||||
if (entry.user === user) {
|
||||
result.push(entry);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
getCollection: (user, collection) => collections.get(key(user, collection)),
|
||||
updateCollection,
|
||||
deleteCollection: (user, collection) => collections.delete(key(user, collection)),
|
||||
ensureCollectionExists: (user, collection) => {
|
||||
const existing = collections.get(key(user, collection));
|
||||
if (existing !== undefined) return existing;
|
||||
return updateCollection(user, collection, collection, "", []);
|
||||
},
|
||||
/** Serialize to a plain array for JSON persistence. */
|
||||
toJSON: () => [...collections.values()],
|
||||
/** Restore from a serialized array. */
|
||||
loadFromJSON: (entries) => {
|
||||
collections.clear();
|
||||
for (const entry of entries) {
|
||||
collections.set(key(entry.user, entry.collection), entry);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -11,10 +11,11 @@
|
|||
import { AzureOpenAI } from "openai";
|
||||
import {
|
||||
Llm,
|
||||
LlmService,
|
||||
makeLlmService,
|
||||
makeFlowProcessorProgram,
|
||||
makeLlmServiceShape,
|
||||
makeLlmSpecs,
|
||||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
|
|
@ -22,27 +23,19 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { Effect, Layer } from "effect";
|
||||
|
||||
export class AzureOpenAIProcessor extends LlmService {
|
||||
private client: AzureOpenAI;
|
||||
private readonly defaultModel: string;
|
||||
private readonly defaultTemperature: number;
|
||||
private readonly maxOutput: number;
|
||||
export type AzureOpenAIProcessorConfig = ProcessorConfig & {
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
endpoint?: string;
|
||||
apiVersion?: string;
|
||||
temperature?: number;
|
||||
maxOutput?: number;
|
||||
};
|
||||
|
||||
constructor(
|
||||
config: ProcessorConfig & {
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
endpoint?: string;
|
||||
apiVersion?: string;
|
||||
temperature?: number;
|
||||
maxOutput?: number;
|
||||
},
|
||||
) {
|
||||
super(config);
|
||||
|
||||
this.defaultModel = config.model ?? process.env.AZURE_MODEL ?? "gpt-4o";
|
||||
this.defaultTemperature = config.temperature ?? 0.0;
|
||||
this.maxOutput = config.maxOutput ?? 4096;
|
||||
export function makeAzureOpenAIProvider(config: AzureOpenAIProcessorConfig): LlmProvider {
|
||||
const defaultModel = config.model ?? process.env.AZURE_MODEL ?? "gpt-4o";
|
||||
const defaultTemperature = config.temperature ?? 0.0;
|
||||
const maxOutput = config.maxOutput ?? 4096;
|
||||
|
||||
const apiKey = config.apiKey ?? process.env.AZURE_TOKEN;
|
||||
if (apiKey === undefined || apiKey.length === 0) {
|
||||
|
|
@ -59,115 +52,122 @@ export class AzureOpenAIProcessor extends LlmService {
|
|||
process.env.AZURE_API_VERSION ??
|
||||
"2024-12-01-preview";
|
||||
|
||||
this.client = new AzureOpenAI({ apiKey, apiVersion, endpoint });
|
||||
const client = new AzureOpenAI({ apiKey, apiVersion, endpoint });
|
||||
|
||||
console.log("[AzureOpenAI] LLM service initialized");
|
||||
}
|
||||
|
||||
async generateContent(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const temp = temperature ?? this.defaultTemperature;
|
||||
return {
|
||||
generateContent: async (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
try {
|
||||
const resp = await this.client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: this.maxOutput,
|
||||
});
|
||||
try {
|
||||
const resp = await client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: maxOutput,
|
||||
});
|
||||
|
||||
return {
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
model: modelName,
|
||||
};
|
||||
} catch (err) {
|
||||
if ((err as any)?.status === 429) {
|
||||
throw tooManyRequestsError();
|
||||
return {
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
model: modelName,
|
||||
};
|
||||
} catch (err) {
|
||||
if ((err as any)?.status === 429) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
generateContentStream: async function* (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
override supportsStreaming(): boolean {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const stream = await client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: maxOutput,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
});
|
||||
|
||||
async *generateContentStream(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const temp = temperature ?? this.defaultTemperature;
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
try {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: this.maxOutput,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
});
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content;
|
||||
if (content !== null && content !== undefined && content.length > 0) {
|
||||
yield {
|
||||
text: content,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
}
|
||||
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content;
|
||||
if (content !== null && content !== undefined && content.length > 0) {
|
||||
yield {
|
||||
text: content,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
if (chunk.usage !== null && chunk.usage !== undefined) {
|
||||
totalInputTokens = chunk.usage.prompt_tokens;
|
||||
totalOutputTokens = chunk.usage.completion_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage !== null && chunk.usage !== undefined) {
|
||||
totalInputTokens = chunk.usage.prompt_tokens;
|
||||
totalOutputTokens = chunk.usage.completion_tokens;
|
||||
yield {
|
||||
text: "",
|
||||
inToken: totalInputTokens,
|
||||
outToken: totalOutputTokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if ((err as any)?.status === 429) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
yield {
|
||||
text: "",
|
||||
inToken: totalInputTokens,
|
||||
outToken: totalOutputTokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if ((err as any)?.status === 429) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type AzureOpenAIProcessor = ReturnType<typeof makeAzureOpenAIProcessor>;
|
||||
|
||||
export function makeAzureOpenAIProcessor(
|
||||
config: AzureOpenAIProcessorConfig,
|
||||
): ReturnType<typeof makeLlmService> {
|
||||
return makeLlmService(config, makeAzureOpenAIProvider(config));
|
||||
}
|
||||
|
||||
export const AzureOpenAIProcessor = makeAzureOpenAIProcessor;
|
||||
|
||||
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({
|
||||
id: "text-completion",
|
||||
specs: () => makeLlmSpecs(),
|
||||
layer: (config) =>
|
||||
Layer.succeed(
|
||||
Llm,
|
||||
Llm.of(makeLlmServiceShape(new AzureOpenAIProcessor(config))),
|
||||
Llm.of(makeLlmServiceShape(makeAzureOpenAIProvider(config))),
|
||||
),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@
|
|||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import {
|
||||
Llm,
|
||||
LlmService,
|
||||
makeLlmService,
|
||||
makeFlowProcessorProgram,
|
||||
makeLlmServiceShape,
|
||||
makeLlmSpecs,
|
||||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
|
|
@ -18,132 +19,130 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { Effect, Layer } from "effect";
|
||||
|
||||
export class ClaudeProcessor extends LlmService {
|
||||
private client: Anthropic;
|
||||
private readonly defaultModel: string;
|
||||
private readonly defaultTemperature: number;
|
||||
private readonly maxOutput: number;
|
||||
|
||||
constructor(config: ProcessorConfig & {
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
temperature?: number;
|
||||
maxOutput?: number;
|
||||
}) {
|
||||
super(config);
|
||||
|
||||
this.defaultModel = config.model ?? "claude-sonnet-4-20250514";
|
||||
this.defaultTemperature = config.temperature ?? 0.0;
|
||||
this.maxOutput = config.maxOutput ?? 8192;
|
||||
export type ClaudeProcessorConfig = ProcessorConfig & {
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
temperature?: number;
|
||||
maxOutput?: number;
|
||||
};
|
||||
|
||||
export function makeClaudeProvider(config: ClaudeProcessorConfig): LlmProvider {
|
||||
const defaultModel = config.model ?? "claude-sonnet-4-20250514";
|
||||
const defaultTemperature = config.temperature ?? 0.0;
|
||||
const maxOutput = config.maxOutput ?? 8192;
|
||||
const apiKey = config.apiKey ?? process.env.CLAUDE_KEY;
|
||||
if (apiKey === undefined || apiKey.length === 0) {
|
||||
throw new Error("Claude API key not specified");
|
||||
}
|
||||
|
||||
this.client = new Anthropic({ apiKey });
|
||||
const client = new Anthropic({ apiKey });
|
||||
|
||||
console.log("[Claude] LLM service initialized");
|
||||
}
|
||||
|
||||
async generateContent(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const temp = temperature ?? this.defaultTemperature;
|
||||
return {
|
||||
generateContent: async (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
try {
|
||||
const response = await this.client.messages.create({
|
||||
model: modelName,
|
||||
max_tokens: this.maxOutput,
|
||||
temperature: temp,
|
||||
system,
|
||||
messages: [
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
});
|
||||
try {
|
||||
const response = await client.messages.create({
|
||||
model: modelName,
|
||||
max_tokens: maxOutput,
|
||||
temperature: temp,
|
||||
system,
|
||||
messages: [
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
});
|
||||
|
||||
const text = response.content[0].type === "text"
|
||||
? response.content[0].text
|
||||
: "";
|
||||
const text = response.content[0].type === "text"
|
||||
? response.content[0].text
|
||||
: "";
|
||||
|
||||
return {
|
||||
text,
|
||||
inToken: response.usage.input_tokens,
|
||||
outToken: response.usage.output_tokens,
|
||||
model: modelName,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof Anthropic.RateLimitError) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
override supportsStreaming(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async *generateContentStream(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const temp = temperature ?? this.defaultTemperature;
|
||||
|
||||
try {
|
||||
const stream = this.client.messages.stream({
|
||||
model: modelName,
|
||||
max_tokens: this.maxOutput,
|
||||
temperature: temp,
|
||||
system,
|
||||
messages: [
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
});
|
||||
|
||||
for await (const event of stream) {
|
||||
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
||||
yield {
|
||||
text: event.delta.text,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
return {
|
||||
text,
|
||||
inToken: response.usage.input_tokens,
|
||||
outToken: response.usage.output_tokens,
|
||||
model: modelName,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof Anthropic.RateLimitError) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
generateContentStream: async function* (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
const finalMessage = await stream.finalMessage();
|
||||
yield {
|
||||
text: "",
|
||||
inToken: finalMessage.usage.input_tokens,
|
||||
outToken: finalMessage.usage.output_tokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof Anthropic.RateLimitError) {
|
||||
throw tooManyRequestsError();
|
||||
try {
|
||||
const stream = client.messages.stream({
|
||||
model: modelName,
|
||||
max_tokens: maxOutput,
|
||||
temperature: temp,
|
||||
system,
|
||||
messages: [
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
});
|
||||
|
||||
for await (const event of stream) {
|
||||
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
||||
yield {
|
||||
text: event.delta.text,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const finalMessage = await stream.finalMessage();
|
||||
yield {
|
||||
text: "",
|
||||
inToken: finalMessage.usage.input_tokens,
|
||||
outToken: finalMessage.usage.output_tokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof Anthropic.RateLimitError) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type ClaudeProcessor = ReturnType<typeof makeClaudeProcessor>;
|
||||
|
||||
export function makeClaudeProcessor(config: ClaudeProcessorConfig): ReturnType<typeof makeLlmService> {
|
||||
return makeLlmService(config, makeClaudeProvider(config));
|
||||
}
|
||||
|
||||
export const ClaudeProcessor = makeClaudeProcessor;
|
||||
|
||||
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({
|
||||
id: "text-completion",
|
||||
specs: () => makeLlmSpecs(),
|
||||
layer: (config) =>
|
||||
Layer.succeed(
|
||||
Llm,
|
||||
Llm.of(makeLlmServiceShape(new ClaudeProcessor(config))),
|
||||
Llm.of(makeLlmServiceShape(makeClaudeProvider(config))),
|
||||
),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@
|
|||
import { Mistral } from "@mistralai/mistralai";
|
||||
import {
|
||||
Llm,
|
||||
LlmService,
|
||||
makeLlmService,
|
||||
makeFlowProcessorProgram,
|
||||
makeLlmServiceShape,
|
||||
makeLlmSpecs,
|
||||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
|
|
@ -20,140 +21,136 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { Effect, Layer } from "effect";
|
||||
|
||||
export class MistralProcessor extends LlmService {
|
||||
private client: Mistral;
|
||||
private readonly defaultModel: string;
|
||||
private readonly defaultTemperature: number;
|
||||
private readonly maxOutput: number;
|
||||
|
||||
constructor(
|
||||
config: ProcessorConfig & {
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
temperature?: number;
|
||||
maxOutput?: number;
|
||||
},
|
||||
) {
|
||||
super(config);
|
||||
|
||||
this.defaultModel =
|
||||
config.model ?? process.env.MISTRAL_MODEL ?? "ministral-8b-latest";
|
||||
this.defaultTemperature = config.temperature ?? 0.0;
|
||||
this.maxOutput = config.maxOutput ?? 4096;
|
||||
export type MistralProcessorConfig = ProcessorConfig & {
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
temperature?: number;
|
||||
maxOutput?: number;
|
||||
};
|
||||
|
||||
export function makeMistralProvider(config: MistralProcessorConfig): LlmProvider {
|
||||
const defaultModel =
|
||||
config.model ?? process.env.MISTRAL_MODEL ?? "ministral-8b-latest";
|
||||
const defaultTemperature = config.temperature ?? 0.0;
|
||||
const maxOutput = config.maxOutput ?? 4096;
|
||||
const apiKey = config.apiKey ?? process.env.MISTRAL_TOKEN;
|
||||
if (apiKey === undefined || apiKey.length === 0) {
|
||||
throw new Error("Mistral API key not specified");
|
||||
}
|
||||
|
||||
this.client = new Mistral({ apiKey });
|
||||
const client = new Mistral({ apiKey });
|
||||
|
||||
console.log("[Mistral] LLM service initialized");
|
||||
}
|
||||
|
||||
async generateContent(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const temp = temperature ?? this.defaultTemperature;
|
||||
return {
|
||||
generateContent: async (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
try {
|
||||
const resp = await this.client.chat.complete({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
maxTokens: this.maxOutput,
|
||||
});
|
||||
try {
|
||||
const resp = await client.chat.complete({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
maxTokens: maxOutput,
|
||||
});
|
||||
|
||||
return {
|
||||
text: (resp.choices?.[0]?.message?.content as string) ?? "",
|
||||
inToken: resp.usage?.promptTokens ?? 0,
|
||||
outToken: resp.usage?.completionTokens ?? 0,
|
||||
model: modelName,
|
||||
};
|
||||
} catch (err) {
|
||||
if ((err as any)?.statusCode === 429 || (err as any)?.status === 429) {
|
||||
throw tooManyRequestsError();
|
||||
return {
|
||||
text: (resp.choices?.[0]?.message?.content as string) ?? "",
|
||||
inToken: resp.usage?.promptTokens ?? 0,
|
||||
outToken: resp.usage?.completionTokens ?? 0,
|
||||
model: modelName,
|
||||
};
|
||||
} catch (err) {
|
||||
if ((err as any)?.statusCode === 429 || (err as any)?.status === 429) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
generateContentStream: async function* (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
override supportsStreaming(): boolean {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const stream = await client.chat.stream({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
maxTokens: maxOutput,
|
||||
});
|
||||
|
||||
async *generateContentStream(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const temp = temperature ?? this.defaultTemperature;
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
try {
|
||||
const stream = await this.client.chat.stream({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
maxTokens: this.maxOutput,
|
||||
});
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.data?.choices?.[0]?.delta;
|
||||
const content = delta?.content;
|
||||
if (typeof content === "string" && content.length > 0) {
|
||||
yield {
|
||||
text: content,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
}
|
||||
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.data?.choices?.[0]?.delta;
|
||||
const content = delta?.content;
|
||||
if (typeof content === "string" && content.length > 0) {
|
||||
yield {
|
||||
text: content,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
if (chunk.data?.usage !== undefined) {
|
||||
totalInputTokens = chunk.data.usage.promptTokens ?? 0;
|
||||
totalOutputTokens = chunk.data.usage.completionTokens ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.data?.usage !== undefined) {
|
||||
totalInputTokens = chunk.data.usage.promptTokens ?? 0;
|
||||
totalOutputTokens = chunk.data.usage.completionTokens ?? 0;
|
||||
yield {
|
||||
text: "",
|
||||
inToken: totalInputTokens,
|
||||
outToken: totalOutputTokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if ((err as any)?.statusCode === 429 || (err as any)?.status === 429) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
yield {
|
||||
text: "",
|
||||
inToken: totalInputTokens,
|
||||
outToken: totalOutputTokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if ((err as any)?.statusCode === 429 || (err as any)?.status === 429) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type MistralProcessor = ReturnType<typeof makeMistralProcessor>;
|
||||
|
||||
export function makeMistralProcessor(config: MistralProcessorConfig): ReturnType<typeof makeLlmService> {
|
||||
return makeLlmService(config, makeMistralProvider(config));
|
||||
}
|
||||
|
||||
export const MistralProcessor = makeMistralProcessor;
|
||||
|
||||
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({
|
||||
id: "text-completion",
|
||||
specs: () => makeLlmSpecs(),
|
||||
layer: (config) =>
|
||||
Layer.succeed(
|
||||
Llm,
|
||||
Llm.of(makeLlmServiceShape(new MistralProcessor(config))),
|
||||
Llm.of(makeLlmServiceShape(makeMistralProvider(config))),
|
||||
),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,27 +9,24 @@
|
|||
import { Ollama } from "ollama";
|
||||
import {
|
||||
Llm,
|
||||
LlmService,
|
||||
makeLlmService,
|
||||
makeFlowProcessorProgram,
|
||||
makeLlmServiceShape,
|
||||
makeLlmSpecs,
|
||||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
} from "@trustgraph/base";
|
||||
import { Effect, Layer } from "effect";
|
||||
|
||||
export class OllamaProcessor extends LlmService {
|
||||
private client: Ollama;
|
||||
private readonly defaultModel: string;
|
||||
export type OllamaProcessorConfig = ProcessorConfig & {
|
||||
model?: string;
|
||||
ollamaUrl?: string;
|
||||
};
|
||||
|
||||
constructor(config: ProcessorConfig & {
|
||||
model?: string;
|
||||
ollamaUrl?: string;
|
||||
}) {
|
||||
super(config);
|
||||
|
||||
this.defaultModel =
|
||||
export function makeOllamaProvider(config: OllamaProcessorConfig): LlmProvider {
|
||||
const defaultModel =
|
||||
config.model ??
|
||||
process.env.OLLAMA_MODEL ??
|
||||
"qwen2.5:0.5b";
|
||||
|
|
@ -39,96 +36,101 @@ export class OllamaProcessor extends LlmService {
|
|||
process.env.OLLAMA_URL ??
|
||||
"http://localhost:11434";
|
||||
|
||||
this.client = new Ollama({ host });
|
||||
const client = new Ollama({ host });
|
||||
|
||||
console.log(
|
||||
`[Ollama] LLM service initialized (host=${host}, model=${this.defaultModel})`,
|
||||
`[Ollama] LLM service initialized (host=${host}, model=${defaultModel})`,
|
||||
);
|
||||
}
|
||||
|
||||
async generateContent(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
_temperature?: number,
|
||||
): Promise<LlmResult> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const fullPrompt = system + "\n\n" + prompt;
|
||||
return {
|
||||
generateContent: async (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
_temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const fullPrompt = system + "\n\n" + prompt;
|
||||
|
||||
const resp = await this.client.generate({
|
||||
model: modelName,
|
||||
prompt: fullPrompt,
|
||||
stream: false,
|
||||
});
|
||||
const resp = await client.generate({
|
||||
model: modelName,
|
||||
prompt: fullPrompt,
|
||||
stream: false,
|
||||
});
|
||||
|
||||
return {
|
||||
text: resp.response,
|
||||
inToken: resp.prompt_eval_count ?? 0,
|
||||
outToken: resp.eval_count ?? 0,
|
||||
model: modelName,
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: resp.response,
|
||||
inToken: resp.prompt_eval_count ?? 0,
|
||||
outToken: resp.eval_count ?? 0,
|
||||
model: modelName,
|
||||
};
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
generateContentStream: async function* (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
_temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? defaultModel;
|
||||
const fullPrompt = system + "\n\n" + prompt;
|
||||
|
||||
override supportsStreaming(): boolean {
|
||||
return true;
|
||||
}
|
||||
const stream = await client.generate({
|
||||
model: modelName,
|
||||
prompt: fullPrompt,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
async *generateContentStream(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
_temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const fullPrompt = system + "\n\n" + prompt;
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
const stream = await this.client.generate({
|
||||
model: modelName,
|
||||
prompt: fullPrompt,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
for await (const chunk of stream) {
|
||||
for await (const chunk of stream) {
|
||||
// Token counts accumulate across chunks; keep the latest values
|
||||
if (chunk.prompt_eval_count !== undefined) {
|
||||
totalInputTokens = chunk.prompt_eval_count;
|
||||
}
|
||||
if (chunk.eval_count !== undefined) {
|
||||
totalOutputTokens = chunk.eval_count;
|
||||
}
|
||||
if (chunk.prompt_eval_count !== undefined) {
|
||||
totalInputTokens = chunk.prompt_eval_count;
|
||||
}
|
||||
if (chunk.eval_count !== undefined) {
|
||||
totalOutputTokens = chunk.eval_count;
|
||||
}
|
||||
|
||||
if (chunk.response.length > 0) {
|
||||
yield {
|
||||
text: chunk.response,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
if (chunk.response.length > 0) {
|
||||
yield {
|
||||
text: chunk.response,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final chunk with accumulated token counts
|
||||
yield {
|
||||
text: "",
|
||||
inToken: totalInputTokens,
|
||||
outToken: totalOutputTokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
}
|
||||
yield {
|
||||
text: "",
|
||||
inToken: totalInputTokens,
|
||||
outToken: totalOutputTokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type OllamaProcessor = ReturnType<typeof makeOllamaProcessor>;
|
||||
|
||||
export function makeOllamaProcessor(config: OllamaProcessorConfig): ReturnType<typeof makeLlmService> {
|
||||
return makeLlmService(config, makeOllamaProvider(config));
|
||||
}
|
||||
|
||||
export const OllamaProcessor = makeOllamaProcessor;
|
||||
|
||||
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({
|
||||
id: "text-completion",
|
||||
specs: () => makeLlmSpecs(),
|
||||
layer: (config) =>
|
||||
Layer.succeed(
|
||||
Llm,
|
||||
Llm.of(makeLlmServiceShape(new OllamaProcessor(config))),
|
||||
Llm.of(makeLlmServiceShape(makeOllamaProvider(config))),
|
||||
),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,37 +12,32 @@
|
|||
import OpenAI from "openai";
|
||||
import {
|
||||
Llm,
|
||||
LlmService,
|
||||
makeLlmService,
|
||||
makeFlowProcessorProgram,
|
||||
makeLlmServiceShape,
|
||||
makeLlmSpecs,
|
||||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
} from "@trustgraph/base";
|
||||
import { Effect, Layer } from "effect";
|
||||
|
||||
export class OpenAICompatibleProcessor extends LlmService {
|
||||
private client: OpenAI;
|
||||
private readonly defaultModel: string;
|
||||
private readonly defaultTemperature: number;
|
||||
private readonly maxOutput: number;
|
||||
export type OpenAICompatibleProcessorConfig = ProcessorConfig & {
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
temperature?: number;
|
||||
maxOutput?: number;
|
||||
};
|
||||
|
||||
constructor(
|
||||
config: ProcessorConfig & {
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
temperature?: number;
|
||||
maxOutput?: number;
|
||||
},
|
||||
) {
|
||||
super(config);
|
||||
|
||||
this.defaultModel =
|
||||
config.model ?? process.env.OPENAI_COMPAT_MODEL ?? "default";
|
||||
this.defaultTemperature = config.temperature ?? 0.0;
|
||||
this.maxOutput = config.maxOutput ?? 4096;
|
||||
export function makeOpenAICompatibleProvider(
|
||||
config: OpenAICompatibleProcessorConfig,
|
||||
): LlmProvider {
|
||||
const defaultModel =
|
||||
config.model ?? process.env.OPENAI_COMPAT_MODEL ?? "default";
|
||||
const defaultTemperature = config.temperature ?? 0.0;
|
||||
const maxOutput = config.maxOutput ?? 4096;
|
||||
|
||||
const baseURL = config.baseUrl ?? process.env.OPENAI_COMPAT_URL;
|
||||
if (baseURL === undefined || baseURL.length === 0) {
|
||||
|
|
@ -54,100 +49,107 @@ export class OpenAICompatibleProcessor extends LlmService {
|
|||
const apiKey =
|
||||
config.apiKey ?? process.env.OPENAI_COMPAT_KEY ?? "sk-no-key-required";
|
||||
|
||||
this.client = new OpenAI({ baseURL, apiKey });
|
||||
const client = new OpenAI({ baseURL, apiKey });
|
||||
|
||||
console.log("[OpenAI-Compatible] LLM service initialized");
|
||||
}
|
||||
|
||||
async generateContent(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const temp = temperature ?? this.defaultTemperature;
|
||||
return {
|
||||
generateContent: async (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
const resp = await this.client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_tokens: this.maxOutput,
|
||||
});
|
||||
const resp = await client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_tokens: maxOutput,
|
||||
});
|
||||
|
||||
return {
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
model: modelName,
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
model: modelName,
|
||||
};
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
generateContentStream: async function* (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
override supportsStreaming(): boolean {
|
||||
return true;
|
||||
}
|
||||
const stream = await client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_tokens: maxOutput,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
async *generateContentStream(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const temp = temperature ?? this.defaultTemperature;
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_tokens: this.maxOutput,
|
||||
stream: true,
|
||||
});
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content;
|
||||
if (content !== null && content !== undefined && content.length > 0) {
|
||||
yield {
|
||||
text: content,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
}
|
||||
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content;
|
||||
if (content !== null && content !== undefined && content.length > 0) {
|
||||
yield {
|
||||
text: content,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
if (chunk.usage !== null && chunk.usage !== undefined) {
|
||||
totalInputTokens = chunk.usage.prompt_tokens;
|
||||
totalOutputTokens = chunk.usage.completion_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage !== null && chunk.usage !== undefined) {
|
||||
totalInputTokens = chunk.usage.prompt_tokens;
|
||||
totalOutputTokens = chunk.usage.completion_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
yield {
|
||||
text: "",
|
||||
inToken: totalInputTokens,
|
||||
outToken: totalOutputTokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
}
|
||||
yield {
|
||||
text: "",
|
||||
inToken: totalInputTokens,
|
||||
outToken: totalOutputTokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type OpenAICompatibleProcessor = ReturnType<typeof makeOpenAICompatibleProcessor>;
|
||||
|
||||
export function makeOpenAICompatibleProcessor(
|
||||
config: OpenAICompatibleProcessorConfig,
|
||||
): ReturnType<typeof makeLlmService> {
|
||||
return makeLlmService(config, makeOpenAICompatibleProvider(config));
|
||||
}
|
||||
|
||||
export const OpenAICompatibleProcessor = makeOpenAICompatibleProcessor;
|
||||
|
||||
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({
|
||||
id: "text-completion",
|
||||
specs: () => makeLlmSpecs(),
|
||||
layer: (config) =>
|
||||
Layer.succeed(
|
||||
Llm,
|
||||
Llm.of(makeLlmServiceShape(new OpenAICompatibleProcessor(config))),
|
||||
Llm.of(makeLlmServiceShape(makeOpenAICompatibleProvider(config))),
|
||||
),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@
|
|||
import OpenAI from "openai";
|
||||
import {
|
||||
Llm,
|
||||
LlmService,
|
||||
makeLlmService,
|
||||
makeFlowProcessorProgram,
|
||||
makeLlmServiceShape,
|
||||
makeLlmSpecs,
|
||||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
|
|
@ -18,142 +19,140 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { Effect, Layer } from "effect";
|
||||
|
||||
export class OpenAIProcessor extends LlmService {
|
||||
private client: OpenAI;
|
||||
private readonly defaultModel: string;
|
||||
private readonly defaultTemperature: number;
|
||||
private readonly maxOutput: number;
|
||||
|
||||
constructor(config: ProcessorConfig & {
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
temperature?: number;
|
||||
maxOutput?: number;
|
||||
}) {
|
||||
super(config);
|
||||
|
||||
this.defaultModel = config.model ?? "gpt-4o";
|
||||
this.defaultTemperature = config.temperature ?? 0.0;
|
||||
this.maxOutput = config.maxOutput ?? 4096;
|
||||
export type OpenAIProcessorConfig = ProcessorConfig & {
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
temperature?: number;
|
||||
maxOutput?: number;
|
||||
};
|
||||
|
||||
export function makeOpenAIProvider(config: OpenAIProcessorConfig): LlmProvider {
|
||||
const defaultModel = config.model ?? "gpt-4o";
|
||||
const defaultTemperature = config.temperature ?? 0.0;
|
||||
const maxOutput = config.maxOutput ?? 4096;
|
||||
const apiKey = config.apiKey ?? process.env.OPENAI_TOKEN;
|
||||
if (apiKey === undefined || apiKey.length === 0) {
|
||||
throw new Error("OpenAI API key not specified");
|
||||
}
|
||||
|
||||
this.client = new OpenAI({
|
||||
const client = new OpenAI({
|
||||
apiKey,
|
||||
baseURL: config.baseUrl ?? process.env.OPENAI_BASE_URL,
|
||||
});
|
||||
|
||||
console.log("[OpenAI] LLM service initialized");
|
||||
}
|
||||
|
||||
async generateContent(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const temp = temperature ?? this.defaultTemperature;
|
||||
return {
|
||||
generateContent: async (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
try {
|
||||
const resp = await this.client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: this.maxOutput,
|
||||
});
|
||||
try {
|
||||
const resp = await client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: maxOutput,
|
||||
});
|
||||
|
||||
return {
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
model: modelName,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof OpenAI.RateLimitError) {
|
||||
throw tooManyRequestsError();
|
||||
return {
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
model: modelName,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof OpenAI.RateLimitError) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
generateContentStream: async function* (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
override supportsStreaming(): boolean {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const stream = await client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: maxOutput,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
});
|
||||
|
||||
async *generateContentStream(
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> {
|
||||
const modelName = model ?? this.defaultModel;
|
||||
const temp = temperature ?? this.defaultTemperature;
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
try {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: this.maxOutput,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
});
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content;
|
||||
if (content !== null && content !== undefined && content.length > 0) {
|
||||
yield {
|
||||
text: content,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
}
|
||||
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content;
|
||||
if (content !== null && content !== undefined && content.length > 0) {
|
||||
yield {
|
||||
text: content,
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: modelName,
|
||||
isFinal: false,
|
||||
};
|
||||
if (chunk.usage !== null && chunk.usage !== undefined) {
|
||||
totalInputTokens = chunk.usage.prompt_tokens;
|
||||
totalOutputTokens = chunk.usage.completion_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage !== null && chunk.usage !== undefined) {
|
||||
totalInputTokens = chunk.usage.prompt_tokens;
|
||||
totalOutputTokens = chunk.usage.completion_tokens;
|
||||
yield {
|
||||
text: "",
|
||||
inToken: totalInputTokens,
|
||||
outToken: totalOutputTokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof OpenAI.RateLimitError) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
yield {
|
||||
text: "",
|
||||
inToken: totalInputTokens,
|
||||
outToken: totalOutputTokens,
|
||||
model: modelName,
|
||||
isFinal: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof OpenAI.RateLimitError) {
|
||||
throw tooManyRequestsError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type OpenAIProcessor = ReturnType<typeof makeOpenAIProcessor>;
|
||||
|
||||
export function makeOpenAIProcessor(config: OpenAIProcessorConfig): ReturnType<typeof makeLlmService> {
|
||||
return makeLlmService(config, makeOpenAIProvider(config));
|
||||
}
|
||||
|
||||
export const OpenAIProcessor = makeOpenAIProcessor;
|
||||
|
||||
export const program = makeFlowProcessorProgram<ProcessorConfig, never, Llm>({
|
||||
id: "text-completion",
|
||||
specs: () => makeLlmSpecs(),
|
||||
layer: (config) =>
|
||||
Layer.succeed(
|
||||
Llm,
|
||||
Llm.of(makeLlmServiceShape(new OpenAIProcessor(config))),
|
||||
Llm.of(makeLlmServiceShape(makeOpenAIProvider(config))),
|
||||
),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -25,12 +25,13 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
FlowProcessor,
|
||||
ConsumerSpec,
|
||||
ProducerSpec,
|
||||
makeFlowProcessor,
|
||||
makeConsumerSpec,
|
||||
makeProducerSpec,
|
||||
type ProcessorConfig,
|
||||
type EffectConfigHandler,
|
||||
type FlowContext,
|
||||
type FlowProcessorRuntime,
|
||||
type FlowResourceNotFoundError,
|
||||
type MessagingDeliveryError,
|
||||
type PromptRequest,
|
||||
|
|
@ -136,11 +137,11 @@ const makePromptTemplateRuntime = (config: PromptTemplateConfig): PromptTemplate
|
|||
|
||||
return {
|
||||
specs: [
|
||||
new ConsumerSpec<PromptRequest, FlowResourceNotFoundError | MessagingDeliveryError>(
|
||||
makeConsumerSpec<PromptRequest, FlowResourceNotFoundError | MessagingDeliveryError>(
|
||||
"prompt-request",
|
||||
onRequest,
|
||||
),
|
||||
new ProducerSpec<PromptResponse>("prompt-response"),
|
||||
makeProducerSpec<PromptResponse>("prompt-response"),
|
||||
],
|
||||
configHandlers: [onPromptConfig],
|
||||
};
|
||||
|
|
@ -154,27 +155,24 @@ const promptTemplateRuntime = (config: PromptTemplateConfig): PromptTemplateRunt
|
|||
return runtime;
|
||||
};
|
||||
|
||||
export class PromptTemplateService extends FlowProcessor {
|
||||
private readonly runtime: PromptTemplateRuntime;
|
||||
export type PromptTemplateService = FlowProcessorRuntime;
|
||||
|
||||
constructor(config: PromptTemplateConfig) {
|
||||
super(config);
|
||||
|
||||
this.runtime = makePromptTemplateRuntime(config);
|
||||
|
||||
for (const spec of this.runtime.specs) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
for (const handler of this.runtime.configHandlers) {
|
||||
this.registerConfigHandler((pushedConfig, version) =>
|
||||
Effect.runPromise(handler(pushedConfig, version)),
|
||||
);
|
||||
}
|
||||
|
||||
console.log("[PromptTemplate] Service initialized");
|
||||
export function makePromptTemplateService(config: PromptTemplateConfig): PromptTemplateService {
|
||||
const runtime = makePromptTemplateRuntime(config);
|
||||
const service = makeFlowProcessor(config, {
|
||||
specifications: runtime.specs,
|
||||
});
|
||||
for (const handler of runtime.configHandlers) {
|
||||
service.registerConfigHandler((pushedConfig, version) =>
|
||||
Effect.runPromise(handler(pushedConfig, version)),
|
||||
);
|
||||
}
|
||||
console.log("[PromptTemplate] Service initialized");
|
||||
return service;
|
||||
}
|
||||
|
||||
export const PromptTemplateService = makePromptTemplateService;
|
||||
|
||||
/**
|
||||
* Simple template rendering: replaces {variable} placeholders with values.
|
||||
* Unmatched placeholders are left as-is.
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
FlowProcessor,
|
||||
ConsumerSpec,
|
||||
ProducerSpec,
|
||||
makeFlowProcessor,
|
||||
makeConsumerSpec,
|
||||
makeProducerSpec,
|
||||
type ProcessorConfig,
|
||||
type FlowProcessorRuntime,
|
||||
type FlowContext,
|
||||
type FlowResourceNotFoundError,
|
||||
type MessagingDeliveryError,
|
||||
|
|
@ -78,37 +79,34 @@ const onDocEmbeddingsQueryMessage = Effect.fn("DocEmbeddingsQueryService.onMessa
|
|||
});
|
||||
|
||||
export const makeDocEmbeddingsQuerySpecs = (): ReadonlyArray<Spec<QdrantDocEmbeddingsQueryService>> => [
|
||||
new ConsumerSpec<
|
||||
makeConsumerSpec<
|
||||
DocumentEmbeddingsRequest,
|
||||
FlowResourceNotFoundError | MessagingDeliveryError,
|
||||
QdrantDocEmbeddingsQueryService
|
||||
>("document-embeddings-request", onDocEmbeddingsQueryMessage),
|
||||
new ProducerSpec<DocumentEmbeddingsResponse>("document-embeddings-response"),
|
||||
makeProducerSpec<DocumentEmbeddingsResponse>("document-embeddings-response"),
|
||||
];
|
||||
|
||||
export class DocEmbeddingsQueryService extends FlowProcessor<QdrantDocEmbeddingsQueryService> {
|
||||
private readonly query = makeQdrantDocEmbeddingsQueryService();
|
||||
export type DocEmbeddingsQueryService = FlowProcessorRuntime<QdrantDocEmbeddingsQueryService>;
|
||||
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
|
||||
for (const spec of makeDocEmbeddingsQuerySpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
|
||||
console.log("[DocEmbeddingsQuery] Service initialized");
|
||||
}
|
||||
|
||||
override startEffect() {
|
||||
return super.startEffect().pipe(
|
||||
Effect.provideService(
|
||||
QdrantDocEmbeddingsQueryService,
|
||||
QdrantDocEmbeddingsQueryService.of(this.query),
|
||||
export function makeDocEmbeddingsQueryService(config: ProcessorConfig): DocEmbeddingsQueryService {
|
||||
const query = makeQdrantDocEmbeddingsQueryService();
|
||||
const service = makeFlowProcessor(config, {
|
||||
specifications: makeDocEmbeddingsQuerySpecs(),
|
||||
provide: (effect) =>
|
||||
effect.pipe(
|
||||
Effect.provideService(
|
||||
QdrantDocEmbeddingsQueryService,
|
||||
QdrantDocEmbeddingsQueryService.of(query),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
console.log("[DocEmbeddingsQuery] Service initialized");
|
||||
return service;
|
||||
}
|
||||
|
||||
export const DocEmbeddingsQueryService = makeDocEmbeddingsQueryService;
|
||||
|
||||
export const program = makeFlowProcessorProgram<ProcessorConfig & QdrantDocQueryConfig, never, QdrantDocEmbeddingsQueryService>({
|
||||
id: "doc-embeddings-query",
|
||||
specs: () => makeDocEmbeddingsQuerySpecs(),
|
||||
|
|
|
|||
|
|
@ -30,22 +30,24 @@ export interface DocEmbeddingsQueryRequest {
|
|||
limit: number;
|
||||
}
|
||||
|
||||
export class QdrantDocEmbeddingsQuery {
|
||||
private client: QdrantClient;
|
||||
export interface QdrantDocEmbeddingsQuery {
|
||||
readonly query: (request: DocEmbeddingsQueryRequest) => Promise<ChunkMatch[]>;
|
||||
}
|
||||
|
||||
constructor(config: QdrantDocQueryConfig = {}) {
|
||||
const url = config.url ?? process.env.QDRANT_URL ?? "http://localhost:6333";
|
||||
const apiKey = config.apiKey ?? process.env.QDRANT_API_KEY;
|
||||
export function makeQdrantDocEmbeddingsQuery(
|
||||
config: QdrantDocQueryConfig = {},
|
||||
): QdrantDocEmbeddingsQuery {
|
||||
const url = config.url ?? process.env.QDRANT_URL ?? "http://localhost:6333";
|
||||
const apiKey = config.apiKey ?? process.env.QDRANT_API_KEY;
|
||||
|
||||
this.client = new QdrantClient({
|
||||
url,
|
||||
...(apiKey !== undefined && apiKey.length > 0 ? { apiKey } : {}),
|
||||
});
|
||||
const client = new QdrantClient({
|
||||
url,
|
||||
...(apiKey !== undefined && apiKey.length > 0 ? { apiKey } : {}),
|
||||
});
|
||||
|
||||
console.log("[QdrantDocQuery] Query service initialized");
|
||||
}
|
||||
console.log("[QdrantDocQuery] Query service initialized");
|
||||
|
||||
async query(request: DocEmbeddingsQueryRequest): Promise<ChunkMatch[]> {
|
||||
const query = async (request: DocEmbeddingsQueryRequest): Promise<ChunkMatch[]> => {
|
||||
const { vector, user, collection, limit } = request;
|
||||
|
||||
if (vector.length === 0) {
|
||||
|
|
@ -56,7 +58,7 @@ export class QdrantDocEmbeddingsQuery {
|
|||
const collectionName = `d_${user}_${collection}_${dim}`;
|
||||
|
||||
// Check if collection exists -- return empty if not
|
||||
const exists = await this.client.collectionExists(collectionName);
|
||||
const exists = await client.collectionExists(collectionName);
|
||||
if (!exists.exists) {
|
||||
console.log(
|
||||
`[QdrantDocQuery] Collection ${collectionName} does not exist, returning empty results`,
|
||||
|
|
@ -64,7 +66,7 @@ export class QdrantDocEmbeddingsQuery {
|
|||
return [];
|
||||
}
|
||||
|
||||
const searchResult = await this.client.search(collectionName, {
|
||||
const searchResult = await client.search(collectionName, {
|
||||
vector,
|
||||
limit,
|
||||
with_payload: true,
|
||||
|
|
@ -84,7 +86,9 @@ export class QdrantDocEmbeddingsQuery {
|
|||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
};
|
||||
|
||||
return { query };
|
||||
}
|
||||
|
||||
export class QdrantDocEmbeddingsQueryError extends S.TaggedErrorClass<QdrantDocEmbeddingsQueryError>()(
|
||||
|
|
@ -119,7 +123,7 @@ const qdrantDocEmbeddingsQueryError = (operation: string, cause: unknown) =>
|
|||
export const makeQdrantDocEmbeddingsQueryService = (
|
||||
config: QdrantDocQueryConfig = {},
|
||||
): QdrantDocEmbeddingsQueryServiceShape => {
|
||||
const query = new QdrantDocEmbeddingsQuery(config);
|
||||
const query = makeQdrantDocEmbeddingsQuery(config);
|
||||
return {
|
||||
query: Effect.fn("QdrantDocEmbeddingsQuery.query")(function* (request) {
|
||||
return yield* Effect.tryPromise({
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
FlowProcessor,
|
||||
ConsumerSpec,
|
||||
ProducerSpec,
|
||||
makeFlowProcessor,
|
||||
makeConsumerSpec,
|
||||
makeProducerSpec,
|
||||
type ProcessorConfig,
|
||||
type FlowProcessorRuntime,
|
||||
type FlowContext,
|
||||
type FlowResourceNotFoundError,
|
||||
type MessagingDeliveryError,
|
||||
|
|
@ -79,37 +80,34 @@ const onGraphEmbeddingsQueryMessage = Effect.fn("GraphEmbeddingsQueryService.onM
|
|||
});
|
||||
|
||||
export const makeGraphEmbeddingsQuerySpecs = (): ReadonlyArray<Spec<QdrantGraphEmbeddingsQueryService>> => [
|
||||
new ConsumerSpec<
|
||||
makeConsumerSpec<
|
||||
GraphEmbeddingsRequest,
|
||||
FlowResourceNotFoundError | MessagingDeliveryError,
|
||||
QdrantGraphEmbeddingsQueryService
|
||||
>("graph-embeddings-request", onGraphEmbeddingsQueryMessage),
|
||||
new ProducerSpec<GraphEmbeddingsResponse>("graph-embeddings-response"),
|
||||
makeProducerSpec<GraphEmbeddingsResponse>("graph-embeddings-response"),
|
||||
];
|
||||
|
||||
export class GraphEmbeddingsQueryService extends FlowProcessor<QdrantGraphEmbeddingsQueryService> {
|
||||
private readonly query = makeQdrantGraphEmbeddingsQueryService();
|
||||
export type GraphEmbeddingsQueryService = FlowProcessorRuntime<QdrantGraphEmbeddingsQueryService>;
|
||||
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
|
||||
for (const spec of makeGraphEmbeddingsQuerySpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
|
||||
console.log("[GraphEmbeddingsQuery] Service initialized");
|
||||
}
|
||||
|
||||
override startEffect() {
|
||||
return super.startEffect().pipe(
|
||||
Effect.provideService(
|
||||
QdrantGraphEmbeddingsQueryService,
|
||||
QdrantGraphEmbeddingsQueryService.of(this.query),
|
||||
export function makeGraphEmbeddingsQueryService(config: ProcessorConfig): GraphEmbeddingsQueryService {
|
||||
const query = makeQdrantGraphEmbeddingsQueryService();
|
||||
const service = makeFlowProcessor(config, {
|
||||
specifications: makeGraphEmbeddingsQuerySpecs(),
|
||||
provide: (effect) =>
|
||||
effect.pipe(
|
||||
Effect.provideService(
|
||||
QdrantGraphEmbeddingsQueryService,
|
||||
QdrantGraphEmbeddingsQueryService.of(query),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
console.log("[GraphEmbeddingsQuery] Service initialized");
|
||||
return service;
|
||||
}
|
||||
|
||||
export const GraphEmbeddingsQueryService = makeGraphEmbeddingsQueryService;
|
||||
|
||||
export const program = makeFlowProcessorProgram<ProcessorConfig & QdrantGraphQueryConfig, never, QdrantGraphEmbeddingsQueryService>({
|
||||
id: "graph-embeddings-query",
|
||||
specs: () => makeGraphEmbeddingsQuerySpecs(),
|
||||
|
|
|
|||
|
|
@ -39,22 +39,24 @@ function createTerm(value: string): Term {
|
|||
return { type: "LITERAL", value };
|
||||
}
|
||||
|
||||
export class QdrantGraphEmbeddingsQuery {
|
||||
private client: QdrantClient;
|
||||
export interface QdrantGraphEmbeddingsQuery {
|
||||
readonly query: (request: GraphEmbeddingsQueryRequest) => Promise<EntityMatch[]>;
|
||||
}
|
||||
|
||||
constructor(config: QdrantGraphQueryConfig = {}) {
|
||||
const url = config.url ?? process.env.QDRANT_URL ?? "http://localhost:6333";
|
||||
const apiKey = config.apiKey ?? process.env.QDRANT_API_KEY;
|
||||
export function makeQdrantGraphEmbeddingsQuery(
|
||||
config: QdrantGraphQueryConfig = {},
|
||||
): QdrantGraphEmbeddingsQuery {
|
||||
const url = config.url ?? process.env.QDRANT_URL ?? "http://localhost:6333";
|
||||
const apiKey = config.apiKey ?? process.env.QDRANT_API_KEY;
|
||||
|
||||
this.client = new QdrantClient({
|
||||
url,
|
||||
...(apiKey !== undefined && apiKey.length > 0 ? { apiKey } : {}),
|
||||
});
|
||||
const client = new QdrantClient({
|
||||
url,
|
||||
...(apiKey !== undefined && apiKey.length > 0 ? { apiKey } : {}),
|
||||
});
|
||||
|
||||
console.log("[QdrantGraphQuery] Query service initialized");
|
||||
}
|
||||
console.log("[QdrantGraphQuery] Query service initialized");
|
||||
|
||||
async query(request: GraphEmbeddingsQueryRequest): Promise<EntityMatch[]> {
|
||||
const query = async (request: GraphEmbeddingsQueryRequest): Promise<EntityMatch[]> => {
|
||||
const { vector, user, collection, limit } = request;
|
||||
|
||||
if (vector.length === 0) {
|
||||
|
|
@ -65,7 +67,7 @@ export class QdrantGraphEmbeddingsQuery {
|
|||
const collectionName = `t_${user}_${collection}_${dim}`;
|
||||
|
||||
// Check if collection exists -- return empty if not
|
||||
const exists = await this.client.collectionExists(collectionName);
|
||||
const exists = await client.collectionExists(collectionName);
|
||||
if (!exists.exists) {
|
||||
console.log(
|
||||
`[QdrantGraphQuery] Collection ${collectionName} does not exist, returning empty results`,
|
||||
|
|
@ -75,7 +77,7 @@ export class QdrantGraphEmbeddingsQuery {
|
|||
|
||||
// Query 2x the limit so we have a better chance of getting `limit`
|
||||
// unique entities after deduplication (same heuristic as Python impl)
|
||||
const searchResult = await this.client.search(collectionName, {
|
||||
const searchResult = await client.search(collectionName, {
|
||||
vector,
|
||||
limit: limit * 2,
|
||||
with_payload: true,
|
||||
|
|
@ -104,7 +106,9 @@ export class QdrantGraphEmbeddingsQuery {
|
|||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
};
|
||||
|
||||
return { query };
|
||||
}
|
||||
|
||||
export class QdrantGraphEmbeddingsQueryError extends S.TaggedErrorClass<QdrantGraphEmbeddingsQueryError>()(
|
||||
|
|
@ -139,7 +143,7 @@ const qdrantGraphEmbeddingsQueryError = (operation: string, cause: unknown) =>
|
|||
export const makeQdrantGraphEmbeddingsQueryService = (
|
||||
config: QdrantGraphQueryConfig = {},
|
||||
): QdrantGraphEmbeddingsQueryServiceShape => {
|
||||
const query = new QdrantGraphEmbeddingsQuery(config);
|
||||
const query = makeQdrantGraphEmbeddingsQuery(config);
|
||||
return {
|
||||
query: Effect.fn("QdrantGraphEmbeddingsQuery.query")(function* (request) {
|
||||
return yield* Effect.tryPromise({
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
FlowProcessor,
|
||||
ConsumerSpec,
|
||||
ProducerSpec,
|
||||
makeFlowProcessor,
|
||||
makeConsumerSpec,
|
||||
makeProducerSpec,
|
||||
type ProcessorConfig,
|
||||
type FlowProcessorRuntime,
|
||||
type FlowContext,
|
||||
type FlowResourceNotFoundError,
|
||||
type MessagingDeliveryError,
|
||||
|
|
@ -65,37 +66,34 @@ const onTriplesQueryMessage = Effect.fn("TriplesQueryService.onMessage")(functio
|
|||
});
|
||||
|
||||
export const makeTriplesQuerySpecs = (): ReadonlyArray<Spec<FalkorDBTriplesQueryService>> => [
|
||||
new ConsumerSpec<
|
||||
makeConsumerSpec<
|
||||
TriplesQueryRequest,
|
||||
FlowResourceNotFoundError | MessagingDeliveryError,
|
||||
FalkorDBTriplesQueryService
|
||||
>("triples-request", onTriplesQueryMessage),
|
||||
new ProducerSpec<TriplesQueryResponse>("triples-response"),
|
||||
makeProducerSpec<TriplesQueryResponse>("triples-response"),
|
||||
];
|
||||
|
||||
export class TriplesQueryService extends FlowProcessor<FalkorDBTriplesQueryService> {
|
||||
private readonly query = makeFalkorDBTriplesQueryService();
|
||||
export type TriplesQueryService = FlowProcessorRuntime<FalkorDBTriplesQueryService>;
|
||||
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
|
||||
for (const spec of makeTriplesQuerySpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
|
||||
console.log("[TriplesQuery] Service initialized");
|
||||
}
|
||||
|
||||
override startEffect() {
|
||||
return super.startEffect().pipe(
|
||||
Effect.provideService(
|
||||
FalkorDBTriplesQueryService,
|
||||
FalkorDBTriplesQueryService.of(this.query),
|
||||
export function makeTriplesQueryService(config: ProcessorConfig): TriplesQueryService {
|
||||
const query = makeFalkorDBTriplesQueryService();
|
||||
const service = makeFlowProcessor(config, {
|
||||
specifications: makeTriplesQuerySpecs(),
|
||||
provide: (effect) =>
|
||||
effect.pipe(
|
||||
Effect.provideService(
|
||||
FalkorDBTriplesQueryService,
|
||||
FalkorDBTriplesQueryService.of(query),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
console.log("[TriplesQuery] Service initialized");
|
||||
return service;
|
||||
}
|
||||
|
||||
export const TriplesQueryService = makeTriplesQueryService;
|
||||
|
||||
export const program = makeFlowProcessorProgram<ProcessorConfig & FalkorDBQueryConfig, never, FalkorDBTriplesQueryService>({
|
||||
id: "triples-query",
|
||||
specs: () => makeTriplesQuerySpecs(),
|
||||
|
|
|
|||
|
|
@ -41,35 +41,194 @@ function field(row: unknown, key: string): string {
|
|||
return (row as Record<string, unknown>)?.[key] as string ?? "";
|
||||
}
|
||||
|
||||
export class FalkorDBTriplesQuery {
|
||||
private graph: Graph;
|
||||
private connectPromise: Promise<void>;
|
||||
export interface FalkorDBTriplesQuery {
|
||||
readonly queryTriples: (
|
||||
s?: Term,
|
||||
p?: Term,
|
||||
o?: Term,
|
||||
limit?: number,
|
||||
) => Promise<Triple[]>;
|
||||
}
|
||||
|
||||
constructor(config: FalkorDBQueryConfig = {}) {
|
||||
const url = config.url ?? process.env.FALKORDB_URL ?? "redis://localhost:6379";
|
||||
const database = config.database ?? "falkordb";
|
||||
export function makeFalkorDBTriplesQuery(
|
||||
config: FalkorDBQueryConfig = {},
|
||||
): FalkorDBTriplesQuery {
|
||||
const url = config.url ?? process.env.FALKORDB_URL ?? "redis://localhost:6379";
|
||||
const database = config.database ?? "falkordb";
|
||||
|
||||
const client = createClient({ url });
|
||||
this.graph = new Graph(client, database);
|
||||
this.connectPromise = client.connect().then(() => {
|
||||
console.log(`[FalkorDBTriplesQuery] Connected to ${url}, graph: ${database}`);
|
||||
}).catch((err) => {
|
||||
console.error(`[FalkorDBTriplesQuery] Connection failed:`, err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
const client = createClient({ url });
|
||||
const graph = new Graph(client, database);
|
||||
const connectPromise = client.connect().then(() => {
|
||||
console.log(`[FalkorDBTriplesQuery] Connected to ${url}, graph: ${database}`);
|
||||
}).catch((err) => {
|
||||
console.error(`[FalkorDBTriplesQuery] Connection failed:`, err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
private async ensureConnected(): Promise<void> {
|
||||
await this.connectPromise;
|
||||
}
|
||||
const ensureConnected = async (): Promise<void> => {
|
||||
await connectPromise;
|
||||
};
|
||||
|
||||
async queryTriples(
|
||||
const matchPattern = async (
|
||||
out: [string, string, string][],
|
||||
sv: string, pv: string, ov: string, limit: number,
|
||||
): Promise<void> => {
|
||||
for (const destType of ["Literal", "Node"] as const) {
|
||||
const destKey = destType === "Literal" ? "value" : "uri";
|
||||
const result = await graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN src.uri LIMIT ${limit}`,
|
||||
{ params: { src: sv, rel: pv, dest: ov } },
|
||||
);
|
||||
for (const _rec of (result.data ?? [])) {
|
||||
out.push([sv, pv, ov]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const matchSP = async (
|
||||
out: [string, string, string][],
|
||||
sv: string, pv: string, limit: number,
|
||||
): Promise<void> => {
|
||||
// Literals
|
||||
const litResult = await graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:Literal) ` +
|
||||
`RETURN dest.value as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv, rel: pv } },
|
||||
);
|
||||
for (const rec of (litResult.data ?? [])) {
|
||||
out.push([sv, pv, field(rec, "dest")]);
|
||||
}
|
||||
// Nodes
|
||||
const nodeResult = await graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:Node) ` +
|
||||
`RETURN dest.uri as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv, rel: pv } },
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? [])) {
|
||||
out.push([sv, pv, field(rec, "dest")]);
|
||||
}
|
||||
};
|
||||
|
||||
const matchSO = async (
|
||||
out: [string, string, string][],
|
||||
sv: string, ov: string, limit: number,
|
||||
): Promise<void> => {
|
||||
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
||||
const result = await graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN rel.uri as rel LIMIT ${limit}`,
|
||||
{ params: { src: sv, dest: ov } },
|
||||
);
|
||||
for (const rec of (result.data ?? [])) {
|
||||
out.push([sv, field(rec, "rel"), ov]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const matchPO = async (
|
||||
out: [string, string, string][],
|
||||
pv: string, ov: string, limit: number,
|
||||
): Promise<void> => {
|
||||
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
||||
const result = await graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN src.uri as src LIMIT ${limit}`,
|
||||
{ params: { rel: pv, dest: ov } },
|
||||
);
|
||||
for (const rec of (result.data ?? [])) {
|
||||
out.push([field(rec, "src"), pv, ov]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const matchS = async (
|
||||
out: [string, string, string][],
|
||||
sv: string, limit: number,
|
||||
): Promise<void> => {
|
||||
const litResult = await graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:Literal) ` +
|
||||
`RETURN rel.uri as rel, dest.value as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv } },
|
||||
);
|
||||
for (const rec of (litResult.data ?? [])) {
|
||||
out.push([sv, field(rec, "rel"), field(rec, "dest")]);
|
||||
}
|
||||
const nodeResult = await graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:Node) ` +
|
||||
`RETURN rel.uri as rel, dest.uri as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv } },
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? [])) {
|
||||
out.push([sv, field(rec, "rel"), field(rec, "dest")]);
|
||||
}
|
||||
};
|
||||
|
||||
const matchP = async (
|
||||
out: [string, string, string][],
|
||||
pv: string, limit: number,
|
||||
): Promise<void> => {
|
||||
const litResult = await graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:Literal) ` +
|
||||
`RETURN src.uri as src, dest.value as dest LIMIT ${limit}`,
|
||||
{ params: { rel: pv } },
|
||||
);
|
||||
for (const rec of (litResult.data ?? [])) {
|
||||
out.push([field(rec, "src"), pv, field(rec, "dest")]);
|
||||
}
|
||||
const nodeResult = await graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:Node) ` +
|
||||
`RETURN src.uri as src, dest.uri as dest LIMIT ${limit}`,
|
||||
{ params: { rel: pv } },
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? [])) {
|
||||
out.push([field(rec, "src"), pv, field(rec, "dest")]);
|
||||
}
|
||||
};
|
||||
|
||||
const matchO = async (
|
||||
out: [string, string, string][],
|
||||
ov: string, limit: number,
|
||||
): Promise<void> => {
|
||||
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
||||
const result = await graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN src.uri as src, rel.uri as rel LIMIT ${limit}`,
|
||||
{ params: { dest: ov } },
|
||||
);
|
||||
for (const rec of (result.data ?? [])) {
|
||||
out.push([field(rec, "src"), field(rec, "rel"), ov]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const matchAll = async (
|
||||
out: [string, string, string][],
|
||||
limit: number,
|
||||
): Promise<void> => {
|
||||
const litResult = await graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel]->(dest:Literal) ` +
|
||||
`RETURN src.uri as src, rel.uri as rel, dest.value as dest LIMIT ${limit}`,
|
||||
);
|
||||
for (const rec of (litResult.data ?? [])) {
|
||||
out.push([field(rec, "src"), field(rec, "rel"), field(rec, "dest")]);
|
||||
}
|
||||
const nodeResult = await graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel]->(dest:Node) ` +
|
||||
`RETURN src.uri as src, rel.uri as rel, dest.uri as dest LIMIT ${limit}`,
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? [])) {
|
||||
out.push([field(rec, "src"), field(rec, "rel"), field(rec, "dest")]);
|
||||
}
|
||||
};
|
||||
|
||||
const queryTriples = async (
|
||||
s?: Term,
|
||||
p?: Term,
|
||||
o?: Term,
|
||||
limit = 100,
|
||||
): Promise<Triple[]> {
|
||||
await this.ensureConnected();
|
||||
): Promise<Triple[]> => {
|
||||
await ensureConnected();
|
||||
const sv = termToValue(s);
|
||||
const pv = termToValue(p);
|
||||
const ov = termToValue(o);
|
||||
|
|
@ -79,28 +238,28 @@ export class FalkorDBTriplesQuery {
|
|||
// Query both Node and Literal targets for each pattern
|
||||
if (sv !== null && pv !== null && ov !== null) {
|
||||
// SPO — exact match
|
||||
await this.matchPattern(rawTriples, sv, pv, ov, limit);
|
||||
await matchPattern(rawTriples, sv, pv, ov, limit);
|
||||
} else if (sv !== null && pv !== null) {
|
||||
// SP — known subject + predicate
|
||||
await this.matchSP(rawTriples, sv, pv, limit);
|
||||
await matchSP(rawTriples, sv, pv, limit);
|
||||
} else if (sv !== null && ov !== null) {
|
||||
// SO — known subject + object
|
||||
await this.matchSO(rawTriples, sv, ov, limit);
|
||||
await matchSO(rawTriples, sv, ov, limit);
|
||||
} else if (pv !== null && ov !== null) {
|
||||
// PO — known predicate + object
|
||||
await this.matchPO(rawTriples, pv, ov, limit);
|
||||
await matchPO(rawTriples, pv, ov, limit);
|
||||
} else if (sv !== null) {
|
||||
// S only
|
||||
await this.matchS(rawTriples, sv, limit);
|
||||
await matchS(rawTriples, sv, limit);
|
||||
} else if (pv !== null) {
|
||||
// P only
|
||||
await this.matchP(rawTriples, pv, limit);
|
||||
await matchP(rawTriples, pv, limit);
|
||||
} else if (ov !== null) {
|
||||
// O only
|
||||
await this.matchO(rawTriples, ov, limit);
|
||||
await matchO(rawTriples, ov, limit);
|
||||
} else {
|
||||
// Wildcard — all triples
|
||||
await this.matchAll(rawTriples, limit);
|
||||
await matchAll(rawTriples, limit);
|
||||
}
|
||||
|
||||
return rawTriples
|
||||
|
|
@ -111,160 +270,9 @@ export class FalkorDBTriplesQuery {
|
|||
p: createTerm(p),
|
||||
o: createTerm(o),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
private async matchPattern(
|
||||
out: [string, string, string][],
|
||||
sv: string, pv: string, ov: string, limit: number,
|
||||
): Promise<void> {
|
||||
for (const destType of ["Literal", "Node"] as const) {
|
||||
const destKey = destType === "Literal" ? "value" : "uri";
|
||||
const result = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN src.uri LIMIT ${limit}`,
|
||||
{ params: { src: sv, rel: pv, dest: ov } },
|
||||
);
|
||||
for (const _rec of (result.data ?? [])) {
|
||||
out.push([sv, pv, ov]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async matchSP(
|
||||
out: [string, string, string][],
|
||||
sv: string, pv: string, limit: number,
|
||||
): Promise<void> {
|
||||
// Literals
|
||||
const litResult = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:Literal) ` +
|
||||
`RETURN dest.value as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv, rel: pv } },
|
||||
);
|
||||
for (const rec of (litResult.data ?? [])) {
|
||||
out.push([sv, pv, field(rec, "dest")]);
|
||||
}
|
||||
// Nodes
|
||||
const nodeResult = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:Node) ` +
|
||||
`RETURN dest.uri as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv, rel: pv } },
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? [])) {
|
||||
out.push([sv, pv, field(rec, "dest")]);
|
||||
}
|
||||
}
|
||||
|
||||
private async matchSO(
|
||||
out: [string, string, string][],
|
||||
sv: string, ov: string, limit: number,
|
||||
): Promise<void> {
|
||||
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
||||
const result = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN rel.uri as rel LIMIT ${limit}`,
|
||||
{ params: { src: sv, dest: ov } },
|
||||
);
|
||||
for (const rec of (result.data ?? [])) {
|
||||
out.push([sv, field(rec, "rel"), ov]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async matchPO(
|
||||
out: [string, string, string][],
|
||||
pv: string, ov: string, limit: number,
|
||||
): Promise<void> {
|
||||
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
||||
const result = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN src.uri as src LIMIT ${limit}`,
|
||||
{ params: { rel: pv, dest: ov } },
|
||||
);
|
||||
for (const rec of (result.data ?? [])) {
|
||||
out.push([field(rec, "src"), pv, ov]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async matchS(
|
||||
out: [string, string, string][],
|
||||
sv: string, limit: number,
|
||||
): Promise<void> {
|
||||
const litResult = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:Literal) ` +
|
||||
`RETURN rel.uri as rel, dest.value as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv } },
|
||||
);
|
||||
for (const rec of (litResult.data ?? [])) {
|
||||
out.push([sv, field(rec, "rel"), field(rec, "dest")]);
|
||||
}
|
||||
const nodeResult = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:Node) ` +
|
||||
`RETURN rel.uri as rel, dest.uri as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv } },
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? [])) {
|
||||
out.push([sv, field(rec, "rel"), field(rec, "dest")]);
|
||||
}
|
||||
}
|
||||
|
||||
private async matchP(
|
||||
out: [string, string, string][],
|
||||
pv: string, limit: number,
|
||||
): Promise<void> {
|
||||
const litResult = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:Literal) ` +
|
||||
`RETURN src.uri as src, dest.value as dest LIMIT ${limit}`,
|
||||
{ params: { rel: pv } },
|
||||
);
|
||||
for (const rec of (litResult.data ?? [])) {
|
||||
out.push([field(rec, "src"), pv, field(rec, "dest")]);
|
||||
}
|
||||
const nodeResult = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:Node) ` +
|
||||
`RETURN src.uri as src, dest.uri as dest LIMIT ${limit}`,
|
||||
{ params: { rel: pv } },
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? [])) {
|
||||
out.push([field(rec, "src"), pv, field(rec, "dest")]);
|
||||
}
|
||||
}
|
||||
|
||||
private async matchO(
|
||||
out: [string, string, string][],
|
||||
ov: string, limit: number,
|
||||
): Promise<void> {
|
||||
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
||||
const result = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN src.uri as src, rel.uri as rel LIMIT ${limit}`,
|
||||
{ params: { dest: ov } },
|
||||
);
|
||||
for (const rec of (result.data ?? [])) {
|
||||
out.push([field(rec, "src"), field(rec, "rel"), ov]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async matchAll(
|
||||
out: [string, string, string][],
|
||||
limit: number,
|
||||
): Promise<void> {
|
||||
const litResult = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel]->(dest:Literal) ` +
|
||||
`RETURN src.uri as src, rel.uri as rel, dest.value as dest LIMIT ${limit}`,
|
||||
);
|
||||
for (const rec of (litResult.data ?? [])) {
|
||||
out.push([field(rec, "src"), field(rec, "rel"), field(rec, "dest")]);
|
||||
}
|
||||
const nodeResult = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel]->(dest:Node) ` +
|
||||
`RETURN src.uri as src, rel.uri as rel, dest.uri as dest LIMIT ${limit}`,
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? [])) {
|
||||
out.push([field(rec, "src"), field(rec, "rel"), field(rec, "dest")]);
|
||||
}
|
||||
}
|
||||
return { queryTriples };
|
||||
}
|
||||
|
||||
export class FalkorDBTriplesQueryError extends S.TaggedErrorClass<FalkorDBTriplesQueryError>()(
|
||||
|
|
@ -302,7 +310,7 @@ const falkorDBTriplesQueryError = (operation: string, cause: unknown) =>
|
|||
export const makeFalkorDBTriplesQueryService = (
|
||||
config: FalkorDBQueryConfig = {},
|
||||
): FalkorDBTriplesQueryServiceShape => {
|
||||
const query = new FalkorDBTriplesQuery(config);
|
||||
const query = makeFalkorDBTriplesQuery(config);
|
||||
return {
|
||||
queryTriples: Effect.fn("FalkorDBTriplesQuery.queryTriples")((
|
||||
s: Term | undefined,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
ConsumerSpec,
|
||||
FlowProcessor,
|
||||
ProducerSpec,
|
||||
RequestResponseSpec,
|
||||
makeConsumerSpec,
|
||||
makeFlowProcessor,
|
||||
makeProducerSpec,
|
||||
makeRequestResponseSpec,
|
||||
makeFlowProcessorProgram,
|
||||
type DocumentEmbeddingsRequest,
|
||||
type DocumentEmbeddingsResponse,
|
||||
|
|
@ -22,6 +22,7 @@ import {
|
|||
type EmbeddingsRequest,
|
||||
type EmbeddingsResponse,
|
||||
type FlowContext,
|
||||
type FlowProcessorRuntime,
|
||||
type FlowRequestOptions,
|
||||
type FlowRequestor,
|
||||
type FlowResourceNotFoundError,
|
||||
|
|
@ -113,48 +114,47 @@ const onDocumentRagRequest = Effect.fn("DocumentRagService.onRequest")(function*
|
|||
});
|
||||
|
||||
export const makeDocumentRagSpecs = (): ReadonlyArray<Spec<DocumentRagEngine>> => [
|
||||
new ConsumerSpec<DocumentRagRequest, FlowResourceNotFoundError | MessagingDeliveryError, DocumentRagEngine>(
|
||||
makeConsumerSpec<DocumentRagRequest, FlowResourceNotFoundError | MessagingDeliveryError, DocumentRagEngine>(
|
||||
"document-rag-request",
|
||||
onDocumentRagRequest,
|
||||
),
|
||||
new ProducerSpec<DocumentRagResponse>("document-rag-response"),
|
||||
new RequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
|
||||
makeProducerSpec<DocumentRagResponse>("document-rag-response"),
|
||||
makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
|
||||
"llm",
|
||||
"text-completion-request",
|
||||
"text-completion-response",
|
||||
),
|
||||
new RequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>(
|
||||
makeRequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>(
|
||||
"embeddings",
|
||||
"embeddings-request",
|
||||
"embeddings-response",
|
||||
),
|
||||
new RequestResponseSpec<DocumentEmbeddingsRequest, DocumentEmbeddingsResponse>(
|
||||
makeRequestResponseSpec<DocumentEmbeddingsRequest, DocumentEmbeddingsResponse>(
|
||||
"doc-embeddings",
|
||||
"document-embeddings-request",
|
||||
"document-embeddings-response",
|
||||
),
|
||||
new RequestResponseSpec<PromptRequest, PromptResponse>(
|
||||
makeRequestResponseSpec<PromptRequest, PromptResponse>(
|
||||
"prompt",
|
||||
"prompt-request",
|
||||
"prompt-response",
|
||||
),
|
||||
];
|
||||
|
||||
export class DocumentRagService extends FlowProcessor<DocumentRagEngine> {
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
for (const spec of makeDocumentRagSpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
}
|
||||
export type DocumentRagService = FlowProcessorRuntime<DocumentRagEngine>;
|
||||
|
||||
override startEffect() {
|
||||
return super.startEffect().pipe(
|
||||
Effect.provideService(DocumentRagEngine, DocumentRagEngine.of(makeDocumentRagEngine())),
|
||||
);
|
||||
}
|
||||
export function makeDocumentRagService(config: ProcessorConfig): DocumentRagService {
|
||||
return makeFlowProcessor(config, {
|
||||
specifications: makeDocumentRagSpecs(),
|
||||
provide: (effect) =>
|
||||
effect.pipe(
|
||||
Effect.provideService(DocumentRagEngine, DocumentRagEngine.of(makeDocumentRagEngine())),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export const DocumentRagService = makeDocumentRagService;
|
||||
|
||||
export const program = makeFlowProcessorProgram({
|
||||
id: "document-rag",
|
||||
specs: makeDocumentRagSpecs,
|
||||
|
|
|
|||
|
|
@ -82,20 +82,19 @@ export const DocumentRagLive: Layer.Layer<DocumentRagEngine> = Layer.succeed(
|
|||
DocumentRagEngine.of(makeDocumentRagEngine()),
|
||||
);
|
||||
|
||||
export class DocumentRag {
|
||||
private readonly engine = makeDocumentRagEngine();
|
||||
private readonly clients: DocumentRagClients;
|
||||
|
||||
constructor(clients: DocumentRagClients) {
|
||||
this.clients = clients;
|
||||
}
|
||||
|
||||
query(
|
||||
export interface DocumentRag {
|
||||
readonly query: (
|
||||
queryText: string,
|
||||
options?: DocumentRagQueryOptions,
|
||||
): Promise<string> {
|
||||
return Effect.runPromise(this.engine.query(this.clients, queryText, options));
|
||||
}
|
||||
) => Promise<string>;
|
||||
}
|
||||
|
||||
export function makeDocumentRag(clients: DocumentRagClients): DocumentRag {
|
||||
const engine = makeDocumentRagEngine();
|
||||
return {
|
||||
query: (queryText, options) =>
|
||||
Effect.runPromise(engine.query(clients, queryText, options)),
|
||||
};
|
||||
}
|
||||
|
||||
async function queryDocumentRag(
|
||||
|
|
|
|||
|
|
@ -8,14 +8,15 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
ConsumerSpec,
|
||||
FlowProcessor,
|
||||
ProducerSpec,
|
||||
RequestResponseSpec,
|
||||
makeConsumerSpec,
|
||||
makeFlowProcessor,
|
||||
makeProducerSpec,
|
||||
makeRequestResponseSpec,
|
||||
makeFlowProcessorProgram,
|
||||
type EffectRequestOptions,
|
||||
type EffectRequestResponse,
|
||||
type FlowContext,
|
||||
type FlowProcessorRuntime,
|
||||
type FlowRequestOptions,
|
||||
type FlowRequestor,
|
||||
type FlowResourceNotFoundError,
|
||||
|
|
@ -139,53 +140,52 @@ const onGraphRagRequest = Effect.fn("GraphRagService.onRequest")(function* (
|
|||
});
|
||||
|
||||
export const makeGraphRagSpecs = (): ReadonlyArray<Spec<GraphRagEngine>> => [
|
||||
new ConsumerSpec<GraphRagRequest, FlowResourceNotFoundError | MessagingDeliveryError, GraphRagEngine>(
|
||||
makeConsumerSpec<GraphRagRequest, FlowResourceNotFoundError | MessagingDeliveryError, GraphRagEngine>(
|
||||
"graph-rag-request",
|
||||
onGraphRagRequest,
|
||||
),
|
||||
new ProducerSpec<GraphRagResponse>("graph-rag-response"),
|
||||
new RequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
|
||||
makeProducerSpec<GraphRagResponse>("graph-rag-response"),
|
||||
makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
|
||||
"llm",
|
||||
"text-completion-request",
|
||||
"text-completion-response",
|
||||
),
|
||||
new RequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>(
|
||||
makeRequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>(
|
||||
"embeddings",
|
||||
"embeddings-request",
|
||||
"embeddings-response",
|
||||
),
|
||||
new RequestResponseSpec<GraphEmbeddingsRequest, GraphEmbeddingsResponse>(
|
||||
makeRequestResponseSpec<GraphEmbeddingsRequest, GraphEmbeddingsResponse>(
|
||||
"graph-embeddings",
|
||||
"graph-embeddings-request",
|
||||
"graph-embeddings-response",
|
||||
),
|
||||
new RequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
|
||||
makeRequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
|
||||
"triples",
|
||||
"triples-request",
|
||||
"triples-response",
|
||||
),
|
||||
new RequestResponseSpec<PromptRequest, PromptResponse>(
|
||||
makeRequestResponseSpec<PromptRequest, PromptResponse>(
|
||||
"prompt",
|
||||
"prompt-request",
|
||||
"prompt-response",
|
||||
),
|
||||
];
|
||||
|
||||
export class GraphRagService extends FlowProcessor<GraphRagEngine> {
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
for (const spec of makeGraphRagSpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
}
|
||||
export type GraphRagService = FlowProcessorRuntime<GraphRagEngine>;
|
||||
|
||||
override startEffect() {
|
||||
return super.startEffect().pipe(
|
||||
Effect.provideService(GraphRagEngine, GraphRagEngine.of(makeGraphRagEngine())),
|
||||
);
|
||||
}
|
||||
export function makeGraphRagService(config: ProcessorConfig): GraphRagService {
|
||||
return makeFlowProcessor(config, {
|
||||
specifications: makeGraphRagSpecs(),
|
||||
provide: (effect) =>
|
||||
effect.pipe(
|
||||
Effect.provideService(GraphRagEngine, GraphRagEngine.of(makeGraphRagEngine())),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export const GraphRagService = makeGraphRagService;
|
||||
|
||||
export const program = makeFlowProcessorProgram({
|
||||
id: "graph-rag",
|
||||
specs: makeGraphRagSpecs,
|
||||
|
|
|
|||
|
|
@ -124,27 +124,22 @@ export const GraphRagLive: Layer.Layer<GraphRagEngine> = Layer.succeed(
|
|||
GraphRagEngine.of(makeGraphRagEngine()),
|
||||
);
|
||||
|
||||
export class GraphRag {
|
||||
private readonly engine = makeGraphRagEngine();
|
||||
private readonly clients: GraphRagClients;
|
||||
private readonly config: GraphRagConfig;
|
||||
|
||||
constructor(
|
||||
clients: GraphRagClients,
|
||||
config: GraphRagConfig = {},
|
||||
) {
|
||||
this.clients = clients;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
query(
|
||||
export interface GraphRag {
|
||||
readonly query: (
|
||||
queryText: string,
|
||||
options?: GraphRagQueryOptions,
|
||||
): Promise<GraphRagResult> {
|
||||
return Effect.runPromise(
|
||||
this.engine.query(this.clients, queryText, options, this.config),
|
||||
);
|
||||
}
|
||||
) => Promise<GraphRagResult>;
|
||||
}
|
||||
|
||||
export function makeGraphRag(
|
||||
clients: GraphRagClients,
|
||||
config: GraphRagConfig = {},
|
||||
): GraphRag {
|
||||
const engine = makeGraphRagEngine();
|
||||
return {
|
||||
query: (queryText, options) =>
|
||||
Effect.runPromise(engine.query(clients, queryText, options, config)),
|
||||
};
|
||||
}
|
||||
|
||||
async function queryGraphRag(
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
FlowProcessor,
|
||||
ConsumerSpec,
|
||||
RequestResponseSpec,
|
||||
makeFlowProcessor,
|
||||
makeConsumerSpec,
|
||||
makeRequestResponseSpec,
|
||||
type ProcessorConfig,
|
||||
type FlowProcessorRuntime,
|
||||
type FlowContext,
|
||||
type FlowResourceNotFoundError,
|
||||
type MessagingDeliveryError,
|
||||
|
|
@ -77,40 +78,37 @@ const onGraphEmbeddingsStoreMessage = Effect.fn("GraphEmbeddingsStoreService.onM
|
|||
});
|
||||
|
||||
export const makeGraphEmbeddingsStoreSpecs = (): ReadonlyArray<Spec<GraphEmbeddingsStoreRequirements>> => [
|
||||
new ConsumerSpec<EntityContexts, GraphEmbeddingsStoreError, GraphEmbeddingsStoreRequirements>(
|
||||
makeConsumerSpec<EntityContexts, GraphEmbeddingsStoreError, GraphEmbeddingsStoreRequirements>(
|
||||
"store-graph-embeddings-input",
|
||||
onGraphEmbeddingsStoreMessage,
|
||||
),
|
||||
new RequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>(
|
||||
makeRequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>(
|
||||
"embeddings-client",
|
||||
"embeddings-request",
|
||||
"embeddings-response",
|
||||
),
|
||||
];
|
||||
|
||||
export class GraphEmbeddingsStoreService extends FlowProcessor<GraphEmbeddingsStoreRequirements> {
|
||||
private readonly store = makeQdrantGraphEmbeddingsStoreService();
|
||||
export type GraphEmbeddingsStoreService = FlowProcessorRuntime<GraphEmbeddingsStoreRequirements>;
|
||||
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
|
||||
for (const spec of makeGraphEmbeddingsStoreSpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
|
||||
console.log("[GraphEmbeddingsStore] Service initialized");
|
||||
}
|
||||
|
||||
override startEffect() {
|
||||
return super.startEffect().pipe(
|
||||
Effect.provideService(
|
||||
QdrantGraphEmbeddingsStoreService,
|
||||
QdrantGraphEmbeddingsStoreService.of(this.store),
|
||||
export function makeGraphEmbeddingsStoreService(config: ProcessorConfig): GraphEmbeddingsStoreService {
|
||||
const store = makeQdrantGraphEmbeddingsStoreService();
|
||||
const service = makeFlowProcessor(config, {
|
||||
specifications: makeGraphEmbeddingsStoreSpecs(),
|
||||
provide: (effect) =>
|
||||
effect.pipe(
|
||||
Effect.provideService(
|
||||
QdrantGraphEmbeddingsStoreService,
|
||||
QdrantGraphEmbeddingsStoreService.of(store),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
console.log("[GraphEmbeddingsStore] Service initialized");
|
||||
return service;
|
||||
}
|
||||
|
||||
export const GraphEmbeddingsStoreService = makeGraphEmbeddingsStoreService;
|
||||
|
||||
export const program = makeFlowProcessorProgram<
|
||||
ProcessorConfig & QdrantGraphEmbeddingsConfig,
|
||||
never,
|
||||
|
|
|
|||
|
|
@ -27,51 +27,53 @@ export interface DocEmbeddingsMessage {
|
|||
chunks: DocEmbeddingChunk[];
|
||||
}
|
||||
|
||||
export class QdrantDocEmbeddingsStore {
|
||||
private client: QdrantClient;
|
||||
private knownCollections = new Set<string>();
|
||||
export interface QdrantDocEmbeddingsStore {
|
||||
readonly store: (message: DocEmbeddingsMessage) => Promise<void>;
|
||||
readonly deleteCollection: (user: string, collection: string) => Promise<void>;
|
||||
}
|
||||
|
||||
constructor(config: QdrantDocEmbeddingsConfig = {}) {
|
||||
const url = config.url ?? process.env.QDRANT_URL ?? "http://localhost:6333";
|
||||
const apiKey = config.apiKey ?? process.env.QDRANT_API_KEY;
|
||||
export function makeQdrantDocEmbeddingsStore(
|
||||
config: QdrantDocEmbeddingsConfig = {},
|
||||
): QdrantDocEmbeddingsStore {
|
||||
const url = config.url ?? process.env.QDRANT_URL ?? "http://localhost:6333";
|
||||
const apiKey = config.apiKey ?? process.env.QDRANT_API_KEY;
|
||||
|
||||
this.client = new QdrantClient({
|
||||
url,
|
||||
...(apiKey !== undefined && apiKey.length > 0 ? { apiKey } : {}),
|
||||
});
|
||||
const client = new QdrantClient({
|
||||
url,
|
||||
...(apiKey !== undefined && apiKey.length > 0 ? { apiKey } : {}),
|
||||
});
|
||||
const knownCollections = new Set<string>();
|
||||
|
||||
console.log("[QdrantDocEmbeddings] Store initialized");
|
||||
}
|
||||
console.log("[QdrantDocEmbeddings] Store initialized");
|
||||
|
||||
private collectionName(user: string, collection: string, dim: number): string {
|
||||
return `d_${user}_${collection}_${dim}`;
|
||||
}
|
||||
const collectionName = (user: string, collection: string, dim: number): string =>
|
||||
`d_${user}_${collection}_${dim}`;
|
||||
|
||||
private async ensureCollection(name: string, dim: number): Promise<void> {
|
||||
if (this.knownCollections.has(name)) return;
|
||||
const ensureCollection = async (name: string, dim: number): Promise<void> => {
|
||||
if (knownCollections.has(name)) return;
|
||||
|
||||
const exists = await this.client.collectionExists(name);
|
||||
const exists = await client.collectionExists(name);
|
||||
if (!exists.exists) {
|
||||
console.log(`[QdrantDocEmbeddings] Creating collection ${name} (dim=${dim})`);
|
||||
await this.client.createCollection(name, {
|
||||
await client.createCollection(name, {
|
||||
vectors: { size: dim, distance: "Cosine" },
|
||||
});
|
||||
}
|
||||
|
||||
this.knownCollections.add(name);
|
||||
}
|
||||
knownCollections.add(name);
|
||||
};
|
||||
|
||||
async store(message: DocEmbeddingsMessage): Promise<void> {
|
||||
const store = async (message: DocEmbeddingsMessage): Promise<void> => {
|
||||
for (const chunk of message.chunks) {
|
||||
if (chunk.chunkId.length === 0) continue;
|
||||
if (chunk.vector.length === 0) continue;
|
||||
|
||||
const dim = chunk.vector.length;
|
||||
const name = this.collectionName(message.user, message.collection, dim);
|
||||
const name = collectionName(message.user, message.collection, dim);
|
||||
|
||||
await this.ensureCollection(name, dim);
|
||||
await ensureCollection(name, dim);
|
||||
|
||||
await this.client.upsert(name, {
|
||||
await client.upsert(name, {
|
||||
points: [
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
|
|
@ -86,12 +88,12 @@ export class QdrantDocEmbeddingsStore {
|
|||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async deleteCollection(user: string, collection: string): Promise<void> {
|
||||
const deleteCollection = async (user: string, collection: string): Promise<void> => {
|
||||
const prefix = `d_${user}_${collection}_`;
|
||||
|
||||
const allCollections = await this.client.getCollections();
|
||||
const allCollections = await client.getCollections();
|
||||
const matching = allCollections.collections.filter((c) =>
|
||||
c.name.startsWith(prefix),
|
||||
);
|
||||
|
|
@ -102,13 +104,15 @@ export class QdrantDocEmbeddingsStore {
|
|||
}
|
||||
|
||||
for (const coll of matching) {
|
||||
await this.client.deleteCollection(coll.name);
|
||||
this.knownCollections.delete(coll.name);
|
||||
await client.deleteCollection(coll.name);
|
||||
knownCollections.delete(coll.name);
|
||||
console.log(`[QdrantDocEmbeddings] Deleted collection: ${coll.name}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[QdrantDocEmbeddings] Deleted ${matching.length} collection(s) for ${user}/${collection}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return { store, deleteCollection };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,57 +43,59 @@ function getTermValue(term: Term): string | null {
|
|||
}
|
||||
}
|
||||
|
||||
export class QdrantGraphEmbeddingsStore {
|
||||
private client: QdrantClient;
|
||||
private knownCollections = new Set<string>();
|
||||
export interface QdrantGraphEmbeddingsStore {
|
||||
readonly store: (message: GraphEmbeddingsMessage) => Promise<void>;
|
||||
readonly deleteCollection: (user: string, collection: string) => Promise<void>;
|
||||
}
|
||||
|
||||
constructor(config: QdrantGraphEmbeddingsConfig = {}) {
|
||||
const url = config.url ?? process.env.QDRANT_URL ?? "http://localhost:6333";
|
||||
const apiKey = config.apiKey ?? process.env.QDRANT_API_KEY;
|
||||
export function makeQdrantGraphEmbeddingsStore(
|
||||
config: QdrantGraphEmbeddingsConfig = {},
|
||||
): QdrantGraphEmbeddingsStore {
|
||||
const url = config.url ?? process.env.QDRANT_URL ?? "http://localhost:6333";
|
||||
const apiKey = config.apiKey ?? process.env.QDRANT_API_KEY;
|
||||
|
||||
this.client = new QdrantClient({
|
||||
url,
|
||||
...(apiKey !== undefined && apiKey.length > 0 ? { apiKey } : {}),
|
||||
});
|
||||
const client = new QdrantClient({
|
||||
url,
|
||||
...(apiKey !== undefined && apiKey.length > 0 ? { apiKey } : {}),
|
||||
});
|
||||
const knownCollections = new Set<string>();
|
||||
|
||||
console.log("[QdrantGraphEmbeddings] Store initialized");
|
||||
}
|
||||
console.log("[QdrantGraphEmbeddings] Store initialized");
|
||||
|
||||
private collectionName(user: string, collection: string, dim: number): string {
|
||||
return `t_${user}_${collection}_${dim}`;
|
||||
}
|
||||
const collectionName = (user: string, collection: string, dim: number): string =>
|
||||
`t_${user}_${collection}_${dim}`;
|
||||
|
||||
private async ensureCollection(name: string, dim: number): Promise<void> {
|
||||
if (this.knownCollections.has(name)) return;
|
||||
const ensureCollection = async (name: string, dim: number): Promise<void> => {
|
||||
if (knownCollections.has(name)) return;
|
||||
|
||||
const exists = await this.client.collectionExists(name);
|
||||
const exists = await client.collectionExists(name);
|
||||
if (!exists.exists) {
|
||||
console.log(`[QdrantGraphEmbeddings] Creating collection ${name} (dim=${dim})`);
|
||||
await this.client.createCollection(name, {
|
||||
await client.createCollection(name, {
|
||||
vectors: { size: dim, distance: "Cosine" },
|
||||
});
|
||||
}
|
||||
|
||||
this.knownCollections.add(name);
|
||||
}
|
||||
knownCollections.add(name);
|
||||
};
|
||||
|
||||
async store(message: GraphEmbeddingsMessage): Promise<void> {
|
||||
const store = async (message: GraphEmbeddingsMessage): Promise<void> => {
|
||||
for (const entry of message.entities) {
|
||||
const entityValue = getTermValue(entry.entity);
|
||||
if (entityValue === null || entityValue.length === 0) continue;
|
||||
if (entry.vector.length === 0) continue;
|
||||
|
||||
const dim = entry.vector.length;
|
||||
const name = this.collectionName(message.user, message.collection, dim);
|
||||
const name = collectionName(message.user, message.collection, dim);
|
||||
|
||||
await this.ensureCollection(name, dim);
|
||||
await ensureCollection(name, dim);
|
||||
|
||||
const payload: Record<string, unknown> = { entity: entityValue };
|
||||
if (entry.chunkId !== undefined && entry.chunkId.length > 0) {
|
||||
payload.chunk_id = entry.chunkId;
|
||||
}
|
||||
|
||||
await this.client.upsert(name, {
|
||||
await client.upsert(name, {
|
||||
points: [
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
|
|
@ -103,12 +105,12 @@ export class QdrantGraphEmbeddingsStore {
|
|||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async deleteCollection(user: string, collection: string): Promise<void> {
|
||||
const deleteCollection = async (user: string, collection: string): Promise<void> => {
|
||||
const prefix = `t_${user}_${collection}_`;
|
||||
|
||||
const allCollections = await this.client.getCollections();
|
||||
const allCollections = await client.getCollections();
|
||||
const matching = allCollections.collections.filter((c) =>
|
||||
c.name.startsWith(prefix),
|
||||
);
|
||||
|
|
@ -119,15 +121,17 @@ export class QdrantGraphEmbeddingsStore {
|
|||
}
|
||||
|
||||
for (const coll of matching) {
|
||||
await this.client.deleteCollection(coll.name);
|
||||
this.knownCollections.delete(coll.name);
|
||||
await client.deleteCollection(coll.name);
|
||||
knownCollections.delete(coll.name);
|
||||
console.log(`[QdrantGraphEmbeddings] Deleted collection: ${coll.name}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[QdrantGraphEmbeddings] Deleted ${matching.length} collection(s) for ${user}/${collection}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return { store, deleteCollection };
|
||||
}
|
||||
|
||||
export class QdrantGraphEmbeddingsStoreError extends S.TaggedErrorClass<QdrantGraphEmbeddingsStoreError>()(
|
||||
|
|
@ -166,7 +170,7 @@ const qdrantGraphEmbeddingsStoreError = (operation: string, cause: unknown) =>
|
|||
export const makeQdrantGraphEmbeddingsStoreService = (
|
||||
config: QdrantGraphEmbeddingsConfig = {},
|
||||
): QdrantGraphEmbeddingsStoreServiceShape => {
|
||||
const store = new QdrantGraphEmbeddingsStore(config);
|
||||
const store = makeQdrantGraphEmbeddingsStore(config);
|
||||
return {
|
||||
store: Effect.fn("QdrantGraphEmbeddingsStore.store")(function* (message) {
|
||||
return yield* Effect.tryPromise({
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
FlowProcessor,
|
||||
ConsumerSpec,
|
||||
makeFlowProcessor,
|
||||
makeConsumerSpec,
|
||||
type ProcessorConfig,
|
||||
type FlowProcessorRuntime,
|
||||
type FlowContext,
|
||||
type Triples,
|
||||
type Spec,
|
||||
|
|
@ -45,35 +46,32 @@ const onStoreTriplesMessage = Effect.fn("TriplesStoreService.onMessage")(functio
|
|||
});
|
||||
|
||||
export const makeTriplesStoreSpecs = (): ReadonlyArray<Spec<FalkorDBTriplesStoreService>> => [
|
||||
new ConsumerSpec<Triples, FalkorDBTriplesStoreError, FalkorDBTriplesStoreService>(
|
||||
makeConsumerSpec<Triples, FalkorDBTriplesStoreError, FalkorDBTriplesStoreService>(
|
||||
"store-triples-input",
|
||||
onStoreTriplesMessage,
|
||||
),
|
||||
];
|
||||
|
||||
export class TriplesStoreService extends FlowProcessor<FalkorDBTriplesStoreService> {
|
||||
private readonly store = makeFalkorDBTriplesStoreService();
|
||||
export type TriplesStoreService = FlowProcessorRuntime<FalkorDBTriplesStoreService>;
|
||||
|
||||
constructor(config: ProcessorConfig) {
|
||||
super(config);
|
||||
|
||||
for (const spec of makeTriplesStoreSpecs()) {
|
||||
this.registerSpecification(spec);
|
||||
}
|
||||
|
||||
console.log("[TriplesStore] Service initialized");
|
||||
}
|
||||
|
||||
override startEffect() {
|
||||
return super.startEffect().pipe(
|
||||
Effect.provideService(
|
||||
FalkorDBTriplesStoreService,
|
||||
FalkorDBTriplesStoreService.of(this.store),
|
||||
export function makeTriplesStoreService(config: ProcessorConfig): TriplesStoreService {
|
||||
const store = makeFalkorDBTriplesStoreService();
|
||||
const service = makeFlowProcessor(config, {
|
||||
specifications: makeTriplesStoreSpecs(),
|
||||
provide: (effect) =>
|
||||
effect.pipe(
|
||||
Effect.provideService(
|
||||
FalkorDBTriplesStoreService,
|
||||
FalkorDBTriplesStoreService.of(store),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
console.log("[TriplesStore] Service initialized");
|
||||
return service;
|
||||
}
|
||||
|
||||
export const TriplesStoreService = makeTriplesStoreService;
|
||||
|
||||
export const program = makeFlowProcessorProgram<ProcessorConfig & FalkorDBConfig, never, FalkorDBTriplesStoreService>({
|
||||
id: "triples-store",
|
||||
specs: () => makeTriplesStoreSpecs(),
|
||||
|
|
|
|||
|
|
@ -30,107 +30,136 @@ function getTermValue(term: Term): string {
|
|||
}
|
||||
}
|
||||
|
||||
export class FalkorDBTriplesStore {
|
||||
private graph: Graph;
|
||||
private connectPromise: Promise<void>;
|
||||
export interface FalkorDBTriplesStore {
|
||||
readonly createNode: (uri: string, user: string, collection: string) => Promise<void>;
|
||||
readonly createLiteral: (value: string, user: string, collection: string) => Promise<void>;
|
||||
readonly relateNode: (
|
||||
src: string,
|
||||
uri: string,
|
||||
dest: string,
|
||||
user: string,
|
||||
collection: string,
|
||||
) => Promise<void>;
|
||||
readonly relateLiteral: (
|
||||
src: string,
|
||||
uri: string,
|
||||
dest: string,
|
||||
user: string,
|
||||
collection: string,
|
||||
) => Promise<void>;
|
||||
readonly storeTriples: (
|
||||
triples: Triple[],
|
||||
user?: string,
|
||||
collection?: string,
|
||||
) => Promise<void>;
|
||||
readonly deleteCollection: (user: string, collection: string) => Promise<void>;
|
||||
}
|
||||
|
||||
constructor(config: FalkorDBConfig = {}) {
|
||||
const url = config.url ?? process.env.FALKORDB_URL ?? "redis://localhost:6379";
|
||||
const database = config.database ?? "falkordb";
|
||||
export function makeFalkorDBTriplesStore(config: FalkorDBConfig = {}): FalkorDBTriplesStore {
|
||||
const url = config.url ?? process.env.FALKORDB_URL ?? "redis://localhost:6379";
|
||||
const database = config.database ?? "falkordb";
|
||||
|
||||
const client = createClient({ url });
|
||||
this.graph = new Graph(client, database);
|
||||
this.connectPromise = client.connect().then(() => {
|
||||
console.log(`[FalkorDBTriplesStore] Connected to ${url}, graph: ${database}`);
|
||||
}).catch((err) => {
|
||||
console.error(`[FalkorDBTriplesStore] Connection failed:`, err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
const client = createClient({ url });
|
||||
const graph = new Graph(client, database);
|
||||
const connectPromise = client.connect().then(() => {
|
||||
console.log(`[FalkorDBTriplesStore] Connected to ${url}, graph: ${database}`);
|
||||
}).catch((err) => {
|
||||
console.error(`[FalkorDBTriplesStore] Connection failed:`, err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
private async ensureConnected(): Promise<void> {
|
||||
await this.connectPromise;
|
||||
}
|
||||
const ensureConnected = async (): Promise<void> => {
|
||||
await connectPromise;
|
||||
};
|
||||
|
||||
async createNode(uri: string, user: string, collection: string): Promise<void> {
|
||||
await this.ensureConnected();
|
||||
await this.graph.query(
|
||||
const createNode = async (uri: string, user: string, collection: string): Promise<void> => {
|
||||
await ensureConnected();
|
||||
await graph.query(
|
||||
"MERGE (n:Node {uri: $uri, user: $user, collection: $collection})",
|
||||
{ params: { uri, user, collection } },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async createLiteral(value: string, user: string, collection: string): Promise<void> {
|
||||
await this.ensureConnected();
|
||||
await this.graph.query(
|
||||
const createLiteral = async (value: string, user: string, collection: string): Promise<void> => {
|
||||
await ensureConnected();
|
||||
await graph.query(
|
||||
"MERGE (n:Literal {value: $value, user: $user, collection: $collection})",
|
||||
{ params: { value, user, collection } },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async relateNode(
|
||||
const relateNode = async (
|
||||
src: string, uri: string, dest: string,
|
||||
user: string, collection: string,
|
||||
): Promise<void> {
|
||||
await this.ensureConnected();
|
||||
await this.graph.query(
|
||||
): Promise<void> => {
|
||||
await ensureConnected();
|
||||
await graph.query(
|
||||
"MATCH (src:Node {uri: $src, user: $user, collection: $collection}) " +
|
||||
"MATCH (dest:Node {uri: $dest, user: $user, collection: $collection}) " +
|
||||
"MERGE (src)-[:Rel {uri: $uri, user: $user, collection: $collection}]->(dest)",
|
||||
{ params: { src, dest, uri, user, collection } },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async relateLiteral(
|
||||
const relateLiteral = async (
|
||||
src: string, uri: string, dest: string,
|
||||
user: string, collection: string,
|
||||
): Promise<void> {
|
||||
await this.ensureConnected();
|
||||
await this.graph.query(
|
||||
): Promise<void> => {
|
||||
await ensureConnected();
|
||||
await graph.query(
|
||||
"MATCH (src:Node {uri: $src, user: $user, collection: $collection}) " +
|
||||
"MATCH (dest:Literal {value: $dest, user: $user, collection: $collection}) " +
|
||||
"MERGE (src)-[:Rel {uri: $uri, user: $user, collection: $collection}]->(dest)",
|
||||
{ params: { src, dest, uri, user, collection } },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async storeTriples(
|
||||
const storeTriples = async (
|
||||
triples: Triple[],
|
||||
user = "default",
|
||||
collection = "default",
|
||||
): Promise<void> {
|
||||
): Promise<void> => {
|
||||
for (const t of triples) {
|
||||
const s = getTermValue(t.s);
|
||||
const p = getTermValue(t.p);
|
||||
const o = getTermValue(t.o);
|
||||
|
||||
await this.createNode(s, user, collection);
|
||||
await createNode(s, user, collection);
|
||||
|
||||
if (t.o.type === "IRI") {
|
||||
await this.createNode(o, user, collection);
|
||||
await this.relateNode(s, p, o, user, collection);
|
||||
await createNode(o, user, collection);
|
||||
await relateNode(s, p, o, user, collection);
|
||||
} else {
|
||||
await this.createLiteral(o, user, collection);
|
||||
await this.relateLiteral(s, p, o, user, collection);
|
||||
await createLiteral(o, user, collection);
|
||||
await relateLiteral(s, p, o, user, collection);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async deleteCollection(user: string, collection: string): Promise<void> {
|
||||
await this.ensureConnected();
|
||||
await this.graph.query(
|
||||
const deleteCollection = async (user: string, collection: string): Promise<void> => {
|
||||
await ensureConnected();
|
||||
await graph.query(
|
||||
"MATCH (n:Node {user: $user, collection: $collection}) DETACH DELETE n",
|
||||
{ params: { user, collection } },
|
||||
);
|
||||
await this.graph.query(
|
||||
await graph.query(
|
||||
"MATCH (n:Literal {user: $user, collection: $collection}) DETACH DELETE n",
|
||||
{ params: { user, collection } },
|
||||
);
|
||||
await this.graph.query(
|
||||
await graph.query(
|
||||
"MATCH (c:CollectionMetadata {user: $user, collection: $collection}) DELETE c",
|
||||
{ params: { user, collection } },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
createNode,
|
||||
createLiteral,
|
||||
relateNode,
|
||||
relateLiteral,
|
||||
storeTriples,
|
||||
deleteCollection,
|
||||
};
|
||||
}
|
||||
|
||||
export class FalkorDBTriplesStoreError extends S.TaggedErrorClass<FalkorDBTriplesStoreError>()(
|
||||
|
|
@ -171,7 +200,7 @@ const falkorDBTriplesStoreError = (operation: string, cause: unknown) =>
|
|||
export const makeFalkorDBTriplesStoreService = (
|
||||
config: FalkorDBConfig = {},
|
||||
): FalkorDBTriplesStoreServiceShape => {
|
||||
const store = new FalkorDBTriplesStore(config);
|
||||
const store = makeFalkorDBTriplesStore(config);
|
||||
return {
|
||||
storeTriples: Effect.fn("FalkorDBTriplesStore.storeTriples")((
|
||||
triples: ReadonlyArray<Triple>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue