Add typed flow spec accessors

This commit is contained in:
elpresidank 2026-06-02 03:23:23 -05:00
parent abb6f3aed0
commit 44110c5bb4
19 changed files with 457 additions and 223 deletions

View file

@ -12,13 +12,13 @@ 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 Base parameter Current signal counts from `ts/packages` after the 2026-06-02 Base
spec accessor slice: producer/requestor spec accessor slice:
| Signal | Count | | Signal | Count |
| --- | ---: | | --- | ---: |
| `Effect.runPromise` | 168 | | `Effect.runPromise` | 168 |
| `Map<` | 82 | | `Map<` | 84 |
| `WebSocket` | 62 | | `WebSocket` | 62 |
| `new Map` | 62 | | `new Map` | 62 |
| `toPromiseRequestor` | 0 | | `toPromiseRequestor` | 0 |
@ -83,6 +83,11 @@ Notes:
`flow.parameter(spec)`. Bare string parameter lookup remains available as an `flow.parameter(spec)`. Bare string parameter lookup remains available as an
`unknown` compatibility escape, while typed parameter access now decodes `unknown` compatibility escape, while typed parameter access now decodes
through Schema and fails with a tagged `FlowParameterDecodeError`. through Schema and fails with a tagged `FlowParameterDecodeError`.
- The base producer/requestor spec accessor slice added typed spec-object
accessors for `ProducerSpec<T>` and `RequestResponseSpec<TReq, TRes>`, then
migrated flow service producer/requestor lookups off caller-chosen generic
string calls. Spec object handles are scoped per `Flow` through WeakMaps and
finalizers delete only the handle they registered.
- `Record<string, any>` and `throwLibrarianServiceError` are now clean in - `Record<string, any>` and `throwLibrarianServiceError` are now clean in
`ts/packages`. `ts/packages`.
@ -647,6 +652,42 @@ Notes:
- `cd ts && bun run test` - `cd ts && bun run test`
- `git diff --check` - `git diff --check`
### 2026-06-02: Base Producer And Requestor Spec Accessor Slice
- Status: migrated and root-verified.
- Completed:
- `ts/packages/base/src/spec/producer-spec.ts` now exposes
`ProducerSpec<T>.producerEffect(flow)` and stores typed producer handles in
a per-spec WeakMap keyed by `Flow`.
- `ts/packages/base/src/spec/request-response-spec.ts` now exposes
`RequestResponseSpec<TReq, TRes>.requestorEffect(flow)` and stores typed
requestor handles in a per-spec WeakMap keyed by `Flow`.
- Spec finalizers remove only the exact handle they registered, avoiding
stale finalizers deleting newer registrations for the same flow/spec pair.
- `ts/packages/base/src/processor/flow.ts` now supports
`flow.producerEffect(spec)`, `flow.requestorEffect(spec)`,
`flow.producer(spec)`, and `flow.requestor(spec)` while keeping string
accessors as untyped compatibility escapes.
- Base service adapters and flow service handlers now reuse the same hoisted
producer/requestor spec object in their spec arrays and handler lookups.
- `ts/packages/base/src/__tests__/flow-spec-runtime.test.ts` covers typed
spec-object lookups, duplicate spec identity failures, and scoped
finalizer cleanup for producer and requestor handles.
- Remaining:
- Bare string `Flow` producer/requestor accessors remain compatibility
escapes for external/legacy callers, but new Effect service code should use
spec objects.
- Verification:
- `bun run --cwd ts/packages/base test -- src/__tests__/flow-spec-runtime.test.ts`
- `bun run --cwd ts/packages/base build`
- `bun run --cwd ts/packages/flow build`
- `bun run --cwd ts/packages/base test`
- `bun run --cwd ts/packages/flow test`
- `cd ts && bun run check`
- `cd ts && bun run build`
- `cd ts && bun run test`
- `git diff --check`
## Subagent Findings To Preserve ## Subagent Findings To Preserve
- MCP/workbench: - MCP/workbench:
@ -673,10 +714,10 @@ Notes:
layers. layers.
- Existing constructor shims preserve callable-plus-newable public exports; - Existing constructor shims preserve callable-plus-newable public exports;
removing them needs a public API split or real class redesign. removing them needs a public API split or real class redesign.
- Typed string registries in `Flow` now have Schema-backed parameter specs. - Typed string registries in `Flow` now have Schema-backed parameter specs
Producer and requestor typed spec-object accessors remain. Effect and typed producer/requestor spec-object accessors. New service handlers
`HashMap`/`MutableHashMap` can improve lookup ergonomics with `Option`, but should hoist spec objects and use those accessors; bare string accessors
it does not remove the string-key type hole by itself. remain compatibility escapes.
- Gateway/client: - Gateway/client:
- `EffectRpcClient` now owns its socket/RPC layer with `ManagedRuntime`. - `EffectRpcClient` now owns its socket/RPC layer with `ManagedRuntime`.
Socket errors/JSON parsing now use tagged errors and Schema decoding. Socket errors/JSON parsing now use tagged errors and Schema decoding.
@ -695,26 +736,6 @@ Notes:
## Ranked Findings ## Ranked Findings
### P1: Base Typed Producer And Requestor Spec Accessors
- TrustGraph evidence:
- `ts/packages/base/src/processor/flow.ts`
- `ts/packages/base/src/spec/producer-spec.ts`
- `ts/packages/base/src/spec/request-response-spec.ts`
- Effect primitives:
- Typed spec-object registries, `Context`, `Layer`, `Effect.fn`, `Option`,
`Predicate`, `HashMap`/`MutableHashMap`.
- Rewrite shape:
- Parameter specs are now Schema-backed and support
`flow.parameterEffect(spec)` / `flow.parameter(spec)`.
- Add typed spec-object accessors for producers and requestors so call sites
stop spelling generic string lookups.
- Do not add assertions to quiet Effect channel inference problems.
- Tests:
- `cd ts && bun run --cwd packages/base test`
- Root `cd ts && bun run check` because this surface easily pollutes Effect
error and requirement channels.
### P1: Make SDK, Storage, And Provider Layers Managed Resources ### P1: Make SDK, Storage, And Provider Layers Managed Resources
- TrustGraph evidence: - TrustGraph evidence:
@ -765,10 +786,9 @@ Notes:
## Recommended PR Order ## Recommended PR Order
1. Complete base typed producer/requestor spec accessors. 1. Gateway RPC callback and client streaming completion cleanup.
2. Gateway RPC callback and client streaming completion cleanup. 2. Storage/provider managed resource cleanup.
3. Storage/provider managed resource cleanup. 3. MCP parity/deletion decision and workbench platform polish.
4. MCP parity/deletion decision and workbench platform polish.
## No-Op Rules ## No-Op Rules

View file

@ -153,12 +153,14 @@ describe("Effect-native flow specifications", () => {
"starts producer specs through Effect factories and exposes typed accessors", "starts producer specs through Effect factories and exposes typed accessors",
Effect.fnUntraced(function* () { Effect.fnUntraced(function* () {
const backend = new RuntimeBackend(new ScriptedConsumer<unknown>()); const backend = new RuntimeBackend(new ScriptedConsumer<unknown>());
const outputProducerSpec = makeProducerSpec<string>("output");
const duplicateOutputProducerSpec = makeProducerSpec<string>("output");
const flow = new Flow( const flow = new Flow(
"default", "default",
"processor", "processor",
backend, backend,
{ topics: { output: "actual-output" } }, { topics: { output: "actual-output" } },
[makeProducerSpec<string>("output")], [outputProducerSpec],
); );
yield* Effect.scoped( yield* Effect.scoped(
@ -166,17 +168,21 @@ describe("Effect-native flow specifications", () => {
backend, backend,
Effect.gen(function* () { Effect.gen(function* () {
yield* flow.startEffect(); yield* flow.startEffect();
const producer = yield* flow.producerEffect<string>("output"); const producer = yield* flow.producerEffect(outputProducerSpec);
const duplicateSpecError = yield* flow.producerEffect(duplicateOutputProducerSpec).pipe(Effect.flip);
expect(duplicateSpecError._tag).toBe("FlowResourceNotFoundError");
yield* producer.send("request-1", "hello"); yield* producer.send("request-1", "hello");
}), }),
), ),
); );
const closedProducerError = yield* flow.producerEffect(outputProducerSpec).pipe(Effect.flip);
expect(backend.producerOptions).toEqual({ topic: "actual-output" }); expect(backend.producerOptions).toEqual({ topic: "actual-output" });
expect(backend.producer.sent).toEqual([ expect(backend.producer.sent).toEqual([
{ message: "hello", properties: { id: "request-1" } }, { message: "hello", properties: { id: "request-1" } },
]); ]);
expect(backend.producer.closeCount).toBe(1); expect(backend.producer.closeCount).toBe(1);
expect(closedProducerError._tag).toBe("FlowResourceNotFoundError");
}), }),
); );
@ -229,6 +235,8 @@ describe("Effect-native flow specifications", () => {
responseConsumer.push(createMessage("response", { id: properties?.id ?? "" })); responseConsumer.push(createMessage("response", { id: properties?.id ?? "" }));
}, },
); );
const requestResponseSpec = makeRequestResponseSpec<string, string>("rr", "request", "response");
const duplicateRequestResponseSpec = makeRequestResponseSpec<string, string>("rr", "request", "response");
const flow = new Flow( const flow = new Flow(
"default", "default",
"processor", "processor",
@ -239,7 +247,7 @@ describe("Effect-native flow specifications", () => {
response: "actual-response", response: "actual-response",
}, },
}, },
[makeRequestResponseSpec<string, string>("rr", "request", "response")], [requestResponseSpec],
); );
const response = yield* Effect.scoped( const response = yield* Effect.scoped(
@ -247,7 +255,9 @@ describe("Effect-native flow specifications", () => {
backend, backend,
Effect.gen(function* () { Effect.gen(function* () {
yield* flow.startEffect(); yield* flow.startEffect();
const requestor = flow.requestor<string, string>("rr"); const duplicateSpecError = yield* flow.requestorEffect(duplicateRequestResponseSpec).pipe(Effect.flip);
expect(duplicateSpecError._tag).toBe("FlowResourceNotFoundError");
const requestor = flow.requestor(requestResponseSpec);
const fiber = yield* Effect.promise(() => const fiber = yield* Effect.promise(() =>
requestor.request("request", { timeoutMs: 250 }), requestor.request("request", { timeoutMs: 250 }),
).pipe(Effect.forkChild); ).pipe(Effect.forkChild);
@ -256,10 +266,12 @@ describe("Effect-native flow specifications", () => {
}), }),
), ),
); );
const closedRequestorError = yield* flow.requestorEffect(requestResponseSpec).pipe(Effect.flip);
expect(response).toBe("response"); expect(response).toBe("response");
expect(backend.producerOptions).toEqual({ topic: "actual-request" }); expect(backend.producerOptions).toEqual({ topic: "actual-request" });
expect(responseConsumer.acknowledged.length).toBe(1); expect(responseConsumer.acknowledged.length).toBe(1);
expect(closedRequestorError._tag).toBe("FlowResourceNotFoundError");
}), }),
); );
@ -282,7 +294,7 @@ describe("Effect-native flow specifications", () => {
backend, backend,
Effect.gen(function* () { Effect.gen(function* () {
yield* flow.startEffect(); yield* flow.startEffect();
const producerError = yield* flow.producerEffect<string>("missing-producer").pipe(Effect.flip); const producerError = yield* flow.producerEffect("missing-producer").pipe(Effect.flip);
const parameter = yield* flow.parameterEffect(presentParameter); const parameter = yield* flow.parameterEffect(presentParameter);
const legacyParameter = yield* flow.parameterEffect("present"); const legacyParameter = yield* flow.parameterEffect("present");
const parameterError = yield* flow.parameterEffect("missing-parameter").pipe(Effect.flip); const parameterError = yield* flow.parameterEffect("missing-parameter").pipe(Effect.flip);

View file

@ -30,6 +30,8 @@ import {
} from "../messaging/runtime.js"; } from "../messaging/runtime.js";
import { loadMessagingRuntimeConfig } from "../runtime/messaging-config.js"; import { loadMessagingRuntimeConfig } from "../runtime/messaging-config.js";
import type { ParameterSpec } from "../spec/parameter-spec.js"; import type { ParameterSpec } from "../spec/parameter-spec.js";
import type { ProducerSpec } from "../spec/producer-spec.js";
import type { RequestResponseSpec } from "../spec/request-response-spec.js";
import type { Spec, SpecRuntimeRequirements } from "../spec/types.js"; import type { Spec, SpecRuntimeRequirements } from "../spec/types.js";
export interface FlowDefinition { export interface FlowDefinition {
@ -131,6 +133,93 @@ export function makeFlow<Requirements = never>(
throw flowParameterDecodeError(name, spec.name, "Parameter value does not match schema"); throw flowParameterDecodeError(name, spec.name, "Parameter value does not match schema");
}; };
const getProducerEffect = (
producerName: string,
): Effect.Effect<EffectProducer<never>, FlowResourceNotFoundError> => {
const producer = producers.get(producerName);
return producer === undefined
? Effect.fail(flowResourceNotFoundError(name, "producer", producerName))
: Effect.succeed(producer);
};
const getProducer = (producerName: string): EffectProducer<never> => {
const producer = producers.get(producerName);
if (producer === undefined) throw flowResourceNotFoundError(name, "producer", producerName);
return producer;
};
const getRequestorEffect = (
requestorName: string,
): Effect.Effect<EffectRequestResponse<never, unknown>, FlowResourceNotFoundError> => {
const requestor = requestors.get(requestorName);
return requestor === undefined
? Effect.fail(flowResourceNotFoundError(name, "requestor", requestorName))
: Effect.succeed(requestor);
};
const getRequestor = (
requestorName: string,
): EffectRequestResponse<never, unknown> => {
const requestor = requestors.get(requestorName);
if (requestor === undefined) throw flowResourceNotFoundError(name, "requestor", requestorName);
return requestor;
};
const toFlowProducer = <T>(producer: EffectProducer<T>): FlowProducer<T> => ({
send: (id, message) => compatibilityRuntime.runPromise(producer.send(id, message)),
flush: () => compatibilityRuntime.runPromise(producer.flush),
stop: () => compatibilityRuntime.runPromise(producer.flush.pipe(Effect.flatMap(() => producer.close))),
});
const toFlowRequestor = <TReq, TRes>(
requestor: EffectRequestResponse<TReq, TRes>,
): FlowRequestor<TReq, TRes> => ({
request: (request, options) =>
compatibilityRuntime.runPromise(
requestor.request(
request,
toEffectRequestOptions(options),
),
),
stop: () => compatibilityRuntime.runPromise(requestor.stop),
});
function producerEffect<T>(
producerSpec: ProducerSpec<T>,
): Effect.Effect<EffectProducer<T>, FlowResourceNotFoundError>;
function producerEffect(
producerName: string,
): Effect.Effect<EffectProducer<never>, FlowResourceNotFoundError>;
function producerEffect<T>(
producer: string | ProducerSpec<T>,
) {
if (typeof producer === "string") {
return getProducerEffect(producer);
}
if (!producers.has(producer.name)) {
return Effect.fail(flowResourceNotFoundError(name, "producer", producer.name));
}
return producer.producerEffect(flow);
}
function requestorEffect<TReq, TRes>(
requestorSpec: RequestResponseSpec<TReq, TRes>,
): Effect.Effect<EffectRequestResponse<TReq, TRes>, FlowResourceNotFoundError>;
function requestorEffect(
requestorName: string,
): Effect.Effect<EffectRequestResponse<never, unknown>, FlowResourceNotFoundError>;
function requestorEffect<TReq, TRes>(
requestor: string | RequestResponseSpec<TReq, TRes>,
) {
if (typeof requestor === "string") {
return getRequestorEffect(requestor);
}
if (!requestors.has(requestor.name)) {
return Effect.fail(flowResourceNotFoundError(name, "requestor", requestor.name));
}
return requestor.requestorEffect(flow);
}
function parameterEffect<T>( function parameterEffect<T>(
parameterSpec: ParameterSpec<T>, parameterSpec: ParameterSpec<T>,
): Effect.Effect<T, FlowParameterError>; ): Effect.Effect<T, FlowParameterError>;
@ -158,6 +247,34 @@ export function makeFlow<Requirements = never>(
return decodeParameter(parameter, value); return decodeParameter(parameter, value);
} }
function producer<T>(producerSpec: ProducerSpec<T>): FlowProducer<T>;
function producer(producerName: string): FlowProducer<never>;
function producer<T>(producer: string | ProducerSpec<T>) {
if (typeof producer === "string") {
return toFlowProducer(getProducer(producer));
}
if (!producers.has(producer.name)) {
throw flowResourceNotFoundError(name, "producer", producer.name);
}
return toFlowProducer(compatibilityRuntime.runSync(producer.producerEffect(flow)));
}
function requestor<TReq, TRes>(
requestorSpec: RequestResponseSpec<TReq, TRes>,
): FlowRequestor<TReq, TRes>;
function requestor(requestorName: string): FlowRequestor<never, unknown>;
function requestor<TReq, TRes>(
requestor: string | RequestResponseSpec<TReq, TRes>,
) {
if (typeof requestor === "string") {
return toFlowRequestor(getRequestor(requestor));
}
if (!requestors.has(requestor.name)) {
throw flowResourceNotFoundError(name, "requestor", requestor.name);
}
return toFlowRequestor(compatibilityRuntime.runSync(requestor.requestorEffect(flow)));
}
const flow = { const flow = {
name, name,
processorId, processorId,
@ -239,36 +356,16 @@ export function makeFlow<Requirements = never>(
setParameter(parameterName: string, value: unknown): void { setParameter(parameterName: string, value: unknown): void {
parameters.set(parameterName, value); parameters.set(parameterName, value);
}, },
producerEffect<T>(producerName: string): Effect.Effect<EffectProducer<T>, FlowResourceNotFoundError> { producerEffect,
const p = producers.get(producerName);
return p === undefined
? Effect.fail(flowResourceNotFoundError(name, "producer", producerName))
: Effect.succeed(p as EffectProducer<T>);
},
consumerEffect(consumerName: string): Effect.Effect<EffectConsumer, FlowResourceNotFoundError> { consumerEffect(consumerName: string): Effect.Effect<EffectConsumer, FlowResourceNotFoundError> {
const c = consumers.get(consumerName); const c = consumers.get(consumerName);
return c === undefined return c === undefined
? Effect.fail(flowResourceNotFoundError(name, "consumer", consumerName)) ? Effect.fail(flowResourceNotFoundError(name, "consumer", consumerName))
: Effect.succeed(c); : Effect.succeed(c);
}, },
requestorEffect<TReq, TRes>( requestorEffect,
requestorName: string,
): Effect.Effect<EffectRequestResponse<TReq, TRes>, FlowResourceNotFoundError> {
const rr = requestors.get(requestorName);
return rr === undefined
? Effect.fail(flowResourceNotFoundError(name, "requestor", requestorName))
: Effect.succeed(rr as EffectRequestResponse<TReq, TRes>);
},
parameterEffect, parameterEffect,
producer<T>(producerName: string): FlowProducer<T> { producer,
const p = producers.get(producerName);
if (p === undefined) throw flowResourceNotFoundError(name, "producer", producerName);
return {
send: (id, message) => compatibilityRuntime.runPromise((p as EffectProducer<T>).send(id, message)),
flush: () => compatibilityRuntime.runPromise(p.flush),
stop: () => compatibilityRuntime.runPromise(p.flush.pipe(Effect.flatMap(() => p.close))),
};
},
consumer(consumerName: string): FlowConsumer { consumer(consumerName: string): FlowConsumer {
const c = consumers.get(consumerName); const c = consumers.get(consumerName);
if (c === undefined) throw flowResourceNotFoundError(name, "consumer", consumerName); if (c === undefined) throw flowResourceNotFoundError(name, "consumer", consumerName);
@ -276,20 +373,7 @@ export function makeFlow<Requirements = never>(
stop: () => compatibilityRuntime.runPromise(c.stop), stop: () => compatibilityRuntime.runPromise(c.stop),
}; };
}, },
requestor<TReq, TRes>(requestorName: string): FlowRequestor<TReq, TRes> { requestor,
const rr = requestors.get(requestorName);
if (rr === undefined) throw flowResourceNotFoundError(name, "requestor", requestorName);
return {
request: (request, options) =>
compatibilityRuntime.runPromise(
(rr as EffectRequestResponse<TReq, TRes>).request(
request,
toEffectRequestOptions(options),
),
),
stop: () => compatibilityRuntime.runPromise(rr.stop),
};
},
parameter, parameter,
}; };

View file

@ -31,6 +31,8 @@ export class Embeddings extends Context.Service<Embeddings, EmbeddingsServiceSha
"@trustgraph/base/services/embeddings-service/Embeddings", "@trustgraph/base/services/embeddings-service/Embeddings",
) {} ) {}
const EmbeddingsResponseProducer = makeProducerSpec<EmbeddingsResponse>("embeddings-response");
const onEmbeddingsRequest = Effect.fn("EmbeddingsService.onRequest")(function* ( const onEmbeddingsRequest = Effect.fn("EmbeddingsService.onRequest")(function* (
msg: EmbeddingsRequest, msg: EmbeddingsRequest,
properties: Record<string, string>, properties: Record<string, string>,
@ -41,7 +43,7 @@ const onEmbeddingsRequest = Effect.fn("EmbeddingsService.onRequest")(function* (
return; return;
} }
const responseProducer = yield* flowCtx.flow.producerEffect<EmbeddingsResponse>("embeddings-response"); const responseProducer = yield* flowCtx.flow.producerEffect(EmbeddingsResponseProducer);
const embeddings = yield* Embeddings; const embeddings = yield* Embeddings;
const response = yield* embeddings.embed(msg.text, msg.model).pipe( const response = yield* embeddings.embed(msg.text, msg.model).pipe(
Effect.map((vectors) => ({ vectors }) satisfies EmbeddingsResponse), Effect.map((vectors) => ({ vectors }) satisfies EmbeddingsResponse),
@ -70,7 +72,7 @@ export const makeEmbeddingsSpecs = (): ReadonlyArray<Spec<Embeddings>> => [
"embeddings-request", "embeddings-request",
onEmbeddingsRequest, onEmbeddingsRequest,
), ),
makeProducerSpec<EmbeddingsResponse>("embeddings-response"), EmbeddingsResponseProducer,
makeParameterSpec("model"), makeParameterSpec("model"),
]; ];

View file

@ -124,6 +124,8 @@ const llmErrorResponse = (error: LlmServiceError): TextCompletionResponse => ({
endOfStream: true, endOfStream: true,
}); });
const TextCompletionResponseProducer = makeProducerSpec<TextCompletionResponse>("text-completion-response");
const sendStreamingResponse = Effect.fn("LlmService.sendStreamingResponse")(function* ( const sendStreamingResponse = Effect.fn("LlmService.sendStreamingResponse")(function* (
llm: LlmServiceShape, llm: LlmServiceShape,
requestId: string, requestId: string,
@ -158,9 +160,7 @@ const onLlmRequest = Effect.fn("LlmService.onRequest")(function* (
const requestId = properties.id; const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return; if (requestId === undefined || requestId.length === 0) return;
const responseProducer = yield* flowCtx.flow.producerEffect<TextCompletionResponse>( const responseProducer = yield* flowCtx.flow.producerEffect(TextCompletionResponseProducer);
"text-completion-response",
);
const llm = yield* Llm; const llm = yield* Llm;
if (msg.streaming === true && llm.supportsStreaming()) { if (msg.streaming === true && llm.supportsStreaming()) {
@ -210,7 +210,7 @@ export const makeLlmSpecs = (): ReadonlyArray<Spec<Llm>> => [
"text-completion-request", "text-completion-request",
onLlmRequest, onLlmRequest,
), ),
makeProducerSpec<TextCompletionResponse>("text-completion-response"), TextCompletionResponseProducer,
makeParameterSpec("model"), makeParameterSpec("model"),
makeParameterSpec("temperature"), makeParameterSpec("temperature"),
]; ];

View file

@ -8,6 +8,11 @@ import { Effect } from "effect";
import type { Spec } from "./types.js"; import type { Spec } from "./types.js";
import type { Flow, FlowDefinition } from "../processor/flow.js"; import type { Flow, FlowDefinition } from "../processor/flow.js";
import { import {
flowResourceNotFoundError,
type FlowResourceNotFoundError,
} from "../errors.js";
import {
type EffectProducer,
ProducerFactory, ProducerFactory,
} from "../messaging/runtime.js"; } from "../messaging/runtime.js";
@ -15,9 +20,41 @@ declare const ProducerSpecType: unique symbol;
export interface ProducerSpec<T> extends Spec { export interface ProducerSpec<T> extends Spec {
readonly [ProducerSpecType]?: (_: T) => T; readonly [ProducerSpecType]?: (_: T) => T;
readonly producerEffect: <Requirements = never>(
flow: Flow<Requirements>,
) => Effect.Effect<EffectProducer<T>, FlowResourceNotFoundError>;
} }
export function makeProducerSpec<T>(name: string): ProducerSpec<T> { export function makeProducerSpec<T>(name: string): ProducerSpec<T> {
const producers = new WeakMap<object, EffectProducer<T>>();
const registerProducer = <Requirements>(
flow: Flow<Requirements>,
producer: EffectProducer<T>,
) =>
Effect.sync(() => {
producers.set(flow, producer);
});
const unregisterProducer = <Requirements>(
flow: Flow<Requirements>,
producer: EffectProducer<T>,
) =>
Effect.sync(() => {
if (producers.get(flow) === producer) {
producers.delete(flow);
}
});
const producerEffect = <Requirements>(
flow: Flow<Requirements>,
): Effect.Effect<EffectProducer<T>, FlowResourceNotFoundError> => {
const producer = producers.get(flow);
return producer === undefined
? Effect.fail(flowResourceNotFoundError(flow.name, "producer", name))
: Effect.succeed(producer);
};
const addEffect = Effect.fn("ProducerSpec.addEffect")(function* ( const addEffect = Effect.fn("ProducerSpec.addEffect")(function* (
flow: Flow, flow: Flow,
definition: FlowDefinition, definition: FlowDefinition,
@ -26,10 +63,13 @@ export function makeProducerSpec<T>(name: string): ProducerSpec<T> {
const factory = yield* ProducerFactory; const factory = yield* ProducerFactory;
const producer = yield* factory.make<T>({ topic }); const producer = yield* factory.make<T>({ topic });
flow.registerProducer(name, producer); flow.registerProducer(name, producer);
yield* registerProducer(flow, producer);
yield* Effect.addFinalizer(() => unregisterProducer(flow, producer));
}); });
return { return {
name, name,
producerEffect,
addEffect, addEffect,
add: (flow, pubsub, definition, context) => add: (flow, pubsub, definition, context) =>
flow.runInCompatibilityScope(addEffect(flow, definition), pubsub, context), flow.runInCompatibilityScope(addEffect(flow, definition), pubsub, context),

View file

@ -11,6 +11,11 @@ import { Effect } from "effect";
import type { Spec } from "./types.js"; import type { Spec } from "./types.js";
import type { Flow, FlowDefinition } from "../processor/flow.js"; import type { Flow, FlowDefinition } from "../processor/flow.js";
import { import {
flowResourceNotFoundError,
type FlowResourceNotFoundError,
} from "../errors.js";
import {
type EffectRequestResponse,
RequestResponseFactory, RequestResponseFactory,
} from "../messaging/runtime.js"; } from "../messaging/runtime.js";
@ -21,6 +26,9 @@ export interface RequestResponseSpec<TReq, TRes> extends Spec {
readonly request: TReq; readonly request: TReq;
readonly response: TRes; readonly response: TRes;
}; };
readonly requestorEffect: <Requirements = never>(
flow: Flow<Requirements>,
) => Effect.Effect<EffectRequestResponse<TReq, TRes>, FlowResourceNotFoundError>;
} }
export function makeRequestResponseSpec<TReq, TRes>( export function makeRequestResponseSpec<TReq, TRes>(
@ -28,6 +36,35 @@ export function makeRequestResponseSpec<TReq, TRes>(
requestTopicName: string, requestTopicName: string,
responseTopicName: string, responseTopicName: string,
): RequestResponseSpec<TReq, TRes> { ): RequestResponseSpec<TReq, TRes> {
const requestors = new WeakMap<object, EffectRequestResponse<TReq, TRes>>();
const registerRequestor = <Requirements>(
flow: Flow<Requirements>,
requestor: EffectRequestResponse<TReq, TRes>,
) =>
Effect.sync(() => {
requestors.set(flow, requestor);
});
const unregisterRequestor = <Requirements>(
flow: Flow<Requirements>,
requestor: EffectRequestResponse<TReq, TRes>,
) =>
Effect.sync(() => {
if (requestors.get(flow) === requestor) {
requestors.delete(flow);
}
});
const requestorEffect = <Requirements>(
flow: Flow<Requirements>,
): Effect.Effect<EffectRequestResponse<TReq, TRes>, FlowResourceNotFoundError> => {
const requestor = requestors.get(flow);
return requestor === undefined
? Effect.fail(flowResourceNotFoundError(flow.name, "requestor", name))
: Effect.succeed(requestor);
};
const addEffect = Effect.fn("RequestResponseSpec.addEffect")(function* ( const addEffect = Effect.fn("RequestResponseSpec.addEffect")(function* (
flow: Flow, flow: Flow,
definition: FlowDefinition, definition: FlowDefinition,
@ -41,10 +78,13 @@ export function makeRequestResponseSpec<TReq, TRes>(
subscription: `${flow.processorId}-${flow.name}-${name}`, subscription: `${flow.processorId}-${flow.name}-${name}`,
}); });
flow.registerRequestor(name, requestor); flow.registerRequestor(name, requestor);
yield* registerRequestor(flow, requestor);
yield* Effect.addFinalizer(() => unregisterRequestor(flow, requestor));
}); });
return { return {
name, name,
requestorEffect,
addEffect, addEffect,
add: (flow, pubsub, definition, context) => add: (flow, pubsub, definition, context) =>
flow.runInCompatibilityScope(addEffect(flow, definition), pubsub, context), flow.runInCompatibilityScope(addEffect(flow, definition), pubsub, context),

View file

@ -71,6 +71,8 @@ export class McpToolRuntime extends Context.Service<
McpToolRuntimeService McpToolRuntimeService
>()("@trustgraph/flow/agent/mcp-tool/service/McpToolRuntime") {} >()("@trustgraph/flow/agent/mcp-tool/service/McpToolRuntime") {}
const McpToolResponseProducer = makeProducerSpec<ToolResponse>("mcp-tool-response");
const mcpToolError = ( const mcpToolError = (
operation: string, operation: string,
cause: unknown, cause: unknown,
@ -246,7 +248,7 @@ const onMcpToolRequest = Effect.fn("McpToolService.onRequest")(function* (
const requestId = properties.id; const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return; if (requestId === undefined || requestId.length === 0) return;
const responseProducer = yield* flowCtx.flow.producerEffect<ToolResponse>("mcp-tool-response"); const responseProducer = yield* flowCtx.flow.producerEffect(McpToolResponseProducer);
const runtime = yield* McpToolRuntime; const runtime = yield* McpToolRuntime;
const result = yield* parametersFromJson(msg.name, msg.parameters).pipe( const result = yield* parametersFromJson(msg.name, msg.parameters).pipe(
@ -284,7 +286,7 @@ export const makeMcpToolSpecs = (): ReadonlyArray<Spec<McpToolRuntime>> => [
"mcp-tool-request", "mcp-tool-request",
onMcpToolRequest, onMcpToolRequest,
), ),
makeProducerSpec<ToolResponse>("mcp-tool-response"), McpToolResponseProducer,
]; ];
export const makeMcpToolConfigHandlers = (): ReadonlyArray< export const makeMcpToolConfigHandlers = (): ReadonlyArray<

View file

@ -71,6 +71,33 @@ class AgentToolExecutionError extends S.TaggedErrorClass<AgentToolExecutionError
}, },
) {} ) {}
const AgentResponseProducer = makeProducerSpec<AgentResponse>("agent-response");
const AgentLlmClient = makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
"llm",
"text-completion-request",
"text-completion-response",
);
const AgentGraphRagClient = makeRequestResponseSpec<GraphRagRequest, GraphRagResponse>(
"graph-rag",
"graph-rag-request",
"graph-rag-response",
);
const AgentDocRagClient = makeRequestResponseSpec<DocumentRagRequest, DocumentRagResponse>(
"doc-rag",
"document-rag-request",
"document-rag-response",
);
const AgentTriplesClient = makeRequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
"triples",
"triples-request",
"triples-response",
);
const AgentMcpToolClient = makeRequestResponseSpec<ToolRequest, ToolResponse>(
"mcp-tool",
"mcp-tool-request",
"mcp-tool-response",
);
const UnknownRecord = S.Record(S.String, S.Unknown); const UnknownRecord = S.Record(S.String, S.Unknown);
const ToolArgumentConfig = S.StructWithRest( const ToolArgumentConfig = S.StructWithRest(
S.Struct({ S.Struct({
@ -248,10 +275,10 @@ const wireTools = Effect.fn("AgentService.wireTools")(function* (
collection: string | undefined, collection: string | undefined,
onExplain: (data: ExplainData) => void, onExplain: (data: ExplainData) => void,
) { ) {
const graphRag = yield* flowCtx.flow.requestorEffect<GraphRagRequest, GraphRagResponse>("graph-rag"); const graphRag = yield* flowCtx.flow.requestorEffect(AgentGraphRagClient);
const docRag = yield* flowCtx.flow.requestorEffect<DocumentRagRequest, DocumentRagResponse>("doc-rag"); const docRag = yield* flowCtx.flow.requestorEffect(AgentDocRagClient);
const triples = yield* flowCtx.flow.requestorEffect<TriplesQueryRequest, TriplesQueryResponse>("triples"); const triples = yield* flowCtx.flow.requestorEffect(AgentTriplesClient);
const mcpTool = yield* flowCtx.flow.requestorEffect<ToolRequest, ToolResponse>("mcp-tool"); const mcpTool = yield* flowCtx.flow.requestorEffect(AgentMcpToolClient);
return tools.map((tool) => { return tools.map((tool) => {
const rawImplType = tool.config?.type; const rawImplType = tool.config?.type;
@ -300,9 +327,9 @@ const defaultTools = Effect.fn("AgentService.defaultTools")(function* (
collection: string | undefined, collection: string | undefined,
onExplain: (data: ExplainData) => void, onExplain: (data: ExplainData) => void,
) { ) {
const graphRag = yield* flowCtx.flow.requestorEffect<GraphRagRequest, GraphRagResponse>("graph-rag"); const graphRag = yield* flowCtx.flow.requestorEffect(AgentGraphRagClient);
const docRag = yield* flowCtx.flow.requestorEffect<DocumentRagRequest, DocumentRagResponse>("doc-rag"); const docRag = yield* flowCtx.flow.requestorEffect(AgentDocRagClient);
const triples = yield* flowCtx.flow.requestorEffect<TriplesQueryRequest, TriplesQueryResponse>("triples"); const triples = yield* flowCtx.flow.requestorEffect(AgentTriplesClient);
return [ return [
createKnowledgeQueryTool( createKnowledgeQueryTool(
@ -346,7 +373,7 @@ const onAgentRequest = Effect.fn("AgentService.onRequest")(function* (
const requestId = properties.id; const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return; if (requestId === undefined || requestId.length === 0) return;
const responseProducer = yield* flowCtx.flow.producerEffect<AgentResponse>("agent-response"); const responseProducer = yield* flowCtx.flow.producerEffect(AgentResponseProducer);
yield* Effect.gen(function* () { yield* Effect.gen(function* () {
const runtime = yield* AgentRuntime; const runtime = yield* AgentRuntime;
@ -367,10 +394,7 @@ const onAgentRequest = Effect.fn("AgentService.onRequest")(function* (
msg.question, msg.question,
); );
const llmClient = yield* flowCtx.flow.requestorEffect< const llmClient = yield* flowCtx.flow.requestorEffect(AgentLlmClient);
TextCompletionRequest,
TextCompletionResponse
>("llm");
let conversation = initialPrompt; let conversation = initialPrompt;
@ -472,32 +496,12 @@ export const makeAgentSpecs = (): ReadonlyArray<Spec<AgentRuntime>> => [
"agent-request", "agent-request",
onAgentRequest, onAgentRequest,
), ),
makeProducerSpec<AgentResponse>("agent-response"), AgentResponseProducer,
makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>( AgentLlmClient,
"llm", AgentGraphRagClient,
"text-completion-request", AgentDocRagClient,
"text-completion-response", AgentTriplesClient,
), AgentMcpToolClient,
makeRequestResponseSpec<GraphRagRequest, GraphRagResponse>(
"graph-rag",
"graph-rag-request",
"graph-rag-response",
),
makeRequestResponseSpec<DocumentRagRequest, DocumentRagResponse>(
"doc-rag",
"document-rag-request",
"document-rag-response",
),
makeRequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
"triples",
"triples-request",
"triples-response",
),
makeRequestResponseSpec<ToolRequest, ToolResponse>(
"mcp-tool",
"mcp-tool-request",
"mcp-tool-response",
),
]; ];
export const makeAgentConfigHandlers = (): ReadonlyArray< export const makeAgentConfigHandlers = (): ReadonlyArray<

View file

@ -34,6 +34,8 @@ const DEFAULT_CHUNK_SIZE = 2000;
const DEFAULT_CHUNK_OVERLAP = 100; const DEFAULT_CHUNK_OVERLAP = 100;
const ChunkSizeParameter = makeParameterSpec("chunk-size", S.Number); const ChunkSizeParameter = makeParameterSpec("chunk-size", S.Number);
const ChunkOverlapParameter = makeParameterSpec("chunk-overlap", S.Number); const ChunkOverlapParameter = makeParameterSpec("chunk-overlap", S.Number);
const ChunkOutputProducer = makeProducerSpec<Chunk>("chunk-output");
const ChunkTriplesProducer = makeProducerSpec<Triples>("chunk-triples");
const onChunkMessage = Effect.fn("ChunkingService.onMessage")(function* ( const onChunkMessage = Effect.fn("ChunkingService.onMessage")(function* (
msg: TextDocument, msg: TextDocument,
@ -62,7 +64,7 @@ const onChunkMessage = Effect.fn("ChunkingService.onMessage")(function* (
`[ChunkingService] Split document ${msg.documentId} into ${chunks.length} chunks (size=${chunkSize}, overlap=${chunkOverlap})`, `[ChunkingService] Split document ${msg.documentId} into ${chunks.length} chunks (size=${chunkSize}, overlap=${chunkOverlap})`,
); );
const outputProducer = yield* flowCtx.flow.producerEffect<Chunk>("chunk-output"); const outputProducer = yield* flowCtx.flow.producerEffect(ChunkOutputProducer);
yield* Effect.forEach( yield* Effect.forEach(
chunks, chunks,
@ -83,8 +85,8 @@ export const makeChunkingSpecs = (): ReadonlyArray<
"chunk-input", "chunk-input",
onChunkMessage, onChunkMessage,
), ),
makeProducerSpec<Chunk>("chunk-output"), ChunkOutputProducer,
makeProducerSpec<Triples>("chunk-triples"), ChunkTriplesProducer,
ChunkSizeParameter, ChunkSizeParameter,
ChunkOverlapParameter, ChunkOverlapParameter,
]; ];

View file

@ -96,6 +96,14 @@ const loadPageText = Effect.fn("loadPageText")(function*(
.join(" "); .join(" ");
}); });
const DecodeOutputProducer = makeProducerSpec<TextDocument>("decode-output");
const DecodeTriplesProducer = makeProducerSpec<Triples>("decode-triples");
const LibrarianClient = makeRequestResponseSpec<LibrarianRequest, LibrarianResponse>(
"librarian-client",
"librarian-request",
"librarian-response",
);
const onPdfDecodeMessage = Effect.fn("PdfDecoderService.onMessage")(function* ( const onPdfDecodeMessage = Effect.fn("PdfDecoderService.onMessage")(function* (
msg: Document, msg: Document,
properties: Record<string, string>, properties: Record<string, string>,
@ -107,9 +115,7 @@ const onPdfDecodeMessage = Effect.fn("PdfDecoderService.onMessage")(function* (
const { documentId } = msg; const { documentId } = msg;
const user = msg.metadata.user; const user = msg.metadata.user;
const librarian = yield* flowCtx.flow.requestorEffect<LibrarianRequest, LibrarianResponse>( const librarian = yield* flowCtx.flow.requestorEffect(LibrarianClient);
"librarian-client",
);
const metadataResp = yield* librarian.request({ const metadataResp = yield* librarian.request({
operation: "get-document-metadata", operation: "get-document-metadata",
@ -152,8 +158,8 @@ const onPdfDecodeMessage = Effect.fn("PdfDecoderService.onMessage")(function* (
yield* Effect.log(`[PdfDecoder] Document ${documentId}: ${pdf.numPages} pages`); yield* Effect.log(`[PdfDecoder] Document ${documentId}: ${pdf.numPages} pages`);
const outputProducer = yield* flowCtx.flow.producerEffect<TextDocument>("decode-output"); const outputProducer = yield* flowCtx.flow.producerEffect(DecodeOutputProducer);
const triplesProducer = yield* flowCtx.flow.producerEffect<Triples>("decode-triples"); const triplesProducer = yield* flowCtx.flow.producerEffect(DecodeTriplesProducer);
for (let i = 1; i <= pdf.numPages; i++) { for (let i = 1; i <= pdf.numPages; i++) {
const pageText = yield* loadPageText(documentId, i, pdf); const pageText = yield* loadPageText(documentId, i, pdf);
@ -219,13 +225,9 @@ const onPdfDecodeMessage = Effect.fn("PdfDecoderService.onMessage")(function* (
export const makePdfDecoderSpecs = (): ReadonlyArray<Spec<never>> => [ export const makePdfDecoderSpecs = (): ReadonlyArray<Spec<never>> => [
makeConsumerSpec<Document, PdfDecoderHandlerError>("decode-input", onPdfDecodeMessage), makeConsumerSpec<Document, PdfDecoderHandlerError>("decode-input", onPdfDecodeMessage),
makeProducerSpec<TextDocument>("decode-output"), DecodeOutputProducer,
makeProducerSpec<Triples>("decode-triples"), DecodeTriplesProducer,
makeRequestResponseSpec<LibrarianRequest, LibrarianResponse>( LibrarianClient,
"librarian-client",
"librarian-request",
"librarian-response",
),
]; ];
export type PdfDecoderService = FlowProcessorRuntime; export type PdfDecoderService = FlowProcessorRuntime;

View file

@ -69,6 +69,19 @@ type KnowledgeExtractHandlerError =
type PromptClient = EffectRequestResponse<PromptRequest, PromptResponse>; type PromptClient = EffectRequestResponse<PromptRequest, PromptResponse>;
type LlmClient = EffectRequestResponse<TextCompletionRequest, TextCompletionResponse>; type LlmClient = EffectRequestResponse<TextCompletionRequest, TextCompletionResponse>;
const ExtractTriplesProducer = makeProducerSpec<Triples>("extract-triples");
const ExtractEntityContextsProducer = makeProducerSpec<EntityContexts>("extract-entity-contexts");
const PromptClientSpec = makeRequestResponseSpec<PromptRequest, PromptResponse>(
"prompt-client",
"prompt-request",
"prompt-response",
);
const LlmClientSpec = makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
"llm-client",
"text-completion-request",
"text-completion-response",
);
const requestPrompt = Effect.fn("KnowledgeExtract.requestPrompt")(function* ( const requestPrompt = Effect.fn("KnowledgeExtract.requestPrompt")(function* (
promptClient: PromptClient, promptClient: PromptClient,
name: string, name: string,
@ -153,10 +166,10 @@ const onKnowledgeExtractMessage = Effect.fn("KnowledgeExtractService.onMessage")
const text = msg.chunk; const text = msg.chunk;
if (text.trim().length === 0) return; if (text.trim().length === 0) return;
const promptClient = yield* flowCtx.flow.requestorEffect<PromptRequest, PromptResponse>("prompt-client"); const promptClient = yield* flowCtx.flow.requestorEffect(PromptClientSpec);
const llmClient = yield* flowCtx.flow.requestorEffect<TextCompletionRequest, TextCompletionResponse>("llm-client"); const llmClient = yield* flowCtx.flow.requestorEffect(LlmClientSpec);
const triplesProducer = yield* flowCtx.flow.producerEffect<Triples>("extract-triples"); const triplesProducer = yield* flowCtx.flow.producerEffect(ExtractTriplesProducer);
const entityContextsProducer = yield* flowCtx.flow.producerEffect<EntityContexts>("extract-entity-contexts"); const entityContextsProducer = yield* flowCtx.flow.producerEffect(ExtractEntityContextsProducer);
const allTriples: Triple[] = []; const allTriples: Triple[] = [];
const allEntityContexts: EntityContext[] = []; const allEntityContexts: EntityContext[] = [];
@ -270,18 +283,10 @@ export const makeKnowledgeExtractSpecs = (): ReadonlyArray<Spec<never>> => [
"extract-input", "extract-input",
onKnowledgeExtractMessage, onKnowledgeExtractMessage,
), ),
makeProducerSpec<Triples>("extract-triples"), ExtractTriplesProducer,
makeProducerSpec<EntityContexts>("extract-entity-contexts"), ExtractEntityContextsProducer,
makeRequestResponseSpec<PromptRequest, PromptResponse>( PromptClientSpec,
"prompt-client", LlmClientSpec,
"prompt-request",
"prompt-response",
),
makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
"llm-client",
"text-completion-request",
"text-completion-response",
),
]; ];
export type KnowledgeExtractService = FlowProcessorRuntime; export type KnowledgeExtractService = FlowProcessorRuntime;

View file

@ -69,6 +69,7 @@ const programRuntimes = new WeakMap<PromptTemplateConfig, PromptTemplateRuntime>
const makePromptTemplateRuntime = (config: PromptTemplateConfig): PromptTemplateRuntime => { const makePromptTemplateRuntime = (config: PromptTemplateConfig): PromptTemplateRuntime => {
const templates = new Map<string, PromptTemplate>(); const templates = new Map<string, PromptTemplate>();
const configKey = config.configKey ?? "prompt"; const configKey = config.configKey ?? "prompt";
const PromptResponseProducer = makeProducerSpec<PromptResponse>("prompt-response");
const onPromptConfig = Effect.fn("PromptTemplateService.onConfig")(function* ( const onPromptConfig = Effect.fn("PromptTemplateService.onConfig")(function* (
pushedConfig: Record<string, unknown>, pushedConfig: Record<string, unknown>,
@ -114,7 +115,7 @@ const makePromptTemplateRuntime = (config: PromptTemplateConfig): PromptTemplate
const requestId = properties.id; const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return; if (requestId === undefined || requestId.length === 0) return;
const responseProducer = yield* flowCtx.flow.producerEffect<PromptResponse>("prompt-response"); const responseProducer = yield* flowCtx.flow.producerEffect(PromptResponseProducer);
const template = templates.get(msg.name); const template = templates.get(msg.name);
if (template === undefined) { if (template === undefined) {
yield* responseProducer.send(requestId, { yield* responseProducer.send(requestId, {
@ -142,7 +143,7 @@ const makePromptTemplateRuntime = (config: PromptTemplateConfig): PromptTemplate
"prompt-request", "prompt-request",
onRequest, onRequest,
), ),
makeProducerSpec<PromptResponse>("prompt-response"), PromptResponseProducer,
], ],
configHandlers: [onPromptConfig], configHandlers: [onPromptConfig],
}; };

View file

@ -30,6 +30,8 @@ import {
type QdrantDocQueryConfig, type QdrantDocQueryConfig,
} from "./qdrant-doc.js"; } from "./qdrant-doc.js";
const DocumentEmbeddingsResponseProducer = makeProducerSpec<DocumentEmbeddingsResponse>("document-embeddings-response");
const onDocEmbeddingsQueryMessage = Effect.fn("DocEmbeddingsQueryService.onMessage")(function* ( const onDocEmbeddingsQueryMessage = Effect.fn("DocEmbeddingsQueryService.onMessage")(function* (
msg: DocumentEmbeddingsRequest, msg: DocumentEmbeddingsRequest,
properties: Record<string, string>, properties: Record<string, string>,
@ -38,7 +40,7 @@ const onDocEmbeddingsQueryMessage = Effect.fn("DocEmbeddingsQueryService.onMessa
const requestId = properties.id; const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return; if (requestId === undefined || requestId.length === 0) return;
const producer = yield* flowCtx.flow.producerEffect<DocumentEmbeddingsResponse>("document-embeddings-response"); const producer = yield* flowCtx.flow.producerEffect(DocumentEmbeddingsResponseProducer);
const query = yield* QdrantDocEmbeddingsQueryService; const query = yield* QdrantDocEmbeddingsQueryService;
const collection = msg.collection ?? "default"; const collection = msg.collection ?? "default";
const allChunks: DocumentEmbeddingsResponse["chunks"] = []; const allChunks: DocumentEmbeddingsResponse["chunks"] = [];
@ -85,7 +87,7 @@ export const makeDocEmbeddingsQuerySpecs = (): ReadonlyArray<Spec<QdrantDocEmbed
FlowResourceNotFoundError | MessagingDeliveryError, FlowResourceNotFoundError | MessagingDeliveryError,
QdrantDocEmbeddingsQueryService QdrantDocEmbeddingsQueryService
>("document-embeddings-request", onDocEmbeddingsQueryMessage), >("document-embeddings-request", onDocEmbeddingsQueryMessage),
makeProducerSpec<DocumentEmbeddingsResponse>("document-embeddings-response"), DocumentEmbeddingsResponseProducer,
]; ];
export type DocEmbeddingsQueryService = FlowProcessorRuntime<QdrantDocEmbeddingsQueryService>; export type DocEmbeddingsQueryService = FlowProcessorRuntime<QdrantDocEmbeddingsQueryService>;

View file

@ -30,6 +30,8 @@ import {
type QdrantGraphQueryConfig, type QdrantGraphQueryConfig,
} from "./qdrant-graph.js"; } from "./qdrant-graph.js";
const GraphEmbeddingsResponseProducer = makeProducerSpec<GraphEmbeddingsResponse>("graph-embeddings-response");
const onGraphEmbeddingsQueryMessage = Effect.fn("GraphEmbeddingsQueryService.onMessage")(function* ( const onGraphEmbeddingsQueryMessage = Effect.fn("GraphEmbeddingsQueryService.onMessage")(function* (
msg: GraphEmbeddingsRequest, msg: GraphEmbeddingsRequest,
properties: Record<string, string>, properties: Record<string, string>,
@ -38,7 +40,7 @@ const onGraphEmbeddingsQueryMessage = Effect.fn("GraphEmbeddingsQueryService.onM
const requestId = properties.id; const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return; if (requestId === undefined || requestId.length === 0) return;
const producer = yield* flowCtx.flow.producerEffect<GraphEmbeddingsResponse>("graph-embeddings-response"); const producer = yield* flowCtx.flow.producerEffect(GraphEmbeddingsResponseProducer);
const query = yield* QdrantGraphEmbeddingsQueryService; const query = yield* QdrantGraphEmbeddingsQueryService;
const user = msg.user ?? "default"; const user = msg.user ?? "default";
const collection = msg.collection ?? "default"; const collection = msg.collection ?? "default";
@ -86,7 +88,7 @@ export const makeGraphEmbeddingsQuerySpecs = (): ReadonlyArray<Spec<QdrantGraphE
FlowResourceNotFoundError | MessagingDeliveryError, FlowResourceNotFoundError | MessagingDeliveryError,
QdrantGraphEmbeddingsQueryService QdrantGraphEmbeddingsQueryService
>("graph-embeddings-request", onGraphEmbeddingsQueryMessage), >("graph-embeddings-request", onGraphEmbeddingsQueryMessage),
makeProducerSpec<GraphEmbeddingsResponse>("graph-embeddings-response"), GraphEmbeddingsResponseProducer,
]; ];
export type GraphEmbeddingsQueryService = FlowProcessorRuntime<QdrantGraphEmbeddingsQueryService>; export type GraphEmbeddingsQueryService = FlowProcessorRuntime<QdrantGraphEmbeddingsQueryService>;

View file

@ -30,6 +30,8 @@ import {
type FalkorDBQueryConfig, type FalkorDBQueryConfig,
} from "./falkordb.js"; } from "./falkordb.js";
const TriplesResponseProducer = makeProducerSpec<TriplesQueryResponse>("triples-response");
const onTriplesQueryMessage = Effect.fn("TriplesQueryService.onMessage")(function* ( const onTriplesQueryMessage = Effect.fn("TriplesQueryService.onMessage")(function* (
msg: TriplesQueryRequest, msg: TriplesQueryRequest,
properties: Record<string, string>, properties: Record<string, string>,
@ -38,7 +40,7 @@ const onTriplesQueryMessage = Effect.fn("TriplesQueryService.onMessage")(functio
const requestId = properties.id; const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return; if (requestId === undefined || requestId.length === 0) return;
const producer = yield* flowCtx.flow.producerEffect<TriplesQueryResponse>("triples-response"); const producer = yield* flowCtx.flow.producerEffect(TriplesResponseProducer);
const query = yield* FalkorDBTriplesQueryService; const query = yield* FalkorDBTriplesQueryService;
const triples = yield* query.queryTriples( const triples = yield* query.queryTriples(
msg.s, msg.s,
@ -72,7 +74,7 @@ export const makeTriplesQuerySpecs = (): ReadonlyArray<Spec<FalkorDBTriplesQuery
FlowResourceNotFoundError | MessagingDeliveryError, FlowResourceNotFoundError | MessagingDeliveryError,
FalkorDBTriplesQueryService FalkorDBTriplesQueryService
>("triples-request", onTriplesQueryMessage), >("triples-request", onTriplesQueryMessage),
makeProducerSpec<TriplesQueryResponse>("triples-response"), TriplesResponseProducer,
]; ];
export type TriplesQueryService = FlowProcessorRuntime<FalkorDBTriplesQueryService>; export type TriplesQueryService = FlowProcessorRuntime<FalkorDBTriplesQueryService>;

View file

@ -40,6 +40,28 @@ import {
type DocumentRagClients, type DocumentRagClients,
} from "./document-rag.js"; } from "./document-rag.js";
const DocumentRagResponseProducer = makeProducerSpec<DocumentRagResponse>("document-rag-response");
const DocumentRagLlmClient = makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
"llm",
"text-completion-request",
"text-completion-response",
);
const DocumentRagEmbeddingsClient = makeRequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>(
"embeddings",
"embeddings-request",
"embeddings-response",
);
const DocumentRagDocEmbeddingsClient = makeRequestResponseSpec<DocumentEmbeddingsRequest, DocumentEmbeddingsResponse>(
"doc-embeddings",
"document-embeddings-request",
"document-embeddings-response",
);
const DocumentRagPromptClient = makeRequestResponseSpec<PromptRequest, PromptResponse>(
"prompt",
"prompt-request",
"prompt-response",
);
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>,
@ -48,14 +70,14 @@ const onDocumentRagRequest = Effect.fn("DocumentRagService.onRequest")(function*
const requestId = properties.id; const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return; if (requestId === undefined || requestId.length === 0) return;
const producer = yield* flowCtx.flow.producerEffect<DocumentRagResponse>("document-rag-response"); const producer = yield* flowCtx.flow.producerEffect(DocumentRagResponseProducer);
const engine = yield* DocumentRagEngine; const engine = yield* DocumentRagEngine;
const clients: DocumentRagClients = { const clients: DocumentRagClients = {
llm: yield* flowCtx.flow.requestorEffect<TextCompletionRequest, TextCompletionResponse>("llm"), llm: yield* flowCtx.flow.requestorEffect(DocumentRagLlmClient),
embeddings: yield* flowCtx.flow.requestorEffect<EmbeddingsRequest, EmbeddingsResponse>("embeddings"), embeddings: yield* flowCtx.flow.requestorEffect(DocumentRagEmbeddingsClient),
docEmbeddings: yield* flowCtx.flow.requestorEffect<DocumentEmbeddingsRequest, DocumentEmbeddingsResponse>("doc-embeddings"), docEmbeddings: yield* flowCtx.flow.requestorEffect(DocumentRagDocEmbeddingsClient),
prompt: yield* flowCtx.flow.requestorEffect<PromptRequest, PromptResponse>("prompt"), prompt: yield* flowCtx.flow.requestorEffect(DocumentRagPromptClient),
}; };
const response = yield* engine.query( const response = yield* engine.query(
@ -90,27 +112,11 @@ export const makeDocumentRagSpecs = (): ReadonlyArray<Spec<DocumentRagEngine>> =
"document-rag-request", "document-rag-request",
onDocumentRagRequest, onDocumentRagRequest,
), ),
makeProducerSpec<DocumentRagResponse>("document-rag-response"), DocumentRagResponseProducer,
makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>( DocumentRagLlmClient,
"llm", DocumentRagEmbeddingsClient,
"text-completion-request", DocumentRagDocEmbeddingsClient,
"text-completion-response", DocumentRagPromptClient,
),
makeRequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>(
"embeddings",
"embeddings-request",
"embeddings-response",
),
makeRequestResponseSpec<DocumentEmbeddingsRequest, DocumentEmbeddingsResponse>(
"doc-embeddings",
"document-embeddings-request",
"document-embeddings-response",
),
makeRequestResponseSpec<PromptRequest, PromptResponse>(
"prompt",
"prompt-request",
"prompt-response",
),
]; ];
export type DocumentRagService = FlowProcessorRuntime<DocumentRagEngine>; export type DocumentRagService = FlowProcessorRuntime<DocumentRagEngine>;

View file

@ -43,6 +43,33 @@ import {
type GraphRagConfig, type GraphRagConfig,
} from "./graph-rag.js"; } from "./graph-rag.js";
const GraphRagResponseProducer = makeProducerSpec<GraphRagResponse>("graph-rag-response");
const GraphRagLlmClient = makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
"llm",
"text-completion-request",
"text-completion-response",
);
const GraphRagEmbeddingsClient = makeRequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>(
"embeddings",
"embeddings-request",
"embeddings-response",
);
const GraphRagGraphEmbeddingsClient = makeRequestResponseSpec<GraphEmbeddingsRequest, GraphEmbeddingsResponse>(
"graph-embeddings",
"graph-embeddings-request",
"graph-embeddings-response",
);
const GraphRagTriplesClient = makeRequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
"triples",
"triples-request",
"triples-response",
);
const GraphRagPromptClient = makeRequestResponseSpec<PromptRequest, PromptResponse>(
"prompt",
"prompt-request",
"prompt-response",
);
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 } : {}),
@ -58,17 +85,17 @@ const onGraphRagRequest = Effect.fn("GraphRagService.onRequest")(function* (
const requestId = properties.id; const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return; if (requestId === undefined || requestId.length === 0) return;
const producer = yield* flowCtx.flow.producerEffect<GraphRagResponse>("graph-rag-response"); const producer = yield* flowCtx.flow.producerEffect(GraphRagResponseProducer);
const engine = yield* GraphRagEngine; const engine = yield* GraphRagEngine;
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: yield* flowCtx.flow.requestorEffect<TextCompletionRequest, TextCompletionResponse>("llm"), llm: yield* flowCtx.flow.requestorEffect(GraphRagLlmClient),
embeddings: yield* flowCtx.flow.requestorEffect<EmbeddingsRequest, EmbeddingsResponse>("embeddings"), embeddings: yield* flowCtx.flow.requestorEffect(GraphRagEmbeddingsClient),
graphEmbeddings: yield* flowCtx.flow.requestorEffect<GraphEmbeddingsRequest, GraphEmbeddingsResponse>("graph-embeddings"), graphEmbeddings: yield* flowCtx.flow.requestorEffect(GraphRagGraphEmbeddingsClient),
triples: yield* flowCtx.flow.requestorEffect<TriplesQueryRequest, TriplesQueryResponse>("triples"), triples: yield* flowCtx.flow.requestorEffect(GraphRagTriplesClient),
prompt: yield* flowCtx.flow.requestorEffect<PromptRequest, PromptResponse>("prompt"), prompt: yield* flowCtx.flow.requestorEffect(GraphRagPromptClient),
}; };
const result = yield* engine.query( const result = yield* engine.query(
@ -118,32 +145,12 @@ export const makeGraphRagSpecs = (): ReadonlyArray<Spec<GraphRagEngine>> => [
"graph-rag-request", "graph-rag-request",
onGraphRagRequest, onGraphRagRequest,
), ),
makeProducerSpec<GraphRagResponse>("graph-rag-response"), GraphRagResponseProducer,
makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>( GraphRagLlmClient,
"llm", GraphRagEmbeddingsClient,
"text-completion-request", GraphRagGraphEmbeddingsClient,
"text-completion-response", GraphRagTriplesClient,
), GraphRagPromptClient,
makeRequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>(
"embeddings",
"embeddings-request",
"embeddings-response",
),
makeRequestResponseSpec<GraphEmbeddingsRequest, GraphEmbeddingsResponse>(
"graph-embeddings",
"graph-embeddings-request",
"graph-embeddings-response",
),
makeRequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
"triples",
"triples-request",
"triples-response",
),
makeRequestResponseSpec<PromptRequest, PromptResponse>(
"prompt",
"prompt-request",
"prompt-response",
),
]; ];
export type GraphRagService = FlowProcessorRuntime<GraphRagEngine>; export type GraphRagService = FlowProcessorRuntime<GraphRagEngine>;

View file

@ -42,6 +42,12 @@ type GraphEmbeddingsStoreError =
| MessagingTimeoutError | MessagingTimeoutError
| QdrantGraphEmbeddingsStoreError; | QdrantGraphEmbeddingsStoreError;
const EmbeddingsClient = makeRequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>(
"embeddings-client",
"embeddings-request",
"embeddings-response",
);
const onGraphEmbeddingsStoreMessage = Effect.fn("GraphEmbeddingsStoreService.onMessage")(function* ( const onGraphEmbeddingsStoreMessage = Effect.fn("GraphEmbeddingsStoreService.onMessage")(function* (
msg: EntityContexts, msg: EntityContexts,
_properties: Record<string, string>, _properties: Record<string, string>,
@ -49,8 +55,7 @@ const onGraphEmbeddingsStoreMessage = Effect.fn("GraphEmbeddingsStoreService.onM
): Effect.fn.Return<void, GraphEmbeddingsStoreError, GraphEmbeddingsStoreRequirements> { ): Effect.fn.Return<void, GraphEmbeddingsStoreError, GraphEmbeddingsStoreRequirements> {
if (msg.entities.length === 0) return; if (msg.entities.length === 0) return;
const embeddingsClient = const embeddingsClient = yield* flowCtx.flow.requestorEffect(EmbeddingsClient);
yield* flowCtx.flow.requestorEffect<EmbeddingsRequest, EmbeddingsResponse>("embeddings-client");
const user = msg.metadata?.user ?? "default"; const user = msg.metadata?.user ?? "default";
const collection = msg.metadata?.collection ?? "default"; const collection = msg.metadata?.collection ?? "default";
@ -83,11 +88,7 @@ export const makeGraphEmbeddingsStoreSpecs = (): ReadonlyArray<Spec<GraphEmbeddi
"store-graph-embeddings-input", "store-graph-embeddings-input",
onGraphEmbeddingsStoreMessage, onGraphEmbeddingsStoreMessage,
), ),
makeRequestResponseSpec<EmbeddingsRequest, EmbeddingsResponse>( EmbeddingsClient,
"embeddings-client",
"embeddings-request",
"embeddings-response",
),
]; ];
export type GraphEmbeddingsStoreService = FlowProcessorRuntime<GraphEmbeddingsStoreRequirements>; export type GraphEmbeddingsStoreService = FlowProcessorRuntime<GraphEmbeddingsStoreRequirements>;