Remove RAG requestor Promise bridges

This commit is contained in:
elpresidank 2026-06-02 00:54:47 -05:00
parent 88db18fbda
commit 5979d38b99
11 changed files with 249 additions and 293 deletions

View file

@ -12,16 +12,16 @@ Verified source roots:
- Effect v4 subtree: `/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4` - Effect v4 subtree: `/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4`
- Installed Effect beta used by this workspace: `ts/node_modules/effect` - Installed Effect beta used by this workspace: `ts/node_modules/effect`
Current signal counts from `ts/packages` after the 2026-06-02 config service Current signal counts from `ts/packages` after the 2026-06-02 RAG and agent
runtime slice: requestor bridge slice:
| Signal | Count | | Signal | Count |
| --- | ---: | | --- | ---: |
| `Effect.runPromise` | 207 | | `Effect.runPromise` | 198 |
| `Map<` | 65 | | `Map<` | 65 |
| `WebSocket` | 51 | | `WebSocket` | 51 |
| `new Map` | 47 | | `new Map` | 47 |
| `toPromiseRequestor` | 19 | | `toPromiseRequestor` | 0 |
| `makeAsyncProcessor` | 19 | | `makeAsyncProcessor` | 19 |
| `receive(` | 18 | | `receive(` | 18 |
| `while (` | 12 | | `while (` | 12 |
@ -133,6 +133,36 @@ Notes:
- `cd ts && bun run test` - `cd ts && bun run test`
- `git diff --check` - `git diff --check`
### 2026-06-02: RAG And Agent Requestor Bridge Slice
- Status: migrated, root-verified, and ready to commit.
- Completed:
- `ts/packages/flow/src/retrieval/graph-rag.ts` and
`ts/packages/flow/src/retrieval/document-rag.ts` now accept
`EffectRequestResponse` clients directly. The engines no longer adapt
Effect requestors back to Promise requestors and then wrap those calls in
`Effect.tryPromise`.
- `ts/packages/flow/src/retrieval/graph-rag-service.ts` and
`ts/packages/flow/src/retrieval/document-rag-service.ts` now pass native
flow requestors directly into the engines.
- `ts/packages/flow/src/agent/react/tools.ts` now accepts
`EffectRequestResponse` clients directly for graph RAG, document RAG,
triples, and MCP tool calls. Tool input narrowing uses Schema and
`effect/Predicate` rather than local request/response type assertions.
- `ts/packages/flow/src/agent/react/service.ts` wires default and configured
tools with native Effect requestors instead of `toPromiseRequestor`.
- Graph RAG, document RAG, and agent service startup now expose `runMain()`
through `NodeRuntime.runMain`; their legacy `run()` Promise facades use
`ManagedRuntime`.
- `ts/scripts/run-graph-rag.ts`, `ts/scripts/run-document-rag.ts`, and
`ts/scripts/run-agent.ts` now delegate to `runMain()`.
- Verification:
- `bun run --cwd ts/packages/flow build`
- `bun run --cwd ts/packages/flow test`
- `cd ts && bun run check`
- `cd ts && bun run build`
- `cd ts && bun run test`
## Subagent Findings To Preserve ## Subagent Findings To Preserve
- MCP/workbench: - MCP/workbench:
@ -163,8 +193,8 @@ Notes:
- WebSocket adapter shims still contain host-boundary `try`/`catch` and - WebSocket adapter shims still contain host-boundary `try`/`catch` and
normal `Error` construction. normal `Error` construction.
- RAG/providers/storage: - RAG/providers/storage:
- RAG and agent helpers still adapt Effect requestors back to Promise - RAG and agent requestor bridges are complete: `toPromiseRequestor` has no
requestors. remaining `ts/packages` matches.
- Provider SDKs and storage clients should become managed resources where - Provider SDKs and storage clients should become managed resources where
they have meaningful lifecycle. they have meaningful lifecycle.
- FalkorDB/Qdrant/Ollama/OpenAI-compatible surfaces still need config, - FalkorDB/Qdrant/Ollama/OpenAI-compatible surfaces still need config,
@ -194,24 +224,6 @@ Notes:
- Tests: - Tests:
- Service-specific tests plus `cd ts && bun run --cwd packages/flow test`. - Service-specific tests plus `cd ts && bun run --cwd packages/flow test`.
### P0: Remove RAG And Agent `toPromiseRequestor` Bridges
- TrustGraph evidence:
- `ts/packages/flow/src/retrieval/document-rag-service.ts`
- `ts/packages/flow/src/retrieval/graph-rag-service.ts`
- `ts/packages/flow/src/agent/react/service.ts`
- Effect primitives:
- Existing `EffectRequestResponse`, `Effect.fn`, `Stream`,
`Effect.runPromiseWith` at true external boundaries only.
- Rewrite shape:
- Update engines and agent helpers to accept Effect requestors or
Effect-returning functions directly.
- Preserve Promise wrappers only for old public APIs or tests that explicitly
verify compatibility.
- Tests:
- Existing RAG and agent service tests.
- Add tests that assert requestor errors stay typed through the Effect path.
### P1: Finish Client RPC Boundary Modernization ### P1: Finish Client RPC Boundary Modernization
- TrustGraph evidence: - TrustGraph evidence:
@ -302,13 +314,12 @@ Notes:
## Recommended PR Order ## Recommended PR Order
1. RAG and agent requestor bridge removal. 1. Librarian, cores, or flow-manager scoped state migration.
2. Librarian, cores, or flow-manager scoped state migration. 2. Client RPC managed runtime/scoped layer cleanup.
3. Client RPC managed runtime/scoped layer cleanup. 3. Base processor registry and constructor shim redesign.
4. Base processor registry and constructor shim redesign. 4. Gateway RPC callback and client streaming completion cleanup.
5. Gateway RPC callback and client streaming completion cleanup. 5. Storage/provider managed resource cleanup.
6. Storage/provider managed resource cleanup. 6. MCP parity/deletion decision and workbench platform polish.
7. MCP parity/deletion decision and workbench platform polish.
## No-Op Rules ## No-Op Rules

View file

@ -5,7 +5,7 @@ import type {
DocumentEmbeddingsResponse, DocumentEmbeddingsResponse,
EmbeddingsRequest, EmbeddingsRequest,
EmbeddingsResponse, EmbeddingsResponse,
FlowRequestor, EffectRequestResponse,
GraphEmbeddingsRequest, GraphEmbeddingsRequest,
GraphEmbeddingsResponse, GraphEmbeddingsResponse,
PromptRequest, PromptRequest,
@ -19,10 +19,10 @@ import { makeDocumentRagEngine, type DocumentRagClients } from "../retrieval/doc
import { makeGraphRagEngine, type GraphRagClients } from "../retrieval/graph-rag.js"; import { makeGraphRagEngine, type GraphRagClients } from "../retrieval/graph-rag.js";
const requestor = <TReq, TRes>( const requestor = <TReq, TRes>(
handler: (request: TReq) => TRes | Promise<TRes>, handler: (request: TReq) => TRes,
): FlowRequestor<TReq, TRes> => ({ ): EffectRequestResponse<TReq, TRes> => ({
request: async (request) => handler(request), request: (request) => Effect.succeed(handler(request)),
stop: async () => undefined, stop: Effect.void,
}); });
describe("RAG engines", () => { describe("RAG engines", () => {

View file

@ -16,6 +16,9 @@
* Python reference: trustgraph-flow/trustgraph/agent/react/service.py * Python reference: trustgraph-flow/trustgraph/agent/react/service.py
*/ */
import {
NodeRuntime,
} from "@effect/platform-node";
import { import {
makeFlowProcessor, makeFlowProcessor,
makeConsumerSpec, makeConsumerSpec,
@ -39,16 +42,13 @@ import {
type ToolRequest, type ToolRequest,
type ToolResponse, type ToolResponse,
type EffectConfigHandler, type EffectConfigHandler,
type EffectRequestOptions,
type EffectRequestResponse,
type FlowRequestOptions,
type FlowRequestor,
type FlowResourceNotFoundError, type FlowResourceNotFoundError,
type MessagingDeliveryError, type MessagingDeliveryError,
type Spec, type Spec,
} from "@trustgraph/base"; } from "@trustgraph/base";
import { Context, Effect, Layer, Ref } from "effect"; import {Context, Effect, Layer, ManagedRuntime, Ref} from "effect";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as Predicate from "effect/Predicate";
import * as S from "effect/Schema"; import * as S from "effect/Schema";
import { import {
@ -106,29 +106,6 @@ export class AgentRuntime extends Context.Service<AgentRuntime, AgentRuntimeServ
"@trustgraph/flow/agent/react/service/AgentRuntime", "@trustgraph/flow/agent/react/service/AgentRuntime",
) {} ) {}
const toEffectRequestOptions = <TRes>(
options: FlowRequestOptions<TRes> | undefined,
): EffectRequestOptions<TRes> | undefined => {
if (options === undefined) return undefined;
return {
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
...(options.recipient === undefined
? {}
: {
recipient: (response: TRes) =>
Effect.promise(() => options.recipient?.(response) ?? Promise.resolve(true)),
}),
};
};
const toPromiseRequestor = <TReq, TRes>(
requestor: EffectRequestResponse<TReq, TRes>,
): FlowRequestor<TReq, TRes> => ({
request: (request, options) =>
Effect.runPromise(requestor.request(request, toEffectRequestOptions(options))),
stop: () => Effect.runPromise(requestor.stop),
});
const buildConfiguredTool = ( const buildConfiguredTool = (
toolId: string, toolId: string,
data: ToolConfigEntry, data: ToolConfigEntry,
@ -137,7 +114,7 @@ const buildConfiguredTool = (
const implType = data.type ?? ""; const implType = data.type ?? "";
const name = data.name ?? ""; const name = data.name ?? "";
const description = data.description ?? ""; const description = data.description ?? "";
const config = { ...data } as Record<string, unknown>; const config: Record<string, unknown> = { ...data };
if (name.length === 0) { if (name.length === 0) {
yield* Effect.logWarning(`[AgentService] Skipping tool with no name: ${toolId}`); yield* Effect.logWarning(`[AgentService] Skipping tool with no name: ${toolId}`);
@ -277,12 +254,13 @@ const wireTools = Effect.fn("AgentService.wireTools")(function* (
const mcpTool = yield* flowCtx.flow.requestorEffect<ToolRequest, ToolResponse>("mcp-tool"); const mcpTool = yield* flowCtx.flow.requestorEffect<ToolRequest, ToolResponse>("mcp-tool");
return tools.map((tool) => { return tools.map((tool) => {
const implType = tool.config?.type as string | undefined; const rawImplType = tool.config?.type;
const implType = Predicate.isString(rawImplType) ? rawImplType : undefined;
switch (implType) { switch (implType) {
case "knowledge-query": { case "knowledge-query": {
const live = createKnowledgeQueryTool( const live = createKnowledgeQueryTool(
toPromiseRequestor(graphRag), graphRag,
collection, collection,
onExplain, onExplain,
); );
@ -290,21 +268,21 @@ const wireTools = Effect.fn("AgentService.wireTools")(function* (
} }
case "document-query": { case "document-query": {
const live = createDocumentQueryTool( const live = createDocumentQueryTool(
toPromiseRequestor(docRag), docRag,
collection, collection,
); );
return { ...tool, execute: live.execute }; return { ...tool, execute: live.execute };
} }
case "triples-query": { case "triples-query": {
const live = createTriplesQueryTool( const live = createTriplesQueryTool(
toPromiseRequestor(triples), triples,
collection, collection,
); );
return { ...tool, execute: live.execute }; return { ...tool, execute: live.execute };
} }
case "mcp-tool": { case "mcp-tool": {
const live = createMcpTool( const live = createMcpTool(
toPromiseRequestor(mcpTool), mcpTool,
tool.name, tool.name,
tool.description, tool.description,
tool.args, tool.args,
@ -328,16 +306,16 @@ const defaultTools = Effect.fn("AgentService.defaultTools")(function* (
return [ return [
createKnowledgeQueryTool( createKnowledgeQueryTool(
toPromiseRequestor(graphRag), graphRag,
collection, collection,
onExplain, onExplain,
), ),
createDocumentQueryTool( createDocumentQueryTool(
toPromiseRequestor(docRag), docRag,
collection, collection,
), ),
createTriplesQueryTool( createTriplesQueryTool(
toPromiseRequestor(triples), triples,
collection, collection,
), ),
]; ];
@ -433,7 +411,7 @@ const onAgentRequest = Effect.fn("AgentService.onRequest")(function* (
content: "", content: "",
explain_id: explain.explainId, explain_id: explain.explainId,
explain_triples: explain.triples, explain_triples: explain.triples,
} as AgentResponse); });
} }
yield* responseProducer.send(requestId, { yield* responseProducer.send(requestId, {
@ -630,6 +608,12 @@ export const program = makeFlowProcessorProgram<ProcessorConfig, never, AgentRun
layer: () => AgentRuntimeLive, layer: () => AgentRuntimeLive,
}); });
const agentRuntime = ManagedRuntime.make(Layer.empty);
export function run(): Promise<void> { export function run(): Promise<void> {
return Effect.runPromise(program); return agentRuntime.runPromise(program);
}
export function runMain(): void {
NodeRuntime.runMain(program);
} }

View file

@ -6,7 +6,7 @@
*/ */
import type { import type {
FlowRequestor, EffectRequestResponse,
GraphRagRequest, GraphRagRequest,
GraphRagResponse, GraphRagResponse,
DocumentRagRequest, DocumentRagRequest,
@ -18,13 +18,16 @@ import type {
Term, Term,
Triple, Triple,
} from "@trustgraph/base"; } from "@trustgraph/base";
import {Term as TermSchema} from "@trustgraph/base";
import { Effect } from "effect"; import { Effect } from "effect";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as Predicate from "effect/Predicate";
import * as S from "effect/Schema"; import * as S from "effect/Schema";
import type { AgentTool, ToolArg } from "./types.js"; import type { AgentTool, ToolArg } from "./types.js";
const decodeJsonUnknown = S.decodeUnknownOption(S.UnknownFromJsonString); const decodeJsonUnknown = S.decodeUnknownOption(S.UnknownFromJsonString);
const decodeTerm = S.decodeUnknownOption(TermSchema);
/** /**
* Format a Term to a human-readable string. * Format a Term to a human-readable string.
@ -71,7 +74,7 @@ export interface ExplainData {
* Query the knowledge graph for information about entities and their relationships. * Query the knowledge graph for information about entities and their relationships.
*/ */
export function createKnowledgeQueryTool( export function createKnowledgeQueryTool(
client: FlowRequestor<GraphRagRequest, GraphRagResponse>, client: EffectRequestResponse<GraphRagRequest, GraphRagResponse>,
collection?: string, collection?: string,
onExplain?: (data: ExplainData) => void, onExplain?: (data: ExplainData) => void,
): AgentTool { ): AgentTool {
@ -93,19 +96,14 @@ export function createKnowledgeQueryTool(
query: question, query: question,
...(collection !== undefined ? { collection } : {}), ...(collection !== undefined ? { collection } : {}),
}; };
const res = yield* Effect.tryPromise(() => client.request(request)); const res = yield* client.request(request);
yield* Effect.log(`[KnowledgeQuery] Response (${res.response?.length ?? 0} chars): ${res.error !== undefined ? `ERROR: ${res.error.message}` : `${res.response?.slice(0, 300)}...`}`); yield* Effect.log(`[KnowledgeQuery] Response (${res.response?.length ?? 0} chars): ${res.error !== undefined ? `ERROR: ${res.error.message}` : `${res.response?.slice(0, 300)}...`}`);
// Extract explain data if embedded in the response const explainTriples = res.explain_triples;
const rawRes = res as Record<string, unknown>; if (res.message_type === "explain" && explainTriples !== undefined && onExplain !== undefined) {
if (
rawRes.message_type === "explain" &&
rawRes.explain_triples !== undefined &&
onExplain !== undefined
) {
yield* Effect.sync(() => onExplain({ yield* Effect.sync(() => onExplain({
explainId: (rawRes.explain_id as string) ?? "", explainId: res.explain_id ?? "",
triples: rawRes.explain_triples as Triple[], triples: Array.from(explainTriples),
})); }));
} }
@ -119,7 +117,7 @@ export function createKnowledgeQueryTool(
* Search documents for relevant information. * Search documents for relevant information.
*/ */
export function createDocumentQueryTool( export function createDocumentQueryTool(
client: FlowRequestor<DocumentRagRequest, DocumentRagResponse>, client: EffectRequestResponse<DocumentRagRequest, DocumentRagResponse>,
collection?: string, collection?: string,
): AgentTool { ): AgentTool {
return { return {
@ -139,13 +137,24 @@ export function createDocumentQueryTool(
query: question, query: question,
...(collection !== undefined ? { collection } : {}), ...(collection !== undefined ? { collection } : {}),
}; };
const res = yield* Effect.tryPromise(() => client.request(request)); const res = yield* client.request(request);
if (res.error !== undefined) return `Error: ${res.error.message}`; if (res.error !== undefined) return `Error: ${res.error.message}`;
return res.response; return res.response;
})), })),
}; };
} }
const objectProperty = (value: object, key: string): unknown =>
Predicate.hasProperty(value, key) ? value[key] : undefined;
const termFromUnknown = (value: unknown): Term | undefined => {
if (Predicate.isString(value)) {
return { type: "LITERAL", value };
}
const decoded = decodeTerm(value);
return O.isSome(decoded) ? decoded.value : undefined;
};
/** /**
* Parse triples query input. Accepts JSON with optional s, p, o fields. * Parse triples query input. Accepts JSON with optional s, p, o fields.
*/ */
@ -166,30 +175,21 @@ function parseTriplesInput(input: string): {
}; };
} }
const parsed = decoded.value as Record<string, unknown>;
const toTerm = (val: unknown): Term | undefined => {
if (typeof val === "string") {
return { type: "LITERAL", value: val };
}
if (typeof val === "object" && val !== null && "type" in val) {
return val as Term;
}
return undefined;
};
const result: { const result: {
s?: Term; s?: Term;
p?: Term; p?: Term;
o?: Term; o?: Term;
limit?: number; limit?: number;
} = {}; } = {};
const s = toTerm(parsed.subject ?? parsed.s); const parsed = decoded.value;
const p = toTerm(parsed.predicate ?? parsed.p); const s = termFromUnknown(objectProperty(parsed, "subject") ?? objectProperty(parsed, "s"));
const o = toTerm(parsed.object ?? parsed.o); const p = termFromUnknown(objectProperty(parsed, "predicate") ?? objectProperty(parsed, "p"));
const o = termFromUnknown(objectProperty(parsed, "object") ?? objectProperty(parsed, "o"));
const limit = objectProperty(parsed, "limit");
if (s !== undefined) result.s = s; if (s !== undefined) result.s = s;
if (p !== undefined) result.p = p; if (p !== undefined) result.p = p;
if (o !== undefined) result.o = o; if (o !== undefined) result.o = o;
if (typeof parsed.limit === "number") result.limit = parsed.limit; if (Predicate.isNumber(limit)) result.limit = limit;
return result; return result;
} }
@ -197,7 +197,7 @@ function parseTriplesInput(input: string): {
* Query for specific triples (subject-predicate-object relationships) in the knowledge graph. * Query for specific triples (subject-predicate-object relationships) in the knowledge graph.
*/ */
export function createTriplesQueryTool( export function createTriplesQueryTool(
client: FlowRequestor<TriplesQueryRequest, TriplesQueryResponse>, client: EffectRequestResponse<TriplesQueryRequest, TriplesQueryResponse>,
collection?: string, collection?: string,
): AgentTool { ): AgentTool {
return { return {
@ -231,7 +231,7 @@ export function createTriplesQueryTool(
...(o !== undefined ? { o } : {}), ...(o !== undefined ? { o } : {}),
...(collection !== undefined ? { collection } : {}), ...(collection !== undefined ? { collection } : {}),
}; };
const res = yield* Effect.tryPromise(() => client.request(request)); const res = yield* client.request(request);
if (res.error !== undefined) return `Error: ${res.error.message}`; if (res.error !== undefined) return `Error: ${res.error.message}`;
@ -255,7 +255,7 @@ export function createTriplesQueryTool(
* this function just wraps it as an AgentTool the ReAct agent can invoke. * this function just wraps it as an AgentTool the ReAct agent can invoke.
*/ */
export function createMcpTool( export function createMcpTool(
client: FlowRequestor<ToolRequest, ToolResponse>, client: EffectRequestResponse<ToolRequest, ToolResponse>,
toolName: string, toolName: string,
description: string, description: string,
args: ToolArg[], args: ToolArg[],
@ -265,7 +265,7 @@ export function createMcpTool(
description, description,
args, args,
execute: (input: string): Promise<string> => Effect.runPromise(Effect.gen(function* () { execute: (input: string): Promise<string> => Effect.runPromise(Effect.gen(function* () {
const res = yield* Effect.tryPromise(() => client.request({ name: toolName, parameters: input })); const res = yield* client.request({ name: toolName, parameters: input });
if (res.error !== undefined) return `Error: ${res.error.message}`; if (res.error !== undefined) return `Error: ${res.error.message}`;
if (res.text !== undefined) return res.text; if (res.text !== undefined) return res.text;
if (res.object !== undefined) return res.object; if (res.object !== undefined) return res.object;

View file

@ -7,6 +7,7 @@
* Python reference: trustgraph-flow/trustgraph/retrieval/document_rag/ * Python reference: trustgraph-flow/trustgraph/retrieval/document_rag/
*/ */
import {NodeRuntime} from "@effect/platform-node";
import { import {
makeConsumerSpec, makeConsumerSpec,
makeFlowProcessor, makeFlowProcessor,
@ -17,14 +18,10 @@ import {
type DocumentEmbeddingsResponse, type DocumentEmbeddingsResponse,
type DocumentRagRequest, type DocumentRagRequest,
type DocumentRagResponse, type DocumentRagResponse,
type EffectRequestOptions,
type EffectRequestResponse,
type EmbeddingsRequest, type EmbeddingsRequest,
type EmbeddingsResponse, type EmbeddingsResponse,
type FlowContext, type FlowContext,
type FlowProcessorRuntime, type FlowProcessorRuntime,
type FlowRequestOptions,
type FlowRequestor,
type FlowResourceNotFoundError, type FlowResourceNotFoundError,
type MessagingDeliveryError, type MessagingDeliveryError,
type ProcessorConfig, type ProcessorConfig,
@ -34,7 +31,7 @@ import {
type TextCompletionRequest, type TextCompletionRequest,
type TextCompletionResponse, type TextCompletionResponse,
} from "@trustgraph/base"; } from "@trustgraph/base";
import { Effect } from "effect"; import {Effect, Layer, ManagedRuntime} from "effect";
import { import {
DocumentRagEngine, DocumentRagEngine,
DocumentRagEngineError, DocumentRagEngineError,
@ -43,29 +40,6 @@ import {
type DocumentRagClients, type DocumentRagClients,
} from "./document-rag.js"; } from "./document-rag.js";
const toEffectRequestOptions = <TRes>(
options: FlowRequestOptions<TRes> | undefined,
): EffectRequestOptions<TRes> | undefined => {
if (options === undefined) return undefined;
return {
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
...(options.recipient === undefined
? {}
: {
recipient: (response: TRes) =>
Effect.promise(() => options.recipient?.(response) ?? Promise.resolve(true)),
}),
};
};
const toPromiseRequestor = <TReq, TRes>(
requestor: EffectRequestResponse<TReq, TRes>,
): FlowRequestor<TReq, TRes> => ({
request: (request, options) =>
Effect.runPromise(requestor.request(request, toEffectRequestOptions(options))),
stop: () => Effect.runPromise(requestor.stop),
});
const onDocumentRagRequest = Effect.fn("DocumentRagService.onRequest")(function* ( const onDocumentRagRequest = Effect.fn("DocumentRagService.onRequest")(function* (
msg: DocumentRagRequest, msg: DocumentRagRequest,
properties: Record<string, string>, properties: Record<string, string>,
@ -78,12 +52,10 @@ const onDocumentRagRequest = Effect.fn("DocumentRagService.onRequest")(function*
const engine = yield* DocumentRagEngine; const engine = yield* DocumentRagEngine;
const clients: DocumentRagClients = { const clients: DocumentRagClients = {
llm: toPromiseRequestor(yield* flowCtx.flow.requestorEffect<TextCompletionRequest, TextCompletionResponse>("llm")), llm: yield* flowCtx.flow.requestorEffect<TextCompletionRequest, TextCompletionResponse>("llm"),
embeddings: toPromiseRequestor(yield* flowCtx.flow.requestorEffect<EmbeddingsRequest, EmbeddingsResponse>("embeddings")), embeddings: yield* flowCtx.flow.requestorEffect<EmbeddingsRequest, EmbeddingsResponse>("embeddings"),
docEmbeddings: toPromiseRequestor( docEmbeddings: yield* flowCtx.flow.requestorEffect<DocumentEmbeddingsRequest, DocumentEmbeddingsResponse>("doc-embeddings"),
yield* flowCtx.flow.requestorEffect<DocumentEmbeddingsRequest, DocumentEmbeddingsResponse>("doc-embeddings"), prompt: yield* flowCtx.flow.requestorEffect<PromptRequest, PromptResponse>("prompt"),
),
prompt: toPromiseRequestor(yield* flowCtx.flow.requestorEffect<PromptRequest, PromptResponse>("prompt")),
}; };
const response = yield* engine.query( const response = yield* engine.query(
@ -161,6 +133,12 @@ export const program = makeFlowProcessorProgram({
layer: () => DocumentRagLive, layer: () => DocumentRagLive,
}); });
const documentRagRuntime = ManagedRuntime.make(Layer.empty);
export function run(): Promise<void> { export function run(): Promise<void> {
return Effect.runPromise(program); return documentRagRuntime.runPromise(program);
}
export function runMain(): void {
NodeRuntime.runMain(program);
} }

View file

@ -9,7 +9,7 @@ import type {
DocumentEmbeddingsResponse, DocumentEmbeddingsResponse,
EmbeddingsRequest, EmbeddingsRequest,
EmbeddingsResponse, EmbeddingsResponse,
FlowRequestor, EffectRequestResponse,
PromptRequest, PromptRequest,
PromptResponse, PromptResponse,
TextCompletionRequest, TextCompletionRequest,
@ -20,10 +20,10 @@ import { Context, Effect, Layer } from "effect";
import * as S from "effect/Schema"; import * as S from "effect/Schema";
export interface DocumentRagClients { export interface DocumentRagClients {
llm: FlowRequestor<TextCompletionRequest, TextCompletionResponse>; llm: EffectRequestResponse<TextCompletionRequest, TextCompletionResponse>;
embeddings: FlowRequestor<EmbeddingsRequest, EmbeddingsResponse>; embeddings: EffectRequestResponse<EmbeddingsRequest, EmbeddingsResponse>;
docEmbeddings: FlowRequestor<DocumentEmbeddingsRequest, DocumentEmbeddingsResponse>; docEmbeddings: EffectRequestResponse<DocumentEmbeddingsRequest, DocumentEmbeddingsResponse>;
prompt: FlowRequestor<PromptRequest, PromptResponse>; prompt: EffectRequestResponse<PromptRequest, PromptResponse>;
} }
export type ChunkCallback = (text: string, endOfStream: boolean) => Promise<void>; export type ChunkCallback = (text: string, endOfStream: boolean) => Promise<void>;
@ -101,21 +101,19 @@ function queryDocumentRag(
return Effect.gen(function* () { return Effect.gen(function* () {
const collection = options?.collection ?? "default"; const collection = options?.collection ?? "default";
const embResp = yield* Effect.tryPromise({ const embResp = yield* clients.embeddings.request({ text: [queryText] }).pipe(
try: () => clients.embeddings.request({ text: [queryText] }), Effect.mapError((cause) => documentRagError("embeddings", cause)),
catch: (cause) => documentRagError("embeddings", cause), );
});
const vectors = embResp.vectors; const vectors = embResp.vectors;
const docResp = yield* Effect.tryPromise({ const docResp = yield* clients.docEmbeddings.request({
try: () => clients.docEmbeddings.request({
vectors, vectors,
limit: 10, limit: 10,
collection, collection,
user: "default", user: "default",
}), }).pipe(
catch: (cause) => documentRagError("document-embeddings", cause), Effect.mapError((cause) => documentRagError("document-embeddings", cause)),
}); );
const chunks = docResp.chunks ?? []; const chunks = docResp.chunks ?? [];
yield* Effect.log(`[DocumentRag] Found ${chunks.length} matching chunks`); yield* Effect.log(`[DocumentRag] Found ${chunks.length} matching chunks`);
@ -125,21 +123,19 @@ function queryDocumentRag(
) )
.join("\n\n---\n\n"); .join("\n\n---\n\n");
const promptResp = yield* Effect.tryPromise({ const promptResp = yield* clients.prompt.request({
try: () => clients.prompt.request({
name: "document-rag-synthesize", name: "document-rag-synthesize",
variables: { query: queryText, context }, variables: { query: queryText, context },
}), }).pipe(
catch: (cause) => documentRagError("prompt", cause), Effect.mapError((cause) => documentRagError("prompt", cause)),
}); );
const resp = yield* Effect.tryPromise({ const resp = yield* clients.llm.request({
try: () => clients.llm.request({
system: promptResp.system, system: promptResp.system,
prompt: promptResp.prompt, prompt: promptResp.prompt,
}), }).pipe(
catch: (cause) => documentRagError("llm", cause), Effect.mapError((cause) => documentRagError("llm", cause)),
}); );
return resp.response; return resp.response;
}); });

View file

@ -7,18 +7,15 @@
* Python reference: trustgraph-flow/trustgraph/retrieval/graph_rag/rag.py * Python reference: trustgraph-flow/trustgraph/retrieval/graph_rag/rag.py
*/ */
import {NodeRuntime} from "@effect/platform-node";
import { import {
makeConsumerSpec, makeConsumerSpec,
makeFlowProcessor, makeFlowProcessor,
makeProducerSpec, makeProducerSpec,
makeRequestResponseSpec, makeRequestResponseSpec,
makeFlowProcessorProgram, makeFlowProcessorProgram,
type EffectRequestOptions,
type EffectRequestResponse,
type FlowContext, type FlowContext,
type FlowProcessorRuntime, type FlowProcessorRuntime,
type FlowRequestOptions,
type FlowRequestor,
type FlowResourceNotFoundError, type FlowResourceNotFoundError,
type GraphEmbeddingsRequest, type GraphEmbeddingsRequest,
type GraphEmbeddingsResponse, type GraphEmbeddingsResponse,
@ -36,7 +33,7 @@ import {
type TriplesQueryRequest, type TriplesQueryRequest,
type TriplesQueryResponse, type TriplesQueryResponse,
} from "@trustgraph/base"; } from "@trustgraph/base";
import { Effect } from "effect"; import {Effect, Layer, ManagedRuntime} from "effect";
import { import {
GraphRagEngine, GraphRagEngine,
GraphRagEngineError, GraphRagEngineError,
@ -46,29 +43,6 @@ import {
type GraphRagConfig, type GraphRagConfig,
} from "./graph-rag.js"; } from "./graph-rag.js";
const toEffectRequestOptions = <TRes>(
options: FlowRequestOptions<TRes> | undefined,
): EffectRequestOptions<TRes> | undefined => {
if (options === undefined) return undefined;
return {
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
...(options.recipient === undefined
? {}
: {
recipient: (response: TRes) =>
Effect.promise(() => options.recipient?.(response) ?? Promise.resolve(true)),
}),
};
};
const toPromiseRequestor = <TReq, TRes>(
requestor: EffectRequestResponse<TReq, TRes>,
): FlowRequestor<TReq, TRes> => ({
request: (request, options) =>
Effect.runPromise(requestor.request(request, toEffectRequestOptions(options))),
stop: () => Effect.runPromise(requestor.stop),
});
const graphRagConfigFromRequest = (msg: GraphRagRequest): GraphRagConfig => ({ const graphRagConfigFromRequest = (msg: GraphRagRequest): GraphRagConfig => ({
...(msg.entityLimit !== undefined ? { entityLimit: msg.entityLimit } : {}), ...(msg.entityLimit !== undefined ? { entityLimit: msg.entityLimit } : {}),
...(msg.tripleLimit !== undefined ? { tripleLimit: msg.tripleLimit } : {}), ...(msg.tripleLimit !== undefined ? { tripleLimit: msg.tripleLimit } : {}),
@ -90,13 +64,11 @@ const onGraphRagRequest = Effect.fn("GraphRagService.onRequest")(function* (
yield* Effect.log(`[GraphRagService] Received request ${requestId}: "${msg.query?.slice(0, 60)}..." collection=${msg.collection}`); yield* Effect.log(`[GraphRagService] Received request ${requestId}: "${msg.query?.slice(0, 60)}..." collection=${msg.collection}`);
const clients: GraphRagClients = { const clients: GraphRagClients = {
llm: toPromiseRequestor(yield* flowCtx.flow.requestorEffect<TextCompletionRequest, TextCompletionResponse>("llm")), llm: yield* flowCtx.flow.requestorEffect<TextCompletionRequest, TextCompletionResponse>("llm"),
embeddings: toPromiseRequestor(yield* flowCtx.flow.requestorEffect<EmbeddingsRequest, EmbeddingsResponse>("embeddings")), embeddings: yield* flowCtx.flow.requestorEffect<EmbeddingsRequest, EmbeddingsResponse>("embeddings"),
graphEmbeddings: toPromiseRequestor( graphEmbeddings: yield* flowCtx.flow.requestorEffect<GraphEmbeddingsRequest, GraphEmbeddingsResponse>("graph-embeddings"),
yield* flowCtx.flow.requestorEffect<GraphEmbeddingsRequest, GraphEmbeddingsResponse>("graph-embeddings"), triples: yield* flowCtx.flow.requestorEffect<TriplesQueryRequest, TriplesQueryResponse>("triples"),
), prompt: yield* flowCtx.flow.requestorEffect<PromptRequest, PromptResponse>("prompt"),
triples: toPromiseRequestor(yield* flowCtx.flow.requestorEffect<TriplesQueryRequest, TriplesQueryResponse>("triples")),
prompt: toPromiseRequestor(yield* flowCtx.flow.requestorEffect<PromptRequest, PromptResponse>("prompt")),
}; };
const result = yield* engine.query( const result = yield* engine.query(
@ -125,16 +97,18 @@ const onGraphRagRequest = Effect.fn("GraphRagService.onRequest")(function* (
if (result === undefined) return; if (result === undefined) return;
const response: GraphRagResponse = { const response: GraphRagResponse = result.subgraph.length === 0
response: result.answer, ? {
endOfStream: true, response: result.answer,
}; endOfStream: true,
}
if (result.subgraph.length > 0) { : {
(response as Record<string, unknown>).message_type = "explain"; response: result.answer,
(response as Record<string, unknown>).explain_id = `explain-${requestId}`; endOfStream: true,
(response as Record<string, unknown>).explain_triples = result.subgraph; message_type: "explain",
} explain_id: `explain-${requestId}`,
explain_triples: result.subgraph,
};
yield* producer.send(requestId, response); yield* producer.send(requestId, response);
}); });
@ -192,6 +166,12 @@ export const program = makeFlowProcessorProgram({
layer: () => GraphRagLive, layer: () => GraphRagLive,
}); });
const graphRagRuntime = ManagedRuntime.make(Layer.empty);
export function run(): Promise<void> { export function run(): Promise<void> {
return Effect.runPromise(program); return graphRagRuntime.runPromise(program);
}
export function runMain(): void {
NodeRuntime.runMain(program);
} }

View file

@ -7,7 +7,8 @@
import type { import type {
EmbeddingsRequest, EmbeddingsRequest,
EmbeddingsResponse, EmbeddingsResponse,
FlowRequestor, EffectRequestOptions,
EffectRequestResponse,
GraphEmbeddingsRequest, GraphEmbeddingsRequest,
GraphEmbeddingsResponse, GraphEmbeddingsResponse,
PromptRequest, PromptRequest,
@ -34,11 +35,11 @@ export interface GraphRagConfig {
} }
export interface GraphRagClients { export interface GraphRagClients {
llm: FlowRequestor<TextCompletionRequest, TextCompletionResponse>; llm: EffectRequestResponse<TextCompletionRequest, TextCompletionResponse>;
embeddings: FlowRequestor<EmbeddingsRequest, EmbeddingsResponse>; embeddings: EffectRequestResponse<EmbeddingsRequest, EmbeddingsResponse>;
graphEmbeddings: FlowRequestor<GraphEmbeddingsRequest, GraphEmbeddingsResponse>; graphEmbeddings: EffectRequestResponse<GraphEmbeddingsRequest, GraphEmbeddingsResponse>;
triples: FlowRequestor<TriplesQueryRequest, TriplesQueryResponse>; triples: EffectRequestResponse<TriplesQueryRequest, TriplesQueryResponse>;
prompt: FlowRequestor<PromptRequest, PromptResponse>; prompt: EffectRequestResponse<PromptRequest, PromptResponse>;
} }
export type ChunkCallback = (text: string, endOfStream: boolean) => Promise<void>; export type ChunkCallback = (text: string, endOfStream: boolean) => Promise<void>;
@ -92,6 +93,16 @@ const graphRagError = (operation: string, cause: unknown) =>
message: errorMessage(cause), message: errorMessage(cause),
}); });
const requestClient = <TReq, TRes>(
requestor: EffectRequestResponse<TReq, TRes>,
operation: string,
request: TReq,
options?: EffectRequestOptions<TRes, GraphRagEngineError>,
): Effect.Effect<TRes, GraphRagEngineError> =>
requestor.request(request, options).pipe(
Effect.mapError((cause) => graphRagError(operation, cause)),
);
export function normalizeGraphRagConfig(config: GraphRagConfig = {}): NormalizedGraphRagConfig { export function normalizeGraphRagConfig(config: GraphRagConfig = {}): NormalizedGraphRagConfig {
return { return {
entityLimit: config.entityLimit ?? 50, entityLimit: config.entityLimit ?? 50,
@ -178,21 +189,23 @@ function queryGraphRag(
function extractConcepts(clients: GraphRagClients, query: string): Effect.Effect<string[], GraphRagEngineError> { function extractConcepts(clients: GraphRagClients, query: string): Effect.Effect<string[], GraphRagEngineError> {
return Effect.gen(function* () { return Effect.gen(function* () {
const promptResp = yield* Effect.tryPromise({ const promptResp = yield* requestClient(
try: () => clients.prompt.request({ clients.prompt,
"extract-concepts-prompt",
{
name: "extract-concepts", name: "extract-concepts",
variables: { query }, variables: { query },
}), },
catch: (cause) => graphRagError("extract-concepts-prompt", cause), );
});
const llmResp = yield* Effect.tryPromise({ const llmResp = yield* requestClient(
try: () => clients.llm.request({ clients.llm,
"extract-concepts-llm",
{
system: promptResp.system, system: promptResp.system,
prompt: promptResp.prompt, prompt: promptResp.prompt,
}), },
catch: (cause) => graphRagError("extract-concepts-llm", cause), );
});
return llmResp.response return llmResp.response
.split("\n") .split("\n")
@ -203,10 +216,7 @@ function extractConcepts(clients: GraphRagClients, query: string): Effect.Effect
function getVectors(clients: GraphRagClients, concepts: string[]): Effect.Effect<number[][], GraphRagEngineError> { function getVectors(clients: GraphRagClients, concepts: string[]): Effect.Effect<number[][], GraphRagEngineError> {
return Effect.gen(function* () { return Effect.gen(function* () {
const resp = yield* Effect.tryPromise({ const resp = yield* requestClient(clients.embeddings, "get-vectors", { text: concepts });
try: () => clients.embeddings.request({ text: concepts }),
catch: (cause) => graphRagError("get-vectors", cause),
});
return resp.vectors; return resp.vectors;
}); });
} }
@ -218,15 +228,16 @@ function getEntities(
collection?: string, collection?: string,
): Effect.Effect<Term[], GraphRagEngineError> { ): Effect.Effect<Term[], GraphRagEngineError> {
return Effect.gen(function* () { return Effect.gen(function* () {
const resp = yield* Effect.tryPromise({ const resp = yield* requestClient(
try: () => clients.graphEmbeddings.request({ clients.graphEmbeddings,
"get-entities",
{
vectors, vectors,
user: "default", user: "default",
collection: collection ?? "default", collection: collection ?? "default",
limit: config.entityLimit, limit: config.entityLimit,
}), },
catch: (cause) => graphRagError("get-entities", cause), );
});
return resp.entities; return resp.entities;
}); });
} }
@ -259,10 +270,7 @@ function followEdges(
limit: config.tripleLimit, limit: config.tripleLimit,
...(collection !== undefined ? { collection } : {}), ...(collection !== undefined ? { collection } : {}),
}; };
return Effect.tryPromise({ return requestClient(clients.triples, "follow-edges-query", request);
try: () => clients.triples.request(request),
catch: (cause) => graphRagError("follow-edges-query", cause),
});
}); });
const results = yield* Effect.all(queries); const results = yield* Effect.all(queries);
@ -321,24 +329,26 @@ function scoreEdges(
Effect.mapError((cause) => graphRagError("edge-score-encode", cause)), Effect.mapError((cause) => graphRagError("edge-score-encode", cause)),
); );
const promptResp = yield* Effect.tryPromise({ const promptResp = yield* requestClient(
try: () => clients.prompt.request({ clients.prompt,
"edge-score-prompt",
{
name: "kg-edge-scoring", name: "kg-edge-scoring",
variables: { variables: {
query, query,
knowledge: knowledgeJson, knowledge: knowledgeJson,
}, },
}), },
catch: (cause) => graphRagError("edge-score-prompt", cause), );
});
const llmResp = yield* Effect.tryPromise({ const llmResp = yield* requestClient(
try: () => clients.llm.request({ clients.llm,
"edge-score-llm",
{
system: promptResp.system, system: promptResp.system,
prompt: promptResp.prompt, prompt: promptResp.prompt,
}), },
catch: (cause) => graphRagError("edge-score-llm", cause), );
});
yield* Effect.log(`[GraphRag] Edge scoring LLM response (first 500 chars): ${llmResp.response.slice(0, 500)}`); yield* Effect.log(`[GraphRag] Edge scoring LLM response (first 500 chars): ${llmResp.response.slice(0, 500)}`);
@ -375,43 +385,49 @@ function synthesize(
.map((triple) => `${termToString(triple.s)} -> ${termToString(triple.p)} -> ${termToString(triple.o)}`) .map((triple) => `${termToString(triple.s)} -> ${termToString(triple.p)} -> ${termToString(triple.o)}`)
.join("\n"); .join("\n");
const promptResp = yield* Effect.tryPromise({ const promptResp = yield* requestClient(
try: () => clients.prompt.request({ clients.prompt,
"synthesize-prompt",
{
name: "graph-rag-synthesize", name: "graph-rag-synthesize",
variables: { query, context }, variables: { query, context },
}), },
catch: (cause) => graphRagError("synthesize-prompt", cause), );
});
if (chunkCallback !== undefined) { if (chunkCallback !== undefined) {
let fullText = ""; let fullText = "";
yield* Effect.tryPromise({ yield* requestClient(
try: () => clients.llm.request( clients.llm,
{ "synthesize-stream",
system: promptResp.system, {
prompt: promptResp.prompt, system: promptResp.system,
streaming: true, prompt: promptResp.prompt,
streaming: true,
},
{
recipient: (resp) => {
if (resp.response.length === 0) {
return Effect.succeed(resp.endOfStream === true);
}
fullText += resp.response;
return Effect.tryPromise({
try: () => chunkCallback(resp.response, resp.endOfStream === true).then(() => resp.endOfStream === true),
catch: (cause) => graphRagError("synthesize-stream-callback", cause),
});
}, },
{ },
recipient: (resp) => { );
if (resp.response.length === 0) return Promise.resolve(resp.endOfStream === true);
fullText += resp.response;
return chunkCallback(resp.response, resp.endOfStream === true).then(() => resp.endOfStream === true);
},
},
),
catch: (cause) => graphRagError("synthesize-stream", cause),
});
return fullText; return fullText;
} }
const resp = yield* Effect.tryPromise({ const resp = yield* requestClient(
try: () => clients.llm.request({ clients.llm,
"synthesize-llm",
{
system: promptResp.system, system: promptResp.system,
prompt: promptResp.prompt, prompt: promptResp.prompt,
}), },
catch: (cause) => graphRagError("synthesize-llm", cause), );
});
return resp.response; return resp.response;
}); });

View file

@ -6,9 +6,6 @@
* Env: * Env:
* NATS_URL (default: nats://localhost:4222) * NATS_URL (default: nats://localhost:4222)
*/ */
import { run } from "../packages/flow/src/agent/react/service.js"; import {runMain} from "../packages/flow/src/agent/react/service.js";
run().catch((err) => { runMain();
console.error("Agent service failed:", err);
process.exit(1);
});

View file

@ -1,6 +1,3 @@
import { run } from "../packages/flow/src/retrieval/document-rag-service.js"; import {runMain} from "../packages/flow/src/retrieval/document-rag-service.js";
run().catch((err) => { runMain();
console.error("Document RAG service failed:", err);
process.exit(1);
});

View file

@ -1,6 +1,3 @@
import { run } from "../packages/flow/src/retrieval/graph-rag-service.js"; import {runMain} from "../packages/flow/src/retrieval/graph-rag-service.js";
run().catch((err) => { runMain();
console.error("Graph RAG service failed:", err);
process.exit(1);
});