mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-03 15:01:00 +02:00
refactor(ts): make port effect native
This commit is contained in:
parent
2868ced2d3
commit
b6759e75df
113 changed files with 4140 additions and 4554 deletions
|
|
@ -11,32 +11,30 @@
|
|||
"test": "bunx --bun vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/ai-anthropic": "4.0.0-beta.75",
|
||||
"@effect/ai-openai": "4.0.0-beta.75",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.75",
|
||||
"@effect/atom-react": "4.0.0-beta.75",
|
||||
"@effect/openapi-generator": "4.0.0-beta.75",
|
||||
"@effect/opentelemetry": "4.0.0-beta.75",
|
||||
"@effect/platform-browser": "4.0.0-beta.75",
|
||||
"@effect/platform-bun": "4.0.0-beta.75",
|
||||
"@effect/platform-node": "4.0.0-beta.75",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.75",
|
||||
"@effect/tsgo": "0.13.0",
|
||||
"@effect/vitest": "4.0.0-beta.75",
|
||||
"@fastify/websocket": "^11.0.0",
|
||||
"@effect/ai-anthropic": "4.0.0-beta.78",
|
||||
"@effect/ai-openai": "4.0.0-beta.78",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.78",
|
||||
"@effect/atom-react": "4.0.0-beta.78",
|
||||
"@effect/openapi-generator": "4.0.0-beta.78",
|
||||
"@effect/opentelemetry": "4.0.0-beta.78",
|
||||
"@effect/platform-browser": "4.0.0-beta.78",
|
||||
"@effect/platform-bun": "4.0.0-beta.78",
|
||||
"@effect/platform-node": "4.0.0-beta.78",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.78",
|
||||
"@effect/tsgo": "0.14.0",
|
||||
"@effect/vitest": "4.0.0-beta.78",
|
||||
"@mistralai/mistralai": "^1.0.0",
|
||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||
"@qdrant/js-client-rest": "^1.13.0",
|
||||
"@trustgraph/base": "workspace:*",
|
||||
"effect": "4.0.0-beta.75",
|
||||
"effect": "4.0.0-beta.78",
|
||||
"falkordb": "^5.0.0",
|
||||
"fastify": "^5.2.0",
|
||||
"ollama": "^0.6.3",
|
||||
"openai": "^4.85.0",
|
||||
"pdfjs-dist": "^5.6.205"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/vitest": "4.0.0-beta.75",
|
||||
"@effect/vitest": "4.0.0-beta.78",
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^4.1.6"
|
||||
|
|
|
|||
|
|
@ -59,17 +59,19 @@ class RecordingProducer<T> implements BackendProducer<T> {
|
|||
closeCount = 0;
|
||||
flushCount = 0;
|
||||
|
||||
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
||||
this.sent.push(properties === undefined ? { message } : { message, properties });
|
||||
send(message: T, properties?: Record<string, string>): Effect.Effect<void> {
|
||||
return Effect.sync(() => {
|
||||
this.sent.push(properties === undefined ? { message } : { message, properties });
|
||||
});
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
readonly flush: Effect.Effect<void> = Effect.sync(() => {
|
||||
this.flushCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
async close(): Promise<void> {
|
||||
readonly close: Effect.Effect<void> = Effect.sync(() => {
|
||||
this.closeCount += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class PushConsumer<T> implements BackendConsumer<T> {
|
||||
|
|
@ -89,33 +91,39 @@ class PushConsumer<T> implements BackendConsumer<T> {
|
|||
this.messages.push(message);
|
||||
}
|
||||
|
||||
async receive(): Promise<Message<T> | null> {
|
||||
const message = this.messages.shift();
|
||||
if (message !== undefined || this.closed) {
|
||||
return message ?? null;
|
||||
}
|
||||
return await new Promise((resolve) => {
|
||||
this.waiters.push(resolve);
|
||||
receive(): Effect.Effect<Message<T> | null> {
|
||||
return Effect.promise(() => {
|
||||
const message = this.messages.shift();
|
||||
if (message !== undefined || this.closed) {
|
||||
return Promise.resolve(message ?? null);
|
||||
}
|
||||
return new Promise<Message<T> | null>((resolve) => {
|
||||
this.waiters.push(resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async acknowledge(message: Message<T>): Promise<void> {
|
||||
this.acknowledged.push(message);
|
||||
acknowledge(message: Message<T>): Effect.Effect<void> {
|
||||
return Effect.sync(() => {
|
||||
this.acknowledged.push(message);
|
||||
});
|
||||
}
|
||||
|
||||
async negativeAcknowledge(message: Message<T>): Promise<void> {
|
||||
this.nacked.push(message);
|
||||
negativeAcknowledge(message: Message<T>): Effect.Effect<void> {
|
||||
return Effect.sync(() => {
|
||||
this.nacked.push(message);
|
||||
});
|
||||
}
|
||||
|
||||
async unsubscribe(): Promise<void> {}
|
||||
readonly unsubscribe: Effect.Effect<void> = Effect.void;
|
||||
|
||||
async close(): Promise<void> {
|
||||
readonly close: Effect.Effect<void> = Effect.sync(() => {
|
||||
this.closed = true;
|
||||
for (const waiter of this.waiters.splice(0)) {
|
||||
waiter(null);
|
||||
}
|
||||
this.closeCount += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class ChunkingBackend implements PubSubBackend {
|
||||
|
|
@ -126,26 +134,30 @@ class ChunkingBackend implements PubSubBackend {
|
|||
readonly consumerOptions: Array<CreateConsumerOptions> = [];
|
||||
closeCount = 0;
|
||||
|
||||
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||
this.producerOptions.push(options);
|
||||
const producer = new RecordingProducer<unknown>();
|
||||
this.producersByTopic.set(options.topic, producer);
|
||||
return producer as BackendProducer<T>;
|
||||
createProducer<T>(options: CreateProducerOptions): Effect.Effect<BackendProducer<T>> {
|
||||
return Effect.sync(() => {
|
||||
this.producerOptions.push(options);
|
||||
const producer = new RecordingProducer<unknown>();
|
||||
this.producersByTopic.set(options.topic, producer);
|
||||
return producer as BackendProducer<T>;
|
||||
});
|
||||
}
|
||||
|
||||
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||
this.consumerOptions.push(options);
|
||||
if (options.topic === topics.configPush) {
|
||||
return this.configConsumer as unknown as BackendConsumer<T>;
|
||||
}
|
||||
const consumer = new PushConsumer<unknown>();
|
||||
this.consumersByTopic.set(options.topic, consumer);
|
||||
return consumer as BackendConsumer<T>;
|
||||
createConsumer<T>(options: CreateConsumerOptions): Effect.Effect<BackendConsumer<T>> {
|
||||
return Effect.sync(() => {
|
||||
this.consumerOptions.push(options);
|
||||
if (options.topic === topics.configPush) {
|
||||
return this.configConsumer as unknown as BackendConsumer<T>;
|
||||
}
|
||||
const consumer = new PushConsumer<unknown>();
|
||||
this.consumersByTopic.set(options.topic, consumer);
|
||||
return consumer as BackendConsumer<T>;
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
readonly close: Effect.Effect<void> = Effect.sync(() => {
|
||||
this.closeCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
pushConfig(): void {
|
||||
this.configConsumer.push(
|
||||
|
|
|
|||
|
|
@ -20,29 +20,29 @@ import type {
|
|||
class NoopPubSub implements PubSubBackend {
|
||||
readonly sentByTopic = new Map<string, Array<unknown>>();
|
||||
|
||||
async createProducer<T>(options: CreateProducerOptions<T>): Promise<BackendProducer<T>> {
|
||||
return {
|
||||
send: async (message) => {
|
||||
createProducer<T>(options: CreateProducerOptions<T>): Effect.Effect<BackendProducer<T>> {
|
||||
return Effect.succeed({
|
||||
send: (message) => Effect.sync(() => {
|
||||
const sent = this.sentByTopic.get(options.topic) ?? [];
|
||||
sent.push(message);
|
||||
this.sentByTopic.set(options.topic, sent);
|
||||
},
|
||||
flush: async () => undefined,
|
||||
close: async () => undefined,
|
||||
};
|
||||
}),
|
||||
flush: Effect.void,
|
||||
close: Effect.void,
|
||||
});
|
||||
}
|
||||
|
||||
async createConsumer<T>(_options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||
return {
|
||||
receive: async () => null,
|
||||
acknowledge: async () => undefined,
|
||||
negativeAcknowledge: async () => undefined,
|
||||
unsubscribe: async () => undefined,
|
||||
close: async () => undefined,
|
||||
};
|
||||
createConsumer<T>(_options: CreateConsumerOptions): Effect.Effect<BackendConsumer<T>> {
|
||||
return Effect.succeed({
|
||||
receive: () => Effect.succeed(null),
|
||||
acknowledge: () => Effect.void,
|
||||
negativeAcknowledge: () => Effect.void,
|
||||
unsubscribe: Effect.void,
|
||||
close: Effect.void,
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
readonly close: Effect.Effect<void> = Effect.void;
|
||||
}
|
||||
|
||||
const makeService = (persistPath?: string) =>
|
||||
|
|
@ -59,9 +59,9 @@ describe("ConfigService operations", () => {
|
|||
const putRequest: ConfigRequest = { operation: "put" };
|
||||
const deleteRequest: ConfigRequest = { operation: "delete" };
|
||||
|
||||
const putError = await service.handlePut(putRequest)
|
||||
const putError = await Effect.runPromise(service.handlePutEffect(putRequest))
|
||||
.catch((caught: unknown) => caught);
|
||||
const deleteError = await service.handleDelete(deleteRequest)
|
||||
const deleteError = await Effect.runPromise(service.handleDeleteEffect(deleteRequest))
|
||||
.catch((caught: unknown) => caught);
|
||||
|
||||
expect(putError).toBeInstanceOf(ConfigServiceError);
|
||||
|
|
@ -81,7 +81,7 @@ describe("ConfigService operations", () => {
|
|||
],
|
||||
};
|
||||
|
||||
await service.handlePut(putRequest);
|
||||
await Effect.runPromise(service.handlePutEffect(putRequest));
|
||||
|
||||
const persisted = await Bun.file(persistPath).json();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
|
|
@ -107,7 +107,7 @@ describe("ConfigService operations", () => {
|
|||
);
|
||||
const service = makeService(persistPath);
|
||||
|
||||
await service.loadFromDisk();
|
||||
await Effect.runPromise(service.loadFromDiskEffect);
|
||||
const getRequest: ConfigRequest = {
|
||||
operation: "get",
|
||||
keys: ["prompt", "system"],
|
||||
|
|
@ -131,7 +131,10 @@ describe("ConfigService operations", () => {
|
|||
{ operation: "put", values: [{ workspace: "beta", type: "prompt", key: "c", value: "three" }] },
|
||||
];
|
||||
|
||||
await Promise.all(requests.map((request) => service.handlePut(request)));
|
||||
await Effect.runPromise(Effect.all(requests.map((request) => service.handlePutEffect(request)), {
|
||||
concurrency: "unbounded",
|
||||
discard: true,
|
||||
}));
|
||||
|
||||
expect(service.handleGet({ operation: "get", keys: ["prompt"] })).toEqual({
|
||||
version: 3,
|
||||
|
|
@ -150,53 +153,53 @@ describe("ConfigService operations", () => {
|
|||
it("dispatches all config operations through the Match-backed handler", async () => {
|
||||
const service = makeService();
|
||||
|
||||
await expect(service.handleOperation({ operation: "put" })).rejects.toMatchObject({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({ operation: "put" }))).rejects.toMatchObject({
|
||||
_tag: "ConfigServiceError",
|
||||
operation: "put",
|
||||
});
|
||||
await expect(service.handleOperation({ operation: "delete" })).rejects.toMatchObject({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({ operation: "delete" }))).rejects.toMatchObject({
|
||||
_tag: "ConfigServiceError",
|
||||
operation: "delete",
|
||||
});
|
||||
|
||||
await expect(service.handleOperation({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({
|
||||
operation: "put",
|
||||
values: [{ type: "prompt", key: "system", value: "hello" }],
|
||||
})).resolves.toEqual({ version: 1 });
|
||||
}))).resolves.toEqual({ version: 1 });
|
||||
|
||||
await expect(service.handleOperation({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({
|
||||
operation: "get",
|
||||
keys: ["prompt", "system"],
|
||||
})).resolves.toEqual({
|
||||
}))).resolves.toEqual({
|
||||
version: 1,
|
||||
values: { system: "hello" },
|
||||
});
|
||||
await expect(service.handleOperation({ operation: "list" })).resolves.toEqual({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({ operation: "list" }))).resolves.toEqual({
|
||||
version: 1,
|
||||
directory: ["prompt"],
|
||||
});
|
||||
await expect(service.handleOperation({ operation: "config" })).resolves.toEqual({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({ operation: "config" }))).resolves.toEqual({
|
||||
version: 1,
|
||||
config: { prompt: { system: "hello" } },
|
||||
});
|
||||
await expect(service.handleOperation({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({
|
||||
operation: "getvalues",
|
||||
type: "prompt",
|
||||
})).resolves.toEqual({
|
||||
}))).resolves.toEqual({
|
||||
version: 1,
|
||||
values: [{ type: "prompt", key: "system", value: "hello" }],
|
||||
});
|
||||
await expect(service.handleOperation({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({
|
||||
operation: "getvalues-all-ws",
|
||||
type: "prompt",
|
||||
})).resolves.toEqual({
|
||||
}))).resolves.toEqual({
|
||||
version: 1,
|
||||
values: [{ workspace: "default", type: "prompt", key: "system", value: "hello" }],
|
||||
});
|
||||
await expect(service.handleOperation({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({
|
||||
operation: "delete",
|
||||
keys: ["prompt", "system"],
|
||||
})).resolves.toEqual({ version: 2 });
|
||||
}))).resolves.toEqual({ version: 2 });
|
||||
});
|
||||
|
||||
it("pushes config from the stored producer handle", async () => {
|
||||
|
|
@ -206,10 +209,10 @@ describe("ConfigService operations", () => {
|
|||
manageProcessSignals: false,
|
||||
pubsub: backend,
|
||||
});
|
||||
const pushProducer = await backend.createProducer<{
|
||||
const pushProducer = await Effect.runPromise(backend.createProducer<{
|
||||
readonly version: number;
|
||||
readonly config: Record<string, unknown>;
|
||||
}>({ topic: topics.configPush });
|
||||
}>({ topic: topics.configPush }));
|
||||
|
||||
await Effect.runPromise(
|
||||
SynchronizedRef.update(service.state, (state) => ({
|
||||
|
|
@ -217,11 +220,11 @@ describe("ConfigService operations", () => {
|
|||
pushProducer,
|
||||
})),
|
||||
);
|
||||
await service.pushConfig();
|
||||
await service.handlePut({
|
||||
await Effect.runPromise(service.pushConfigEffect);
|
||||
await Effect.runPromise(service.handlePutEffect({
|
||||
operation: "put",
|
||||
values: [{ type: "prompt", key: "system", value: "hello" }],
|
||||
});
|
||||
}));
|
||||
|
||||
expect(backend.sentByTopic.get(topics.configPush)).toEqual([
|
||||
{ version: 0, config: {} },
|
||||
|
|
|
|||
|
|
@ -17,30 +17,34 @@ class FakeFalkorDBClient implements FalkorDBStoreClient, FalkorDBQueryClient {
|
|||
connectCount = 0;
|
||||
disconnectCount = 0;
|
||||
|
||||
async connect(): Promise<void> {
|
||||
readonly connect: Effect.Effect<void> = Effect.sync(() => {
|
||||
this.connectCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
readonly disconnect: Effect.Effect<void> = Effect.sync(() => {
|
||||
this.disconnectCount += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class FakeStoreGraph implements FalkorDBStoreGraph {
|
||||
readonly queries: string[] = [];
|
||||
|
||||
async query<T = unknown>(query: string): Promise<{ readonly data?: Array<T> }> {
|
||||
this.queries.push(query);
|
||||
return {};
|
||||
query<T = unknown>(query: string): Effect.Effect<{ readonly data?: Array<T> }> {
|
||||
return Effect.sync(() => {
|
||||
this.queries.push(query);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class FakeQueryGraph implements FalkorDBQueryGraph {
|
||||
readonly queries: string[] = [];
|
||||
|
||||
async query<T = unknown>(query: string): Promise<{ readonly data?: Array<T> }> {
|
||||
this.queries.push(query);
|
||||
return {};
|
||||
query<T = unknown>(query: string): Effect.Effect<{ readonly data?: Array<T> }> {
|
||||
return Effect.sync(() => {
|
||||
this.queries.push(query);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,29 +19,29 @@ import {FlowManagerError, makeFlowManagerService} from "../flow-manager/service.
|
|||
class NoopPubSub implements PubSubBackend {
|
||||
readonly sentByTopic = new Map<string, Array<unknown>>();
|
||||
|
||||
async createProducer<T>(options: CreateProducerOptions<T>): Promise<BackendProducer<T>> {
|
||||
return {
|
||||
send: async (message) => {
|
||||
createProducer<T>(options: CreateProducerOptions<T>): Effect.Effect<BackendProducer<T>> {
|
||||
return Effect.succeed({
|
||||
send: (message) => Effect.sync(() => {
|
||||
const sent = this.sentByTopic.get(options.topic) ?? [];
|
||||
sent.push(message);
|
||||
this.sentByTopic.set(options.topic, sent);
|
||||
},
|
||||
flush: async () => undefined,
|
||||
close: async () => undefined,
|
||||
};
|
||||
}),
|
||||
flush: Effect.void,
|
||||
close: Effect.void,
|
||||
});
|
||||
}
|
||||
|
||||
async createConsumer<T>(_options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||
return {
|
||||
receive: async () => null,
|
||||
acknowledge: async () => undefined,
|
||||
negativeAcknowledge: async () => undefined,
|
||||
unsubscribe: async () => undefined,
|
||||
close: async () => undefined,
|
||||
};
|
||||
createConsumer<T>(_options: CreateConsumerOptions): Effect.Effect<BackendConsumer<T>> {
|
||||
return Effect.succeed({
|
||||
receive: () => Effect.succeed(null),
|
||||
acknowledge: () => Effect.void,
|
||||
negativeAcknowledge: () => Effect.void,
|
||||
unsubscribe: Effect.void,
|
||||
close: Effect.void,
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
readonly close: Effect.Effect<void> = Effect.void;
|
||||
}
|
||||
|
||||
class RecordingConfigClient implements RequestResponse<ConfigRequest, ConfigResponse> {
|
||||
|
|
@ -53,25 +53,27 @@ class RecordingConfigClient implements RequestResponse<ConfigRequest, ConfigResp
|
|||
private readonly legacyFlows: Array<{readonly key: string; readonly value: unknown}> = [],
|
||||
) {}
|
||||
|
||||
async start(): Promise<void> {}
|
||||
readonly start: Effect.Effect<void> = Effect.void;
|
||||
|
||||
async stop(): Promise<void> {}
|
||||
readonly stop: Effect.Effect<void> = Effect.void;
|
||||
|
||||
async request(request: ConfigRequest): Promise<ConfigResponse> {
|
||||
this.requests.push(request);
|
||||
if (request.operation !== "getvalues") return {};
|
||||
request(request: ConfigRequest): Effect.Effect<ConfigResponse> {
|
||||
return Effect.sync(() => {
|
||||
this.requests.push(request);
|
||||
if (request.operation !== "getvalues") return {};
|
||||
|
||||
if (request.type === "flow-blueprint") {
|
||||
return {values: this.blueprints};
|
||||
}
|
||||
if (request.type === "flow") {
|
||||
return {values: this.flows};
|
||||
}
|
||||
if (request.type === "flows") {
|
||||
return {values: this.legacyFlows};
|
||||
}
|
||||
if (request.type === "flow-blueprint") {
|
||||
return {values: this.blueprints};
|
||||
}
|
||||
if (request.type === "flow") {
|
||||
return {values: this.flows};
|
||||
}
|
||||
if (request.type === "flows") {
|
||||
return {values: this.legacyFlows};
|
||||
}
|
||||
|
||||
return {values: []};
|
||||
return {values: []};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,9 +99,9 @@ const seedResponseProducer = async (
|
|||
backend: NoopPubSub,
|
||||
service: ReturnType<typeof makeFlowManagerService>,
|
||||
) => {
|
||||
const responseProducer = await backend.createProducer<FlowResponse>({
|
||||
const responseProducer = await Effect.runPromise(backend.createProducer<FlowResponse>({
|
||||
topic: topics.flowResponse,
|
||||
});
|
||||
}));
|
||||
await Effect.runPromise(
|
||||
SynchronizedRef.update(service.state, (state) => ({
|
||||
...state,
|
||||
|
|
@ -127,43 +129,43 @@ describe("FlowManagerService operations", () => {
|
|||
const service = makeService();
|
||||
await seedConfigClient(service, configClient);
|
||||
|
||||
await expect(service.handleOperation({operation: "list-blueprints"})).resolves.toEqual({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({operation: "list-blueprints"}))).resolves.toEqual({
|
||||
"blueprint-names": ["custom", "default"],
|
||||
});
|
||||
await expect(service.handleOperation({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({
|
||||
operation: "get-blueprint",
|
||||
"blueprint-name": "custom",
|
||||
})).resolves.toMatchObject({
|
||||
}))).resolves.toMatchObject({
|
||||
"blueprint-definition": "{\"description\":\"Custom\",\"topics\":{\"input\":\"topic.in\"}}",
|
||||
});
|
||||
await expect(service.handleOperation({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({
|
||||
operation: "put-blueprint",
|
||||
"blueprint-name": "added",
|
||||
"blueprint-definition": {description: "Added", topics: {input: "topic.added"}},
|
||||
})).resolves.toEqual({});
|
||||
await expect(service.handleOperation({
|
||||
}))).resolves.toEqual({});
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({
|
||||
operation: "delete-blueprint",
|
||||
"blueprint-name": "custom",
|
||||
})).resolves.toEqual({});
|
||||
await expect(service.handleOperation({operation: "list-flows"})).resolves.toEqual({
|
||||
}))).resolves.toEqual({});
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({operation: "list-flows"}))).resolves.toEqual({
|
||||
"flow-ids": ["flow-a"],
|
||||
});
|
||||
await expect(service.handleOperation({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({
|
||||
operation: "get-flow",
|
||||
"flow-id": "flow-a",
|
||||
})).resolves.toEqual({
|
||||
}))).resolves.toEqual({
|
||||
flow: "{\"blueprint-name\":\"custom\",\"description\":\"Alpha\",\"parameters\":{\"limit\":3}}",
|
||||
});
|
||||
await expect(service.handleOperation({
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({
|
||||
operation: "start-flow",
|
||||
"flow-id": "flow-b",
|
||||
"blueprint-name": "custom",
|
||||
})).resolves.toEqual({});
|
||||
await expect(service.handleOperation({
|
||||
}))).resolves.toEqual({});
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({
|
||||
operation: "stop-flow",
|
||||
"flow-id": "flow-a",
|
||||
})).resolves.toEqual({});
|
||||
await expect(service.handleOperation({operation: "unknown-flow"})).rejects.toMatchObject({
|
||||
}))).resolves.toEqual({});
|
||||
await expect(Effect.runPromise(service.handleOperationEffect({operation: "unknown-flow"}))).rejects.toMatchObject({
|
||||
_tag: "FlowManagerError",
|
||||
operation: "operation",
|
||||
message: "Unknown flow operation: unknown-flow",
|
||||
|
|
@ -180,9 +182,9 @@ describe("FlowManagerService operations", () => {
|
|||
it("uses tagged errors for invalid flow mutations", async () => {
|
||||
const service = makeService();
|
||||
|
||||
const startError = await service.handleStartFlow({operation: "start-flow"})
|
||||
const startError = await Effect.runPromise(service.handleStartFlowEffect({operation: "start-flow"}))
|
||||
.catch((caught: unknown) => caught);
|
||||
const stopError = await service.handleStopFlow({operation: "stop-flow"})
|
||||
const stopError = await Effect.runPromise(service.handleStopFlowEffect({operation: "stop-flow"}))
|
||||
.catch((caught: unknown) => caught);
|
||||
|
||||
expect(startError).toBeInstanceOf(FlowManagerError);
|
||||
|
|
@ -196,12 +198,12 @@ describe("FlowManagerService operations", () => {
|
|||
const service = makeService();
|
||||
await seedConfigClient(service, configClient);
|
||||
|
||||
await service.handleStartFlow({
|
||||
await Effect.runPromise(service.handleStartFlowEffect({
|
||||
operation: "start-flow",
|
||||
"flow-id": "flow-a",
|
||||
description: "alpha",
|
||||
parameters: {limit: 3},
|
||||
});
|
||||
}));
|
||||
let state = await Effect.runPromise(SynchronizedRef.get(service.state));
|
||||
expect(Option.getOrUndefined(HashMap.get(state.flows, "flow-a"))).toMatchObject({
|
||||
id: "flow-a",
|
||||
|
|
@ -211,10 +213,10 @@ describe("FlowManagerService operations", () => {
|
|||
status: "running",
|
||||
});
|
||||
|
||||
await service.handleStopFlow({
|
||||
await Effect.runPromise(service.handleStopFlowEffect({
|
||||
operation: "stop-flow",
|
||||
"flow-id": "flow-a",
|
||||
});
|
||||
}));
|
||||
state = await Effect.runPromise(SynchronizedRef.get(service.state));
|
||||
|
||||
expect(HashMap.has(state.flows, "flow-a")).toBe(false);
|
||||
|
|
@ -245,7 +247,7 @@ describe("FlowManagerService operations", () => {
|
|||
const service = makeService();
|
||||
await seedConfigClient(service, configClient);
|
||||
|
||||
await service.refreshBlueprintsFromConfig();
|
||||
await Effect.runPromise(service.refreshBlueprintsFromConfigEffect);
|
||||
const state = await Effect.runPromise(SynchronizedRef.get(service.state));
|
||||
|
||||
expect(Option.getOrUndefined(HashMap.get(state.blueprints, "custom"))).toMatchObject({
|
||||
|
|
@ -263,8 +265,8 @@ describe("FlowManagerService operations", () => {
|
|||
await seedConfigClient(service, configClient);
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
service.handleStartFlow({operation: "start-flow", "flow-id": "flow-a"}),
|
||||
service.handleStartFlow({operation: "start-flow", "flow-id": "flow-a"}),
|
||||
Effect.runPromise(service.handleStartFlowEffect({operation: "start-flow", "flow-id": "flow-a"})),
|
||||
Effect.runPromise(service.handleStartFlowEffect({operation: "start-flow", "flow-id": "flow-a"})),
|
||||
]);
|
||||
const state = await Effect.runPromise(SynchronizedRef.get(service.state));
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import type {
|
|||
Message,
|
||||
PubSubBackend,
|
||||
} from "@trustgraph/base";
|
||||
import { pubSubError } from "@trustgraph/base";
|
||||
|
||||
function createMessage<T>(value: T, properties: Record<string, string> = {}): Message<T> {
|
||||
return {
|
||||
|
|
@ -44,32 +45,38 @@ class TopicConsumer<T> implements BackendConsumer<T> {
|
|||
this.messages.push(message);
|
||||
}
|
||||
|
||||
async receive(): Promise<Message<T> | null> {
|
||||
const message = this.messages.shift();
|
||||
if (message !== undefined || this.closed) return message ?? null;
|
||||
receive(): Effect.Effect<Message<T> | null> {
|
||||
return Effect.promise(() => {
|
||||
const message = this.messages.shift();
|
||||
if (message !== undefined || this.closed) return Promise.resolve(message ?? null);
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
this.waiters.push(resolve);
|
||||
return new Promise((resolve) => {
|
||||
this.waiters.push(resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async acknowledge(message: Message<T>): Promise<void> {
|
||||
this.acknowledged.push(message);
|
||||
acknowledge(message: Message<T>): Effect.Effect<void> {
|
||||
return Effect.sync(() => {
|
||||
this.acknowledged.push(message);
|
||||
});
|
||||
}
|
||||
|
||||
async negativeAcknowledge(message: Message<T>): Promise<void> {
|
||||
this.nacked.push(message);
|
||||
negativeAcknowledge(message: Message<T>): Effect.Effect<void> {
|
||||
return Effect.sync(() => {
|
||||
this.nacked.push(message);
|
||||
});
|
||||
}
|
||||
|
||||
async unsubscribe(): Promise<void> {}
|
||||
readonly unsubscribe: Effect.Effect<void> = Effect.void;
|
||||
|
||||
async close(): Promise<void> {
|
||||
readonly close: Effect.Effect<void> = Effect.sync(() => {
|
||||
this.closed = true;
|
||||
for (const waiter of this.waiters.splice(0)) {
|
||||
waiter(null);
|
||||
}
|
||||
this.closeCount += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class RecordingProducer<T> implements BackendProducer<T> {
|
||||
|
|
@ -82,18 +89,23 @@ class RecordingProducer<T> implements BackendProducer<T> {
|
|||
private readonly onSend: (topic: string, message: T, properties?: Record<string, string>) => void,
|
||||
) {}
|
||||
|
||||
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
||||
this.sent.push(properties === undefined ? { message } : { message, properties });
|
||||
this.onSend(this.topic, message, properties);
|
||||
send(message: T, properties?: Record<string, string>): Effect.Effect<void> {
|
||||
return Effect.try({
|
||||
try: () => {
|
||||
this.sent.push(properties === undefined ? { message } : { message, properties });
|
||||
this.onSend(this.topic, message, properties);
|
||||
},
|
||||
catch: (error) => pubSubError(`send:${this.topic}`, error),
|
||||
});
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
readonly flush: Effect.Effect<void> = Effect.sync(() => {
|
||||
this.flushCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
async close(): Promise<void> {
|
||||
readonly close: Effect.Effect<void> = Effect.sync(() => {
|
||||
this.closeCount += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class DispatchBackend implements PubSubBackend {
|
||||
|
|
@ -104,31 +116,35 @@ class DispatchBackend implements PubSubBackend {
|
|||
readonly consumersByTopic = new Map<string, TopicConsumer<unknown>>();
|
||||
readonly failSendTopics = new Set<string>();
|
||||
|
||||
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||
this.producerOptions.push(options);
|
||||
let producer = this.producersByTopic.get(options.topic);
|
||||
if (producer === undefined) {
|
||||
producer = new RecordingProducer<unknown>(options.topic, (topic, message, properties) => {
|
||||
this.handleSend(topic, message, properties);
|
||||
});
|
||||
this.producersByTopic.set(options.topic, producer);
|
||||
}
|
||||
return producer as BackendProducer<T>;
|
||||
createProducer<T>(options: CreateProducerOptions): Effect.Effect<BackendProducer<T>> {
|
||||
return Effect.sync(() => {
|
||||
this.producerOptions.push(options);
|
||||
let producer = this.producersByTopic.get(options.topic);
|
||||
if (producer === undefined) {
|
||||
producer = new RecordingProducer<unknown>(options.topic, (topic, message, properties) => {
|
||||
this.handleSend(topic, message, properties);
|
||||
});
|
||||
this.producersByTopic.set(options.topic, producer);
|
||||
}
|
||||
return producer as BackendProducer<T>;
|
||||
});
|
||||
}
|
||||
|
||||
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||
this.consumerOptions.push(options);
|
||||
let consumer = this.consumersByTopic.get(options.topic);
|
||||
if (consumer === undefined) {
|
||||
consumer = new TopicConsumer<unknown>();
|
||||
this.consumersByTopic.set(options.topic, consumer);
|
||||
}
|
||||
return consumer as BackendConsumer<T>;
|
||||
createConsumer<T>(options: CreateConsumerOptions): Effect.Effect<BackendConsumer<T>> {
|
||||
return Effect.sync(() => {
|
||||
this.consumerOptions.push(options);
|
||||
let consumer = this.consumersByTopic.get(options.topic);
|
||||
if (consumer === undefined) {
|
||||
consumer = new TopicConsumer<unknown>();
|
||||
this.consumersByTopic.set(options.topic, consumer);
|
||||
}
|
||||
return consumer as BackendConsumer<T>;
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
readonly close: Effect.Effect<void> = Effect.sync(() => {
|
||||
this.closeCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
private handleSend(topic: string, message: unknown, properties?: Record<string, string>): void {
|
||||
if (this.failSendTopics.has(topic)) {
|
||||
|
|
@ -230,10 +246,10 @@ describe("gateway dispatcher manager", () => {
|
|||
pubsub: backend,
|
||||
});
|
||||
|
||||
await manager.start();
|
||||
const first = await manager.dispatchGlobalService("config", { operation: "get" });
|
||||
const second = await manager.dispatchGlobalService("config", { operation: "list" });
|
||||
await manager.stop();
|
||||
await Effect.runPromise(manager.start);
|
||||
const first = await Effect.runPromise(manager.dispatchGlobalService("config", { operation: "get" }));
|
||||
const second = await Effect.runPromise(manager.dispatchGlobalService("config", { operation: "list" }));
|
||||
await Effect.runPromise(manager.stop);
|
||||
|
||||
expect(first).toEqual({ ok: true, echo: { operation: "get" } });
|
||||
expect(second).toEqual({ ok: true, echo: { operation: "list" } });
|
||||
|
|
@ -252,12 +268,12 @@ describe("gateway dispatcher manager", () => {
|
|||
pubsub: backend,
|
||||
});
|
||||
|
||||
await manager.start();
|
||||
const [first, second] = await Promise.all([
|
||||
await Effect.runPromise(manager.start);
|
||||
const [first, second] = await Effect.runPromise(Effect.all([
|
||||
manager.dispatchGlobalService("config", { operation: "get" }),
|
||||
manager.dispatchGlobalService("config", { operation: "list" }),
|
||||
]);
|
||||
await manager.stop();
|
||||
], { concurrency: "unbounded" }));
|
||||
await Effect.runPromise(manager.stop);
|
||||
|
||||
expect(first).toEqual({ ok: true, echo: { operation: "get" } });
|
||||
expect(second).toEqual({ ok: true, echo: { operation: "list" } });
|
||||
|
|
@ -274,12 +290,12 @@ describe("gateway dispatcher manager", () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
manager.dispatchGlobalService("knowledge", { term: { t: "t" } }),
|
||||
Effect.runPromise(manager.dispatchGlobalService("knowledge", { term: { t: "t" } })),
|
||||
).rejects.toMatchObject({
|
||||
_tag: "DispatchSerializationError",
|
||||
operation: "client-term-to-internal",
|
||||
});
|
||||
await manager.stop();
|
||||
await Effect.runPromise(manager.stop);
|
||||
|
||||
expect(backend.producerOptions).toHaveLength(0);
|
||||
expect(backend.consumerOptions).toHaveLength(0);
|
||||
|
|
@ -296,12 +312,12 @@ describe("gateway dispatcher manager", () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
manager.publishToTopic("tg.flow.ingest", { text: "hello" }, "msg-1"),
|
||||
Effect.runPromise(manager.publishToTopic("tg.flow.ingest", { text: "hello" }, "msg-1")),
|
||||
).rejects.toMatchObject({
|
||||
_tag: "MessagingDeliveryError",
|
||||
operation: "send",
|
||||
});
|
||||
await manager.stop();
|
||||
await Effect.runPromise(manager.stop);
|
||||
|
||||
expect(backend.producersByTopic.get("tg.flow.ingest")?.closeCount).toBe(1);
|
||||
expect(backend.closeCount).toBe(0);
|
||||
|
|
@ -316,10 +332,14 @@ describe("gateway dispatcher manager", () => {
|
|||
});
|
||||
const chunks: Array<{ readonly response: unknown; readonly complete: boolean }> = [];
|
||||
|
||||
await manager.dispatchGlobalServiceStreaming("knowledge", { query: "hello" }, async (response, complete) => {
|
||||
chunks.push({ response, complete });
|
||||
});
|
||||
await manager.stop();
|
||||
await Effect.runPromise(
|
||||
manager.dispatchGlobalServiceStreaming("knowledge", { query: "hello" }, (response, complete) =>
|
||||
Effect.sync(() => {
|
||||
chunks.push({ response, complete });
|
||||
})
|
||||
),
|
||||
);
|
||||
await Effect.runPromise(manager.stop);
|
||||
|
||||
expect(chunks).toEqual([
|
||||
{ response: { chunk: 1 }, complete: false },
|
||||
|
|
@ -337,13 +357,13 @@ describe("gateway dispatcher manager", () => {
|
|||
const chunks: Array<{ readonly response: unknown; readonly complete: boolean }> = [];
|
||||
|
||||
await Effect.runPromise(
|
||||
manager.dispatchGlobalServiceStreamingEffect("knowledge", { query: "hello" }, (response, complete) =>
|
||||
manager.dispatchGlobalServiceStreaming("knowledge", { query: "hello" }, (response, complete) =>
|
||||
Effect.sync(() => {
|
||||
chunks.push({ response, complete });
|
||||
})
|
||||
),
|
||||
);
|
||||
await manager.stop();
|
||||
await Effect.runPromise(manager.stop);
|
||||
|
||||
expect(chunks).toEqual([
|
||||
{ response: { chunk: 1 }, complete: false },
|
||||
|
|
|
|||
|
|
@ -20,29 +20,29 @@ import {makeKnowledgeCoreService} from "../cores/service.js";
|
|||
class NoopPubSub implements PubSubBackend {
|
||||
readonly sentByTopic = new Map<string, Array<unknown>>();
|
||||
|
||||
async createProducer<T>(options: CreateProducerOptions<T>): Promise<BackendProducer<T>> {
|
||||
return {
|
||||
send: async (message) => {
|
||||
createProducer<T>(options: CreateProducerOptions<T>): Effect.Effect<BackendProducer<T>> {
|
||||
return Effect.succeed({
|
||||
send: (message) => Effect.sync(() => {
|
||||
const sent = this.sentByTopic.get(options.topic) ?? [];
|
||||
sent.push(message);
|
||||
this.sentByTopic.set(options.topic, sent);
|
||||
},
|
||||
flush: async () => undefined,
|
||||
close: async () => undefined,
|
||||
};
|
||||
}),
|
||||
flush: Effect.void,
|
||||
close: Effect.void,
|
||||
});
|
||||
}
|
||||
|
||||
async createConsumer<T>(_options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||
return {
|
||||
receive: async () => null,
|
||||
acknowledge: async (_message: Message<T>) => undefined,
|
||||
negativeAcknowledge: async (_message: Message<T>) => undefined,
|
||||
unsubscribe: async () => undefined,
|
||||
close: async () => undefined,
|
||||
};
|
||||
createConsumer<T>(_options: CreateConsumerOptions): Effect.Effect<BackendConsumer<T>> {
|
||||
return Effect.succeed({
|
||||
receive: () => Effect.succeed(null),
|
||||
acknowledge: (_message: Message<T>) => Effect.void,
|
||||
negativeAcknowledge: (_message: Message<T>) => Effect.void,
|
||||
unsubscribe: Effect.void,
|
||||
close: Effect.void,
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
readonly close: Effect.Effect<void> = Effect.void;
|
||||
}
|
||||
|
||||
const sampleTriple: Triple = {
|
||||
|
|
@ -63,9 +63,9 @@ const seedResponseProducer = async (
|
|||
backend: NoopPubSub,
|
||||
service: ReturnType<typeof makeKnowledgeCoreService>,
|
||||
) => {
|
||||
const responseProducer = await backend.createProducer<KnowledgeResponse>({
|
||||
const responseProducer = await Effect.runPromise(backend.createProducer<KnowledgeResponse>({
|
||||
topic: topics.knowledgeResponse,
|
||||
});
|
||||
}));
|
||||
await Effect.runPromise(
|
||||
SynchronizedRef.update(service.state, (state) => ({
|
||||
...state,
|
||||
|
|
@ -94,15 +94,15 @@ describe("KnowledgeCoreService operations", () => {
|
|||
],
|
||||
};
|
||||
|
||||
await service.putKgCore(request, "put-1");
|
||||
await Effect.runPromise(service.putKgCoreEffect(request, "put-1"));
|
||||
const state = await Effect.runPromise(SynchronizedRef.get(service.state));
|
||||
const core = Option.getOrUndefined(HashMap.get(state.kgCores, "alice:core-a"));
|
||||
|
||||
await service.getKgCore({
|
||||
await Effect.runPromise(service.getKgCoreEffect({
|
||||
operation: "get-kg-core",
|
||||
user: "alice",
|
||||
id: "core-a",
|
||||
}, "get-1");
|
||||
}, "get-1"));
|
||||
await rm(dir, {recursive: true, force: true});
|
||||
|
||||
expect(core?.triples).toEqual([sampleTriple]);
|
||||
|
|
@ -142,14 +142,14 @@ describe("KnowledgeCoreService operations", () => {
|
|||
const service = makeService(dir, backend);
|
||||
await seedResponseProducer(backend, service);
|
||||
|
||||
await Promise.all([
|
||||
service.putKgCore({
|
||||
await Effect.runPromise(Effect.all([
|
||||
service.putKgCoreEffect({
|
||||
operation: "put-kg-core",
|
||||
user: "alice",
|
||||
id: "core-b",
|
||||
triples: [sampleTriple],
|
||||
}, "put-a"),
|
||||
service.putKgCore({
|
||||
service.putKgCoreEffect({
|
||||
operation: "put-kg-core",
|
||||
user: "alice",
|
||||
id: "core-b",
|
||||
|
|
@ -161,7 +161,10 @@ describe("KnowledgeCoreService operations", () => {
|
|||
},
|
||||
],
|
||||
}, "put-b"),
|
||||
]);
|
||||
], {
|
||||
concurrency: "unbounded",
|
||||
discard: true,
|
||||
}));
|
||||
|
||||
const state = await Effect.runPromise(SynchronizedRef.get(service.state));
|
||||
await rm(dir, {recursive: true, force: true});
|
||||
|
|
@ -183,7 +186,7 @@ describe("KnowledgeCoreService operations", () => {
|
|||
);
|
||||
const service = makeService(dir);
|
||||
|
||||
await service.loadFromDisk();
|
||||
await Effect.runPromise(service.loadFromDiskEffect);
|
||||
const state = await Effect.runPromise(SynchronizedRef.get(service.state));
|
||||
await rm(dir, {recursive: true, force: true});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {mkdtemp, rm} from "node:fs/promises";
|
||||
import {tmpdir} from "node:os";
|
||||
import {join} from "node:path";
|
||||
import {Effect} from "effect";
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {
|
||||
type BackendConsumer,
|
||||
|
|
@ -15,25 +16,25 @@ import {
|
|||
import {makeLibrarianService} from "../librarian/service.js";
|
||||
|
||||
class NoopPubSub implements PubSubBackend {
|
||||
async createProducer<T>(_options: CreateProducerOptions<T>): Promise<BackendProducer<T>> {
|
||||
return {
|
||||
send: async () => undefined,
|
||||
flush: async () => undefined,
|
||||
close: async () => undefined,
|
||||
};
|
||||
createProducer<T>(_options: CreateProducerOptions<T>): Effect.Effect<BackendProducer<T>> {
|
||||
return Effect.succeed({
|
||||
send: () => Effect.void,
|
||||
flush: Effect.void,
|
||||
close: Effect.void,
|
||||
});
|
||||
}
|
||||
|
||||
async createConsumer<T>(_options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||
return {
|
||||
receive: async () => null,
|
||||
acknowledge: async (_message: Message<T>) => undefined,
|
||||
negativeAcknowledge: async (_message: Message<T>) => undefined,
|
||||
unsubscribe: async () => undefined,
|
||||
close: async () => undefined,
|
||||
};
|
||||
createConsumer<T>(_options: CreateConsumerOptions): Effect.Effect<BackendConsumer<T>> {
|
||||
return Effect.succeed({
|
||||
receive: () => Effect.succeed(null),
|
||||
acknowledge: (_message: Message<T>) => Effect.void,
|
||||
negativeAcknowledge: (_message: Message<T>) => Effect.void,
|
||||
unsubscribe: Effect.void,
|
||||
close: Effect.void,
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
readonly close: Effect.Effect<void> = Effect.void;
|
||||
}
|
||||
|
||||
const sampleTriple: Triple = {
|
||||
|
|
@ -66,40 +67,40 @@ describe("LibrarianService schema-backed boundaries", () => {
|
|||
const service = makeService(dir);
|
||||
|
||||
try {
|
||||
await expect(service.handleLibrarianOperation({
|
||||
await expect(Effect.runPromise(service.handleLibrarianOperation({
|
||||
operation: "list-documents",
|
||||
user: "alice",
|
||||
})).resolves.toEqual({
|
||||
}))).resolves.toEqual({
|
||||
documents: [],
|
||||
"document-metadatas": [],
|
||||
});
|
||||
|
||||
const upload = await service.handleLibrarianOperation({
|
||||
const upload = await Effect.runPromise(service.handleLibrarianOperation({
|
||||
operation: "begin-upload",
|
||||
documentMetadata: sampleDocument,
|
||||
"document-metadata": sampleDocument,
|
||||
"total-size": 12,
|
||||
"chunk-size": 4,
|
||||
});
|
||||
await expect(service.handleLibrarianOperation({
|
||||
}));
|
||||
await expect(Effect.runPromise(service.handleLibrarianOperation({
|
||||
operation: "get-upload-status",
|
||||
"upload-id": upload["upload-id"],
|
||||
})).resolves.toMatchObject({
|
||||
}))).resolves.toMatchObject({
|
||||
"upload-id": upload["upload-id"],
|
||||
"upload-state": "in-progress",
|
||||
"missing-chunks": [0, 1, 2],
|
||||
});
|
||||
|
||||
await expect(service.handleLibrarianOperation({
|
||||
await expect(Effect.runPromise(service.handleLibrarianOperation({
|
||||
operation: "stream-document",
|
||||
"document-id": "doc-a",
|
||||
})).rejects.toMatchObject({
|
||||
}))).rejects.toMatchObject({
|
||||
_tag: "LibrarianServiceError",
|
||||
operation: "stream-document",
|
||||
message: "stream-document must be handled as a streaming operation",
|
||||
});
|
||||
|
||||
await expect(service.handleLibrarianOperation(JSON.parse(`{"operation":"unknown-librarian"}`))).rejects.toMatchObject({
|
||||
await expect(Effect.runPromise(service.handleLibrarianOperation(JSON.parse(`{"operation":"unknown-librarian"}`)))).rejects.toMatchObject({
|
||||
_tag: "LibrarianServiceError",
|
||||
operation: "operation",
|
||||
message: "Unknown librarian operation: unknown-librarian",
|
||||
|
|
@ -114,14 +115,14 @@ describe("LibrarianService schema-backed boundaries", () => {
|
|||
const service = makeService(dir);
|
||||
|
||||
try {
|
||||
await expect(service.handleCollectionOperation({
|
||||
await expect(Effect.runPromise(service.handleCollectionOperation({
|
||||
operation: "update-collection",
|
||||
user: "alice",
|
||||
collection: "docs",
|
||||
name: "Docs",
|
||||
description: "Documentation",
|
||||
tags: ["reference"],
|
||||
})).resolves.toEqual({
|
||||
}))).resolves.toEqual({
|
||||
collections: [{
|
||||
user: "alice",
|
||||
collection: "docs",
|
||||
|
|
@ -131,10 +132,10 @@ describe("LibrarianService schema-backed boundaries", () => {
|
|||
}],
|
||||
});
|
||||
|
||||
await expect(service.handleCollectionOperation({
|
||||
await expect(Effect.runPromise(service.handleCollectionOperation({
|
||||
operation: "list-collections",
|
||||
user: "alice",
|
||||
})).resolves.toEqual({
|
||||
}))).resolves.toEqual({
|
||||
collections: [{
|
||||
user: "alice",
|
||||
collection: "docs",
|
||||
|
|
@ -144,17 +145,17 @@ describe("LibrarianService schema-backed boundaries", () => {
|
|||
}],
|
||||
});
|
||||
|
||||
await expect(service.handleCollectionOperation({
|
||||
await expect(Effect.runPromise(service.handleCollectionOperation({
|
||||
operation: "delete-collection",
|
||||
user: "alice",
|
||||
collection: "docs",
|
||||
})).resolves.toEqual({});
|
||||
await expect(service.handleCollectionOperation({
|
||||
}))).resolves.toEqual({});
|
||||
await expect(Effect.runPromise(service.handleCollectionOperation({
|
||||
operation: "list-collections",
|
||||
user: "alice",
|
||||
})).resolves.toEqual({collections: []});
|
||||
}))).resolves.toEqual({collections: []});
|
||||
|
||||
await expect(service.handleCollectionOperation(JSON.parse(`{"operation":"unknown-collection"}`))).rejects.toMatchObject({
|
||||
await expect(Effect.runPromise(service.handleCollectionOperation(JSON.parse(`{"operation":"unknown-collection"}`)))).rejects.toMatchObject({
|
||||
_tag: "LibrarianServiceError",
|
||||
operation: "collection-operation",
|
||||
message: "Unknown collection operation: unknown-collection",
|
||||
|
|
@ -168,18 +169,18 @@ describe("LibrarianService schema-backed boundaries", () => {
|
|||
const dir = await mkdtemp(join(tmpdir(), "trustgraph-librarian-service-"));
|
||||
const service = makeService(dir);
|
||||
|
||||
const response = await service.beginUpload({
|
||||
const response = await Effect.runPromise(service.beginUpload({
|
||||
operation: "begin-upload",
|
||||
documentMetadata: sampleDocument,
|
||||
"document-metadata": sampleDocument,
|
||||
"total-size": 12,
|
||||
"chunk-size": 4,
|
||||
});
|
||||
}));
|
||||
const uploadId = response["upload-id"];
|
||||
const status = await service.getUploadStatus({
|
||||
const status = await Effect.runPromise(service.getUploadStatus({
|
||||
operation: "get-upload-status",
|
||||
"upload-id": uploadId,
|
||||
});
|
||||
}));
|
||||
await rm(dir, {recursive: true, force: true});
|
||||
|
||||
expect(uploadId).toEqual(expect.any(String));
|
||||
|
|
@ -202,8 +203,8 @@ describe("LibrarianService schema-backed boundaries", () => {
|
|||
);
|
||||
const service = makeService(dir);
|
||||
|
||||
await service.loadFromDisk();
|
||||
const documents = service.listDocuments({operation: "list-documents", user: "alice"}).documents;
|
||||
await Effect.runPromise(service.loadFromDisk);
|
||||
const documents = (await Effect.runPromise(service.listDocuments({operation: "list-documents", user: "alice"}))).documents;
|
||||
await rm(dir, {recursive: true, force: true});
|
||||
|
||||
expect(documents).toEqual([{
|
||||
|
|
@ -217,15 +218,15 @@ describe("LibrarianService schema-backed boundaries", () => {
|
|||
const dir = await mkdtemp(join(tmpdir(), "trustgraph-librarian-service-"));
|
||||
const service = makeService(dir);
|
||||
|
||||
const valid = await service.normaliseDocumentMetadata({
|
||||
const valid = await Effect.runPromise(service.normaliseDocumentMetadata({
|
||||
...sampleDocument,
|
||||
metadata: [sampleTriple],
|
||||
});
|
||||
const invalid = await service.normaliseDocumentMetadata({
|
||||
}));
|
||||
const invalid = await Effect.runPromise(service.normaliseDocumentMetadata({
|
||||
...sampleDocument,
|
||||
id: "doc-b",
|
||||
metadata: [{not: "a triple"}],
|
||||
});
|
||||
}));
|
||||
await rm(dir, {recursive: true, force: true});
|
||||
|
||||
expect(valid.metadata).toEqual([sampleTriple]);
|
||||
|
|
|
|||
|
|
@ -55,13 +55,15 @@ const waitFor = (condition: () => boolean, label: string) =>
|
|||
class RecordingProducer<T> implements BackendProducer<T> {
|
||||
readonly sent: Array<{ readonly message: T; readonly properties?: Record<string, string> }> = [];
|
||||
|
||||
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
||||
this.sent.push(properties === undefined ? { message } : { message, properties });
|
||||
send(message: T, properties?: Record<string, string>): Effect.Effect<void> {
|
||||
return Effect.sync(() => {
|
||||
this.sent.push(properties === undefined ? { message } : { message, properties });
|
||||
});
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {}
|
||||
readonly flush: Effect.Effect<void> = Effect.void;
|
||||
|
||||
async close(): Promise<void> {}
|
||||
readonly close: Effect.Effect<void> = Effect.void;
|
||||
}
|
||||
|
||||
class PushConsumer<T> implements BackendConsumer<T> {
|
||||
|
|
@ -79,30 +81,36 @@ class PushConsumer<T> implements BackendConsumer<T> {
|
|||
this.messages.push(message);
|
||||
}
|
||||
|
||||
async receive(): Promise<Message<T> | null> {
|
||||
const message = this.messages.shift();
|
||||
if (message !== undefined || this.closed) {
|
||||
return message ?? null;
|
||||
}
|
||||
return await new Promise((resolve) => {
|
||||
this.waiters.push(resolve);
|
||||
receive(): Effect.Effect<Message<T> | null> {
|
||||
return Effect.promise(() => {
|
||||
const message = this.messages.shift();
|
||||
if (message !== undefined || this.closed) {
|
||||
return Promise.resolve(message ?? null);
|
||||
}
|
||||
return new Promise<Message<T> | null>((resolve) => {
|
||||
this.waiters.push(resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async acknowledge(message: Message<T>): Promise<void> {
|
||||
this.acknowledged.push(message);
|
||||
acknowledge(message: Message<T>): Effect.Effect<void> {
|
||||
return Effect.sync(() => {
|
||||
this.acknowledged.push(message);
|
||||
});
|
||||
}
|
||||
|
||||
async negativeAcknowledge(): Promise<void> {}
|
||||
negativeAcknowledge(): Effect.Effect<void> {
|
||||
return Effect.void;
|
||||
}
|
||||
|
||||
async unsubscribe(): Promise<void> {}
|
||||
readonly unsubscribe: Effect.Effect<void> = Effect.void;
|
||||
|
||||
async close(): Promise<void> {
|
||||
readonly close: Effect.Effect<void> = Effect.sync(() => {
|
||||
this.closed = true;
|
||||
for (const waiter of this.waiters.splice(0)) {
|
||||
waiter(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class PromptBackend implements PubSubBackend {
|
||||
|
|
@ -110,22 +118,26 @@ class PromptBackend implements PubSubBackend {
|
|||
readonly consumersByTopic = new Map<string, PushConsumer<unknown>>();
|
||||
readonly producersByTopic = new Map<string, RecordingProducer<unknown>>();
|
||||
|
||||
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||
const producer = new RecordingProducer<unknown>();
|
||||
this.producersByTopic.set(options.topic, producer);
|
||||
return producer as BackendProducer<T>;
|
||||
createProducer<T>(options: CreateProducerOptions): Effect.Effect<BackendProducer<T>> {
|
||||
return Effect.sync(() => {
|
||||
const producer = new RecordingProducer<unknown>();
|
||||
this.producersByTopic.set(options.topic, producer);
|
||||
return producer as BackendProducer<T>;
|
||||
});
|
||||
}
|
||||
|
||||
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||
if (options.topic === topics.configPush) {
|
||||
return this.configConsumer as unknown as BackendConsumer<T>;
|
||||
}
|
||||
const consumer = new PushConsumer<unknown>();
|
||||
this.consumersByTopic.set(options.topic, consumer);
|
||||
return consumer as BackendConsumer<T>;
|
||||
createConsumer<T>(options: CreateConsumerOptions): Effect.Effect<BackendConsumer<T>> {
|
||||
return Effect.sync(() => {
|
||||
if (options.topic === topics.configPush) {
|
||||
return this.configConsumer as unknown as BackendConsumer<T>;
|
||||
}
|
||||
const consumer = new PushConsumer<unknown>();
|
||||
this.consumersByTopic.set(options.topic, consumer);
|
||||
return consumer as BackendConsumer<T>;
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
readonly close: Effect.Effect<void> = Effect.void;
|
||||
|
||||
pushPromptConfig(): void {
|
||||
this.configConsumer.push(createMessage({
|
||||
|
|
|
|||
|
|
@ -33,44 +33,51 @@ class FakeQdrantClient implements QdrantClientLike {
|
|||
readonly deletedCollections: string[] = [];
|
||||
searchResults: ReadonlyArray<QdrantScoredPoint> = [];
|
||||
|
||||
async collectionExists(collectionName: string): Promise<{ readonly exists: boolean }> {
|
||||
this.collectionExistsCalls.push(collectionName);
|
||||
return { exists: this.collections.has(collectionName) };
|
||||
collectionExists(collectionName: string): Effect.Effect<{ readonly exists: boolean }> {
|
||||
return Effect.sync(() => {
|
||||
this.collectionExistsCalls.push(collectionName);
|
||||
return { exists: this.collections.has(collectionName) };
|
||||
});
|
||||
}
|
||||
|
||||
async createCollection(
|
||||
createCollection(
|
||||
collectionName: string,
|
||||
options: { readonly vectors: { readonly size: number; readonly distance: "Cosine" } },
|
||||
): Promise<void> {
|
||||
this.collections.add(collectionName);
|
||||
this.createdCollections.push({ name: collectionName, size: options.vectors.size });
|
||||
): Effect.Effect<void> {
|
||||
return Effect.sync(() => {
|
||||
this.collections.add(collectionName);
|
||||
this.createdCollections.push({ name: collectionName, size: options.vectors.size });
|
||||
});
|
||||
}
|
||||
|
||||
async upsert(
|
||||
upsert(
|
||||
collectionName: string,
|
||||
options: { readonly points: ReadonlyArray<FakePoint> },
|
||||
): Promise<void> {
|
||||
this.upserts.push({ collectionName, points: options.points });
|
||||
): Effect.Effect<void> {
|
||||
return Effect.sync(() => {
|
||||
this.upserts.push({ collectionName, points: options.points });
|
||||
});
|
||||
}
|
||||
|
||||
async getCollections(): Promise<{ readonly collections: ReadonlyArray<{ readonly name: string }> }> {
|
||||
return { collections: Array.from(this.collections, (name) => ({ name })) };
|
||||
readonly getCollections: Effect.Effect<{ readonly collections: ReadonlyArray<{ readonly name: string }> }> =
|
||||
Effect.sync(() => ({ collections: Array.from(this.collections, (name) => ({ name })) }));
|
||||
|
||||
deleteCollection(collectionName: string): Effect.Effect<void> {
|
||||
return Effect.sync(() => {
|
||||
this.collections.delete(collectionName);
|
||||
this.deletedCollections.push(collectionName);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteCollection(collectionName: string): Promise<void> {
|
||||
this.collections.delete(collectionName);
|
||||
this.deletedCollections.push(collectionName);
|
||||
}
|
||||
|
||||
async search(
|
||||
search(
|
||||
_collectionName: string,
|
||||
_options: {
|
||||
readonly vector: ReadonlyArray<number>;
|
||||
readonly limit: number;
|
||||
readonly with_payload: boolean;
|
||||
},
|
||||
): Promise<ReadonlyArray<QdrantScoredPoint>> {
|
||||
return this.searchResults;
|
||||
): Effect.Effect<ReadonlyArray<QdrantScoredPoint>> {
|
||||
return Effect.sync(() => this.searchResults);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -206,22 +213,22 @@ describe("Qdrant embeddings", () => {
|
|||
});
|
||||
|
||||
await Effect.runPromise(
|
||||
store.storeEffect({
|
||||
store.store({
|
||||
user: "alice",
|
||||
collection: "docs",
|
||||
chunks: [{ chunkId: "chunk-a", vector: [1, 2], content: "alpha" }],
|
||||
}),
|
||||
);
|
||||
await Effect.runPromise(
|
||||
store.storeEffect({
|
||||
store.store({
|
||||
user: "alice",
|
||||
collection: "docs",
|
||||
chunks: [{ chunkId: "chunk-b", vector: [2, 1], content: "beta" }],
|
||||
}),
|
||||
);
|
||||
await Effect.runPromise(store.deleteCollectionEffect("alice", "docs"));
|
||||
await Effect.runPromise(store.deleteCollection("alice", "docs"));
|
||||
await Effect.runPromise(
|
||||
store.storeEffect({
|
||||
store.store({
|
||||
user: "alice",
|
||||
collection: "docs",
|
||||
chunks: [{ chunkId: "chunk-c", vector: [1, 1], content: "gamma" }],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { describe, expect, it } from "@effect/vitest";
|
||||
import type { LlmChunk } from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime, Stream } from "effect";
|
||||
import { Context, Effect, Stream } from "effect";
|
||||
import { AiError, LanguageModel, Response } from "effect/unstable/ai";
|
||||
import {
|
||||
llmStreamPart,
|
||||
|
|
@ -9,10 +8,9 @@ import {
|
|||
providerStatusError,
|
||||
streamTextCompletionChunks,
|
||||
textFromContent,
|
||||
toAsyncGenerator,
|
||||
} from "../model/text-completion/common.js";
|
||||
|
||||
const languageModelRuntime = ManagedRuntime.make(Layer.empty);
|
||||
const languageModelContext = Context.empty();
|
||||
|
||||
const usage = (inputTokens: number, outputTokens: number) => ({
|
||||
inputTokens: {
|
||||
|
|
@ -42,12 +40,6 @@ const aiError = (reason: AiError.AiErrorReason) =>
|
|||
reason,
|
||||
});
|
||||
|
||||
const emptyChunkIterator = (): AsyncIterable<LlmChunk> => ({
|
||||
[Symbol.asyncIterator]: () => ({
|
||||
next: () => Promise.resolve({ done: true, value: undefined }),
|
||||
}),
|
||||
});
|
||||
|
||||
describe("text completion common helpers", () => {
|
||||
it("maps provider rate-limit status fields to tagged retry errors", () => {
|
||||
expect(providerStatusError("test-provider", { status: 429 })).toMatchObject({
|
||||
|
|
@ -61,19 +53,6 @@ describe("text completion common helpers", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("maps fallback generator throw failures into tagged provider errors", async () => {
|
||||
const generator = toAsyncGenerator(
|
||||
emptyChunkIterator(),
|
||||
(error) => providerRuntimeError("test-provider", error),
|
||||
);
|
||||
|
||||
await expect(generator.throw("provider failed")).rejects.toMatchObject({
|
||||
_tag: "TextCompletionProviderError",
|
||||
provider: "test-provider",
|
||||
message: "provider failed",
|
||||
});
|
||||
});
|
||||
|
||||
it.effect(
|
||||
"builds streaming chunks from async iterables with final token totals",
|
||||
Effect.fnUntraced(function* () {
|
||||
|
|
@ -117,107 +96,129 @@ describe("text completion common helpers", () => {
|
|||
expect(textFromContent([{ text: 1 }])).toBe("");
|
||||
});
|
||||
|
||||
it("adapts Effect LanguageModel generateText responses to LlmProvider results", async () => {
|
||||
const provider = makeLanguageModelProvider({
|
||||
provider: "FakeLanguageModel",
|
||||
defaultModel: "fake-model",
|
||||
defaultTemperature: 0.1,
|
||||
runtime: languageModelRuntime,
|
||||
makeLanguageModel: ({ model, temperature }) =>
|
||||
LanguageModel.make({
|
||||
generateText: () =>
|
||||
Effect.succeed([
|
||||
{ type: "text", text: `model=${model};temperature=${temperature}` },
|
||||
finishPart(11, 7),
|
||||
]),
|
||||
streamText: () => Stream.empty,
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(provider.generateContent("system", "prompt", "override-model", 0.4)).resolves.toEqual({
|
||||
text: "model=override-model;temperature=0.4",
|
||||
inToken: 11,
|
||||
outToken: 7,
|
||||
model: "override-model",
|
||||
});
|
||||
});
|
||||
|
||||
it("adapts Effect LanguageModel stream parts to TrustGraph chunks", async () => {
|
||||
const provider = makeLanguageModelProvider({
|
||||
provider: "FakeLanguageModel",
|
||||
defaultModel: "fake-stream-model",
|
||||
defaultTemperature: 0,
|
||||
runtime: languageModelRuntime,
|
||||
makeLanguageModel: () =>
|
||||
LanguageModel.make({
|
||||
generateText: () =>
|
||||
Effect.succeed([
|
||||
{ type: "text", text: "unused" },
|
||||
finishPart(1, 1),
|
||||
]),
|
||||
streamText: () =>
|
||||
Stream.fromArray([
|
||||
Response.makePart("text-start", { id: "part-1" }),
|
||||
{ type: "text-delta", id: "part-1", delta: "hel" },
|
||||
{ type: "text-delta", id: "part-1", delta: "lo" },
|
||||
finishPart(13, 8),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
|
||||
const chunks: Array<LlmChunk> = [];
|
||||
for await (const chunk of provider.generateContentStream("system", "prompt")) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
expect(chunks).toEqual([
|
||||
{
|
||||
text: "hel",
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: "fake-stream-model",
|
||||
isFinal: false,
|
||||
},
|
||||
{
|
||||
text: "lo",
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: "fake-stream-model",
|
||||
isFinal: false,
|
||||
},
|
||||
{
|
||||
text: "",
|
||||
inToken: 13,
|
||||
outToken: 8,
|
||||
model: "fake-stream-model",
|
||||
isFinal: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps Effect AI rate and quota failures to TrustGraph retry errors", async () => {
|
||||
const reasons = [
|
||||
new AiError.RateLimitError({}),
|
||||
new AiError.QuotaExhaustedError({}),
|
||||
];
|
||||
|
||||
for (const reason of reasons) {
|
||||
it.effect(
|
||||
"adapts Effect LanguageModel generateText responses to LlmProvider results",
|
||||
Effect.fnUntraced(function* () {
|
||||
const provider = makeLanguageModelProvider({
|
||||
provider: "FakeLanguageModel",
|
||||
defaultModel: "fake-model",
|
||||
defaultTemperature: 0,
|
||||
runtime: languageModelRuntime,
|
||||
makeLanguageModel: () =>
|
||||
defaultTemperature: 0.1,
|
||||
context: languageModelContext,
|
||||
makeLanguageModel: ({ model, temperature }) =>
|
||||
LanguageModel.make({
|
||||
generateText: () => Effect.fail(aiError(reason)),
|
||||
streamText: () => Stream.fail(aiError(reason)),
|
||||
generateText: () =>
|
||||
Effect.succeed([
|
||||
{ type: "text", text: `model=${model};temperature=${temperature}` },
|
||||
finishPart(11, 7),
|
||||
]),
|
||||
streamText: () => Stream.empty,
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(provider.generateContent("system", "prompt")).rejects.toMatchObject({
|
||||
_tag: "TooManyRequestsError",
|
||||
message: "Rate limit exceeded",
|
||||
const result = yield* provider.generateContent("system", "prompt", "override-model", 0.4);
|
||||
expect(result).toEqual({
|
||||
text: "model=override-model;temperature=0.4",
|
||||
inToken: 11,
|
||||
outToken: 7,
|
||||
model: "override-model",
|
||||
});
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
it.effect(
|
||||
"adapts Effect LanguageModel stream parts to TrustGraph chunks",
|
||||
Effect.fnUntraced(function* () {
|
||||
const provider = makeLanguageModelProvider({
|
||||
provider: "FakeLanguageModel",
|
||||
defaultModel: "fake-stream-model",
|
||||
defaultTemperature: 0,
|
||||
context: languageModelContext,
|
||||
makeLanguageModel: () =>
|
||||
LanguageModel.make({
|
||||
generateText: () =>
|
||||
Effect.succeed([
|
||||
{ type: "text", text: "unused" },
|
||||
finishPart(1, 1),
|
||||
]),
|
||||
streamText: () =>
|
||||
Stream.fromArray([
|
||||
Response.makePart("text-start", { id: "part-1" }),
|
||||
{ type: "text-delta", id: "part-1", delta: "hel" },
|
||||
{ type: "text-delta", id: "part-1", delta: "lo" },
|
||||
finishPart(13, 8),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
|
||||
const chunks = yield* Stream.runCollect(
|
||||
provider.generateContentStream("system", "prompt"),
|
||||
);
|
||||
|
||||
expect(Array.from(chunks)).toEqual([
|
||||
{
|
||||
text: "hel",
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: "fake-stream-model",
|
||||
isFinal: false,
|
||||
},
|
||||
{
|
||||
text: "lo",
|
||||
inToken: null,
|
||||
outToken: null,
|
||||
model: "fake-stream-model",
|
||||
isFinal: false,
|
||||
},
|
||||
{
|
||||
text: "",
|
||||
inToken: 13,
|
||||
outToken: 8,
|
||||
model: "fake-stream-model",
|
||||
isFinal: true,
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
it.effect(
|
||||
"maps Effect AI rate and quota failures to TrustGraph retry errors",
|
||||
Effect.fnUntraced(function* () {
|
||||
const reasons = [
|
||||
new AiError.RateLimitError({}),
|
||||
new AiError.QuotaExhaustedError({}),
|
||||
];
|
||||
|
||||
for (const reason of reasons) {
|
||||
const provider = makeLanguageModelProvider({
|
||||
provider: "FakeLanguageModel",
|
||||
defaultModel: "fake-model",
|
||||
defaultTemperature: 0,
|
||||
context: languageModelContext,
|
||||
makeLanguageModel: () =>
|
||||
LanguageModel.make({
|
||||
generateText: () => Effect.fail(aiError(reason)),
|
||||
streamText: () => Stream.fail(aiError(reason)),
|
||||
}),
|
||||
});
|
||||
|
||||
const generateError = yield* provider.generateContent("system", "prompt").pipe(
|
||||
Effect.flip,
|
||||
);
|
||||
expect(generateError).toMatchObject({
|
||||
_tag: "TooManyRequestsError",
|
||||
message: "Rate limit exceeded",
|
||||
});
|
||||
|
||||
const streamError = yield* Stream.runCollect(
|
||||
provider.generateContentStream("system", "prompt"),
|
||||
).pipe(
|
||||
Effect.flip,
|
||||
);
|
||||
expect(streamError).toMatchObject({
|
||||
_tag: "TooManyRequestsError",
|
||||
message: "Rate limit exceeded",
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { McpToolService, run, runMain } from "./service.js";
|
||||
export { McpToolService, program, runMain } from "./service.js";
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import {
|
|||
type MessagingDeliveryError,
|
||||
type Spec,
|
||||
} from "@trustgraph/base";
|
||||
import { Context, Effect, Layer, ManagedRuntime, Ref } from "effect";
|
||||
import { Context, Effect, Layer, Ref } from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import * as S from "effect/Schema";
|
||||
|
||||
|
|
@ -342,9 +342,9 @@ export function makeMcpToolService(config: ProcessorConfig): McpToolService {
|
|||
provide: (effect) => effect.pipe(Effect.provideService(McpToolRuntime, runtime)),
|
||||
});
|
||||
service.registerConfigHandler((pushedConfig, version) =>
|
||||
Effect.runPromise(onMcpConfig(pushedConfig, version).pipe(
|
||||
onMcpConfig(pushedConfig, version).pipe(
|
||||
Effect.provideService(McpToolRuntime, runtime),
|
||||
)),
|
||||
),
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
|
@ -358,12 +358,6 @@ export const program = makeFlowProcessorProgram<ProcessorConfig, never, McpToolR
|
|||
layer: () => McpToolRuntimeLive,
|
||||
});
|
||||
|
||||
const mcpToolRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return mcpToolRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import {
|
|||
type MessagingDeliveryError,
|
||||
type Spec,
|
||||
} from "@trustgraph/base";
|
||||
import {Context, Effect, Layer, ManagedRuntime, Match, Ref} from "effect";
|
||||
import {Context, Effect, Layer, Match, Ref} from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import * as Predicate from "effect/Predicate";
|
||||
import * as S from "effect/Schema";
|
||||
|
|
@ -64,13 +64,6 @@ import type { AgentTool, ToolArg } from "./types.js";
|
|||
|
||||
const MAX_ITERATIONS = 10;
|
||||
|
||||
class AgentToolExecutionError extends S.TaggedErrorClass<AgentToolExecutionError>()(
|
||||
"AgentToolExecutionError",
|
||||
{
|
||||
message: S.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
const AgentResponseProducer = makeProducerSpec<AgentResponse>("agent-response");
|
||||
const AgentLlmClient = makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
|
||||
"llm",
|
||||
|
|
@ -157,7 +150,7 @@ const buildConfiguredTool = Effect.fn("AgentService.buildConfiguredTool")(functi
|
|||
: "Query the knowledge graph for information about entities and their relationships.",
|
||||
args: [{ name: "question", type: "string", description: "The question to ask" }],
|
||||
config,
|
||||
execute: () => Promise.resolve(""),
|
||||
execute: () => Effect.succeed(""),
|
||||
})
|
||||
),
|
||||
|
||||
|
|
@ -170,7 +163,7 @@ const buildConfiguredTool = Effect.fn("AgentService.buildConfiguredTool")(functi
|
|||
: "Search documents for relevant information.",
|
||||
args: [{ name: "question", type: "string", description: "The question to search for" }],
|
||||
config,
|
||||
execute: () => Promise.resolve(""),
|
||||
execute: () => Effect.succeed(""),
|
||||
})
|
||||
),
|
||||
|
||||
|
|
@ -187,7 +180,7 @@ const buildConfiguredTool = Effect.fn("AgentService.buildConfiguredTool")(functi
|
|||
{ name: "object", type: "string", description: "Object entity (optional)" },
|
||||
],
|
||||
config,
|
||||
execute: () => Promise.resolve(""),
|
||||
execute: () => Effect.succeed(""),
|
||||
})
|
||||
),
|
||||
|
||||
|
|
@ -203,7 +196,7 @@ const buildConfiguredTool = Effect.fn("AgentService.buildConfiguredTool")(functi
|
|||
description,
|
||||
args,
|
||||
config,
|
||||
execute: () => Promise.resolve(""),
|
||||
execute: () => Effect.succeed(""),
|
||||
});
|
||||
}),
|
||||
|
||||
|
|
@ -355,12 +348,9 @@ const executeTool = (
|
|||
tool: AgentTool,
|
||||
input: string,
|
||||
): Effect.Effect<string> =>
|
||||
Effect.tryPromise({
|
||||
try: () => tool.execute(input),
|
||||
catch: (cause) => AgentToolExecutionError.make({ message: errorMessage(cause) }),
|
||||
}).pipe(
|
||||
Effect.catch((error: AgentToolExecutionError) =>
|
||||
Effect.succeed(`Error executing tool: ${error.message}`),
|
||||
tool.execute(input).pipe(
|
||||
Effect.catch((cause) =>
|
||||
Effect.succeed(`Error executing tool: ${errorMessage(cause)}`),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -520,9 +510,9 @@ export function makeAgentService(config: ProcessorConfig): AgentService {
|
|||
provide: (effect) => effect.pipe(Effect.provideService(AgentRuntime, runtime)),
|
||||
});
|
||||
service.registerConfigHandler((pushedConfig, version) =>
|
||||
Effect.runPromise(onToolsConfig(pushedConfig, version).pipe(
|
||||
onToolsConfig(pushedConfig, version).pipe(
|
||||
Effect.provideService(AgentRuntime, runtime),
|
||||
)),
|
||||
),
|
||||
);
|
||||
Effect.runSync(Effect.log("[AgentService] Service initialized"));
|
||||
return service;
|
||||
|
|
@ -616,12 +606,6 @@ export const program = makeFlowProcessorProgram<ProcessorConfig, never, AgentRun
|
|||
layer: () => AgentRuntimeLive,
|
||||
});
|
||||
|
||||
const agentRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return agentRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import * as O from "effect/Option";
|
|||
import * as Predicate from "effect/Predicate";
|
||||
import * as S from "effect/Schema";
|
||||
|
||||
import type { AgentTool, ToolArg } from "./types.js";
|
||||
import { agentToolError, type AgentTool, type ToolArg } from "./types.js";
|
||||
|
||||
const decodeJsonUnknown = S.decodeUnknownOption(S.UnknownFromJsonString);
|
||||
const decodeTerm = S.decodeUnknownOption(TermSchema);
|
||||
|
|
@ -88,14 +88,16 @@ export function createKnowledgeQueryTool(
|
|||
description: "The question to ask the knowledge graph",
|
||||
},
|
||||
],
|
||||
execute: (input: string): Promise<string> => Effect.runPromise(Effect.gen(function* () {
|
||||
execute: Effect.fn("KnowledgeQuery.execute")(function* (input: string) {
|
||||
const question = parseQuestion(input);
|
||||
yield* Effect.log(`[KnowledgeQuery] Executing: "${question.slice(0, 60)}..." collection=${collection}`);
|
||||
const request: GraphRagRequest = {
|
||||
query: question,
|
||||
...(collection !== undefined ? { collection } : {}),
|
||||
};
|
||||
const res = yield* client.request(request);
|
||||
const res = yield* client.request(request).pipe(
|
||||
Effect.mapError((cause) => agentToolError("knowledge-query", cause)),
|
||||
);
|
||||
yield* Effect.log(`[KnowledgeQuery] Response (${res.response?.length ?? 0} chars): ${res.error !== undefined ? `ERROR: ${res.error.message}` : `${res.response?.slice(0, 300)}...`}`);
|
||||
|
||||
const explainTriples = res.explain_triples;
|
||||
|
|
@ -108,7 +110,7 @@ export function createKnowledgeQueryTool(
|
|||
|
||||
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||
return res.response;
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -130,16 +132,18 @@ export function createDocumentQueryTool(
|
|||
description: "The question to search documents for",
|
||||
},
|
||||
],
|
||||
execute: (input: string): Promise<string> => Effect.runPromise(Effect.gen(function* () {
|
||||
execute: Effect.fn("DocumentQuery.execute")(function* (input: string) {
|
||||
const question = parseQuestion(input);
|
||||
const request: DocumentRagRequest = {
|
||||
query: question,
|
||||
...(collection !== undefined ? { collection } : {}),
|
||||
};
|
||||
const res = yield* client.request(request);
|
||||
const res = yield* client.request(request).pipe(
|
||||
Effect.mapError((cause) => agentToolError("document-query", cause)),
|
||||
);
|
||||
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||
return res.response;
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +225,7 @@ export function createTriplesQueryTool(
|
|||
description: "The object entity to search for (optional)",
|
||||
},
|
||||
],
|
||||
execute: (input: string): Promise<string> => Effect.runPromise(Effect.gen(function* () {
|
||||
execute: Effect.fn("TriplesQuery.execute")(function* (input: string) {
|
||||
const { s, p, o, limit } = parseTriplesInput(input);
|
||||
const request: TriplesQueryRequest = {
|
||||
limit: limit ?? 20,
|
||||
|
|
@ -230,7 +234,9 @@ export function createTriplesQueryTool(
|
|||
...(o !== undefined ? { o } : {}),
|
||||
...(collection !== undefined ? { collection } : {}),
|
||||
};
|
||||
const res = yield* client.request(request);
|
||||
const res = yield* client.request(request).pipe(
|
||||
Effect.mapError((cause) => agentToolError("triples-query", cause)),
|
||||
);
|
||||
|
||||
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||
|
||||
|
|
@ -243,7 +249,7 @@ export function createTriplesQueryTool(
|
|||
`(${termToString(t.s)}) -[${termToString(t.p)}]-> (${termToString(t.o)})`,
|
||||
);
|
||||
return lines.join("\n");
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -263,12 +269,14 @@ export function createMcpTool(
|
|||
name: toolName,
|
||||
description,
|
||||
args,
|
||||
execute: (input: string): Promise<string> => Effect.runPromise(Effect.gen(function* () {
|
||||
const res = yield* client.request({ name: toolName, parameters: input });
|
||||
execute: Effect.fn("McpTool.execute")(function* (input: string) {
|
||||
const res = yield* client.request({ name: toolName, parameters: input }).pipe(
|
||||
Effect.mapError((cause) => agentToolError("mcp-tool", cause)),
|
||||
);
|
||||
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||
if (res.text !== undefined) return res.text;
|
||||
if (res.object !== undefined) return res.object;
|
||||
return "No content";
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,24 @@
|
|||
* Types for the ReAct agent service.
|
||||
*/
|
||||
|
||||
import type { Effect } from "effect";
|
||||
import * as S from "effect/Schema";
|
||||
import { errorMessage } from "@trustgraph/base";
|
||||
|
||||
export class AgentToolError extends S.TaggedErrorClass<AgentToolError>()(
|
||||
"AgentToolError",
|
||||
{
|
||||
message: S.String,
|
||||
operation: S.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export const agentToolError = (operation: string, cause: unknown): AgentToolError =>
|
||||
AgentToolError.make({
|
||||
operation,
|
||||
message: errorMessage(cause),
|
||||
});
|
||||
|
||||
export interface ToolArg {
|
||||
name: string;
|
||||
type: string;
|
||||
|
|
@ -12,7 +30,7 @@ export interface AgentTool {
|
|||
name: string;
|
||||
description: string;
|
||||
args: ToolArg[];
|
||||
execute: (input: string) => Promise<string>;
|
||||
execute: (input: string) => Effect.Effect<string, AgentToolError>;
|
||||
/** Full tool config from config-push (used by tool filtering). */
|
||||
config?: Record<string, unknown>;
|
||||
}
|
||||
|
|
@ -30,6 +48,6 @@ export interface ParsedEvent {
|
|||
content: string;
|
||||
}
|
||||
|
||||
export type OnThought = (text: string, isFinal: boolean) => Promise<void>;
|
||||
export type OnObservation = (text: string, isFinal: boolean) => Promise<void>;
|
||||
export type OnAnswer = (text: string) => Promise<void>;
|
||||
export type OnThought = (text: string, isFinal: boolean) => Effect.Effect<void, AgentToolError>;
|
||||
export type OnObservation = (text: string, isFinal: boolean) => Effect.Effect<void, AgentToolError>;
|
||||
export type OnAnswer = (text: string) => Effect.Effect<void, AgentToolError>;
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { makeFlowProcessorProgram } from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime } from "effect";
|
||||
import { Effect } from "effect";
|
||||
import * as S from "effect/Schema";
|
||||
import { recursiveSplit } from "./recursive-splitter.js";
|
||||
|
||||
const DEFAULT_CHUNK_SIZE = 2000;
|
||||
const DEFAULT_CHUNK_OVERLAP = 100;
|
||||
const ChunkSizeParameter = makeParameterSpec("chunk-size", S.Number);
|
||||
const ChunkOverlapParameter = makeParameterSpec("chunk-overlap", S.Number);
|
||||
const ChunkSizeParameter = makeParameterSpec("chunk-size", S.Finite);
|
||||
const ChunkOverlapParameter = makeParameterSpec("chunk-overlap", S.Finite);
|
||||
const ChunkOutputProducer = makeProducerSpec<Chunk>("chunk-output");
|
||||
const ChunkTriplesProducer = makeProducerSpec<Triples>("chunk-triples");
|
||||
|
||||
|
|
@ -108,12 +108,6 @@ export const program = makeFlowProcessorProgram({
|
|||
specs: () => makeChunkingSpecs(),
|
||||
});
|
||||
|
||||
const chunkingRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return chunkingRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import {NodeRuntime} from "@effect/platform-node";
|
||||
import {Duration, Effect, HashMap, Layer, ManagedRuntime, Match, Option, SynchronizedRef} from "effect";
|
||||
import {Duration, Effect, HashMap, Match, Option, SynchronizedRef} from "effect";
|
||||
import * as Predicate from "effect/Predicate";
|
||||
import * as S from "effect/Schema";
|
||||
import {
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
makeAsyncProcessor,
|
||||
makeProcessorProgram,
|
||||
optionalStringConfig,
|
||||
processorLifecycleError,
|
||||
topics,
|
||||
type AsyncProcessorRuntime,
|
||||
type BackendConsumer,
|
||||
|
|
@ -26,7 +27,7 @@ import {
|
|||
type Message,
|
||||
type ProcessorConfig,
|
||||
} from "@trustgraph/base";
|
||||
import {readTextFile, writeTextFile} from "../runtime/effect-files.js";
|
||||
import {readTextFileEffect, writeTextFileEffect} from "../runtime/effect-files.js";
|
||||
|
||||
export interface ConfigServiceConfig extends ProcessorConfig {
|
||||
readonly persistPath?: string;
|
||||
|
|
@ -38,7 +39,7 @@ interface ConfigPush {
|
|||
}
|
||||
|
||||
const ConfigPushSchema = S.Struct({
|
||||
version: S.Number,
|
||||
version: S.Finite,
|
||||
config: S.Record(S.String, S.Unknown),
|
||||
});
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ interface ConfigServiceState {
|
|||
}
|
||||
|
||||
const PersistedConfigSchema = S.Struct({
|
||||
version: S.optionalKey(S.Number),
|
||||
version: S.optionalKey(S.Finite),
|
||||
data: S.optionalKey(S.Record(S.String, S.Record(S.String, S.Unknown))),
|
||||
workspaces: S.optionalKey(S.Record(S.String, S.Record(S.String, S.Record(S.String, S.Unknown)))),
|
||||
});
|
||||
|
|
@ -94,24 +95,17 @@ type PersistedConfig = typeof PersistedConfigSchema.Type;
|
|||
export interface ConfigService extends AsyncProcessorRuntime<ConfigServiceError> {
|
||||
readonly state: SynchronizedRef.SynchronizedRef<ConfigServiceState>;
|
||||
readonly persistPath: string | null;
|
||||
readonly handleMessage: (msg: Message<ConfigRequest>) => Promise<void>;
|
||||
readonly handleMessageEffect: (msg: Message<ConfigRequest>) => Effect.Effect<void, ConfigServiceError>;
|
||||
readonly handleOperation: (request: ConfigRequest) => Promise<ConfigResponse>;
|
||||
readonly handleOperationEffect: (request: ConfigRequest) => Effect.Effect<ConfigResponse, ConfigServiceError>;
|
||||
readonly handleGet: (request: ConfigRequest) => ConfigResponse;
|
||||
readonly handlePut: (request: ConfigRequest) => Promise<ConfigResponse>;
|
||||
readonly handlePutEffect: (request: ConfigRequest) => Effect.Effect<ConfigResponse, ConfigServiceError>;
|
||||
readonly handleDelete: (request: ConfigRequest) => Promise<ConfigResponse>;
|
||||
readonly handleDeleteEffect: (request: ConfigRequest) => Effect.Effect<ConfigResponse, ConfigServiceError>;
|
||||
readonly handleList: (request: ConfigRequest) => ConfigResponse;
|
||||
readonly handleGetValues: (request: ConfigRequest) => ConfigResponse;
|
||||
readonly handleGetValuesAllWorkspaces: (request: ConfigRequest) => ConfigResponse;
|
||||
readonly handleConfigDump: (request: ConfigRequest) => ConfigResponse;
|
||||
readonly pushConfig: () => Promise<void>;
|
||||
readonly pushConfigEffect: Effect.Effect<void, ConfigServiceError>;
|
||||
readonly persist: () => Promise<void>;
|
||||
readonly persistEffect: Effect.Effect<void>;
|
||||
readonly loadFromDisk: () => Promise<void>;
|
||||
readonly loadFromDiskEffect: Effect.Effect<void>;
|
||||
}
|
||||
|
||||
|
|
@ -325,10 +319,9 @@ const persistStateEffect = Effect.fn("ConfigService.persistState")(
|
|||
Effect.mapError((cause) => configServiceError("persist-encode", cause)),
|
||||
);
|
||||
|
||||
yield* Effect.tryPromise({
|
||||
try: () => writeTextFile(persistPath, json),
|
||||
catch: (cause) => configServiceError("persist-write", cause),
|
||||
});
|
||||
yield* writeTextFileEffect(persistPath, json).pipe(
|
||||
Effect.mapError((cause) => configServiceError("persist-write", cause)),
|
||||
);
|
||||
},
|
||||
(effect) =>
|
||||
effect.pipe(
|
||||
|
|
@ -344,24 +337,21 @@ const pushConfigWithStateEffect = Effect.fn("ConfigService.pushConfigWithState")
|
|||
const pushProducer = state.pushProducer;
|
||||
if (pushProducer === null) return;
|
||||
|
||||
yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
pushProducer.send({
|
||||
version: state.version,
|
||||
config: configDumpForState(state),
|
||||
}),
|
||||
catch: (cause) => configServiceError("push-config", cause),
|
||||
});
|
||||
yield* pushProducer.send({
|
||||
version: state.version,
|
||||
config: configDumpForState(state),
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => configServiceError("push-config", cause)),
|
||||
);
|
||||
|
||||
yield* Effect.log(`[ConfigService] Pushed configuration version ${state.version}`);
|
||||
});
|
||||
|
||||
const readPersistedConfigEffect = Effect.fn("ConfigService.readPersistedConfig")(
|
||||
function* (persistPath: string) {
|
||||
const raw = yield* Effect.tryPromise({
|
||||
try: () => readTextFile(persistPath),
|
||||
catch: (cause) => configServiceError("persist-read", cause),
|
||||
});
|
||||
const raw = yield* readTextFileEffect(persistPath).pipe(
|
||||
Effect.mapError((cause) => configServiceError("persist-read", cause)),
|
||||
);
|
||||
return yield* S.decodeUnknownEffect(PersistedConfigJsonSchema)(raw).pipe(
|
||||
Effect.mapError((cause) => configServiceError("persist-decode", cause)),
|
||||
);
|
||||
|
|
@ -644,24 +634,21 @@ const closeConfigResourcesEffect = Effect.fn("ConfigService.closeResources")(fun
|
|||
|
||||
const consumer = state.consumer;
|
||||
if (consumer !== null) {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => consumer.close(),
|
||||
catch: (cause) => configServiceError("close-consumer", cause),
|
||||
});
|
||||
yield* consumer.close.pipe(
|
||||
Effect.mapError((cause) => configServiceError("close-consumer", cause)),
|
||||
);
|
||||
}
|
||||
const responseProducer = state.responseProducer;
|
||||
if (responseProducer !== null) {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => responseProducer.close(),
|
||||
catch: (cause) => configServiceError("close-response-producer", cause),
|
||||
});
|
||||
yield* responseProducer.close.pipe(
|
||||
Effect.mapError((cause) => configServiceError("close-response-producer", cause)),
|
||||
);
|
||||
}
|
||||
const pushProducer = state.pushProducer;
|
||||
if (pushProducer !== null) {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => pushProducer.close(),
|
||||
catch: (cause) => configServiceError("close-push-producer", cause),
|
||||
});
|
||||
yield* pushProducer.close.pipe(
|
||||
Effect.mapError((cause) => configServiceError("close-push-producer", cause)),
|
||||
);
|
||||
}
|
||||
|
||||
yield* updateHandles(stateRef, {
|
||||
|
|
@ -680,17 +667,15 @@ const consumeOnceEffect = Effect.fnUntraced(function* (
|
|||
return yield* configServiceError("consume", "Config consumer not started");
|
||||
}
|
||||
|
||||
const msg = yield* Effect.tryPromise({
|
||||
try: () => consumer.receive(2000),
|
||||
catch: (cause) => configServiceError("consume-receive", cause),
|
||||
});
|
||||
const msg = yield* consumer.receive(2000).pipe(
|
||||
Effect.mapError((cause) => configServiceError("consume-receive", cause)),
|
||||
);
|
||||
if (msg === null) return;
|
||||
|
||||
yield* service.handleMessageEffect(msg);
|
||||
yield* Effect.tryPromise({
|
||||
try: () => consumer.acknowledge(msg),
|
||||
catch: (cause) => configServiceError("consume-acknowledge", cause),
|
||||
});
|
||||
yield* consumer.acknowledge(msg).pipe(
|
||||
Effect.mapError((cause) => configServiceError("consume-acknowledge", cause)),
|
||||
);
|
||||
});
|
||||
|
||||
const runConfigServiceEffect = Effect.fn("ConfigService.run")(function* (
|
||||
|
|
@ -698,35 +683,29 @@ const runConfigServiceEffect = Effect.fn("ConfigService.run")(function* (
|
|||
) {
|
||||
yield* service.loadFromDiskEffect;
|
||||
|
||||
const responseProducer = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
service.pubsub.createProducer<ConfigResponse>({
|
||||
topic: topics.configResponse,
|
||||
schema: ConfigResponseSchema,
|
||||
}),
|
||||
catch: (cause) => configServiceError("response-producer", cause),
|
||||
});
|
||||
const responseProducer = yield* service.pubsub.createProducer<ConfigResponse>({
|
||||
topic: topics.configResponse,
|
||||
schema: ConfigResponseSchema,
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => configServiceError("response-producer", cause)),
|
||||
);
|
||||
yield* updateHandles(service.state, {responseProducer});
|
||||
|
||||
const pushProducer = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
service.pubsub.createProducer<ConfigPush>({
|
||||
topic: topics.configPush,
|
||||
schema: ConfigPushSchema,
|
||||
}),
|
||||
catch: (cause) => configServiceError("push-producer", cause),
|
||||
});
|
||||
const pushProducer = yield* service.pubsub.createProducer<ConfigPush>({
|
||||
topic: topics.configPush,
|
||||
schema: ConfigPushSchema,
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => configServiceError("push-producer", cause)),
|
||||
);
|
||||
yield* updateHandles(service.state, {pushProducer});
|
||||
|
||||
const consumer = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
service.pubsub.createConsumer<ConfigRequest>({
|
||||
topic: topics.configRequest,
|
||||
subscription: `${service.config.id}-config-request`,
|
||||
schema: ConfigRequestSchema,
|
||||
}),
|
||||
catch: (cause) => configServiceError("consumer", cause),
|
||||
});
|
||||
const consumer = yield* service.pubsub.createConsumer<ConfigRequest>({
|
||||
topic: topics.configRequest,
|
||||
subscription: `${service.config.id}-config-request`,
|
||||
schema: ConfigRequestSchema,
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => configServiceError("consumer", cause)),
|
||||
);
|
||||
const state = yield* updateHandles(service.state, {consumer});
|
||||
|
||||
yield* pushConfigWithStateEffect(state);
|
||||
|
|
@ -762,7 +741,6 @@ export function makeConfigService(config: ConfigServiceConfig): ConfigService {
|
|||
const base = makeAsyncProcessor<ConfigServiceError>(config, {
|
||||
runEffect: () => getService.pipe(Effect.flatMap(runConfigServiceEffect)),
|
||||
});
|
||||
const baseStop = base.stop;
|
||||
const persistPath = config.persistPath ?? null;
|
||||
|
||||
const handleOperationEffect = Effect.fn("ConfigService.handleOperation")(function* (
|
||||
|
|
@ -800,10 +778,9 @@ export function makeConfigService(config: ConfigServiceConfig): ConfigService {
|
|||
if (responseProducer === null) {
|
||||
return yield* configServiceError("respond", "Config response producer not started");
|
||||
}
|
||||
yield* Effect.tryPromise({
|
||||
try: () => responseProducer.send(response, {id: requestId}),
|
||||
catch: (cause) => configServiceError("respond", cause),
|
||||
});
|
||||
yield* responseProducer.send(response, {id: requestId}).pipe(
|
||||
Effect.mapError((cause) => configServiceError("respond", cause)),
|
||||
);
|
||||
});
|
||||
|
||||
yield* handleOperationEffect(request).pipe(
|
||||
|
|
@ -830,40 +807,42 @@ export function makeConfigService(config: ConfigServiceConfig): ConfigService {
|
|||
yield* Effect.log(`[ConfigService] Loaded persisted config (version=${next.version}, workspaces=${HashMap.size(next.store)})`);
|
||||
});
|
||||
|
||||
service = Object.assign(base, {
|
||||
const serviceStopEffect = closeConfigResourcesEffect(state).pipe(
|
||||
Effect.mapError((cause) => processorLifecycleError(config.id, "stop", cause)),
|
||||
Effect.flatMap(() => base.stop),
|
||||
);
|
||||
|
||||
const serviceBase = Object.create(base, {
|
||||
stop: {
|
||||
value: serviceStopEffect,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
stopEffect: {
|
||||
value: serviceStopEffect,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
|
||||
service = Object.assign(serviceBase, {
|
||||
state,
|
||||
persistPath,
|
||||
handleMessage: (msg: Message<ConfigRequest>) => Effect.runPromise(handleMessageEffect(msg)),
|
||||
handleMessageEffect,
|
||||
handleOperation: (request: ConfigRequest) => Effect.runPromise(handleOperationEffect(request)),
|
||||
handleOperationEffect,
|
||||
handleGet: (request: ConfigRequest) => handleGetWithState(stateSnapshot(state), request),
|
||||
handlePut: (request: ConfigRequest) => Effect.runPromise(handlePutEffect(state, persistPath, request)),
|
||||
handlePutEffect: (request: ConfigRequest) => handlePutEffect(state, persistPath, request),
|
||||
handleDelete: (request: ConfigRequest) => Effect.runPromise(handleDeleteEffect(state, persistPath, request)),
|
||||
handleDeleteEffect: (request: ConfigRequest) => handleDeleteEffect(state, persistPath, request),
|
||||
handleList: (request: ConfigRequest) => handleListWithState(stateSnapshot(state), request),
|
||||
handleGetValues: (request: ConfigRequest) => handleGetValuesWithState(stateSnapshot(state), request),
|
||||
handleGetValuesAllWorkspaces: (request: ConfigRequest) => handleGetValuesAllWorkspacesWithState(stateSnapshot(state), request),
|
||||
handleConfigDump: (request: ConfigRequest) => handleConfigDumpWithState(stateSnapshot(state), request),
|
||||
pushConfig: () => Effect.runPromise(SynchronizedRef.get(state).pipe(Effect.flatMap(pushConfigWithStateEffect))),
|
||||
pushConfigEffect: SynchronizedRef.get(state).pipe(Effect.flatMap(pushConfigWithStateEffect)),
|
||||
persist: () => Effect.runPromise(SynchronizedRef.get(state).pipe(Effect.flatMap((current) => persistStateEffect(persistPath, current)))),
|
||||
persistEffect: SynchronizedRef.get(state).pipe(Effect.flatMap((current) => persistStateEffect(persistPath, current))),
|
||||
loadFromDisk: () => Effect.runPromise(loadFromDiskEffect()),
|
||||
loadFromDiskEffect: loadFromDiskEffect(),
|
||||
stop: () =>
|
||||
Effect.runPromise(
|
||||
closeConfigResourcesEffect(state).pipe(
|
||||
Effect.flatMap(() =>
|
||||
Effect.tryPromise({
|
||||
try: () => baseStop(),
|
||||
catch: (cause) => configServiceError("stop", cause),
|
||||
})
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
}) as ConfigService;
|
||||
|
||||
return service;
|
||||
}
|
||||
|
|
@ -887,12 +866,6 @@ export const program = makeProcessorProgram({
|
|||
make: (config) => makeConfigService(config),
|
||||
});
|
||||
|
||||
const configServiceRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return configServiceRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
makeAsyncProcessor,
|
||||
makeProcessorProgram,
|
||||
optionalStringConfig,
|
||||
processorLifecycleError,
|
||||
topics,
|
||||
type AsyncProcessorRuntime,
|
||||
type BackendConsumer,
|
||||
|
|
@ -24,17 +25,18 @@ import {
|
|||
type KnowledgeResponse,
|
||||
type Message,
|
||||
type ProcessorConfig,
|
||||
type PubSubError,
|
||||
} from "@trustgraph/base";
|
||||
import {Duration, Effect, HashMap, Layer, ManagedRuntime, Match, SynchronizedRef} from "effect";
|
||||
import {Duration, Effect, HashMap, Match, SynchronizedRef} from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import * as S from "effect/Schema";
|
||||
import {ensureDirectory, joinPath, readTextFile, writeTextFile} from "../runtime/effect-files.js";
|
||||
import {ensureDirectoryEffect, joinPath, readTextFileEffect, writeTextFileEffect} from "../runtime/effect-files.js";
|
||||
|
||||
export interface KnowledgeCoreServiceConfig extends ProcessorConfig {
|
||||
readonly dataDir?: string;
|
||||
}
|
||||
|
||||
const NumberArray = S.Array(S.Number).pipe(S.mutable);
|
||||
const NumberArray = S.Array(S.Finite).pipe(S.mutable);
|
||||
const NumberArrays = S.Array(NumberArray).pipe(S.mutable);
|
||||
|
||||
const GraphEmbeddingSchema = S.Struct({
|
||||
|
|
@ -98,35 +100,20 @@ export interface KnowledgeCoreService extends AsyncProcessorRuntime<KnowledgeCor
|
|||
readonly coreKey: (user: string, id: string) => string;
|
||||
readonly graphEmbeddings: (request: KnowledgeRequest) => ReadonlyArray<GraphEmbedding>;
|
||||
readonly documentEmbeddings: (request: KnowledgeRequest) => DocumentEmbeddingsCore | undefined;
|
||||
readonly handleMessage: (msg: Message<KnowledgeRequest>) => Promise<void>;
|
||||
readonly handleMessageEffect: (msg: Message<KnowledgeRequest>) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly handleOperation: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly handleOperationEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly listKgCores: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly listKgCoresEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly getKgCore: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly getKgCoreEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly deleteKgCore: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly deleteKgCoreEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly putKgCore: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly putKgCoreEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly loadKgCore: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly loadKgCoreEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly unloadKgCore: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly unloadKgCoreEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly listDeCores: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly listDeCoresEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly getDeCore: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly getDeCoreEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly deleteDeCore: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly deleteDeCoreEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly putDeCore: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly putDeCoreEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly loadDeCore: (request: KnowledgeRequest, requestId: string) => Promise<void>;
|
||||
readonly loadDeCoreEffect: (request: KnowledgeRequest, requestId: string) => Effect.Effect<void, KnowledgeCoreServiceError>;
|
||||
readonly persist: () => Promise<void>;
|
||||
readonly persistEffect: Effect.Effect<void, never>;
|
||||
readonly loadFromDisk: () => Promise<void>;
|
||||
readonly loadFromDiskEffect: Effect.Effect<void, never>;
|
||||
}
|
||||
|
||||
|
|
@ -204,20 +191,12 @@ const updateHandles = (
|
|||
responseProducer: handles.responseProducer === undefined ? state.responseProducer : handles.responseProducer,
|
||||
}));
|
||||
|
||||
const tryPromise = <A>(
|
||||
operation: string,
|
||||
evaluate: () => Promise<A>,
|
||||
): Effect.Effect<A, KnowledgeCoreServiceError> =>
|
||||
Effect.tryPromise({
|
||||
try: evaluate,
|
||||
catch: (cause) => knowledgeCoreServiceError(operation, cause),
|
||||
});
|
||||
|
||||
const closeResource = (
|
||||
resource: {readonly close: () => Promise<void>},
|
||||
resource: {readonly close: Effect.Effect<void, PubSubError>},
|
||||
operation: string,
|
||||
): Effect.Effect<void> =>
|
||||
tryPromise(operation, () => resource.close()).pipe(
|
||||
resource.close.pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError(operation, cause)),
|
||||
Effect.catch((error) =>
|
||||
Effect.logError("[KnowledgeCoreService] Failed to close resource", {
|
||||
error: error.message,
|
||||
|
|
@ -237,12 +216,16 @@ const sendResponse = Effect.fnUntraced(function* (
|
|||
return yield* knowledgeCoreServiceError(operation, "Knowledge response producer not started");
|
||||
}
|
||||
|
||||
yield* tryPromise(operation, () => responseProducer.send(response, {id: requestId}));
|
||||
yield* responseProducer.send(response, {id: requestId}).pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError(operation, cause)),
|
||||
);
|
||||
});
|
||||
|
||||
const readPersistedKnowledgeEffect = Effect.fn("KnowledgeCoreService.readPersistedKnowledge")(
|
||||
function* (persistPath: string) {
|
||||
const raw = yield* tryPromise("load-read", () => readTextFile(persistPath));
|
||||
const raw = yield* readTextFileEffect(persistPath).pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("load-read", cause)),
|
||||
);
|
||||
const current = S.decodeUnknownOption(PersistedKnowledgeSnapshotJsonSchema)(raw);
|
||||
if (O.isSome(current)) {
|
||||
return {
|
||||
|
|
@ -282,7 +265,9 @@ const persistStateEffect = Effect.fn("KnowledgeCoreService.persistState")(
|
|||
const json = yield* S.encodeUnknownEffect(S.UnknownFromJsonString)(snapshot).pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("persist-encode", cause)),
|
||||
);
|
||||
yield* tryPromise("persist-write", () => writeTextFile(persistPath, json));
|
||||
yield* writeTextFileEffect(persistPath, json).pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("persist-write", cause)),
|
||||
);
|
||||
},
|
||||
(effect) =>
|
||||
effect.pipe(
|
||||
|
|
@ -317,12 +302,16 @@ const closeKnowledgeResourcesEffect = Effect.fn("KnowledgeCoreService.closeResou
|
|||
|
||||
const consumer = state.consumer;
|
||||
if (consumer !== null) {
|
||||
yield* tryPromise("close-consumer", () => consumer.close());
|
||||
yield* consumer.close.pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("close-consumer", cause)),
|
||||
);
|
||||
}
|
||||
|
||||
const responseProducer = state.responseProducer;
|
||||
if (responseProducer !== null) {
|
||||
yield* tryPromise("close-response-producer", () => responseProducer.close());
|
||||
yield* responseProducer.close.pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("close-response-producer", cause)),
|
||||
);
|
||||
}
|
||||
|
||||
yield* updateHandles(stateRef, {
|
||||
|
|
@ -339,33 +328,39 @@ const consumeOnceEffect = Effect.fnUntraced(function* (
|
|||
return yield* knowledgeCoreServiceError("consume", "Knowledge request consumer not started");
|
||||
}
|
||||
|
||||
const msg = yield* tryPromise("consume-receive", () => consumer.receive(2000));
|
||||
const msg = yield* consumer.receive(2000).pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("consume-receive", cause)),
|
||||
);
|
||||
if (msg === null) return;
|
||||
|
||||
yield* service.handleMessageEffect(msg);
|
||||
yield* tryPromise("consume-acknowledge", () => consumer.acknowledge(msg));
|
||||
yield* consumer.acknowledge(msg).pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("consume-acknowledge", cause)),
|
||||
);
|
||||
});
|
||||
|
||||
const runKnowledgeCoreServiceEffect = Effect.fn("KnowledgeCoreService.run")(function* (
|
||||
service: KnowledgeCoreService,
|
||||
) {
|
||||
yield* tryPromise("ensure-directory", () => ensureDirectory(service.dataDir));
|
||||
yield* ensureDirectoryEffect(service.dataDir).pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("ensure-directory", cause)),
|
||||
);
|
||||
yield* service.loadFromDiskEffect;
|
||||
|
||||
const responseProducer = yield* tryPromise("response-producer", () =>
|
||||
service.pubsub.createProducer<KnowledgeResponse>({
|
||||
topic: topics.knowledgeResponse,
|
||||
schema: KnowledgeResponseSchema,
|
||||
}),
|
||||
const responseProducer = yield* service.pubsub.createProducer<KnowledgeResponse>({
|
||||
topic: topics.knowledgeResponse,
|
||||
schema: KnowledgeResponseSchema,
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("response-producer", cause)),
|
||||
);
|
||||
yield* updateHandles(service.state, {responseProducer});
|
||||
|
||||
const consumer = yield* tryPromise("consumer", () =>
|
||||
service.pubsub.createConsumer<KnowledgeRequest>({
|
||||
topic: topics.knowledgeRequest,
|
||||
subscription: `${service.config.id}-knowledge-request`,
|
||||
schema: KnowledgeRequestSchema,
|
||||
}),
|
||||
const consumer = yield* service.pubsub.createConsumer<KnowledgeRequest>({
|
||||
topic: topics.knowledgeRequest,
|
||||
subscription: `${service.config.id}-knowledge-request`,
|
||||
schema: KnowledgeRequestSchema,
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("consumer", cause)),
|
||||
);
|
||||
yield* updateHandles(service.state, {consumer});
|
||||
|
||||
|
|
@ -504,12 +499,11 @@ const loadKgCoreEffect = Effect.fn("loadKgCoreEffect")(function* (
|
|||
|
||||
if (core.triples.length > 0) {
|
||||
yield* Effect.acquireUseRelease(
|
||||
tryPromise("triples-producer", () =>
|
||||
service.pubsub.createProducer<unknown>({topic: "tg.flow.triples"}),
|
||||
service.pubsub.createProducer<unknown>({topic: "tg.flow.triples"}).pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("triples-producer", cause)),
|
||||
),
|
||||
(producer) =>
|
||||
tryPromise("send-triples", () =>
|
||||
producer.send({
|
||||
producer.send({
|
||||
metadata: {
|
||||
id: coreId,
|
||||
root: coreId,
|
||||
|
|
@ -517,8 +511,9 @@ const loadKgCoreEffect = Effect.fn("loadKgCoreEffect")(function* (
|
|||
collection: request.collection ?? "default",
|
||||
},
|
||||
triples: core.triples,
|
||||
}),
|
||||
),
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => knowledgeCoreServiceError("send-triples", cause)),
|
||||
),
|
||||
(producer) => closeResource(producer, "close-triples-producer"),
|
||||
);
|
||||
}
|
||||
|
|
@ -637,7 +632,6 @@ export function makeKnowledgeCoreService(config: KnowledgeCoreServiceConfig): Kn
|
|||
const base = makeAsyncProcessor<KnowledgeCoreServiceError>(config, {
|
||||
runEffect: () => getService.pipe(Effect.flatMap(runKnowledgeCoreServiceEffect)),
|
||||
});
|
||||
const baseStop = base.stop;
|
||||
|
||||
const handleOperationEffect = Effect.fn("KnowledgeCoreService.handleOperation")(function* (
|
||||
request: KnowledgeRequest,
|
||||
|
|
@ -699,54 +693,50 @@ export function makeKnowledgeCoreService(config: KnowledgeCoreServiceConfig): Kn
|
|||
yield* Effect.log(`[KnowledgeCoreService] Loaded persisted state (kg=${HashMap.size(next.kgCores)}, de=${HashMap.size(next.deCores)})`);
|
||||
});
|
||||
|
||||
service = Object.assign(base, {
|
||||
const serviceStopEffect = closeKnowledgeResourcesEffect(state).pipe(
|
||||
Effect.mapError((cause) => processorLifecycleError(config.id, "stop", cause)),
|
||||
Effect.flatMap(() => base.stop),
|
||||
);
|
||||
|
||||
const serviceBase = Object.create(base, {
|
||||
stop: {
|
||||
value: serviceStopEffect,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
stopEffect: {
|
||||
value: serviceStopEffect,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
|
||||
service = Object.assign(serviceBase, {
|
||||
state,
|
||||
dataDir,
|
||||
persistPath,
|
||||
coreKey,
|
||||
graphEmbeddings: graphEmbeddingsFor,
|
||||
documentEmbeddings: documentEmbeddingsFor,
|
||||
handleMessage: (msg: Message<KnowledgeRequest>) => Effect.runPromise(handleMessageEffect(msg)),
|
||||
handleMessageEffect,
|
||||
handleOperation: (request: KnowledgeRequest, requestId: string) => Effect.runPromise(handleOperationEffect(request, requestId)),
|
||||
handleOperationEffect,
|
||||
listKgCores: (request: KnowledgeRequest, requestId: string) => Effect.runPromise(listKgCoresEffect(state, request, requestId)),
|
||||
listKgCoresEffect: (request: KnowledgeRequest, requestId: string) => listKgCoresEffect(state, request, requestId),
|
||||
getKgCore: (request: KnowledgeRequest, requestId: string) => Effect.runPromise(getKgCoreEffect(state, request, requestId)),
|
||||
getKgCoreEffect: (request: KnowledgeRequest, requestId: string) => getKgCoreEffect(state, request, requestId),
|
||||
deleteKgCore: (request: KnowledgeRequest, requestId: string) => Effect.runPromise(deleteKgCoreEffect(state, persistPath, request, requestId)),
|
||||
deleteKgCoreEffect: (request: KnowledgeRequest, requestId: string) => deleteKgCoreEffect(state, persistPath, request, requestId),
|
||||
putKgCore: (request: KnowledgeRequest, requestId: string) => Effect.runPromise(putKgCoreEffect(state, persistPath, request, requestId)),
|
||||
putKgCoreEffect: (request: KnowledgeRequest, requestId: string) => putKgCoreEffect(state, persistPath, request, requestId),
|
||||
loadKgCore: (request: KnowledgeRequest, requestId: string) =>
|
||||
Effect.runPromise(getService.pipe(Effect.flatMap((current) => loadKgCoreEffect(state, current, request, requestId)))),
|
||||
loadKgCoreEffect: (request: KnowledgeRequest, requestId: string) =>
|
||||
getService.pipe(Effect.flatMap((current) => loadKgCoreEffect(state, current, request, requestId))),
|
||||
unloadKgCore: (_request: KnowledgeRequest, requestId: string) => Effect.runPromise(sendResponse(state, {}, requestId)),
|
||||
unloadKgCoreEffect: (_request: KnowledgeRequest, requestId: string) => sendResponse(state, {}, requestId),
|
||||
listDeCores: (request: KnowledgeRequest, requestId: string) => Effect.runPromise(listDeCoresEffect(state, request, requestId)),
|
||||
listDeCoresEffect: (request: KnowledgeRequest, requestId: string) => listDeCoresEffect(state, request, requestId),
|
||||
getDeCore: (request: KnowledgeRequest, requestId: string) => Effect.runPromise(getDeCoreEffect(state, request, requestId)),
|
||||
getDeCoreEffect: (request: KnowledgeRequest, requestId: string) => getDeCoreEffect(state, request, requestId),
|
||||
deleteDeCore: (request: KnowledgeRequest, requestId: string) => Effect.runPromise(deleteDeCoreEffect(state, persistPath, request, requestId)),
|
||||
deleteDeCoreEffect: (request: KnowledgeRequest, requestId: string) => deleteDeCoreEffect(state, persistPath, request, requestId),
|
||||
putDeCore: (request: KnowledgeRequest, requestId: string) => Effect.runPromise(putDeCoreEffect(state, persistPath, request, requestId)),
|
||||
putDeCoreEffect: (request: KnowledgeRequest, requestId: string) => putDeCoreEffect(state, persistPath, request, requestId),
|
||||
loadDeCore: (request: KnowledgeRequest, requestId: string) => Effect.runPromise(loadDeCoreEffect(state, request, requestId)),
|
||||
loadDeCoreEffect: (request: KnowledgeRequest, requestId: string) => loadDeCoreEffect(state, request, requestId),
|
||||
persist: () => Effect.runPromise(SynchronizedRef.get(state).pipe(Effect.flatMap((current) => persistStateEffect(persistPath, current)))),
|
||||
persistEffect: SynchronizedRef.get(state).pipe(Effect.flatMap((current) => persistStateEffect(persistPath, current))),
|
||||
loadFromDisk: () => Effect.runPromise(loadFromDiskEffect()),
|
||||
loadFromDiskEffect: loadFromDiskEffect(),
|
||||
stop: () =>
|
||||
Effect.runPromise(
|
||||
closeKnowledgeResourcesEffect(state).pipe(
|
||||
Effect.flatMap(() =>
|
||||
tryPromise("base-stop", () => baseStop())
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
}) as KnowledgeCoreService;
|
||||
|
||||
return service;
|
||||
}
|
||||
|
|
@ -770,12 +760,6 @@ export const program = makeProcessorProgram({
|
|||
make: (config) => makeKnowledgeCoreService(config),
|
||||
});
|
||||
|
||||
const knowledgeCoreRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return knowledgeCoreRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { makeFlowProcessorProgram } from "@trustgraph/base";
|
||||
import { Clock, Effect, Layer, ManagedRuntime } from "effect";
|
||||
import { Clock, Effect } from "effect";
|
||||
import * as S from "effect/Schema";
|
||||
|
||||
export class PdfDecoderError extends S.TaggedErrorClass<PdfDecoderError>()(
|
||||
|
|
@ -48,7 +48,7 @@ export class PdfDecoderError extends S.TaggedErrorClass<PdfDecoderError>()(
|
|||
message: S.String,
|
||||
operation: S.String,
|
||||
documentId: S.String,
|
||||
cause: S.DefectWithStack,
|
||||
cause: S.Defect({ includeStack: true }),
|
||||
},
|
||||
) {}
|
||||
|
||||
|
|
@ -257,12 +257,6 @@ export const program = makeFlowProcessorProgram({
|
|||
specs: () => makePdfDecoderSpecs(),
|
||||
});
|
||||
|
||||
const pdfDecoderRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return pdfDecoderRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { Config, Effect, Layer, ManagedRuntime } from "effect";
|
||||
import { Config, Effect, Layer } from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import * as S from "effect/Schema";
|
||||
import {
|
||||
|
|
@ -25,7 +25,7 @@ export interface OllamaEmbeddingsConfig extends ProcessorConfig {
|
|||
fetch?: typeof fetch;
|
||||
}
|
||||
|
||||
const EmbeddingVector = S.Array(S.Number);
|
||||
const EmbeddingVector = S.Array(S.Finite);
|
||||
|
||||
const OllamaEmbedResponse = S.Struct({
|
||||
embeddings: S.Array(EmbeddingVector),
|
||||
|
|
@ -71,9 +71,6 @@ const loadOllamaEmbeddingsConfig = Effect.fn("OllamaEmbeddings.loadConfig")(func
|
|||
} satisfies ResolvedOllamaEmbeddingsConfig;
|
||||
});
|
||||
|
||||
const responseJson = (response: Response): Promise<unknown> =>
|
||||
response.json();
|
||||
|
||||
const makeOllamaEmbeddingsFromConfig = ({
|
||||
defaultModel,
|
||||
ollamaHost,
|
||||
|
|
@ -116,7 +113,7 @@ const makeOllamaEmbeddingsFromConfig = ({
|
|||
}
|
||||
|
||||
const data = yield* Effect.tryPromise({
|
||||
try: () => responseJson(response),
|
||||
try: () => response.json(),
|
||||
catch: (error) => ollamaEmbeddingsError("ollama.response-json", error),
|
||||
});
|
||||
const decoded = yield* S.decodeUnknownEffect(OllamaEmbedResponse)(data).pipe(
|
||||
|
|
@ -166,12 +163,6 @@ export const program = makeFlowProcessorProgram<OllamaEmbeddingsConfig, Embeddin
|
|||
layer: (config) => OllamaEmbeddingsLive(config),
|
||||
});
|
||||
|
||||
const ollamaEmbeddingsRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return ollamaEmbeddingsRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import {
|
|||
type Spec,
|
||||
} from "@trustgraph/base";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { Effect, Layer, ManagedRuntime } from "effect";
|
||||
import { Effect } from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import * as S from "effect/Schema";
|
||||
|
||||
|
|
@ -392,12 +392,6 @@ export const program = makeFlowProcessorProgram({
|
|||
specs: () => makeKnowledgeExtractSpecs(),
|
||||
});
|
||||
|
||||
const knowledgeExtractRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return knowledgeExtractRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,23 +30,45 @@ import {
|
|||
type FlowRequest,
|
||||
type FlowResponse,
|
||||
errorMessage,
|
||||
processorLifecycleError,
|
||||
} from "@trustgraph/base";
|
||||
import { makeProcessorProgram } from "@trustgraph/base";
|
||||
import type { Message } from "@trustgraph/base";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { Duration, Effect, HashMap, Layer, ManagedRuntime, Match, Option, SynchronizedRef } from "effect";
|
||||
import { Duration, Effect, HashMap, Match, Option, SynchronizedRef } from "effect";
|
||||
import * as S from "effect/Schema";
|
||||
|
||||
// ---------- Internal state types ----------
|
||||
|
||||
interface FlowInstance {
|
||||
id: string;
|
||||
blueprintName: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>;
|
||||
status: "running" | "stopped";
|
||||
class FlowInstanceRunning extends S.Class<FlowInstanceRunning>("FlowInstanceRunning")({
|
||||
id: S.String,
|
||||
blueprintName: S.String,
|
||||
description: S.optionalKey(S.String),
|
||||
parameters: S.Record(S.String, S.Unknown),
|
||||
status: S.tag("running")
|
||||
}) {}
|
||||
|
||||
class FlowInstanceStopped extends S.Class<FlowInstanceStopped>("FlowInstanceStopped")({
|
||||
id: S.String,
|
||||
blueprintName: S.String,
|
||||
description: S.optionalKey(S.String),
|
||||
parameters: S.Record(S.String, S.Unknown),
|
||||
status: S.tag("stopped")
|
||||
}) {
|
||||
|
||||
}
|
||||
|
||||
export const FlowInstance = S.Union(
|
||||
[
|
||||
FlowInstanceRunning,
|
||||
FlowInstanceStopped
|
||||
]
|
||||
).pipe(
|
||||
S.toTaggedUnion("status")
|
||||
);
|
||||
|
||||
export type FlowInstance = typeof FlowInstance.Type;
|
||||
|
||||
interface Blueprint {
|
||||
description: string;
|
||||
topics: Record<string, string>;
|
||||
|
|
@ -175,35 +197,21 @@ interface FlowManagerServiceState {
|
|||
|
||||
export interface FlowManagerService extends AsyncProcessorRuntime<FlowManagerError> {
|
||||
readonly state: SynchronizedRef.SynchronizedRef<FlowManagerServiceState>;
|
||||
readonly handleMessage: (msg: Message<FlowRequest>) => Promise<void>;
|
||||
readonly handleMessageEffect: (msg: Message<FlowRequest>) => Effect.Effect<void, FlowManagerError>;
|
||||
readonly configRequest: (request: ConfigRequest) => Promise<ConfigResponse>;
|
||||
readonly configRequestEffect: (request: ConfigRequest) => Effect.Effect<ConfigResponse, FlowManagerError>;
|
||||
readonly ensureDefaultBlueprint: () => Promise<void>;
|
||||
readonly ensureDefaultBlueprintEffect: Effect.Effect<void, FlowManagerError>;
|
||||
readonly refreshBlueprintsFromConfig: () => Promise<void>;
|
||||
readonly refreshBlueprintsFromConfigEffect: Effect.Effect<void, FlowManagerError>;
|
||||
readonly refreshFlowsFromConfig: () => Promise<void>;
|
||||
readonly refreshFlowsFromConfigEffect: Effect.Effect<void, FlowManagerError>;
|
||||
readonly handleOperation: (request: FlowRequest) => Promise<FlowResponse>;
|
||||
readonly handleOperationEffect: (request: FlowRequest) => Effect.Effect<FlowResponse, FlowManagerError>;
|
||||
readonly handleListBlueprints: () => FlowResponse;
|
||||
readonly handleGetBlueprint: (request: FlowRequest) => Promise<FlowResponse>;
|
||||
readonly handleGetBlueprintEffect: (request: FlowRequest) => Effect.Effect<FlowResponse, FlowManagerError>;
|
||||
readonly handlePutBlueprint: (request: FlowRequest) => Promise<FlowResponse>;
|
||||
readonly handlePutBlueprintEffect: (request: FlowRequest) => Effect.Effect<FlowResponse, FlowManagerError>;
|
||||
readonly handleDeleteBlueprint: (request: FlowRequest) => Promise<FlowResponse>;
|
||||
readonly handleDeleteBlueprintEffect: (request: FlowRequest) => Effect.Effect<FlowResponse, FlowManagerError>;
|
||||
readonly handleListFlows: () => FlowResponse;
|
||||
readonly handleGetFlow: (request: FlowRequest) => Promise<FlowResponse>;
|
||||
readonly handleGetFlowEffect: (request: FlowRequest) => Effect.Effect<FlowResponse, FlowManagerError>;
|
||||
readonly handleStartFlow: (request: FlowRequest) => Promise<FlowResponse>;
|
||||
readonly handleStartFlowEffect: (request: FlowRequest) => Effect.Effect<FlowResponse, FlowManagerError>;
|
||||
readonly handleStopFlow: (request: FlowRequest) => Promise<FlowResponse>;
|
||||
readonly handleStopFlowEffect: (request: FlowRequest) => Effect.Effect<FlowResponse, FlowManagerError>;
|
||||
readonly pushFlowsConfig: () => Promise<void>;
|
||||
readonly pushFlowsConfigEffect: Effect.Effect<void>;
|
||||
readonly deleteFlowConfig: (id: string) => Promise<void>;
|
||||
readonly deleteFlowConfigEffect: (id: string) => Effect.Effect<void, FlowManagerError>;
|
||||
}
|
||||
|
||||
|
|
@ -259,13 +267,13 @@ function blueprintFromConfig(value: unknown): Blueprint | undefined {
|
|||
function flowFromConfig(id: string, value: unknown): FlowInstance | undefined {
|
||||
const parsed = parseConfigRecord(value);
|
||||
if (parsed === undefined) return undefined;
|
||||
return {
|
||||
return FlowInstanceRunning.make({
|
||||
id,
|
||||
blueprintName: optionalString(parsed["blueprint-name"]) ?? optionalString(parsed.blueprintName) ?? "default",
|
||||
description: optionalString(parsed.description) ?? "",
|
||||
parameters: isRecord(parsed.parameters) ? parsed.parameters : {},
|
||||
status: "running",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const updateHandles = (
|
||||
|
|
@ -291,10 +299,9 @@ const configRequestEffect = Effect.fn("FlowManager.configRequest")(function* (
|
|||
if (configClient === null) {
|
||||
return yield* flowManagerError("config-request", "Config client not started");
|
||||
}
|
||||
return yield* Effect.tryPromise({
|
||||
try: () => configClient.request(request),
|
||||
catch: (cause) => flowManagerError("config-request", cause),
|
||||
});
|
||||
return yield* configClient.request(request).pipe(
|
||||
Effect.mapError((cause) => flowManagerError("config-request", cause)),
|
||||
);
|
||||
});
|
||||
|
||||
const ensureDefaultBlueprintEffect = Effect.fn("FlowManager.ensureDefaultBlueprint")(function* (
|
||||
|
|
@ -571,24 +578,20 @@ const pushFlowsConfigEffect = Effect.fn("FlowManager.pushFlowsConfig")(
|
|||
}
|
||||
}
|
||||
|
||||
yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
configClient.request({
|
||||
operation: "put",
|
||||
keys: ["flows"],
|
||||
values: flowsConfig,
|
||||
}),
|
||||
catch: (cause) => flowManagerError("put-flows-config", cause),
|
||||
});
|
||||
yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
configClient.request({
|
||||
operation: "put",
|
||||
keys: ["flow"],
|
||||
values: flowRecords,
|
||||
}),
|
||||
catch: (cause) => flowManagerError("put-flow-records", cause),
|
||||
});
|
||||
yield* configClient.request({
|
||||
operation: "put",
|
||||
keys: ["flows"],
|
||||
values: flowsConfig,
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => flowManagerError("put-flows-config", cause)),
|
||||
);
|
||||
yield* configClient.request({
|
||||
operation: "put",
|
||||
keys: ["flow"],
|
||||
values: flowRecords,
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => flowManagerError("put-flow-records", cause)),
|
||||
);
|
||||
yield* Effect.log(`[FlowManager] Pushed flows config (${HashMap.size(state.flows)} active flows)`);
|
||||
},
|
||||
(effect) =>
|
||||
|
|
@ -605,22 +608,18 @@ const deleteFlowConfigEffect = Effect.fn("FlowManager.deleteFlowConfig")(functio
|
|||
) {
|
||||
const configClient = (yield* SynchronizedRef.get(stateRef)).configClient;
|
||||
if (configClient === null) return;
|
||||
yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
configClient.request({
|
||||
operation: "delete",
|
||||
keys: ["flows", id],
|
||||
}),
|
||||
catch: (cause) => flowManagerError("delete-flows-config", cause),
|
||||
});
|
||||
yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
configClient.request({
|
||||
operation: "delete",
|
||||
keys: ["flow", id],
|
||||
}),
|
||||
catch: (cause) => flowManagerError("delete-flow-record", cause),
|
||||
});
|
||||
yield* configClient.request({
|
||||
operation: "delete",
|
||||
keys: ["flows", id],
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => flowManagerError("delete-flows-config", cause)),
|
||||
);
|
||||
yield* configClient.request({
|
||||
operation: "delete",
|
||||
keys: ["flow", id],
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => flowManagerError("delete-flow-record", cause)),
|
||||
);
|
||||
});
|
||||
|
||||
const closeFlowManagerResourcesEffect = Effect.fn("FlowManager.closeResources")(function* (
|
||||
|
|
@ -630,24 +629,19 @@ const closeFlowManagerResourcesEffect = Effect.fn("FlowManager.closeResources")(
|
|||
|
||||
const consumer = state.consumer;
|
||||
if (consumer !== null) {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => consumer.close(),
|
||||
catch: (cause) => flowManagerError("consumer-close", cause),
|
||||
});
|
||||
yield* consumer.close.pipe(
|
||||
Effect.mapError((cause) => flowManagerError("consumer-close", cause)),
|
||||
);
|
||||
}
|
||||
const responseProducer = state.responseProducer;
|
||||
if (responseProducer !== null) {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => responseProducer.close(),
|
||||
catch: (cause) => flowManagerError("response-producer-close", cause),
|
||||
});
|
||||
yield* responseProducer.close.pipe(
|
||||
Effect.mapError((cause) => flowManagerError("response-producer-close", cause)),
|
||||
);
|
||||
}
|
||||
const configClient = state.configClient;
|
||||
if (configClient !== null) {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => configClient.stop(),
|
||||
catch: (cause) => flowManagerError("config-client-stop", cause),
|
||||
});
|
||||
yield* configClient.stop;
|
||||
}
|
||||
|
||||
yield* updateHandles(stateRef, {
|
||||
|
|
@ -665,17 +659,15 @@ const consumeOnceEffect = Effect.fnUntraced(function* (
|
|||
return yield* flowManagerError("consume", "Flow request consumer not started");
|
||||
}
|
||||
|
||||
const msg = yield* Effect.tryPromise({
|
||||
try: () => consumer.receive(2000),
|
||||
catch: (cause) => flowManagerError("consume-receive", cause),
|
||||
});
|
||||
const msg = yield* consumer.receive(2000).pipe(
|
||||
Effect.mapError((cause) => flowManagerError("consume-receive", cause)),
|
||||
);
|
||||
if (msg === null) return;
|
||||
|
||||
yield* service.handleMessageEffect(msg);
|
||||
yield* Effect.tryPromise({
|
||||
try: () => consumer.acknowledge(msg),
|
||||
catch: (cause) => flowManagerError("consume-acknowledge", cause),
|
||||
});
|
||||
yield* consumer.acknowledge(msg).pipe(
|
||||
Effect.mapError((cause) => flowManagerError("consume-acknowledge", cause)),
|
||||
);
|
||||
});
|
||||
|
||||
const runFlowManagerServiceEffect = Effect.fn("FlowManager.runService")(function* (
|
||||
|
|
@ -688,32 +680,27 @@ const runFlowManagerServiceEffect = Effect.fn("FlowManager.runService")(function
|
|||
subscription: `${service.config.id}-config-client`,
|
||||
});
|
||||
yield* updateHandles(service.state, { configClient });
|
||||
yield* Effect.tryPromise({
|
||||
try: () => configClient.start(),
|
||||
catch: (cause) => flowManagerError("config-client-start", cause),
|
||||
});
|
||||
yield* configClient.start.pipe(
|
||||
Effect.mapError((cause) => flowManagerError("config-client-start", cause)),
|
||||
);
|
||||
yield* ensureDefaultBlueprintEffect(service.state);
|
||||
yield* refreshBlueprintsFromConfigEffect(service.state);
|
||||
|
||||
const responseProducer = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
service.pubsub.createProducer<FlowResponse>({
|
||||
topic: topics.flowResponse,
|
||||
schema: FlowResponseSchema,
|
||||
}),
|
||||
catch: (cause) => flowManagerError("response-producer", cause),
|
||||
});
|
||||
const responseProducer = yield* service.pubsub.createProducer<FlowResponse>({
|
||||
topic: topics.flowResponse,
|
||||
schema: FlowResponseSchema,
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => flowManagerError("response-producer", cause)),
|
||||
);
|
||||
yield* updateHandles(service.state, { responseProducer });
|
||||
|
||||
const consumer = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
service.pubsub.createConsumer<FlowRequest>({
|
||||
topic: topics.flowRequest,
|
||||
subscription: `${service.config.id}-flow-request`,
|
||||
schema: FlowRequestSchema,
|
||||
}),
|
||||
catch: (cause) => flowManagerError("consumer", cause),
|
||||
});
|
||||
const consumer = yield* service.pubsub.createConsumer<FlowRequest>({
|
||||
topic: topics.flowRequest,
|
||||
subscription: `${service.config.id}-flow-request`,
|
||||
schema: FlowRequestSchema,
|
||||
}).pipe(
|
||||
Effect.mapError((cause) => flowManagerError("consumer", cause)),
|
||||
);
|
||||
yield* updateHandles(service.state, { consumer });
|
||||
|
||||
yield* Effect.log(`[FlowManager] Listening on ${topics.flowRequest}`);
|
||||
|
|
@ -748,7 +735,6 @@ export function makeFlowManagerService(config: ProcessorConfig): FlowManagerServ
|
|||
const base = makeAsyncProcessor<FlowManagerError>(config, {
|
||||
runEffect: () => getService.pipe(Effect.flatMap(runFlowManagerServiceEffect)),
|
||||
});
|
||||
const baseStop = base.stop;
|
||||
|
||||
const handleOperationEffect = Effect.fn("FlowManager.handleOperation")(function* (request: FlowRequest) {
|
||||
const op = optionalString(request.operation);
|
||||
|
|
@ -784,10 +770,9 @@ export function makeFlowManagerService(config: ProcessorConfig): FlowManagerServ
|
|||
if (responseProducer === null) {
|
||||
return yield* flowManagerError("respond", "Flow response producer not started");
|
||||
}
|
||||
yield* Effect.tryPromise({
|
||||
try: () => responseProducer.send(response, { id: requestId }),
|
||||
catch: (cause) => flowManagerError("respond", cause),
|
||||
});
|
||||
yield* responseProducer.send(response, { id: requestId }).pipe(
|
||||
Effect.mapError((cause) => flowManagerError("respond", cause)),
|
||||
);
|
||||
});
|
||||
|
||||
yield* handleOperationEffect(request).pipe(
|
||||
|
|
@ -800,50 +785,45 @@ export function makeFlowManagerService(config: ProcessorConfig): FlowManagerServ
|
|||
);
|
||||
});
|
||||
|
||||
const flowManagerService: FlowManagerService = Object.assign(base, {
|
||||
const serviceStopEffect = closeFlowManagerResourcesEffect(state).pipe(
|
||||
Effect.mapError((cause) => processorLifecycleError(config.id, "stop", cause)),
|
||||
Effect.flatMap(() => base.stop),
|
||||
);
|
||||
|
||||
const serviceBase = Object.create(base, {
|
||||
stop: {
|
||||
value: serviceStopEffect,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
stopEffect: {
|
||||
value: serviceStopEffect,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
|
||||
const flowManagerService = Object.assign(serviceBase, {
|
||||
state,
|
||||
handleMessage: (msg: Message<FlowRequest>) => Effect.runPromise(handleMessageEffect(msg)),
|
||||
handleMessageEffect,
|
||||
configRequest: (request: ConfigRequest) => Effect.runPromise(configRequestEffect(state, request)),
|
||||
configRequestEffect: (request: ConfigRequest) => configRequestEffect(state, request),
|
||||
ensureDefaultBlueprint: () => Effect.runPromise(ensureDefaultBlueprintEffect(state)),
|
||||
ensureDefaultBlueprintEffect: ensureDefaultBlueprintEffect(state),
|
||||
refreshBlueprintsFromConfig: () => Effect.runPromise(refreshBlueprintsFromConfigEffect(state)),
|
||||
refreshBlueprintsFromConfigEffect: refreshBlueprintsFromConfigEffect(state),
|
||||
refreshFlowsFromConfig: () => Effect.runPromise(refreshFlowsFromConfigEffect(state)),
|
||||
refreshFlowsFromConfigEffect: refreshFlowsFromConfigEffect(state),
|
||||
handleOperation: (request: FlowRequest) => Effect.runPromise(handleOperationEffect(request)),
|
||||
handleOperationEffect,
|
||||
handleListBlueprints: () => handleListBlueprintsWithState(state.pipe(stateSnapshot)),
|
||||
handleGetBlueprint: (request: FlowRequest) => Effect.runPromise(handleGetBlueprintEffect(state, request)),
|
||||
handleGetBlueprintEffect: (request: FlowRequest) => handleGetBlueprintEffect(state, request),
|
||||
handlePutBlueprint: (request: FlowRequest) => Effect.runPromise(handlePutBlueprintEffect(state, request)),
|
||||
handlePutBlueprintEffect: (request: FlowRequest) => handlePutBlueprintEffect(state, request),
|
||||
handleDeleteBlueprint: (request: FlowRequest) => Effect.runPromise(handleDeleteBlueprintEffect(state, request)),
|
||||
handleDeleteBlueprintEffect: (request: FlowRequest) => handleDeleteBlueprintEffect(state, request),
|
||||
handleListFlows: () => handleListFlowsWithState(state.pipe(stateSnapshot)),
|
||||
handleGetFlow: (request: FlowRequest) => Effect.runPromise(handleGetFlowEffect(state, request)),
|
||||
handleGetFlowEffect: (request: FlowRequest) => handleGetFlowEffect(state, request),
|
||||
handleStartFlow: (request: FlowRequest) => Effect.runPromise(handleStartFlowEffect(state, request)),
|
||||
handleStartFlowEffect: (request: FlowRequest) => handleStartFlowEffect(state, request),
|
||||
handleStopFlow: (request: FlowRequest) => Effect.runPromise(handleStopFlowEffect(state, request)),
|
||||
handleStopFlowEffect: (request: FlowRequest) => handleStopFlowEffect(state, request),
|
||||
pushFlowsConfig: () => Effect.runPromise(pushFlowsConfigEffect(state)),
|
||||
pushFlowsConfigEffect: pushFlowsConfigEffect(state),
|
||||
deleteFlowConfig: (id: string) => Effect.runPromise(deleteFlowConfigEffect(state, id)),
|
||||
deleteFlowConfigEffect: (id: string) => deleteFlowConfigEffect(state, id),
|
||||
stop: () =>
|
||||
Effect.runPromise(
|
||||
closeFlowManagerResourcesEffect(state).pipe(
|
||||
Effect.flatMap(() =>
|
||||
Effect.tryPromise({
|
||||
try: () => baseStop(),
|
||||
catch: (cause) => flowManagerError("base-stop", cause),
|
||||
})
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
}) as FlowManagerService;
|
||||
|
||||
service = flowManagerService;
|
||||
return flowManagerService;
|
||||
|
|
@ -856,12 +836,6 @@ export const program = makeProcessorProgram({
|
|||
make: (config) => makeFlowManagerService(config),
|
||||
});
|
||||
|
||||
const flowManagerRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return flowManagerRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import {
|
|||
type DispatchSerializationError,
|
||||
} from "./serialize.js";
|
||||
|
||||
export type Responder = (response: unknown, complete: boolean) => Promise<void>;
|
||||
export type EffectResponder<E = never, R = never> = (
|
||||
response: unknown,
|
||||
complete: boolean,
|
||||
|
|
@ -106,18 +105,13 @@ function topicName(name: string): string {
|
|||
// ---------- Manager ----------
|
||||
|
||||
export interface DispatcherManager {
|
||||
readonly start: () => Promise<void>;
|
||||
readonly stop: () => Promise<void>;
|
||||
readonly start: Effect.Effect<void, MessagingLifecycleError>;
|
||||
readonly stop: Effect.Effect<void, MessagingLifecycleError>;
|
||||
readonly dispatchGlobalService: (
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
) => Promise<unknown>;
|
||||
readonly dispatchGlobalServiceStreaming: (
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
responder: Responder,
|
||||
) => Promise<void>;
|
||||
readonly dispatchGlobalServiceStreamingEffect: <E = never, R = never>(
|
||||
) => Effect.Effect<unknown, DispatcherStreamError>;
|
||||
readonly dispatchGlobalServiceStreaming: <E = never, R = never>(
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
responder: EffectResponder<E, R>,
|
||||
|
|
@ -126,14 +120,8 @@ export interface DispatcherManager {
|
|||
flow: string,
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
) => Promise<unknown>;
|
||||
readonly dispatchFlowServiceStreaming: (
|
||||
flow: string,
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
responder: Responder,
|
||||
) => Promise<void>;
|
||||
readonly dispatchFlowServiceStreamingEffect: <E = never, R = never>(
|
||||
) => Effect.Effect<unknown, DispatcherStreamError>;
|
||||
readonly dispatchFlowServiceStreaming: <E = never, R = never>(
|
||||
flow: string,
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
|
|
@ -143,7 +131,7 @@ export interface DispatcherManager {
|
|||
topic: string,
|
||||
message: unknown,
|
||||
id?: string,
|
||||
) => Promise<void>;
|
||||
) => Effect.Effect<void, MessagingDeliveryError>;
|
||||
}
|
||||
|
||||
export const dispatcherManagerFlowServiceNames = (): readonly string[] => [
|
||||
|
|
@ -214,8 +202,6 @@ export function makeDispatcherManager(config: GatewayConfig): DispatcherManager
|
|||
runtime = nextRuntime;
|
||||
});
|
||||
|
||||
const start = (): Promise<void> => Effect.runPromise(startEffect());
|
||||
|
||||
const stopEffect = Effect.fn("DispatcherManager.stop")(function* () {
|
||||
const current = runtime;
|
||||
runtime = null;
|
||||
|
|
@ -225,15 +211,12 @@ export function makeDispatcherManager(config: GatewayConfig): DispatcherManager
|
|||
}
|
||||
|
||||
if (ownsPubSub) {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => pubsub.close(),
|
||||
catch: (cause) => messagingLifecycleError("gateway-dispatcher", "close-pubsub", cause),
|
||||
});
|
||||
yield* pubsub.close.pipe(
|
||||
Effect.mapError((cause) => messagingLifecycleError("gateway-dispatcher", "close-pubsub", cause)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const stop = (): Promise<void> => Effect.runPromise(stopEffect());
|
||||
|
||||
// ---------- Internal helpers ----------
|
||||
|
||||
const ensureRuntimeEffect = Effect.fn("DispatcherManager.ensureRuntime")(function* () {
|
||||
|
|
@ -303,13 +286,7 @@ export function makeDispatcherManager(config: GatewayConfig): DispatcherManager
|
|||
|
||||
// ---------- Global service dispatch ----------
|
||||
|
||||
const dispatchGlobalService = (
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
): Promise<unknown> =>
|
||||
Effect.runPromise(dispatchGlobalServiceEffect(kind, request));
|
||||
|
||||
const dispatchGlobalServiceEffect = Effect.fn("DispatcherManager.dispatchGlobalService")(function* (
|
||||
const dispatchGlobalService = Effect.fn("DispatcherManager.dispatchGlobalService")(function* (
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
) {
|
||||
|
|
@ -321,7 +298,7 @@ export function makeDispatcherManager(config: GatewayConfig): DispatcherManager
|
|||
return yield* translateResponseEffect(kind, response);
|
||||
});
|
||||
|
||||
const dispatchGlobalServiceStreamingEffect = Effect.fn("DispatcherManager.dispatchGlobalServiceStreaming")(function* <
|
||||
const dispatchGlobalServiceStreaming = Effect.fn("DispatcherManager.dispatchGlobalServiceStreaming")(function* <
|
||||
E,
|
||||
R,
|
||||
>(
|
||||
|
|
@ -342,34 +319,9 @@ export function makeDispatcherManager(config: GatewayConfig): DispatcherManager
|
|||
});
|
||||
});
|
||||
|
||||
const dispatchGlobalServiceStreaming = (
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
responder: Responder,
|
||||
): Promise<void> =>
|
||||
Effect.runPromise(
|
||||
dispatchGlobalServiceStreamingEffect(kind, request, (response, complete) =>
|
||||
Effect.tryPromise({
|
||||
try: () => responder(response, complete),
|
||||
catch: (error) => messagingDeliveryError(
|
||||
resolveGlobalTopics(kind).responseTopic,
|
||||
"stream-responder",
|
||||
error,
|
||||
),
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
// ---------- Flow-scoped service dispatch ----------
|
||||
|
||||
const dispatchFlowService = (
|
||||
flow: string,
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
): Promise<unknown> =>
|
||||
Effect.runPromise(dispatchFlowServiceEffect(flow, kind, request));
|
||||
|
||||
const dispatchFlowServiceEffect = Effect.fn("DispatcherManager.dispatchFlowService")(function* (
|
||||
const dispatchFlowService = Effect.fn("DispatcherManager.dispatchFlowService")(function* (
|
||||
flow: string,
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
|
|
@ -386,7 +338,7 @@ export function makeDispatcherManager(config: GatewayConfig): DispatcherManager
|
|||
return yield* translateResponseEffect(kind, response);
|
||||
});
|
||||
|
||||
const dispatchFlowServiceStreamingEffect = Effect.fn("DispatcherManager.dispatchFlowServiceStreaming")(function* <
|
||||
const dispatchFlowServiceStreaming = Effect.fn("DispatcherManager.dispatchFlowServiceStreaming")(function* <
|
||||
E,
|
||||
R,
|
||||
>(
|
||||
|
|
@ -412,65 +364,40 @@ export function makeDispatcherManager(config: GatewayConfig): DispatcherManager
|
|||
});
|
||||
});
|
||||
|
||||
const dispatchFlowServiceStreaming = (
|
||||
flow: string,
|
||||
kind: string,
|
||||
request: Record<string, unknown>,
|
||||
responder: Responder,
|
||||
): Promise<void> =>
|
||||
Effect.runPromise(
|
||||
dispatchFlowServiceStreamingEffect(flow, kind, request, (response, complete) =>
|
||||
Effect.tryPromise({
|
||||
try: () => responder(response, complete),
|
||||
catch: (error) => messagingDeliveryError(
|
||||
resolveFlowTopics(kind).responseTopic,
|
||||
"stream-responder",
|
||||
error,
|
||||
),
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
// ---------- Fire-and-forget publish ----------
|
||||
|
||||
/**
|
||||
* Publish a single message to an arbitrary topic (no request/response).
|
||||
* Used for injecting documents into the processing pipeline.
|
||||
*/
|
||||
const publishToTopic = (topic: string, message: unknown, id?: string): Promise<void> =>
|
||||
Effect.runPromise(
|
||||
Effect.acquireUseRelease(
|
||||
Effect.tryPromise({
|
||||
try: () => pubsub.createProducer<unknown>({ topic }),
|
||||
catch: (cause) => messagingDeliveryError(topic, "create-producer", cause),
|
||||
}),
|
||||
(producer) =>
|
||||
Effect.gen(function* () {
|
||||
const timestamp = yield* Clock.currentTimeMillis;
|
||||
const suffix = yield* Random.nextIntBetween(0, 36 ** 6, { halfOpen: true });
|
||||
const messageId = id ?? `pub-${timestamp}-${suffix.toString(36).padStart(6, "0")}`;
|
||||
|
||||
yield* Effect.tryPromise({
|
||||
try: () => producer.send(message, { id: messageId }),
|
||||
catch: (cause) => messagingDeliveryError(topic, "send", cause),
|
||||
});
|
||||
}),
|
||||
(producer) => Effect.tryPromise({
|
||||
try: () => producer.close(),
|
||||
catch: (cause) => messagingDeliveryError(topic, "close-producer", cause),
|
||||
}),
|
||||
const publishToTopic = (topic: string, message: unknown, id?: string) =>
|
||||
Effect.acquireUseRelease(
|
||||
pubsub.createProducer<unknown>({ topic }).pipe(
|
||||
Effect.mapError((cause) => messagingDeliveryError(topic, "create-producer", cause)),
|
||||
),
|
||||
(producer) =>
|
||||
Effect.gen(function* () {
|
||||
const timestamp = yield* Clock.currentTimeMillis;
|
||||
const suffix = yield* Random.nextIntBetween(0, 36 ** 6, { halfOpen: true });
|
||||
const messageId = id ?? `pub-${timestamp}-${suffix.toString(36).padStart(6, "0")}`;
|
||||
|
||||
yield* producer.send(message, { id: messageId }).pipe(
|
||||
Effect.mapError((cause) => messagingDeliveryError(topic, "send", cause)),
|
||||
);
|
||||
}),
|
||||
(producer) =>
|
||||
producer.close.pipe(
|
||||
Effect.mapError((cause) => messagingDeliveryError(topic, "close-producer", cause)),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
start: startEffect(),
|
||||
stop: stopEffect(),
|
||||
dispatchGlobalService,
|
||||
dispatchGlobalServiceStreaming,
|
||||
dispatchGlobalServiceStreamingEffect,
|
||||
dispatchFlowService,
|
||||
dispatchFlowServiceStreaming,
|
||||
dispatchFlowServiceStreamingEffect,
|
||||
publishToTopic,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export { createGateway, run, type GatewayConfig } from "./server.js";
|
||||
export { createGateway, program, runMain, type GatewayConfig } from "./server.js";
|
||||
export {
|
||||
dispatcherManagerFlowServiceNames,
|
||||
dispatcherManagerGlobalServiceNames,
|
||||
|
|
|
|||
|
|
@ -40,10 +40,9 @@ const makeGatewayRpcHandlers = (dispatcher: DispatcherManager) =>
|
|||
TrustGraphRpcs.toLayer(Effect.succeed(
|
||||
TrustGraphRpcs.of({
|
||||
Dispatch: (payload) =>
|
||||
Effect.tryPromise({
|
||||
try: () => dispatchOne(dispatcher, payload),
|
||||
catch: (cause) => DispatchError.make({ message: errorMessage(cause) }),
|
||||
}),
|
||||
dispatchOne(dispatcher, payload).pipe(
|
||||
Effect.mapError((cause) => DispatchError.make({ message: errorMessage(cause) })),
|
||||
),
|
||||
DispatchStream: Effect.fn("GatewayRpc.DispatchStream")(function* (payload) {
|
||||
const queue = yield* Queue.bounded<DispatchStreamChunk, DispatchError | Cause.Done>(16);
|
||||
yield* Effect.addFinalizer(() => Queue.shutdown(queue));
|
||||
|
|
@ -64,7 +63,7 @@ const makeGatewayRpcHandlers = (dispatcher: DispatcherManager) =>
|
|||
function dispatchOne(
|
||||
dispatcher: DispatcherManager,
|
||||
payload: DispatchPayload,
|
||||
): Promise<unknown> {
|
||||
): Effect.Effect<unknown, DispatcherStreamError> {
|
||||
if (payload.scope === "flow") {
|
||||
return dispatcher.dispatchFlowService(
|
||||
payload.flow ?? "default",
|
||||
|
|
@ -81,7 +80,7 @@ function dispatchStreamEffect(
|
|||
responder: (response: unknown, complete: boolean) => Effect.Effect<void>,
|
||||
): Effect.Effect<void, DispatcherStreamError> {
|
||||
if (payload.scope === "flow") {
|
||||
return dispatcher.dispatchFlowServiceStreamingEffect(
|
||||
return dispatcher.dispatchFlowServiceStreaming(
|
||||
payload.flow ?? "default",
|
||||
payload.service,
|
||||
payload.request,
|
||||
|
|
@ -89,7 +88,7 @@ function dispatchStreamEffect(
|
|||
);
|
||||
}
|
||||
|
||||
return dispatcher.dispatchGlobalServiceStreamingEffect(
|
||||
return dispatcher.dispatchGlobalServiceStreaming(
|
||||
payload.service,
|
||||
payload.request,
|
||||
responder,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,16 @@
|
|||
/** @effect-diagnostics nodeBuiltinImport:skip-file effectFnOpportunity:skip-file catchToOrElseSucceed:skip-file */
|
||||
/**
|
||||
* API Gateway — HTTP + WebSocket server.
|
||||
*
|
||||
* Replaces the Python aiohttp gateway with Fastify.
|
||||
* Uses Effect RPC over WebSocket for streaming client requests.
|
||||
* API Gateway -- Effect HTTP + RPC server.
|
||||
*
|
||||
* Python reference: trustgraph-flow/trustgraph/gateway/service.py
|
||||
*/
|
||||
|
||||
import Fastify, { type FastifyReply } from "fastify";
|
||||
import websocketPlugin from "@fastify/websocket";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { Cause, Clock, Config, Effect, Exit, Layer, ManagedRuntime, Random, Scope } from "effect";
|
||||
import { createServer } from "node:http";
|
||||
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
|
||||
import { Clock, Config, Effect, Exit, Layer, Random, Scope } from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
||||
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
|
||||
import * as EffectSocket from "effect/unstable/socket/Socket";
|
||||
import {
|
||||
formatPrometheusMetrics,
|
||||
messagingLifecycleError,
|
||||
|
|
@ -22,8 +19,8 @@ import {
|
|||
toTgError,
|
||||
type PubSubBackend,
|
||||
} from "@trustgraph/base";
|
||||
import { makeDispatcherManager } from "./dispatch/manager.js";
|
||||
import { makeGatewayRpcServer } from "./rpc-server.js";
|
||||
import { makeDispatcherManager, type DispatcherManager } from "./dispatch/manager.js";
|
||||
import { makeGatewayRpcServer, type GatewayRpcServer } from "./rpc-server.js";
|
||||
|
||||
export interface GatewayConfig {
|
||||
port: number;
|
||||
|
|
@ -33,231 +30,253 @@ export interface GatewayConfig {
|
|||
pubsub?: PubSubBackend;
|
||||
}
|
||||
|
||||
export function createGateway(config: GatewayConfig) {
|
||||
const app = Fastify({ logger: true });
|
||||
const dispatcher = makeDispatcherManager(config);
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const sendDispatchResult = (reply: FastifyReply, result: unknown): unknown => {
|
||||
const err = (result as Record<string, unknown>)?.error as { type?: string; message?: string } | undefined;
|
||||
if (err !== undefined) {
|
||||
const statusCode = err.type === "not-found" ? 404 : 400;
|
||||
return reply.code(statusCode).send(result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const json = (body: unknown, status = 200) =>
|
||||
HttpServerResponse.jsonUnsafe(body, { status });
|
||||
|
||||
const sendDispatchError = (reply: FastifyReply, error: unknown): unknown =>
|
||||
reply.code(500).send({ error: toTgError(error) });
|
||||
const badRequest = (message: string) =>
|
||||
json({ error: { type: "bad-request", message } }, 400);
|
||||
|
||||
return Effect.runPromise(
|
||||
const dispatchError = (error: unknown) =>
|
||||
json({ error: toTgError(error) }, 500);
|
||||
|
||||
const dispatchResult = (result: unknown) => {
|
||||
const err = isRecord(result) && isRecord(result.error)
|
||||
? result.error as { readonly type?: string; readonly message?: string }
|
||||
: undefined;
|
||||
if (err !== undefined) {
|
||||
return json(result, err.type === "not-found" ? 404 : 400);
|
||||
}
|
||||
return json(result);
|
||||
};
|
||||
|
||||
const readJsonRecord = Effect.gen(function* () {
|
||||
const request = yield* HttpServerRequest.HttpServerRequest;
|
||||
const body = yield* request.json;
|
||||
return isRecord(body) ? body : {};
|
||||
});
|
||||
|
||||
const bearerAuthResponse = (config: GatewayConfig) =>
|
||||
Effect.gen(function* () {
|
||||
if (config.secret === undefined || config.secret.length === 0) return null;
|
||||
const request = yield* HttpServerRequest.HttpServerRequest;
|
||||
const auth = request.headers.authorization;
|
||||
return auth === `Bearer ${config.secret}`
|
||||
? null
|
||||
: json({ error: "Unauthorized" }, 401);
|
||||
});
|
||||
|
||||
type RouteRequirements =
|
||||
| HttpServerRequest.HttpServerRequest
|
||||
| HttpRouter.RouteContext;
|
||||
|
||||
const withBearerAuth = (
|
||||
config: GatewayConfig,
|
||||
handler: Effect.Effect<HttpServerResponse.HttpServerResponse, never, RouteRequirements>,
|
||||
) =>
|
||||
Effect.gen(function* () {
|
||||
const denied = yield* bearerAuthResponse(config);
|
||||
if (denied !== null) return denied;
|
||||
return yield* handler;
|
||||
});
|
||||
|
||||
const withDispatchError = <A, E>(
|
||||
effect: Effect.Effect<A, E>,
|
||||
operation: string,
|
||||
): Effect.Effect<HttpServerResponse.HttpServerResponse> =>
|
||||
effect.pipe(
|
||||
Effect.mapError((cause) => messagingLifecycleError("gateway", operation, cause)),
|
||||
Effect.map(dispatchResult),
|
||||
Effect.catch((error) => Effect.succeed(dispatchError(error))),
|
||||
);
|
||||
|
||||
const workbenchDispatch = (
|
||||
config: GatewayConfig,
|
||||
dispatcher: DispatcherManager,
|
||||
) =>
|
||||
withBearerAuth(
|
||||
config,
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => app.register(websocketPlugin),
|
||||
catch: (cause) => messagingLifecycleError("gateway", "register-websocket", cause),
|
||||
});
|
||||
const body = yield* readJsonRecord.pipe(
|
||||
Effect.catch(() => Effect.succeed<Record<string, unknown>>({})),
|
||||
);
|
||||
const service = typeof body.service === "string" ? body.service : undefined;
|
||||
const payload = isRecord(body.request) ? body.request : undefined;
|
||||
if (service === undefined || service.length === 0 || payload === undefined) {
|
||||
return badRequest("service and request are required");
|
||||
}
|
||||
|
||||
yield* Effect.tryPromise({
|
||||
try: () => dispatcher.start(),
|
||||
catch: (cause) => messagingLifecycleError("gateway", "dispatcher-start", cause),
|
||||
});
|
||||
const dispatch = body.scope === "flow"
|
||||
? dispatcher.dispatchFlowService(
|
||||
typeof body.flow === "string" ? body.flow : "default",
|
||||
service,
|
||||
payload,
|
||||
)
|
||||
: dispatcher.dispatchGlobalService(service, payload);
|
||||
|
||||
return yield* withDispatchError(dispatch, "workbench-dispatch");
|
||||
}),
|
||||
);
|
||||
|
||||
const globalDispatch = (
|
||||
config: GatewayConfig,
|
||||
dispatcher: DispatcherManager,
|
||||
) =>
|
||||
withBearerAuth(
|
||||
config,
|
||||
Effect.gen(function* () {
|
||||
const params = yield* HttpRouter.params;
|
||||
const body = yield* readJsonRecord.pipe(
|
||||
Effect.catch(() => Effect.succeed<Record<string, unknown>>({})),
|
||||
);
|
||||
return yield* withDispatchError(
|
||||
dispatcher.dispatchGlobalService(params.kind ?? "", body),
|
||||
"global-dispatch",
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const flowDispatch = (
|
||||
config: GatewayConfig,
|
||||
dispatcher: DispatcherManager,
|
||||
) =>
|
||||
withBearerAuth(
|
||||
config,
|
||||
Effect.gen(function* () {
|
||||
const params = yield* HttpRouter.params;
|
||||
const body = yield* readJsonRecord.pipe(
|
||||
Effect.catch(() => Effect.succeed<Record<string, unknown>>({})),
|
||||
);
|
||||
return yield* withDispatchError(
|
||||
dispatcher.dispatchFlowService(params.flow ?? "default", params.kind ?? "", body),
|
||||
"flow-dispatch",
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const flowLoad = (
|
||||
config: GatewayConfig,
|
||||
dispatcher: DispatcherManager,
|
||||
) =>
|
||||
withBearerAuth(
|
||||
config,
|
||||
Effect.gen(function* () {
|
||||
const params = yield* HttpRouter.params;
|
||||
const body = yield* readJsonRecord.pipe(
|
||||
Effect.catch(() => Effect.succeed<Record<string, unknown>>({})),
|
||||
);
|
||||
const documentId = typeof body.documentId === "string" ? body.documentId : undefined;
|
||||
if (documentId === undefined || documentId.length === 0) {
|
||||
return badRequest("documentId is required");
|
||||
}
|
||||
|
||||
const user = typeof body.user === "string" ? body.user : "default";
|
||||
const collection = typeof body.collection === "string" ? body.collection : "default";
|
||||
const timestamp = yield* Clock.currentTimeMillis;
|
||||
const suffix = yield* Random.nextIntBetween(0, 36 ** 6, { halfOpen: true });
|
||||
const metadata = {
|
||||
id: `load-${timestamp}-${suffix.toString(36).padStart(6, "0")}`,
|
||||
root: documentId,
|
||||
user,
|
||||
collection,
|
||||
};
|
||||
|
||||
yield* dispatcher.publishToTopic("tg.flow.document", { metadata, documentId }).pipe(
|
||||
Effect.mapError((cause) => messagingLifecycleError("gateway", "publish-load", cause)),
|
||||
);
|
||||
|
||||
return json({ status: "processing", documentId, flow: params.flow ?? "default" });
|
||||
}).pipe(
|
||||
Effect.catch((error) => Effect.succeed(dispatchError(error))),
|
||||
),
|
||||
);
|
||||
|
||||
const rpcRoute = (
|
||||
config: GatewayConfig,
|
||||
rpcServer: GatewayRpcServer,
|
||||
rpcScope: Scope.Scope,
|
||||
) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpServerRequest.HttpServerRequest;
|
||||
const url = new URL(request.url, "http://localhost");
|
||||
const token = url.searchParams.get("token");
|
||||
if (config.secret !== undefined && config.secret.length > 0 && token !== config.secret) {
|
||||
return json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const socket = yield* request.upgrade;
|
||||
yield* rpcServer.onSocket(socket, headersFrom(request.headers)).pipe(
|
||||
Scope.provide(rpcScope),
|
||||
);
|
||||
return HttpServerResponse.empty();
|
||||
}).pipe(
|
||||
Effect.catch((error) => Effect.succeed(dispatchError(error))),
|
||||
);
|
||||
|
||||
const metricsRoute =
|
||||
formatPrometheusMetrics.pipe(
|
||||
Effect.map((body) =>
|
||||
HttpServerResponse.text(body, {
|
||||
headers: { "content-type": prometheusContentType },
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
const gatewayRoutes = (
|
||||
config: GatewayConfig,
|
||||
dispatcher: DispatcherManager,
|
||||
rpcServer: GatewayRpcServer,
|
||||
rpcScope: Scope.Scope,
|
||||
) =>
|
||||
Layer.mergeAll(
|
||||
HttpRouter.add("POST", "/api/v1/workbench/dispatch", workbenchDispatch(config, dispatcher)),
|
||||
HttpRouter.add("POST", "/api/v1/:kind", globalDispatch(config, dispatcher)),
|
||||
HttpRouter.add("POST", "/api/v1/flow/:flow/service/:kind", flowDispatch(config, dispatcher)),
|
||||
HttpRouter.add("POST", "/api/v1/flow/:flow/load", flowLoad(config, dispatcher)),
|
||||
HttpRouter.add("GET", "/api/v1/rpc", rpcRoute(config, rpcServer, rpcScope)),
|
||||
HttpRouter.add("GET", "/api/v1/metrics", metricsRoute),
|
||||
);
|
||||
|
||||
export function createGateway(config: GatewayConfig) {
|
||||
return Layer.effectDiscard(
|
||||
Effect.scoped(Effect.gen(function* () {
|
||||
const dispatcher = makeDispatcherManager(config);
|
||||
yield* dispatcher.start.pipe(
|
||||
Effect.mapError((cause) => messagingLifecycleError("gateway", "dispatcher-start", cause)),
|
||||
);
|
||||
yield* Effect.addFinalizer(() =>
|
||||
dispatcher.stop.pipe(
|
||||
Effect.catch((cause) =>
|
||||
Effect.logError("[Gateway] Failed to stop dispatcher", {
|
||||
error: cause.message,
|
||||
operation: cause.operation,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const rpcScope = yield* Scope.make();
|
||||
yield* Effect.addFinalizer(() => Scope.close(rpcScope, Exit.void));
|
||||
const rpcServer = yield* makeGatewayRpcServer(dispatcher).pipe(
|
||||
Effect.provideService(RpcSerialization.RpcSerialization, RpcSerialization.ndjson),
|
||||
Scope.provide(rpcScope),
|
||||
);
|
||||
|
||||
return { rpcScope, rpcServer };
|
||||
}),
|
||||
).then(({ rpcScope, rpcServer }) => {
|
||||
// Authentication middleware
|
||||
app.addHook("onRequest", (request, reply) => {
|
||||
if (request.url === "/api/v1/metrics") return;
|
||||
if (request.url.startsWith("/api/v1/rpc")) return; // RPC socket auth via query param
|
||||
|
||||
if (config.secret !== undefined && config.secret.length > 0) {
|
||||
const auth = request.headers.authorization;
|
||||
if (auth === undefined || auth !== `Bearer ${config.secret}`) {
|
||||
reply.code(401).send({ error: "Unauthorized" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.post<{
|
||||
Body: {
|
||||
scope?: string;
|
||||
service?: string;
|
||||
flow?: string;
|
||||
request?: Record<string, unknown>;
|
||||
};
|
||||
}>("/api/v1/workbench/dispatch", (request, reply) => {
|
||||
const body = request.body;
|
||||
const service = body.service;
|
||||
const payload = body.request;
|
||||
if (service === undefined || service.length === 0 || payload === undefined) {
|
||||
return reply.code(400).send({
|
||||
error: { type: "bad-request", message: "service and request are required" },
|
||||
});
|
||||
}
|
||||
|
||||
return Effect.runPromise(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
body.scope === "flow"
|
||||
? dispatcher.dispatchFlowService(body.flow ?? "default", service, payload)
|
||||
: dispatcher.dispatchGlobalService(service, payload),
|
||||
catch: (cause) => messagingLifecycleError("gateway", "workbench-dispatch", cause),
|
||||
}).pipe(
|
||||
Effect.map((result) => sendDispatchResult(reply, result)),
|
||||
Effect.catch((error) => Effect.succeed(sendDispatchError(reply, error))),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// REST endpoint: POST /api/v1/:kind (global services)
|
||||
app.post<{ Params: { kind: string } }>("/api/v1/:kind", (request, reply) => {
|
||||
const { kind } = request.params;
|
||||
const body = request.body as Record<string, unknown>;
|
||||
|
||||
return Effect.runPromise(
|
||||
Effect.tryPromise({
|
||||
try: () => dispatcher.dispatchGlobalService(kind, body),
|
||||
catch: (cause) => messagingLifecycleError("gateway", "global-dispatch", cause),
|
||||
}).pipe(
|
||||
Effect.map((result) => sendDispatchResult(reply, result)),
|
||||
Effect.catch((error) => Effect.succeed(sendDispatchError(reply, error))),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// REST endpoint: POST /api/v1/flow/:flow/service/:kind (flow-scoped services)
|
||||
app.post<{ Params: { flow: string; kind: string } }>(
|
||||
"/api/v1/flow/:flow/service/:kind",
|
||||
(request, reply) => {
|
||||
const { flow, kind } = request.params;
|
||||
const body = request.body as Record<string, unknown>;
|
||||
|
||||
return Effect.runPromise(
|
||||
Effect.tryPromise({
|
||||
try: () => dispatcher.dispatchFlowService(flow, kind, body),
|
||||
catch: (cause) => messagingLifecycleError("gateway", "flow-dispatch", cause),
|
||||
}).pipe(
|
||||
Effect.map((result) => sendDispatchResult(reply, result)),
|
||||
Effect.catch((error) => Effect.succeed(sendDispatchError(reply, error))),
|
||||
),
|
||||
);
|
||||
},
|
||||
const serverLayer = HttpRouter.serve(
|
||||
gatewayRoutes(config, dispatcher, rpcServer, rpcScope),
|
||||
).pipe(
|
||||
Layer.provideMerge(NodeHttpServer.layer(createServer, {
|
||||
port: config.port,
|
||||
host: "0.0.0.0",
|
||||
})),
|
||||
);
|
||||
|
||||
// REST endpoint: POST /api/v1/flow/:flow/load (trigger document processing)
|
||||
app.post<{ Params: { flow: string } }>(
|
||||
"/api/v1/flow/:flow/load",
|
||||
(request, reply) => {
|
||||
const { flow } = request.params;
|
||||
const body = request.body as {
|
||||
documentId?: string;
|
||||
user?: string;
|
||||
collection?: string;
|
||||
};
|
||||
|
||||
if (body.documentId === undefined || body.documentId.length === 0) {
|
||||
return reply.code(400).send({
|
||||
error: { type: "bad-request", message: "documentId is required" },
|
||||
});
|
||||
}
|
||||
|
||||
return Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const user = body.user ?? "default";
|
||||
const collection = body.collection ?? "default";
|
||||
const documentId = body.documentId;
|
||||
const timestamp = yield* Clock.currentTimeMillis;
|
||||
const suffix = yield* Random.nextIntBetween(0, 36 ** 6, { halfOpen: true });
|
||||
|
||||
// Publish Document message to the decode-input topic
|
||||
const topic = "tg.flow.document";
|
||||
const metadata = {
|
||||
id: `load-${timestamp}-${suffix.toString(36).padStart(6, "0")}`,
|
||||
root: documentId,
|
||||
user,
|
||||
collection,
|
||||
};
|
||||
|
||||
yield* Effect.tryPromise({
|
||||
try: () => dispatcher.publishToTopic(topic, { metadata, documentId }),
|
||||
catch: (cause) => messagingLifecycleError("gateway", "publish-load", cause),
|
||||
});
|
||||
|
||||
return { status: "processing", documentId, flow };
|
||||
}).pipe(
|
||||
Effect.catch((error) => Effect.succeed(sendDispatchError(reply, error))),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Effect RPC WebSocket endpoint: /api/v1/rpc
|
||||
app.get("/api/v1/rpc", { websocket: true }, (socket, request) => {
|
||||
const url = new URL(request.url, `http://${request.headers.host}`);
|
||||
const token = url.searchParams.get("token");
|
||||
if (config.secret !== undefined && config.secret.length > 0 && token !== config.secret) {
|
||||
socket.close(4001, "Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
const program = Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const effectSocket = yield* EffectSocket.fromWebSocket(
|
||||
Effect.succeed(socket as unknown as globalThis.WebSocket),
|
||||
{ closeCodeIsError: (code) => code !== 1000 },
|
||||
);
|
||||
yield* rpcServer.onSocket(effectSocket, headersFrom(request.headers));
|
||||
}),
|
||||
);
|
||||
|
||||
void Effect.runPromise(
|
||||
program.pipe(
|
||||
Scope.provide(rpcScope),
|
||||
Effect.sandbox,
|
||||
Effect.catch((cause) =>
|
||||
Effect.logError("[Gateway] RPC WebSocket error", { error: Cause.pretty(cause) }).pipe(
|
||||
Effect.flatMap(() =>
|
||||
Effect.sync(() => {
|
||||
if (socket.readyState === 1) {
|
||||
socket.close(1011, "Internal server error");
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Metrics endpoint — returns Effect metrics in Prometheus exposition format.
|
||||
app.get("/api/v1/metrics", (_, reply) => {
|
||||
reply.header("content-type", prometheusContentType);
|
||||
return Effect.runPromise(formatPrometheusMetrics);
|
||||
});
|
||||
|
||||
return {
|
||||
start: () => app.listen({ port: config.port, host: "0.0.0.0" }),
|
||||
stop: () =>
|
||||
Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => app.close(),
|
||||
catch: (cause) => messagingLifecycleError("gateway", "app-close", cause),
|
||||
});
|
||||
yield* Scope.close(rpcScope, Exit.void);
|
||||
yield* Effect.tryPromise({
|
||||
try: () => dispatcher.stop(),
|
||||
catch: (cause) => messagingLifecycleError("gateway", "dispatcher-stop", cause),
|
||||
});
|
||||
}),
|
||||
),
|
||||
};
|
||||
});
|
||||
yield* Effect.log(`[Gateway] Listening on port ${config.port}`);
|
||||
return yield* Layer.launch(serverLayer);
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
function headersFrom(headers: Record<string, string | string[] | number | undefined>): ReadonlyArray<[string, string]> {
|
||||
|
|
@ -269,10 +288,6 @@ function headersFrom(headers: Record<string, string | string[] | number | undefi
|
|||
});
|
||||
}
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return gatewayRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
@ -290,22 +305,8 @@ export const loadGatewayConfig = Effect.fn("loadGatewayConfig")(function* () {
|
|||
} satisfies GatewayConfig;
|
||||
});
|
||||
|
||||
export const program = Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const config = yield* loadGatewayConfig();
|
||||
const gateway = yield* Effect.promise(() => createGateway(config)).pipe(Effect.orDie);
|
||||
yield* Effect.addFinalizer(() => Effect.promise(() => gateway.stop()).pipe(Effect.orDie));
|
||||
yield* Effect.promise(() => gateway.start()).pipe(
|
||||
Effect.orDie,
|
||||
Effect.withSpan("trustgraph.gateway.start", {
|
||||
attributes: {
|
||||
"trustgraph.gateway.port": config.port,
|
||||
},
|
||||
}),
|
||||
);
|
||||
yield* Effect.log(`[Gateway] Listening on port ${config.port}`);
|
||||
return yield* Effect.never;
|
||||
}),
|
||||
);
|
||||
export const gatewayProgram = (config: GatewayConfig) => Layer.launch(createGateway(config));
|
||||
|
||||
const gatewayRuntime = ManagedRuntime.make(Layer.empty);
|
||||
export const program = loadGatewayConfig().pipe(
|
||||
Effect.flatMap(gatewayProgram),
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -18,9 +18,8 @@ import {
|
|||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
} from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime, Stream } from "effect";
|
||||
import { Effect, Stream } from "effect";
|
||||
import {
|
||||
llmStreamPart,
|
||||
makeTextCompletionLayer,
|
||||
|
|
@ -28,7 +27,6 @@ import {
|
|||
providerStatusError,
|
||||
requiredString,
|
||||
streamTextCompletionChunks,
|
||||
toAsyncGenerator,
|
||||
type TextCompletionConfigError,
|
||||
type TextCompletionRuntimeError,
|
||||
} from "./common.ts";
|
||||
|
|
@ -89,7 +87,7 @@ const mapAzureOpenAIError = (error: unknown): TextCompletionRuntimeError =>
|
|||
const makeAzureOpenAIProviderFromClient = (
|
||||
resolved: ResolvedAzureOpenAIConfig,
|
||||
client: AzureOpenAI,
|
||||
): LlmProvider => {
|
||||
): LlmProvider<TextCompletionRuntimeError> => {
|
||||
const {
|
||||
defaultModel,
|
||||
defaultTemperature,
|
||||
|
|
@ -102,31 +100,29 @@ const makeAzureOpenAIProviderFromClient = (
|
|||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
) => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
return Effect.runPromise(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: maxOutput,
|
||||
}),
|
||||
catch: mapAzureOpenAIError,
|
||||
}).pipe(
|
||||
Effect.map((resp): LlmResult => ({
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
return Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.completions.create({
|
||||
model: modelName,
|
||||
})),
|
||||
),
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: maxOutput,
|
||||
}),
|
||||
catch: mapAzureOpenAIError,
|
||||
}).pipe(
|
||||
Effect.map((resp): LlmResult => ({
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
model: modelName,
|
||||
})),
|
||||
);
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
|
|
@ -135,11 +131,11 @@ const makeAzureOpenAIProviderFromClient = (
|
|||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> => {
|
||||
) => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
const stream = Stream.fromEffect(
|
||||
return Stream.fromEffect(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.completions.create({
|
||||
|
|
@ -169,13 +165,13 @@ const makeAzureOpenAIProviderFromClient = (
|
|||
})
|
||||
),
|
||||
);
|
||||
|
||||
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapAzureOpenAIError);
|
||||
},
|
||||
} satisfies LlmProvider;
|
||||
} satisfies LlmProvider<TextCompletionRuntimeError>;
|
||||
};
|
||||
|
||||
export function makeAzureOpenAIProvider(config: AzureOpenAIProcessorConfig): LlmProvider {
|
||||
export function makeAzureOpenAIProvider(
|
||||
config: AzureOpenAIProcessorConfig,
|
||||
): LlmProvider<TextCompletionRuntimeError> {
|
||||
return Effect.runSync(makeAzureOpenAIProviderEffect(config));
|
||||
}
|
||||
|
||||
|
|
@ -217,12 +213,6 @@ export const program = makeFlowProcessorProgram<
|
|||
layer: (config) => makeTextCompletionLayer(makeAzureOpenAIProviderEffect(config)),
|
||||
});
|
||||
|
||||
const azureOpenAITextCompletionRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return azureOpenAITextCompletionRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
} from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime, Redacted } from "effect";
|
||||
import { Effect, Layer, Redacted } from "effect";
|
||||
import { FetchHttpClient } from "effect/unstable/http";
|
||||
import {
|
||||
makeLanguageModelProvider,
|
||||
|
|
@ -55,30 +55,31 @@ const loadClaudeConfig = Effect.fn("loadClaudeConfig")(function* (config: Claude
|
|||
} satisfies ResolvedClaudeConfig;
|
||||
});
|
||||
|
||||
const makeClaudeRuntime = (apiKey: string) =>
|
||||
ManagedRuntime.make(
|
||||
AnthropicClient.layer({
|
||||
apiKey: Redacted.make(apiKey),
|
||||
}).pipe(
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
),
|
||||
const makeClaudeLayer = (apiKey: string) =>
|
||||
AnthropicClient.layer({
|
||||
apiKey: Redacted.make(apiKey),
|
||||
}).pipe(
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
);
|
||||
|
||||
export function makeClaudeProvider(config: ClaudeProcessorConfig): LlmProvider {
|
||||
return Effect.runSync(makeClaudeProviderEffect(config));
|
||||
export function makeClaudeProvider(
|
||||
config: ClaudeProcessorConfig,
|
||||
): LlmProvider<TextCompletionRuntimeError> {
|
||||
return Effect.runSync(Effect.scoped(makeClaudeProviderEffect(config)));
|
||||
}
|
||||
|
||||
export const makeClaudeProviderEffect = Effect.fn("makeClaudeProvider")(function* (
|
||||
config: ClaudeProcessorConfig,
|
||||
) {
|
||||
const resolved = yield* loadClaudeConfig(config);
|
||||
const context = yield* Layer.build(makeClaudeLayer(resolved.apiKey));
|
||||
|
||||
yield* Effect.log("[Claude] LLM service initialized");
|
||||
return makeLanguageModelProvider({
|
||||
provider: "Claude",
|
||||
defaultModel: resolved.defaultModel,
|
||||
defaultTemperature: resolved.defaultTemperature,
|
||||
runtime: makeClaudeRuntime(resolved.apiKey),
|
||||
context,
|
||||
makeLanguageModel: ({ model, temperature }) =>
|
||||
AnthropicLanguageModel.make({
|
||||
model,
|
||||
|
|
@ -110,12 +111,6 @@ export const program = makeFlowProcessorProgram<
|
|||
layer: (config) => makeTextCompletionLayer(makeClaudeProviderEffect(config)),
|
||||
});
|
||||
|
||||
const claudeTextCompletionRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return claudeTextCompletionRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ import {
|
|||
type LlmResult,
|
||||
type LlmProvider,
|
||||
} from "@trustgraph/base";
|
||||
import { Config, Effect, Layer, ManagedRuntime, Match, Ref, Result, Stream } from "effect";
|
||||
import { Config, Context, Effect, Layer, Match, Ref, Result, Stream } from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import * as Predicate from "effect/Predicate";
|
||||
import * as S from "effect/Schema";
|
||||
import type * as Scope from "effect/Scope";
|
||||
import { AiError, LanguageModel, Prompt, Response } from "effect/unstable/ai";
|
||||
|
||||
export class TextCompletionConfigError extends S.TaggedErrorClass<TextCompletionConfigError>()(
|
||||
|
|
@ -43,15 +44,15 @@ export interface LanguageModelProviderOptions<Requirements> {
|
|||
readonly provider: string;
|
||||
readonly defaultModel: string;
|
||||
readonly defaultTemperature: number;
|
||||
readonly runtime: ManagedRuntime.ManagedRuntime<Requirements, TextCompletionRuntimeError>;
|
||||
readonly context: Context.Context<Requirements>;
|
||||
readonly makeLanguageModel: (
|
||||
request: LanguageModelProviderRequest,
|
||||
) => Effect.Effect<LanguageModel.Service, TextCompletionRuntimeError, Requirements>;
|
||||
}
|
||||
|
||||
export const makeTextCompletionLayer = <E, R>(
|
||||
provider: Effect.Effect<LlmProvider, E, R>,
|
||||
): Layer.Layer<Llm, E, R> =>
|
||||
export const makeTextCompletionLayer = <ProviderError, E, R>(
|
||||
provider: Effect.Effect<LlmProvider<ProviderError>, E, R>,
|
||||
): Layer.Layer<Llm, E, Exclude<R, Scope.Scope>> =>
|
||||
Layer.effect(Llm)(
|
||||
provider.pipe(
|
||||
Effect.map((resolvedProvider) =>
|
||||
|
|
@ -279,39 +280,25 @@ const languageModelStreamChunk = (
|
|||
Match.orElse(() => Effect.succeed(Result.fail(undefined))),
|
||||
);
|
||||
|
||||
const runLanguageModelStream = <RuntimeRequirements, StreamRequirements extends RuntimeRequirements>(
|
||||
runtime: ManagedRuntime.ManagedRuntime<RuntimeRequirements, TextCompletionRuntimeError>,
|
||||
stream: Stream.Stream<LlmChunk, TextCompletionRuntimeError, StreamRequirements>,
|
||||
): AsyncIterable<LlmChunk> => ({
|
||||
[Symbol.asyncIterator]: () => {
|
||||
const iterator = runtime.context().then((context) =>
|
||||
Stream.toAsyncIterableWith(stream, context)[Symbol.asyncIterator]()
|
||||
);
|
||||
return {
|
||||
next: () => iterator.then((current) => current.next()),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const makeLanguageModelProvider = <Requirements>(
|
||||
options: LanguageModelProviderOptions<Requirements>,
|
||||
): LlmProvider => ({
|
||||
): LlmProvider<TextCompletionRuntimeError> => ({
|
||||
generateContent: (system, prompt, model, temperature) => {
|
||||
const modelName = model ?? options.defaultModel;
|
||||
const temp = temperature ?? options.defaultTemperature;
|
||||
return options.runtime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const languageModel = yield* options.makeLanguageModel({
|
||||
model: modelName,
|
||||
temperature: temp,
|
||||
});
|
||||
const response = yield* languageModel.generateText({
|
||||
prompt: languageModelPrompt(system, prompt),
|
||||
}).pipe(
|
||||
Effect.mapError((error) => effectAiProviderError(options.provider, error)),
|
||||
);
|
||||
return languageModelResult(response, modelName);
|
||||
}),
|
||||
return Effect.gen(function* () {
|
||||
const languageModel = yield* options.makeLanguageModel({
|
||||
model: modelName,
|
||||
temperature: temp,
|
||||
});
|
||||
const response = yield* languageModel.generateText({
|
||||
prompt: languageModelPrompt(system, prompt),
|
||||
}).pipe(
|
||||
Effect.mapError((error) => effectAiProviderError(options.provider, error)),
|
||||
);
|
||||
return languageModelResult(response, modelName);
|
||||
}).pipe(
|
||||
Effect.provideContext(options.context),
|
||||
);
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
|
|
@ -333,30 +320,9 @@ export const makeLanguageModelProvider = <Requirements>(
|
|||
),
|
||||
);
|
||||
}),
|
||||
).pipe(
|
||||
Stream.provideContext(options.context),
|
||||
);
|
||||
return toAsyncGenerator(runLanguageModelStream(options.runtime, stream), (error) =>
|
||||
effectAiProviderError(options.provider, error)
|
||||
);
|
||||
return stream;
|
||||
},
|
||||
});
|
||||
|
||||
export const toAsyncGenerator = (
|
||||
iterable: AsyncIterable<LlmChunk>,
|
||||
mapError: (error: unknown) => TextCompletionRuntimeError,
|
||||
): AsyncGenerator<LlmChunk> => {
|
||||
const iterator = iterable[Symbol.asyncIterator]();
|
||||
let generator: AsyncGenerator<LlmChunk>;
|
||||
generator = {
|
||||
next: (value?: unknown) => iterator.next(value),
|
||||
return: (value?: unknown) =>
|
||||
iterator.return === undefined
|
||||
? Promise.resolve({ done: true, value })
|
||||
: iterator.return(value),
|
||||
throw: (error?: unknown) =>
|
||||
iterator.throw === undefined
|
||||
? Promise.reject(mapError(error))
|
||||
: iterator.throw(error),
|
||||
[Symbol.asyncIterator]: () => generator,
|
||||
};
|
||||
return generator;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@ import {
|
|||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
} from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime, Stream } from "effect";
|
||||
import { Effect, Stream } from "effect";
|
||||
import {
|
||||
llmStreamPart,
|
||||
makeTextCompletionLayer,
|
||||
|
|
@ -27,7 +26,6 @@ import {
|
|||
requiredString,
|
||||
streamTextCompletionChunks,
|
||||
textFromContent,
|
||||
toAsyncGenerator,
|
||||
type TextCompletionConfigError,
|
||||
type TextCompletionRuntimeError,
|
||||
} from "./common.ts";
|
||||
|
|
@ -71,7 +69,7 @@ const mapMistralError = (error: unknown): TextCompletionRuntimeError =>
|
|||
const makeMistralProviderFromClient = (
|
||||
resolved: ResolvedMistralConfig,
|
||||
client: Mistral,
|
||||
): LlmProvider => {
|
||||
): LlmProvider<TextCompletionRuntimeError> => {
|
||||
const {
|
||||
defaultModel,
|
||||
defaultTemperature,
|
||||
|
|
@ -84,31 +82,29 @@ const makeMistralProviderFromClient = (
|
|||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
) => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
return Effect.runPromise(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.complete({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
maxTokens: maxOutput,
|
||||
}),
|
||||
catch: mapMistralError,
|
||||
}).pipe(
|
||||
Effect.map((resp): LlmResult => ({
|
||||
text: textFromContent(resp.choices?.[0]?.message?.content),
|
||||
inToken: resp.usage?.promptTokens ?? 0,
|
||||
outToken: resp.usage?.completionTokens ?? 0,
|
||||
return Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.complete({
|
||||
model: modelName,
|
||||
})),
|
||||
),
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
maxTokens: maxOutput,
|
||||
}),
|
||||
catch: mapMistralError,
|
||||
}).pipe(
|
||||
Effect.map((resp): LlmResult => ({
|
||||
text: textFromContent(resp.choices?.[0]?.message?.content),
|
||||
inToken: resp.usage?.promptTokens ?? 0,
|
||||
outToken: resp.usage?.completionTokens ?? 0,
|
||||
model: modelName,
|
||||
})),
|
||||
);
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
|
|
@ -117,11 +113,11 @@ const makeMistralProviderFromClient = (
|
|||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> => {
|
||||
) => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
const stream = Stream.fromEffect(
|
||||
return Stream.fromEffect(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.stream({
|
||||
|
|
@ -149,13 +145,13 @@ const makeMistralProviderFromClient = (
|
|||
})
|
||||
),
|
||||
);
|
||||
|
||||
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapMistralError);
|
||||
},
|
||||
} satisfies LlmProvider;
|
||||
} satisfies LlmProvider<TextCompletionRuntimeError>;
|
||||
};
|
||||
|
||||
export function makeMistralProvider(config: MistralProcessorConfig): LlmProvider {
|
||||
export function makeMistralProvider(
|
||||
config: MistralProcessorConfig,
|
||||
): LlmProvider<TextCompletionRuntimeError> {
|
||||
return Effect.runSync(makeMistralProviderEffect(config));
|
||||
}
|
||||
|
||||
|
|
@ -192,12 +188,6 @@ export const program = makeFlowProcessorProgram<
|
|||
layer: (config) => makeTextCompletionLayer(makeMistralProviderEffect(config)),
|
||||
});
|
||||
|
||||
const mistralTextCompletionRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return mistralTextCompletionRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,14 @@ import {
|
|||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
} from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime, Stream } from "effect";
|
||||
import { Effect, Stream } from "effect";
|
||||
import {
|
||||
llmStreamPart,
|
||||
makeTextCompletionLayer,
|
||||
optionalStringConfig,
|
||||
providerRuntimeError,
|
||||
streamTextCompletionChunks,
|
||||
toAsyncGenerator,
|
||||
type TextCompletionConfigError,
|
||||
type TextCompletionRuntimeError,
|
||||
} from "./common.ts";
|
||||
|
|
@ -59,7 +57,7 @@ const mapOllamaError = (error: unknown): TextCompletionRuntimeError =>
|
|||
const makeOllamaProviderFromClient = (
|
||||
resolved: ResolvedOllamaConfig,
|
||||
client: Ollama,
|
||||
): LlmProvider => {
|
||||
): LlmProvider<TextCompletionRuntimeError> => {
|
||||
const { defaultModel } = resolved;
|
||||
|
||||
return {
|
||||
|
|
@ -68,27 +66,25 @@ const makeOllamaProviderFromClient = (
|
|||
prompt: string,
|
||||
model?: string,
|
||||
_temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
) => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const fullPrompt = system + "\n\n" + prompt;
|
||||
|
||||
return Effect.runPromise(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
client.generate({
|
||||
model: modelName,
|
||||
prompt: fullPrompt,
|
||||
stream: false,
|
||||
}),
|
||||
catch: mapOllamaError,
|
||||
}).pipe(
|
||||
Effect.map((resp): LlmResult => ({
|
||||
text: resp.response,
|
||||
inToken: resp.prompt_eval_count ?? 0,
|
||||
outToken: resp.eval_count ?? 0,
|
||||
return Effect.tryPromise({
|
||||
try: () =>
|
||||
client.generate({
|
||||
model: modelName,
|
||||
})),
|
||||
),
|
||||
prompt: fullPrompt,
|
||||
stream: false,
|
||||
}),
|
||||
catch: mapOllamaError,
|
||||
}).pipe(
|
||||
Effect.map((resp): LlmResult => ({
|
||||
text: resp.response,
|
||||
inToken: resp.prompt_eval_count ?? 0,
|
||||
outToken: resp.eval_count ?? 0,
|
||||
model: modelName,
|
||||
})),
|
||||
);
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
|
|
@ -97,11 +93,11 @@ const makeOllamaProviderFromClient = (
|
|||
prompt: string,
|
||||
model?: string,
|
||||
_temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> => {
|
||||
) => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const fullPrompt = system + "\n\n" + prompt;
|
||||
|
||||
const stream = Stream.fromEffect(
|
||||
return Stream.fromEffect(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
client.generate({
|
||||
|
|
@ -125,13 +121,13 @@ const makeOllamaProviderFromClient = (
|
|||
})
|
||||
),
|
||||
);
|
||||
|
||||
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapOllamaError);
|
||||
},
|
||||
} satisfies LlmProvider;
|
||||
} satisfies LlmProvider<TextCompletionRuntimeError>;
|
||||
};
|
||||
|
||||
export function makeOllamaProvider(config: OllamaProcessorConfig): LlmProvider {
|
||||
export function makeOllamaProvider(
|
||||
config: OllamaProcessorConfig,
|
||||
): LlmProvider<TextCompletionRuntimeError> {
|
||||
return Effect.runSync(makeOllamaProviderEffect(config));
|
||||
}
|
||||
|
||||
|
|
@ -170,12 +166,6 @@ export const program = makeFlowProcessorProgram<
|
|||
layer: (config) => makeTextCompletionLayer(makeOllamaProviderEffect(config)),
|
||||
});
|
||||
|
||||
const ollamaTextCompletionRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return ollamaTextCompletionRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,8 @@ import {
|
|||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
} from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime, Stream } from "effect";
|
||||
import { Effect, Stream } from "effect";
|
||||
import {
|
||||
llmStreamPart,
|
||||
makeTextCompletionLayer,
|
||||
|
|
@ -29,7 +28,6 @@ import {
|
|||
providerStatusError,
|
||||
requiredString,
|
||||
streamTextCompletionChunks,
|
||||
toAsyncGenerator,
|
||||
type TextCompletionConfigError,
|
||||
type TextCompletionRuntimeError,
|
||||
} from "./common.ts";
|
||||
|
|
@ -79,7 +77,7 @@ const mapOpenAICompatibleError = (error: unknown): TextCompletionRuntimeError =>
|
|||
const makeOpenAICompatibleProviderFromClient = (
|
||||
resolved: ResolvedOpenAICompatibleConfig,
|
||||
client: OpenAI,
|
||||
): LlmProvider => {
|
||||
): LlmProvider<TextCompletionRuntimeError> => {
|
||||
const {
|
||||
defaultModel,
|
||||
defaultTemperature,
|
||||
|
|
@ -92,31 +90,29 @@ const makeOpenAICompatibleProviderFromClient = (
|
|||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
) => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
return Effect.runPromise(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_tokens: maxOutput,
|
||||
}),
|
||||
catch: mapOpenAICompatibleError,
|
||||
}).pipe(
|
||||
Effect.map((resp): LlmResult => ({
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
return Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.completions.create({
|
||||
model: modelName,
|
||||
})),
|
||||
),
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_tokens: maxOutput,
|
||||
}),
|
||||
catch: mapOpenAICompatibleError,
|
||||
}).pipe(
|
||||
Effect.map((resp): LlmResult => ({
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
model: modelName,
|
||||
})),
|
||||
);
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
|
|
@ -125,11 +121,11 @@ const makeOpenAICompatibleProviderFromClient = (
|
|||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> => {
|
||||
) => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
const stream = Stream.fromEffect(
|
||||
return Stream.fromEffect(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.completions.create({
|
||||
|
|
@ -158,15 +154,13 @@ const makeOpenAICompatibleProviderFromClient = (
|
|||
})
|
||||
),
|
||||
);
|
||||
|
||||
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapOpenAICompatibleError);
|
||||
},
|
||||
} satisfies LlmProvider;
|
||||
} satisfies LlmProvider<TextCompletionRuntimeError>;
|
||||
};
|
||||
|
||||
export function makeOpenAICompatibleProvider(
|
||||
config: OpenAICompatibleProcessorConfig,
|
||||
): LlmProvider {
|
||||
): LlmProvider<TextCompletionRuntimeError> {
|
||||
return Effect.runSync(makeOpenAICompatibleProviderEffect(config));
|
||||
}
|
||||
|
||||
|
|
@ -203,12 +197,6 @@ export const program = makeFlowProcessorProgram<
|
|||
layer: (config) => makeTextCompletionLayer(makeOpenAICompatibleProviderEffect(config)),
|
||||
});
|
||||
|
||||
const openAICompatibleTextCompletionRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return openAICompatibleTextCompletionRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@ import {
|
|||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
} from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime, Stream } from "effect";
|
||||
import { Effect, Stream } from "effect";
|
||||
import {
|
||||
llmStreamPart,
|
||||
makeTextCompletionLayer,
|
||||
|
|
@ -24,7 +23,6 @@ import {
|
|||
providerStatusError,
|
||||
requiredString,
|
||||
streamTextCompletionChunks,
|
||||
toAsyncGenerator,
|
||||
type TextCompletionConfigError,
|
||||
type TextCompletionRuntimeError,
|
||||
} from "./common.ts";
|
||||
|
|
@ -68,7 +66,7 @@ const mapOpenAIError = (error: unknown): TextCompletionRuntimeError =>
|
|||
const makeOpenAIProviderFromClient = (
|
||||
resolved: ResolvedOpenAIConfig,
|
||||
client: OpenAI,
|
||||
): LlmProvider => {
|
||||
): LlmProvider<TextCompletionRuntimeError> => {
|
||||
const {
|
||||
defaultModel,
|
||||
defaultTemperature,
|
||||
|
|
@ -81,31 +79,29 @@ const makeOpenAIProviderFromClient = (
|
|||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
) => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
return Effect.runPromise(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: maxOutput,
|
||||
}),
|
||||
catch: mapOpenAIError,
|
||||
}).pipe(
|
||||
Effect.map((resp): LlmResult => ({
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
return Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.completions.create({
|
||||
model: modelName,
|
||||
})),
|
||||
),
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
temperature: temp,
|
||||
max_completion_tokens: maxOutput,
|
||||
}),
|
||||
catch: mapOpenAIError,
|
||||
}).pipe(
|
||||
Effect.map((resp): LlmResult => ({
|
||||
text: resp.choices[0].message.content ?? "",
|
||||
inToken: resp.usage?.prompt_tokens ?? 0,
|
||||
outToken: resp.usage?.completion_tokens ?? 0,
|
||||
model: modelName,
|
||||
})),
|
||||
);
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
|
|
@ -114,11 +110,11 @@ const makeOpenAIProviderFromClient = (
|
|||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> => {
|
||||
) => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
const stream = Stream.fromEffect(
|
||||
return Stream.fromEffect(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
client.chat.completions.create({
|
||||
|
|
@ -148,13 +144,13 @@ const makeOpenAIProviderFromClient = (
|
|||
})
|
||||
),
|
||||
);
|
||||
|
||||
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapOpenAIError);
|
||||
},
|
||||
} satisfies LlmProvider;
|
||||
} satisfies LlmProvider<TextCompletionRuntimeError>;
|
||||
};
|
||||
|
||||
export function makeOpenAIProvider(config: OpenAIProcessorConfig): LlmProvider {
|
||||
export function makeOpenAIProvider(
|
||||
config: OpenAIProcessorConfig,
|
||||
): LlmProvider<TextCompletionRuntimeError> {
|
||||
return Effect.runSync(makeOpenAIProviderEffect(config));
|
||||
}
|
||||
|
||||
|
|
@ -195,12 +191,6 @@ export const program = makeFlowProcessorProgram<
|
|||
layer: (config) => makeTextCompletionLayer(makeOpenAIProviderEffect(config)),
|
||||
});
|
||||
|
||||
const openAITextCompletionRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return openAITextCompletionRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { makeFlowProcessorProgram } from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime } from "effect";
|
||||
import { Effect } from "effect";
|
||||
import * as MutableHashMap from "effect/MutableHashMap";
|
||||
import * as O from "effect/Option";
|
||||
import * as S from "effect/Schema";
|
||||
|
|
@ -167,9 +167,7 @@ export function makePromptTemplateService(config: PromptTemplateConfig): PromptT
|
|||
specifications: runtime.specs,
|
||||
});
|
||||
for (const handler of runtime.configHandlers) {
|
||||
service.registerConfigHandler((pushedConfig, version) =>
|
||||
Effect.runPromise(handler(pushedConfig, version)),
|
||||
);
|
||||
service.registerConfigHandler(handler);
|
||||
}
|
||||
Effect.runSync(Effect.log("[PromptTemplate] Service initialized"));
|
||||
return service;
|
||||
|
|
@ -199,12 +197,6 @@ export const program = makeFlowProcessorProgram({
|
|||
configHandlers: (config: PromptTemplateConfig) => promptTemplateRuntime(config).configHandlers,
|
||||
});
|
||||
|
||||
const promptRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return promptRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { QdrantClient, type QdrantClientParams } from "@qdrant/js-client-rest";
|
||||
import { errorMessage } from "@trustgraph/base";
|
||||
import { Effect } from "effect";
|
||||
import * as S from "effect/Schema";
|
||||
|
||||
export interface QdrantCollectionStatus {
|
||||
readonly exists: boolean;
|
||||
|
|
@ -17,8 +20,21 @@ export interface QdrantScoredPoint {
|
|||
readonly payload?: unknown;
|
||||
}
|
||||
|
||||
export class QdrantClientError extends S.TaggedErrorClass<QdrantClientError>()("QdrantClientError", {
|
||||
message: S.String,
|
||||
operation: S.String,
|
||||
cause: S.Defect({ includeStack: true }),
|
||||
}) {}
|
||||
|
||||
const qdrantClientError = (operation: string, cause: unknown) =>
|
||||
QdrantClientError.make({
|
||||
operation,
|
||||
message: errorMessage(cause),
|
||||
cause,
|
||||
});
|
||||
|
||||
export interface QdrantClientLike {
|
||||
readonly collectionExists: (collectionName: string) => Promise<QdrantCollectionStatus>;
|
||||
readonly collectionExists: (collectionName: string) => Effect.Effect<QdrantCollectionStatus, QdrantClientError>;
|
||||
readonly createCollection: (
|
||||
collectionName: string,
|
||||
options: {
|
||||
|
|
@ -27,7 +43,7 @@ export interface QdrantClientLike {
|
|||
readonly distance: "Cosine";
|
||||
};
|
||||
},
|
||||
) => Promise<unknown>;
|
||||
) => Effect.Effect<void, QdrantClientError>;
|
||||
readonly upsert: (
|
||||
collectionName: string,
|
||||
options: {
|
||||
|
|
@ -37,9 +53,9 @@ export interface QdrantClientLike {
|
|||
readonly payload?: Record<string, unknown>;
|
||||
}>;
|
||||
},
|
||||
) => Promise<unknown>;
|
||||
readonly getCollections: () => Promise<QdrantCollections>;
|
||||
readonly deleteCollection: (collectionName: string) => Promise<unknown>;
|
||||
) => Effect.Effect<void, QdrantClientError>;
|
||||
readonly getCollections: Effect.Effect<QdrantCollections, QdrantClientError>;
|
||||
readonly deleteCollection: (collectionName: string) => Effect.Effect<void, QdrantClientError>;
|
||||
readonly search: (
|
||||
collectionName: string,
|
||||
options: {
|
||||
|
|
@ -47,7 +63,7 @@ export interface QdrantClientLike {
|
|||
readonly limit: number;
|
||||
readonly with_payload: boolean;
|
||||
},
|
||||
) => Promise<ReadonlyArray<QdrantScoredPoint>>;
|
||||
) => Effect.Effect<ReadonlyArray<QdrantScoredPoint>, QdrantClientError>;
|
||||
}
|
||||
|
||||
export type QdrantClientFactory = (params: QdrantClientParams) => QdrantClientLike;
|
||||
|
|
@ -61,24 +77,41 @@ export const makeQdrantClient = (
|
|||
}
|
||||
|
||||
const client = new QdrantClient(params);
|
||||
const tryQdrantPromise = <A>(operation: string, try_: () => PromiseLike<A>) =>
|
||||
Effect.tryPromise({
|
||||
try: try_,
|
||||
catch: (cause) => qdrantClientError(operation, cause),
|
||||
});
|
||||
|
||||
return {
|
||||
collectionExists: (collectionName) => client.collectionExists(collectionName),
|
||||
createCollection: (collectionName, options) => client.createCollection(collectionName, options),
|
||||
collectionExists: (collectionName) =>
|
||||
tryQdrantPromise("collection-exists", () => client.collectionExists(collectionName)),
|
||||
createCollection: (collectionName, options) =>
|
||||
tryQdrantPromise("create-collection", () => client.createCollection(collectionName, options)).pipe(
|
||||
Effect.asVoid,
|
||||
),
|
||||
upsert: (collectionName, options) =>
|
||||
client.upsert(collectionName, {
|
||||
points: options.points.map((point) => ({
|
||||
id: point.id,
|
||||
vector: Array.from(point.vector),
|
||||
...(point.payload !== undefined ? { payload: point.payload } : {}),
|
||||
})),
|
||||
}),
|
||||
getCollections: () => client.getCollections(),
|
||||
deleteCollection: (collectionName) => client.deleteCollection(collectionName),
|
||||
tryQdrantPromise("upsert", () =>
|
||||
client.upsert(collectionName, {
|
||||
points: options.points.map((point) => ({
|
||||
id: point.id,
|
||||
vector: Array.from(point.vector),
|
||||
...(point.payload !== undefined ? { payload: point.payload } : {}),
|
||||
})),
|
||||
})
|
||||
).pipe(Effect.asVoid),
|
||||
getCollections: tryQdrantPromise("get-collections", () => client.getCollections()),
|
||||
deleteCollection: (collectionName) =>
|
||||
tryQdrantPromise("delete-collection", () => client.deleteCollection(collectionName)).pipe(
|
||||
Effect.asVoid,
|
||||
),
|
||||
search: (collectionName, options) =>
|
||||
client.search(collectionName, {
|
||||
vector: Array.from(options.vector),
|
||||
limit: options.limit,
|
||||
with_payload: options.with_payload,
|
||||
}),
|
||||
tryQdrantPromise("search", () =>
|
||||
client.search(collectionName, {
|
||||
vector: Array.from(options.vector),
|
||||
limit: options.limit,
|
||||
with_payload: options.with_payload,
|
||||
})
|
||||
),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { makeFlowProcessorProgram } from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime } from "effect";
|
||||
import { Effect } from "effect";
|
||||
import {
|
||||
QdrantDocEmbeddingsQueryLive,
|
||||
QdrantDocEmbeddingsQueryService,
|
||||
|
|
@ -111,12 +111,10 @@ const provideQdrantDocEmbeddingsQuery = (processorId: string) =>
|
|||
});
|
||||
|
||||
export function makeDocEmbeddingsQueryService(config: ProcessorConfig): DocEmbeddingsQueryService {
|
||||
const service = makeFlowProcessor(config, {
|
||||
return makeFlowProcessor(config, {
|
||||
specifications: makeDocEmbeddingsQuerySpecs(),
|
||||
provide: provideQdrantDocEmbeddingsQuery(config.id),
|
||||
});
|
||||
void Effect.runPromise(Effect.log("[DocEmbeddingsQuery] Service initialized"));
|
||||
return service;
|
||||
}
|
||||
|
||||
export const DocEmbeddingsQueryService = makeDocEmbeddingsQueryService;
|
||||
|
|
@ -131,12 +129,6 @@ export const program = makeFlowProcessorProgram<
|
|||
layer: (config) => QdrantDocEmbeddingsQueryLive(config),
|
||||
});
|
||||
|
||||
const docEmbeddingsQueryRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return docEmbeddingsQueryRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class QdrantDocEmbeddingsQueryError extends S.TaggedErrorClass<QdrantDocE
|
|||
{
|
||||
message: S.String,
|
||||
operation: S.String,
|
||||
cause: S.DefectWithStack,
|
||||
cause: S.Defect({ includeStack: true }),
|
||||
},
|
||||
) {}
|
||||
|
||||
|
|
@ -73,8 +73,7 @@ const decodeDocPointPayload = (payload: unknown) =>
|
|||
S.decodeUnknownEffect(DocPointPayloadSchema)(payload).pipe(Effect.option);
|
||||
|
||||
export interface QdrantDocEmbeddingsQuery {
|
||||
readonly query: (request: DocEmbeddingsQueryRequest) => Promise<ReadonlyArray<ChunkMatch>>;
|
||||
readonly queryEffect: (
|
||||
readonly query: (
|
||||
request: DocEmbeddingsQueryRequest,
|
||||
) => Effect.Effect<ReadonlyArray<ChunkMatch>, QdrantDocEmbeddingsQueryError>;
|
||||
}
|
||||
|
|
@ -95,7 +94,7 @@ const makeQdrantDocEmbeddingsQueryClient = (
|
|||
const makeQdrantDocEmbeddingsQueryFromClient = (
|
||||
client: QdrantClientLike,
|
||||
): QdrantDocEmbeddingsQueryServiceShape => {
|
||||
const queryEffect = Effect.fn("QdrantDocEmbeddingsQuery.query")(function* (request: DocEmbeddingsQueryRequest) {
|
||||
const queryImpl = Effect.fn("QdrantDocEmbeddingsQuery.query")(function* (request: DocEmbeddingsQueryRequest) {
|
||||
const { vector, user, collection, limit } = request;
|
||||
|
||||
if (vector.length === 0) {
|
||||
|
|
@ -106,10 +105,9 @@ const makeQdrantDocEmbeddingsQueryFromClient = (
|
|||
const collectionName = `d_${user}_${collection}_${dim}`;
|
||||
|
||||
// Check if collection exists -- return empty if not
|
||||
const exists = yield* Effect.tryPromise({
|
||||
try: () => client.collectionExists(collectionName),
|
||||
catch: (cause) => qdrantDocEmbeddingsQueryError("collection-exists", cause),
|
||||
});
|
||||
const exists = yield* client.collectionExists(collectionName).pipe(
|
||||
Effect.mapError((cause) => qdrantDocEmbeddingsQueryError("collection-exists", cause)),
|
||||
);
|
||||
if (!exists.exists) {
|
||||
yield* Effect.log(
|
||||
`[QdrantDocQuery] Collection ${collectionName} does not exist, returning empty results`,
|
||||
|
|
@ -117,15 +115,16 @@ const makeQdrantDocEmbeddingsQueryFromClient = (
|
|||
return [];
|
||||
}
|
||||
|
||||
const searchResult = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
client.search(collectionName, {
|
||||
vector,
|
||||
limit,
|
||||
with_payload: true,
|
||||
}),
|
||||
catch: (cause) => qdrantDocEmbeddingsQueryError("search", cause),
|
||||
});
|
||||
const searchResult = yield* client.search(
|
||||
collectionName,
|
||||
{
|
||||
vector,
|
||||
limit,
|
||||
with_payload: true,
|
||||
},
|
||||
).pipe(
|
||||
Effect.mapError((cause) => qdrantDocEmbeddingsQueryError("search", cause)),
|
||||
);
|
||||
|
||||
const chunks: ChunkMatch[] = [];
|
||||
for (const point of searchResult) {
|
||||
|
|
@ -146,7 +145,7 @@ const makeQdrantDocEmbeddingsQueryFromClient = (
|
|||
});
|
||||
|
||||
return {
|
||||
query: queryEffect,
|
||||
query: queryImpl,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -172,12 +171,8 @@ const withQdrantDocEmbeddingsQuery = <A>(
|
|||
export function makeQdrantDocEmbeddingsQuery(
|
||||
config: QdrantDocQueryConfig = {},
|
||||
): QdrantDocEmbeddingsQuery {
|
||||
const queryEffect = (request: DocEmbeddingsQueryRequest) =>
|
||||
withQdrantDocEmbeddingsQuery(config, (query) => query.query(request));
|
||||
|
||||
return {
|
||||
query: (request) => Effect.runPromise(queryEffect(request)),
|
||||
queryEffect,
|
||||
query: (request) => withQdrantDocEmbeddingsQuery(config, (query) => query.query(request)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { makeFlowProcessorProgram } from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime } from "effect";
|
||||
import { Effect } from "effect";
|
||||
import {
|
||||
QdrantGraphEmbeddingsQueryLive,
|
||||
QdrantGraphEmbeddingsQueryService,
|
||||
|
|
@ -112,12 +112,10 @@ const provideQdrantGraphEmbeddingsQuery = (processorId: string) =>
|
|||
});
|
||||
|
||||
export function makeGraphEmbeddingsQueryService(config: ProcessorConfig): GraphEmbeddingsQueryService {
|
||||
const service = makeFlowProcessor(config, {
|
||||
return makeFlowProcessor(config, {
|
||||
specifications: makeGraphEmbeddingsQuerySpecs(),
|
||||
provide: provideQdrantGraphEmbeddingsQuery(config.id),
|
||||
});
|
||||
void Effect.runPromise(Effect.log("[GraphEmbeddingsQuery] Service initialized"));
|
||||
return service;
|
||||
}
|
||||
|
||||
export const GraphEmbeddingsQueryService = makeGraphEmbeddingsQueryService;
|
||||
|
|
@ -132,12 +130,6 @@ export const program = makeFlowProcessorProgram<
|
|||
layer: (config) => QdrantGraphEmbeddingsQueryLive(config),
|
||||
});
|
||||
|
||||
const graphEmbeddingsQueryRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return graphEmbeddingsQueryRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export class QdrantGraphEmbeddingsQueryError extends S.TaggedErrorClass<QdrantGr
|
|||
{
|
||||
message: S.String,
|
||||
operation: S.String,
|
||||
cause: S.DefectWithStack,
|
||||
cause: S.Defect({ includeStack: true }),
|
||||
},
|
||||
) {}
|
||||
|
||||
|
|
@ -81,8 +81,7 @@ const decodeGraphPointPayload = (payload: unknown) =>
|
|||
S.decodeUnknownEffect(GraphPointPayloadSchema)(payload).pipe(Effect.option);
|
||||
|
||||
export interface QdrantGraphEmbeddingsQuery {
|
||||
readonly query: (request: GraphEmbeddingsQueryRequest) => Promise<ReadonlyArray<EntityMatch>>;
|
||||
readonly queryEffect: (
|
||||
readonly query: (
|
||||
request: GraphEmbeddingsQueryRequest,
|
||||
) => Effect.Effect<ReadonlyArray<EntityMatch>, QdrantGraphEmbeddingsQueryError>;
|
||||
}
|
||||
|
|
@ -104,7 +103,7 @@ const makeQdrantGraphEmbeddingsQueryFromClient = (
|
|||
client: QdrantClientLike,
|
||||
): QdrantGraphEmbeddingsQueryServiceShape => {
|
||||
|
||||
const queryEffect = Effect.fn("QdrantGraphEmbeddingsQuery.query")(function* (
|
||||
const queryImpl = Effect.fn("QdrantGraphEmbeddingsQuery.query")(function* (
|
||||
request: GraphEmbeddingsQueryRequest,
|
||||
) {
|
||||
const { vector, user, collection, limit } = request;
|
||||
|
|
@ -117,10 +116,9 @@ const makeQdrantGraphEmbeddingsQueryFromClient = (
|
|||
const collectionName = `t_${user}_${collection}_${dim}`;
|
||||
|
||||
// Check if collection exists -- return empty if not
|
||||
const exists = yield* Effect.tryPromise({
|
||||
try: () => client.collectionExists(collectionName),
|
||||
catch: (cause) => qdrantGraphEmbeddingsQueryError("collection-exists", cause),
|
||||
});
|
||||
const exists = yield* client.collectionExists(collectionName).pipe(
|
||||
Effect.mapError((cause) => qdrantGraphEmbeddingsQueryError("collection-exists", cause)),
|
||||
);
|
||||
if (!exists.exists) {
|
||||
yield* Effect.log(
|
||||
`[QdrantGraphQuery] Collection ${collectionName} does not exist, returning empty results`,
|
||||
|
|
@ -130,15 +128,16 @@ const makeQdrantGraphEmbeddingsQueryFromClient = (
|
|||
|
||||
// Query 2x the limit so we have a better chance of getting `limit`
|
||||
// unique entities after deduplication (same heuristic as Python impl)
|
||||
const searchResult = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
client.search(collectionName, {
|
||||
vector,
|
||||
limit: limit * 2,
|
||||
with_payload: true,
|
||||
}),
|
||||
catch: (cause) => qdrantGraphEmbeddingsQueryError("search", cause),
|
||||
});
|
||||
const searchResult = yield* client.search(
|
||||
collectionName,
|
||||
{
|
||||
vector,
|
||||
limit: limit * 2,
|
||||
with_payload: true,
|
||||
},
|
||||
).pipe(
|
||||
Effect.mapError((cause) => qdrantGraphEmbeddingsQueryError("search", cause)),
|
||||
);
|
||||
|
||||
const entitySet = new Set<string>();
|
||||
const entities: EntityMatch[] = [];
|
||||
|
|
@ -168,7 +167,7 @@ const makeQdrantGraphEmbeddingsQueryFromClient = (
|
|||
});
|
||||
|
||||
return {
|
||||
query: queryEffect,
|
||||
query: queryImpl,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -194,12 +193,8 @@ const withQdrantGraphEmbeddingsQuery = <A>(
|
|||
export function makeQdrantGraphEmbeddingsQuery(
|
||||
config: QdrantGraphQueryConfig = {},
|
||||
): QdrantGraphEmbeddingsQuery {
|
||||
const queryEffect = (request: GraphEmbeddingsQueryRequest) =>
|
||||
withQdrantGraphEmbeddingsQuery(config, (query) => query.query(request));
|
||||
|
||||
return {
|
||||
query: (request) => Effect.runPromise(queryEffect(request)),
|
||||
queryEffect,
|
||||
query: (request) => withQdrantGraphEmbeddingsQuery(config, (query) => query.query(request)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { makeFlowProcessorProgram } from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime } from "effect";
|
||||
import { Effect } from "effect";
|
||||
import {
|
||||
FalkorDBTriplesQueryLive,
|
||||
FalkorDBTriplesQueryService,
|
||||
|
|
@ -98,12 +98,10 @@ const provideFalkorDBTriplesQuery = (processorId: string) =>
|
|||
});
|
||||
|
||||
export function makeTriplesQueryService(config: ProcessorConfig): TriplesQueryService {
|
||||
const service = makeFlowProcessor(config, {
|
||||
return makeFlowProcessor(config, {
|
||||
specifications: makeTriplesQuerySpecs(),
|
||||
provide: provideFalkorDBTriplesQuery(config.id),
|
||||
});
|
||||
void Effect.runPromise(Effect.log("[TriplesQuery] Service initialized"));
|
||||
return service;
|
||||
}
|
||||
|
||||
export const TriplesQueryService = makeTriplesQueryService;
|
||||
|
|
@ -118,12 +116,6 @@ export const program = makeFlowProcessorProgram<
|
|||
layer: (config) => FalkorDBTriplesQueryLive(config),
|
||||
});
|
||||
|
||||
const triplesQueryRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return triplesQueryRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import * as Predicate from "effect/Predicate";
|
|||
import * as S from "effect/Schema";
|
||||
|
||||
export interface FalkorDBClosableClient {
|
||||
readonly connect: () => Promise<unknown>;
|
||||
readonly disconnect: () => Promise<unknown>;
|
||||
readonly connect: Effect.Effect<void, FalkorDBTriplesQueryError>;
|
||||
readonly disconnect: Effect.Effect<void, FalkorDBTriplesQueryError>;
|
||||
}
|
||||
|
||||
export type FalkorDBQueryOptions = Parameters<Graph["query"]>[1];
|
||||
|
|
@ -23,7 +23,7 @@ export interface FalkorDBQueryGraph {
|
|||
readonly query: <T = unknown>(
|
||||
query: string,
|
||||
options?: FalkorDBQueryOptions,
|
||||
) => Promise<{ readonly data?: Array<T> }>;
|
||||
) => Effect.Effect<{ readonly data?: Array<T> }, FalkorDBTriplesQueryError>;
|
||||
}
|
||||
|
||||
export type FalkorDBQueryClientFactory = (url: string) => FalkorDBClosableClient;
|
||||
|
|
@ -73,7 +73,7 @@ export interface FalkorDBTriplesQuery {
|
|||
p?: Term,
|
||||
o?: Term,
|
||||
limit?: number,
|
||||
) => Promise<Triple[]>;
|
||||
) => Effect.Effect<ReadonlyArray<Triple>, FalkorDBTriplesQueryError>;
|
||||
}
|
||||
|
||||
export class FalkorDBTriplesQueryError extends S.TaggedErrorClass<FalkorDBTriplesQueryError>()(
|
||||
|
|
@ -81,7 +81,7 @@ export class FalkorDBTriplesQueryError extends S.TaggedErrorClass<FalkorDBTriple
|
|||
{
|
||||
message: S.String,
|
||||
operation: S.String,
|
||||
cause: S.DefectWithStack,
|
||||
cause: S.Defect({ includeStack: true }),
|
||||
},
|
||||
) {}
|
||||
|
||||
|
|
@ -113,6 +113,12 @@ interface FalkorDBQueryConnection {
|
|||
readonly graph: FalkorDBQueryGraph;
|
||||
}
|
||||
|
||||
const tryFalkorDBPromise = <A>(operation: string, try_: () => PromiseLike<A>) =>
|
||||
Effect.tryPromise({
|
||||
try: try_,
|
||||
catch: (cause) => falkorDBTriplesQueryError(operation, cause),
|
||||
});
|
||||
|
||||
const resolveFalkorDBQueryConfig = Effect.fn("FalkorDBTriplesQuery.resolveConfig")(function* (
|
||||
config: FalkorDBQueryConfig,
|
||||
) {
|
||||
|
|
@ -149,16 +155,21 @@ const connectFalkorDBTriplesQuery = Effect.fn("FalkorDBTriplesQuery.connect")(fu
|
|||
const client = clientFactory(url);
|
||||
return { client, graph: graphFactory(client, database) };
|
||||
}
|
||||
const client = createClient({ url });
|
||||
return { client, graph: new Graph(client, database) };
|
||||
const sdkClient = createClient({ url });
|
||||
const client: FalkorDBClosableClient = {
|
||||
connect: tryFalkorDBPromise("connect", () => sdkClient.connect()).pipe(Effect.asVoid),
|
||||
disconnect: tryFalkorDBPromise("disconnect", () => sdkClient.disconnect()).pipe(Effect.asVoid),
|
||||
};
|
||||
const sdkGraph = new Graph(sdkClient, database);
|
||||
const graph: FalkorDBQueryGraph = {
|
||||
query: (query, options) => tryFalkorDBPromise("graph-query", () => sdkGraph.query(query, options)),
|
||||
};
|
||||
return { client, graph };
|
||||
},
|
||||
catch: (cause) => falkorDBTriplesQueryError("create-client", cause),
|
||||
});
|
||||
|
||||
yield* Effect.tryPromise({
|
||||
try: () => client.connect(),
|
||||
catch: (cause) => falkorDBTriplesQueryError("connect", cause),
|
||||
}).pipe(
|
||||
yield* client.connect.pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logError("[FalkorDBTriplesQuery] Connection failed", {
|
||||
error: error.message,
|
||||
|
|
@ -174,10 +185,7 @@ const connectFalkorDBTriplesQuery = Effect.fn("FalkorDBTriplesQuery.connect")(fu
|
|||
const disconnectFalkorDBTriplesQuery = (
|
||||
connection: FalkorDBQueryConnection,
|
||||
): Effect.Effect<void> =>
|
||||
Effect.tryPromise({
|
||||
try: () => connection.client.disconnect(),
|
||||
catch: (cause) => falkorDBTriplesQueryError("disconnect", cause),
|
||||
}).pipe(
|
||||
connection.client.disconnect.pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.logError("[FalkorDBTriplesQuery] Disconnect failed", {
|
||||
error: error.message,
|
||||
|
|
@ -201,10 +209,8 @@ const queryRows = (
|
|||
query: string,
|
||||
options?: FalkorDBQueryOptions,
|
||||
): Effect.Effect<ReadonlyArray<unknown>, FalkorDBTriplesQueryError> =>
|
||||
Effect.tryPromise({
|
||||
try: () => graph.query<unknown>(query, options),
|
||||
catch: (cause) => falkorDBTriplesQueryError(operation, cause),
|
||||
}).pipe(
|
||||
graph.query<unknown>(query, options).pipe(
|
||||
Effect.mapError((cause) => falkorDBTriplesQueryError(operation, cause)),
|
||||
Effect.map((result) => result.data ?? []),
|
||||
);
|
||||
|
||||
|
|
@ -480,9 +486,7 @@ export function makeFalkorDBTriplesQuery(
|
|||
): FalkorDBTriplesQuery {
|
||||
return {
|
||||
queryTriples: (s, p, o, limit = 100) =>
|
||||
Effect.runPromise(
|
||||
withFalkorDBTriplesQuery(config, (query) => query.queryTriples(s, p, o, limit)),
|
||||
).then((triples) => Array.from(triples)),
|
||||
withFalkorDBTriplesQuery(config, (query) => query.queryTriples(s, p, o, limit)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import {
|
|||
type TextCompletionRequest,
|
||||
type TextCompletionResponse,
|
||||
} from "@trustgraph/base";
|
||||
import {Effect, Layer, ManagedRuntime} from "effect";
|
||||
import {Effect} from "effect";
|
||||
import {
|
||||
DocumentRagEngine,
|
||||
DocumentRagEngineError,
|
||||
|
|
@ -139,12 +139,6 @@ export const program = makeFlowProcessorProgram({
|
|||
layer: () => DocumentRagLive,
|
||||
});
|
||||
|
||||
const documentRagRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return documentRagRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ export interface DocumentRagClients {
|
|||
prompt: EffectRequestResponse<PromptRequest, PromptResponse>;
|
||||
}
|
||||
|
||||
export type ChunkCallback = (text: string, endOfStream: boolean) => Promise<void>;
|
||||
export type ChunkCallback = (
|
||||
text: string,
|
||||
endOfStream: boolean,
|
||||
) => Effect.Effect<void, DocumentRagEngineError>;
|
||||
|
||||
export interface DocumentRagQueryOptions {
|
||||
readonly collection?: string;
|
||||
|
|
@ -39,7 +42,7 @@ export class DocumentRagEngineError extends S.TaggedErrorClass<DocumentRagEngine
|
|||
{
|
||||
message: S.String,
|
||||
operation: S.String,
|
||||
cause: S.DefectWithStack,
|
||||
cause: S.Defect({ includeStack: true }),
|
||||
},
|
||||
) {}
|
||||
|
||||
|
|
@ -82,14 +85,13 @@ export interface DocumentRag {
|
|||
readonly query: (
|
||||
queryText: string,
|
||||
options?: DocumentRagQueryOptions,
|
||||
) => Promise<string>;
|
||||
) => Effect.Effect<string, DocumentRagEngineError>;
|
||||
}
|
||||
|
||||
export function makeDocumentRag(clients: DocumentRagClients): DocumentRag {
|
||||
const engine = makeDocumentRagEngine();
|
||||
return {
|
||||
query: (queryText, options) =>
|
||||
Effect.runPromise(engine.query(clients, queryText, options)),
|
||||
query: (queryText, options) => engine.query(clients, queryText, options),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import {
|
|||
type TriplesQueryRequest,
|
||||
type TriplesQueryResponse,
|
||||
} from "@trustgraph/base";
|
||||
import {Effect, Layer, ManagedRuntime} from "effect";
|
||||
import {Effect} from "effect";
|
||||
import {
|
||||
GraphRagEngine,
|
||||
GraphRagEngineError,
|
||||
|
|
@ -173,12 +173,6 @@ export const program = makeFlowProcessorProgram({
|
|||
layer: () => GraphRagLive,
|
||||
});
|
||||
|
||||
const graphRagRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return graphRagRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@ export interface GraphRagClients {
|
|||
prompt: EffectRequestResponse<PromptRequest, PromptResponse>;
|
||||
}
|
||||
|
||||
export type ChunkCallback = (text: string, endOfStream: boolean) => Promise<void>;
|
||||
export type ChunkCallback = (
|
||||
text: string,
|
||||
endOfStream: boolean,
|
||||
) => Effect.Effect<void, GraphRagEngineError>;
|
||||
|
||||
export interface GraphRagQueryOptions {
|
||||
readonly collection?: string;
|
||||
|
|
@ -69,7 +72,7 @@ export class GraphRagEngineError extends S.TaggedErrorClass<GraphRagEngineError>
|
|||
{
|
||||
message: S.String,
|
||||
operation: S.String,
|
||||
cause: S.DefectWithStack,
|
||||
cause: S.Defect({ includeStack: true }),
|
||||
},
|
||||
) {}
|
||||
|
||||
|
|
@ -135,7 +138,7 @@ export interface GraphRag {
|
|||
readonly query: (
|
||||
queryText: string,
|
||||
options?: GraphRagQueryOptions,
|
||||
) => Promise<GraphRagResult>;
|
||||
) => Effect.Effect<GraphRagResult, GraphRagEngineError>;
|
||||
}
|
||||
|
||||
export function makeGraphRag(
|
||||
|
|
@ -144,8 +147,7 @@ export function makeGraphRag(
|
|||
): GraphRag {
|
||||
const engine = makeGraphRagEngine();
|
||||
return {
|
||||
query: (queryText, options) =>
|
||||
Effect.runPromise(engine.query(clients, queryText, options, config)),
|
||||
query: (queryText, options) => engine.query(clients, queryText, options, config),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -403,10 +405,9 @@ const synthesize = Effect.fn("GraphRagEngine.synthesize")(function* (
|
|||
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),
|
||||
});
|
||||
return chunkCallback(resp.response, resp.endOfStream === true).pipe(
|
||||
Effect.as(resp.endOfStream === true),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -427,7 +428,7 @@ const synthesize = Effect.fn("GraphRagEngine.synthesize")(function* (
|
|||
|
||||
const ScoredEdge = S.Struct({
|
||||
id: S.String,
|
||||
score: S.Number,
|
||||
score: S.Finite,
|
||||
});
|
||||
const ScoredEdgesFromJson = S.Array(ScoredEdge).pipe(S.fromJsonString);
|
||||
const ScoredEdgeFromJson = ScoredEdge.pipe(S.fromJsonString);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/** @effect-diagnostics strictEffectProvide:skip-file */
|
||||
|
||||
import * as BunFileSystem from "@effect/platform-bun/BunFileSystem";
|
||||
import { Effect, ManagedRuntime } from "effect";
|
||||
import { Effect } from "effect";
|
||||
import * as FileSystem from "effect/FileSystem";
|
||||
import type { PlatformError } from "effect/PlatformError";
|
||||
|
||||
const fileSystemRuntime = ManagedRuntime.make(BunFileSystem.layer);
|
||||
|
||||
export function joinPath(...segments: string[]): string {
|
||||
const joined = segments
|
||||
.filter((segment) => segment.length > 0)
|
||||
|
|
@ -22,52 +22,33 @@ export function dirnamePath(path: string): string {
|
|||
return normalized.slice(0, index);
|
||||
}
|
||||
|
||||
export const ensureDirectoryEffect = (path: string): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
|
||||
Effect.flatMap(FileSystem.FileSystem, (fs) =>
|
||||
const withFileSystem = <A, E>(
|
||||
effect: Effect.Effect<A, E, FileSystem.FileSystem>,
|
||||
): Effect.Effect<A, E> =>
|
||||
effect.pipe(Effect.provide(BunFileSystem.layer));
|
||||
|
||||
export const ensureDirectoryEffect = (path: string): Effect.Effect<void, PlatformError> =>
|
||||
withFileSystem(Effect.flatMap(FileSystem.FileSystem, (fs) =>
|
||||
fs.makeDirectory(path, { recursive: true })
|
||||
);
|
||||
));
|
||||
|
||||
export function ensureDirectory(path: string): Promise<void> {
|
||||
return fileSystemRuntime.runPromise(ensureDirectoryEffect(path));
|
||||
}
|
||||
export const readTextFileEffect = (path: string): Effect.Effect<string, PlatformError> =>
|
||||
withFileSystem(Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFileString(path)));
|
||||
|
||||
export const readTextFileEffect = (path: string): Effect.Effect<string, PlatformError, FileSystem.FileSystem> =>
|
||||
Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFileString(path));
|
||||
|
||||
export function readTextFile(path: string): Promise<string> {
|
||||
return fileSystemRuntime.runPromise(readTextFileEffect(path));
|
||||
}
|
||||
|
||||
export const readBinaryFileEffect = (path: string): Effect.Effect<Uint8Array, PlatformError, FileSystem.FileSystem> =>
|
||||
Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFile(path));
|
||||
|
||||
export function readBinaryFile(path: string): Promise<Uint8Array> {
|
||||
return fileSystemRuntime.runPromise(readBinaryFileEffect(path));
|
||||
}
|
||||
export const readBinaryFileEffect = (path: string): Effect.Effect<Uint8Array, PlatformError> =>
|
||||
withFileSystem(Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFile(path)));
|
||||
|
||||
export const writeTextFileEffect = (
|
||||
path: string,
|
||||
data: string,
|
||||
): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
|
||||
Effect.flatMap(FileSystem.FileSystem, (fs) => fs.writeFileString(path, data));
|
||||
|
||||
export function writeTextFile(path: string, data: string): Promise<void> {
|
||||
return fileSystemRuntime.runPromise(writeTextFileEffect(path, data));
|
||||
}
|
||||
): Effect.Effect<void, PlatformError> =>
|
||||
withFileSystem(Effect.flatMap(FileSystem.FileSystem, (fs) => fs.writeFileString(path, data)));
|
||||
|
||||
export const writeBinaryFileEffect = (
|
||||
path: string,
|
||||
data: Uint8Array,
|
||||
): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
|
||||
Effect.flatMap(FileSystem.FileSystem, (fs) => fs.writeFile(path, data));
|
||||
): Effect.Effect<void, PlatformError> =>
|
||||
withFileSystem(Effect.flatMap(FileSystem.FileSystem, (fs) => fs.writeFile(path, data)));
|
||||
|
||||
export function writeBinaryFile(path: string, data: Uint8Array): Promise<void> {
|
||||
return fileSystemRuntime.runPromise(writeBinaryFileEffect(path, data));
|
||||
}
|
||||
|
||||
export const removePathEffect = (path: string): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
|
||||
Effect.flatMap(FileSystem.FileSystem, (fs) => fs.remove(path));
|
||||
|
||||
export function removePath(path: string): Promise<void> {
|
||||
return fileSystemRuntime.runPromise(removePathEffect(path));
|
||||
}
|
||||
export const removePathEffect = (path: string): Effect.Effect<void, PlatformError> =>
|
||||
withFileSystem(Effect.flatMap(FileSystem.FileSystem, (fs) => fs.remove(path)));
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { makeFlowProcessorProgram } from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime } from "effect";
|
||||
import { Effect } from "effect";
|
||||
import {
|
||||
QdrantGraphEmbeddingsStoreLive,
|
||||
QdrantGraphEmbeddingsStoreService,
|
||||
|
|
@ -113,12 +113,10 @@ const provideQdrantGraphEmbeddingsStore = (processorId: string) =>
|
|||
});
|
||||
|
||||
export function makeGraphEmbeddingsStoreService(config: ProcessorConfig): GraphEmbeddingsStoreService {
|
||||
const service = makeFlowProcessor(config, {
|
||||
return makeFlowProcessor(config, {
|
||||
specifications: makeGraphEmbeddingsStoreSpecs(),
|
||||
provide: provideQdrantGraphEmbeddingsStore(config.id),
|
||||
});
|
||||
void Effect.runPromise(Effect.log("[GraphEmbeddingsStore] Service initialized"));
|
||||
return service;
|
||||
}
|
||||
|
||||
export const GraphEmbeddingsStoreService = makeGraphEmbeddingsStoreService;
|
||||
|
|
@ -133,12 +131,6 @@ export const program = makeFlowProcessorProgram<
|
|||
layer: (config) => QdrantGraphEmbeddingsStoreLive(config),
|
||||
});
|
||||
|
||||
const graphEmbeddingsStoreRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return graphEmbeddingsStoreRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export class QdrantDocEmbeddingsStoreError extends S.TaggedErrorClass<QdrantDocE
|
|||
{
|
||||
message: S.String,
|
||||
operation: S.String,
|
||||
cause: S.DefectWithStack,
|
||||
cause: S.Defect({ includeStack: true }),
|
||||
},
|
||||
) {}
|
||||
|
||||
|
|
@ -85,12 +85,10 @@ const randomPointId = Effect.fn("QdrantDocEmbeddings.randomPointId")(function* (
|
|||
});
|
||||
|
||||
export interface QdrantDocEmbeddingsStore {
|
||||
readonly store: (message: DocEmbeddingsMessage) => Promise<void>;
|
||||
readonly deleteCollection: (user: string, collection: string) => Promise<void>;
|
||||
readonly storeEffect: (
|
||||
readonly store: (
|
||||
message: DocEmbeddingsMessage,
|
||||
) => Effect.Effect<void, QdrantDocEmbeddingsStoreError>;
|
||||
readonly deleteCollectionEffect: (
|
||||
readonly deleteCollection: (
|
||||
user: string,
|
||||
collection: string,
|
||||
) => Effect.Effect<void, QdrantDocEmbeddingsStoreError>;
|
||||
|
|
@ -133,25 +131,25 @@ const makeQdrantDocEmbeddingsStoreFromClient = (
|
|||
) {
|
||||
if (MutableHashSet.has(knownCollections, name)) return;
|
||||
|
||||
const exists = yield* Effect.tryPromise({
|
||||
try: () => client.collectionExists(name),
|
||||
catch: (cause) => qdrantDocEmbeddingsStoreError("collection-exists", cause),
|
||||
});
|
||||
const exists = yield* client.collectionExists(name).pipe(
|
||||
Effect.mapError((cause) => qdrantDocEmbeddingsStoreError("collection-exists", cause)),
|
||||
);
|
||||
if (!exists.exists) {
|
||||
yield* Effect.log(`[QdrantDocEmbeddings] Creating collection ${name} (dim=${dim})`);
|
||||
yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
client.createCollection(name, {
|
||||
vectors: { size: dim, distance: "Cosine" },
|
||||
}),
|
||||
catch: (cause) => qdrantDocEmbeddingsStoreError("create-collection", cause),
|
||||
});
|
||||
yield* client.createCollection(
|
||||
name,
|
||||
{
|
||||
vectors: { size: dim, distance: "Cosine" },
|
||||
},
|
||||
).pipe(
|
||||
Effect.mapError((cause) => qdrantDocEmbeddingsStoreError("create-collection", cause)),
|
||||
);
|
||||
}
|
||||
|
||||
MutableHashSet.add(knownCollections, name);
|
||||
});
|
||||
|
||||
const storeEffect = Effect.fn("QdrantDocEmbeddings.store")(function* (message: DocEmbeddingsMessage) {
|
||||
const storeImpl = Effect.fn("QdrantDocEmbeddings.store")(function* (message: DocEmbeddingsMessage) {
|
||||
for (const chunk of message.chunks) {
|
||||
if (chunk.chunkId.length === 0) continue;
|
||||
if (chunk.vector.length === 0) continue;
|
||||
|
|
@ -162,37 +160,37 @@ const makeQdrantDocEmbeddingsStoreFromClient = (
|
|||
yield* ensureCollectionEffect(name, dim);
|
||||
|
||||
const id = yield* randomPointId();
|
||||
yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
client.upsert(name, {
|
||||
points: [
|
||||
{
|
||||
id,
|
||||
vector: chunk.vector,
|
||||
payload: {
|
||||
chunk_id: chunk.chunkId,
|
||||
...(chunk.content !== undefined && chunk.content.length > 0
|
||||
? { content: chunk.content }
|
||||
: {}),
|
||||
},
|
||||
yield* client.upsert(
|
||||
name,
|
||||
{
|
||||
points: [
|
||||
{
|
||||
id,
|
||||
vector: chunk.vector,
|
||||
payload: {
|
||||
chunk_id: chunk.chunkId,
|
||||
...(chunk.content !== undefined && chunk.content.length > 0
|
||||
? { content: chunk.content }
|
||||
: {}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
catch: (cause) => qdrantDocEmbeddingsStoreError("upsert", cause),
|
||||
});
|
||||
},
|
||||
],
|
||||
},
|
||||
).pipe(
|
||||
Effect.mapError((cause) => qdrantDocEmbeddingsStoreError("upsert", cause)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const deleteCollectionEffect = Effect.fn("QdrantDocEmbeddings.deleteCollection")(function* (
|
||||
const deleteCollectionImpl = Effect.fn("QdrantDocEmbeddings.deleteCollection")(function* (
|
||||
user: string,
|
||||
collection: string,
|
||||
) {
|
||||
const prefix = `d_${user}_${collection}_`;
|
||||
|
||||
const allCollections = yield* Effect.tryPromise({
|
||||
try: () => client.getCollections(),
|
||||
catch: (cause) => qdrantDocEmbeddingsStoreError("get-collections", cause),
|
||||
});
|
||||
const allCollections = yield* client.getCollections.pipe(
|
||||
Effect.mapError((cause) => qdrantDocEmbeddingsStoreError("get-collections", cause)),
|
||||
);
|
||||
const matching = allCollections.collections.filter((c) =>
|
||||
c.name.startsWith(prefix),
|
||||
);
|
||||
|
|
@ -203,10 +201,9 @@ const makeQdrantDocEmbeddingsStoreFromClient = (
|
|||
}
|
||||
|
||||
for (const coll of matching) {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => client.deleteCollection(coll.name),
|
||||
catch: (cause) => qdrantDocEmbeddingsStoreError("delete-collection", cause),
|
||||
});
|
||||
yield* client.deleteCollection(coll.name).pipe(
|
||||
Effect.mapError((cause) => qdrantDocEmbeddingsStoreError("delete-collection", cause)),
|
||||
);
|
||||
MutableHashSet.remove(knownCollections, coll.name);
|
||||
yield* Effect.log(`[QdrantDocEmbeddings] Deleted collection: ${coll.name}`);
|
||||
}
|
||||
|
|
@ -217,8 +214,8 @@ const makeQdrantDocEmbeddingsStoreFromClient = (
|
|||
});
|
||||
|
||||
return {
|
||||
store: storeEffect,
|
||||
deleteCollection: deleteCollectionEffect,
|
||||
store: storeImpl,
|
||||
deleteCollection: deleteCollectionImpl,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -244,16 +241,9 @@ const withQdrantDocEmbeddingsStore = <A>(
|
|||
export function makeQdrantDocEmbeddingsStore(
|
||||
config: QdrantDocEmbeddingsConfig = {},
|
||||
): QdrantDocEmbeddingsStore {
|
||||
const storeEffect = (message: DocEmbeddingsMessage) =>
|
||||
withQdrantDocEmbeddingsStore(config, (store) => store.store(message));
|
||||
const deleteCollectionEffect = (user: string, collection: string) =>
|
||||
withQdrantDocEmbeddingsStore(config, (store) => store.deleteCollection(user, collection));
|
||||
|
||||
return {
|
||||
store: (message) => Effect.runPromise(storeEffect(message)),
|
||||
store: (message) => withQdrantDocEmbeddingsStore(config, (store) => store.store(message)),
|
||||
deleteCollection: (user, collection) =>
|
||||
Effect.runPromise(deleteCollectionEffect(user, collection)),
|
||||
storeEffect,
|
||||
deleteCollectionEffect,
|
||||
withQdrantDocEmbeddingsStore(config, (store) => store.deleteCollection(user, collection)),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export class QdrantGraphEmbeddingsStoreError extends S.TaggedErrorClass<QdrantGr
|
|||
{
|
||||
message: S.String,
|
||||
operation: S.String,
|
||||
cause: S.DefectWithStack,
|
||||
cause: S.Defect({ includeStack: true }),
|
||||
},
|
||||
) {}
|
||||
|
||||
|
|
@ -96,12 +96,10 @@ function getTermValue(term: Term): string | null {
|
|||
}
|
||||
|
||||
export interface QdrantGraphEmbeddingsStore {
|
||||
readonly store: (message: GraphEmbeddingsMessage) => Promise<void>;
|
||||
readonly deleteCollection: (user: string, collection: string) => Promise<void>;
|
||||
readonly storeEffect: (
|
||||
readonly store: (
|
||||
message: GraphEmbeddingsMessage,
|
||||
) => Effect.Effect<void, QdrantGraphEmbeddingsStoreError>;
|
||||
readonly deleteCollectionEffect: (
|
||||
readonly deleteCollection: (
|
||||
user: string,
|
||||
collection: string,
|
||||
) => Effect.Effect<void, QdrantGraphEmbeddingsStoreError>;
|
||||
|
|
@ -134,25 +132,25 @@ const makeQdrantGraphEmbeddingsStoreFromClient = (
|
|||
) {
|
||||
if (MutableHashSet.has(knownCollections, name)) return;
|
||||
|
||||
const exists = yield* Effect.tryPromise({
|
||||
try: () => client.collectionExists(name),
|
||||
catch: (cause) => qdrantGraphEmbeddingsStoreError("collection-exists", cause),
|
||||
});
|
||||
const exists = yield* client.collectionExists(name).pipe(
|
||||
Effect.mapError((cause) => qdrantGraphEmbeddingsStoreError("collection-exists", cause)),
|
||||
);
|
||||
if (!exists.exists) {
|
||||
yield* Effect.log(`[QdrantGraphEmbeddings] Creating collection ${name} (dim=${dim})`);
|
||||
yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
client.createCollection(name, {
|
||||
vectors: { size: dim, distance: "Cosine" },
|
||||
}),
|
||||
catch: (cause) => qdrantGraphEmbeddingsStoreError("create-collection", cause),
|
||||
});
|
||||
yield* client.createCollection(
|
||||
name,
|
||||
{
|
||||
vectors: { size: dim, distance: "Cosine" },
|
||||
},
|
||||
).pipe(
|
||||
Effect.mapError((cause) => qdrantGraphEmbeddingsStoreError("create-collection", cause)),
|
||||
);
|
||||
}
|
||||
|
||||
MutableHashSet.add(knownCollections, name);
|
||||
});
|
||||
|
||||
const storeEffect = Effect.fn("QdrantGraphEmbeddings.store")(function* (message: GraphEmbeddingsMessage) {
|
||||
const storeImpl = Effect.fn("QdrantGraphEmbeddings.store")(function* (message: GraphEmbeddingsMessage) {
|
||||
for (const entry of message.entities) {
|
||||
const entityValue = getTermValue(entry.entity);
|
||||
if (entityValue === null || entityValue.length === 0) continue;
|
||||
|
|
@ -169,32 +167,32 @@ const makeQdrantGraphEmbeddingsStoreFromClient = (
|
|||
}
|
||||
|
||||
const id = yield* randomPointId();
|
||||
yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
client.upsert(name, {
|
||||
points: [
|
||||
{
|
||||
id,
|
||||
vector: entry.vector,
|
||||
payload,
|
||||
},
|
||||
],
|
||||
}),
|
||||
catch: (cause) => qdrantGraphEmbeddingsStoreError("upsert", cause),
|
||||
});
|
||||
yield* client.upsert(
|
||||
name,
|
||||
{
|
||||
points: [
|
||||
{
|
||||
id,
|
||||
vector: entry.vector,
|
||||
payload,
|
||||
},
|
||||
],
|
||||
},
|
||||
).pipe(
|
||||
Effect.mapError((cause) => qdrantGraphEmbeddingsStoreError("upsert", cause)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const deleteCollectionEffect = Effect.fn("QdrantGraphEmbeddings.deleteCollection")(function* (
|
||||
const deleteCollectionImpl = Effect.fn("QdrantGraphEmbeddings.deleteCollection")(function* (
|
||||
user: string,
|
||||
collection: string,
|
||||
) {
|
||||
const prefix = `t_${user}_${collection}_`;
|
||||
|
||||
const allCollections = yield* Effect.tryPromise({
|
||||
try: () => client.getCollections(),
|
||||
catch: (cause) => qdrantGraphEmbeddingsStoreError("get-collections", cause),
|
||||
});
|
||||
const allCollections = yield* client.getCollections.pipe(
|
||||
Effect.mapError((cause) => qdrantGraphEmbeddingsStoreError("get-collections", cause)),
|
||||
);
|
||||
const matching = allCollections.collections.filter((c) =>
|
||||
c.name.startsWith(prefix),
|
||||
);
|
||||
|
|
@ -205,10 +203,9 @@ const makeQdrantGraphEmbeddingsStoreFromClient = (
|
|||
}
|
||||
|
||||
for (const coll of matching) {
|
||||
yield* Effect.tryPromise({
|
||||
try: () => client.deleteCollection(coll.name),
|
||||
catch: (cause) => qdrantGraphEmbeddingsStoreError("delete-collection", cause),
|
||||
});
|
||||
yield* client.deleteCollection(coll.name).pipe(
|
||||
Effect.mapError((cause) => qdrantGraphEmbeddingsStoreError("delete-collection", cause)),
|
||||
);
|
||||
MutableHashSet.remove(knownCollections, coll.name);
|
||||
yield* Effect.log(`[QdrantGraphEmbeddings] Deleted collection: ${coll.name}`);
|
||||
}
|
||||
|
|
@ -219,8 +216,8 @@ const makeQdrantGraphEmbeddingsStoreFromClient = (
|
|||
});
|
||||
|
||||
return {
|
||||
store: storeEffect,
|
||||
deleteCollection: deleteCollectionEffect,
|
||||
store: storeImpl,
|
||||
deleteCollection: deleteCollectionImpl,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -246,17 +243,10 @@ const withQdrantGraphEmbeddingsStore = <A>(
|
|||
export function makeQdrantGraphEmbeddingsStore(
|
||||
config: QdrantGraphEmbeddingsConfig = {},
|
||||
): QdrantGraphEmbeddingsStore {
|
||||
const storeEffect = (message: GraphEmbeddingsMessage) =>
|
||||
withQdrantGraphEmbeddingsStore(config, (store) => store.store(message));
|
||||
const deleteCollectionEffect = (user: string, collection: string) =>
|
||||
withQdrantGraphEmbeddingsStore(config, (store) => store.deleteCollection(user, collection));
|
||||
|
||||
return {
|
||||
store: (message) => Effect.runPromise(storeEffect(message)),
|
||||
store: (message) => withQdrantGraphEmbeddingsStore(config, (store) => store.store(message)),
|
||||
deleteCollection: (user, collection) =>
|
||||
Effect.runPromise(deleteCollectionEffect(user, collection)),
|
||||
storeEffect,
|
||||
deleteCollectionEffect,
|
||||
withQdrantGraphEmbeddingsStore(config, (store) => store.deleteCollection(user, collection)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from "@trustgraph/base";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import { makeFlowProcessorProgram } from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime } from "effect";
|
||||
import { Effect } from "effect";
|
||||
import {
|
||||
FalkorDBTriplesStoreLive,
|
||||
FalkorDBTriplesStoreService,
|
||||
|
|
@ -73,12 +73,10 @@ const provideFalkorDBTriplesStore = (processorId: string) =>
|
|||
});
|
||||
|
||||
export function makeTriplesStoreService(config: ProcessorConfig): TriplesStoreService {
|
||||
const service = makeFlowProcessor(config, {
|
||||
return makeFlowProcessor(config, {
|
||||
specifications: makeTriplesStoreSpecs(),
|
||||
provide: provideFalkorDBTriplesStore(config.id),
|
||||
});
|
||||
void Effect.runPromise(Effect.log("[TriplesStore] Service initialized"));
|
||||
return service;
|
||||
}
|
||||
|
||||
export const TriplesStoreService = makeTriplesStoreService;
|
||||
|
|
@ -93,12 +91,6 @@ export const program = makeFlowProcessorProgram<
|
|||
layer: (config) => FalkorDBTriplesStoreLive(config),
|
||||
});
|
||||
|
||||
const triplesStoreRuntime = ManagedRuntime.make(Layer.empty);
|
||||
|
||||
export function run(): Promise<void> {
|
||||
return triplesStoreRuntime.runPromise(program);
|
||||
}
|
||||
|
||||
export function runMain(): void {
|
||||
NodeRuntime.runMain(program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import { Config, Context, Effect, Layer, Match } from "effect";
|
|||
import * as S from "effect/Schema";
|
||||
|
||||
export interface FalkorDBClosableClient {
|
||||
readonly connect: () => Promise<unknown>;
|
||||
readonly disconnect: () => Promise<unknown>;
|
||||
readonly connect: Effect.Effect<void, FalkorDBTriplesStoreError>;
|
||||
readonly disconnect: Effect.Effect<void, FalkorDBTriplesStoreError>;
|
||||
}
|
||||
|
||||
export type FalkorDBStoreQueryOptions = Parameters<Graph["query"]>[1];
|
||||
|
|
@ -23,7 +23,7 @@ export interface FalkorDBStoreGraph {
|
|||
readonly query: <T = unknown>(
|
||||
query: string,
|
||||
options?: FalkorDBStoreQueryOptions,
|
||||
) => Promise<{ readonly data?: Array<T> }>;
|
||||
) => Effect.Effect<{ readonly data?: Array<T> }, FalkorDBTriplesStoreError>;
|
||||
}
|
||||
|
||||
export type FalkorDBStoreClientFactory = (url: string) => FalkorDBClosableClient;
|
||||
|
|
@ -51,28 +51,39 @@ function getTermValue(term: Term): string {
|
|||
}
|
||||
|
||||
export interface FalkorDBTriplesStore {
|
||||
readonly createNode: (uri: string, user: string, collection: string) => Promise<void>;
|
||||
readonly createLiteral: (value: string, user: string, collection: string) => Promise<void>;
|
||||
readonly createNode: (
|
||||
uri: string,
|
||||
user: string,
|
||||
collection: string,
|
||||
) => Effect.Effect<void, FalkorDBTriplesStoreError>;
|
||||
readonly createLiteral: (
|
||||
value: string,
|
||||
user: string,
|
||||
collection: string,
|
||||
) => Effect.Effect<void, FalkorDBTriplesStoreError>;
|
||||
readonly relateNode: (
|
||||
src: string,
|
||||
uri: string,
|
||||
dest: string,
|
||||
user: string,
|
||||
collection: string,
|
||||
) => Promise<void>;
|
||||
) => Effect.Effect<void, FalkorDBTriplesStoreError>;
|
||||
readonly relateLiteral: (
|
||||
src: string,
|
||||
uri: string,
|
||||
dest: string,
|
||||
user: string,
|
||||
collection: string,
|
||||
) => Promise<void>;
|
||||
) => Effect.Effect<void, FalkorDBTriplesStoreError>;
|
||||
readonly storeTriples: (
|
||||
triples: Triple[],
|
||||
user?: string,
|
||||
collection?: string,
|
||||
) => Promise<void>;
|
||||
readonly deleteCollection: (user: string, collection: string) => Promise<void>;
|
||||
) => Effect.Effect<void, FalkorDBTriplesStoreError>;
|
||||
readonly deleteCollection: (
|
||||
user: string,
|
||||
collection: string,
|
||||
) => Effect.Effect<void, FalkorDBTriplesStoreError>;
|
||||
}
|
||||
|
||||
export class FalkorDBTriplesStoreError extends S.TaggedErrorClass<FalkorDBTriplesStoreError>()(
|
||||
|
|
@ -80,7 +91,7 @@ export class FalkorDBTriplesStoreError extends S.TaggedErrorClass<FalkorDBTriple
|
|||
{
|
||||
message: S.String,
|
||||
operation: S.String,
|
||||
cause: S.DefectWithStack,
|
||||
cause: S.Defect({ includeStack: true }),
|
||||
},
|
||||
) {}
|
||||
|
||||
|
|
@ -115,6 +126,12 @@ interface FalkorDBStoreConnection {
|
|||
readonly graph: FalkorDBStoreGraph;
|
||||
}
|
||||
|
||||
const tryFalkorDBPromise = <A>(operation: string, try_: () => PromiseLike<A>) =>
|
||||
Effect.tryPromise({
|
||||
try: try_,
|
||||
catch: (cause) => falkorDBTriplesStoreError(operation, cause),
|
||||
});
|
||||
|
||||
interface FalkorDBTriplesStoreEffectShape {
|
||||
readonly createNode: (
|
||||
uri: string,
|
||||
|
|
@ -187,16 +204,21 @@ const connectFalkorDBTriplesStore = Effect.fn("FalkorDBTriplesStore.connect")(fu
|
|||
const client = clientFactory(url);
|
||||
return { client, graph: graphFactory(client, database) };
|
||||
}
|
||||
const client = createClient({ url });
|
||||
return { client, graph: new Graph(client, database) };
|
||||
const sdkClient = createClient({ url });
|
||||
const client: FalkorDBClosableClient = {
|
||||
connect: tryFalkorDBPromise("connect", () => sdkClient.connect()).pipe(Effect.asVoid),
|
||||
disconnect: tryFalkorDBPromise("disconnect", () => sdkClient.disconnect()).pipe(Effect.asVoid),
|
||||
};
|
||||
const sdkGraph = new Graph(sdkClient, database);
|
||||
const graph: FalkorDBStoreGraph = {
|
||||
query: (query, options) => tryFalkorDBPromise("graph-query", () => sdkGraph.query(query, options)),
|
||||
};
|
||||
return { client, graph };
|
||||
},
|
||||
catch: (cause) => falkorDBTriplesStoreError("create-client", cause),
|
||||
});
|
||||
|
||||
yield* Effect.tryPromise({
|
||||
try: () => client.connect(),
|
||||
catch: (cause) => falkorDBTriplesStoreError("connect", cause),
|
||||
}).pipe(
|
||||
yield* client.connect.pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logError("[FalkorDBTriplesStore] Connection failed", {
|
||||
error: error.message,
|
||||
|
|
@ -212,10 +234,7 @@ const connectFalkorDBTriplesStore = Effect.fn("FalkorDBTriplesStore.connect")(fu
|
|||
const disconnectFalkorDBTriplesStore = (
|
||||
connection: FalkorDBStoreConnection,
|
||||
): Effect.Effect<void> =>
|
||||
Effect.tryPromise({
|
||||
try: () => connection.client.disconnect(),
|
||||
catch: (cause) => falkorDBTriplesStoreError("disconnect", cause),
|
||||
}).pipe(
|
||||
connection.client.disconnect.pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.logError("[FalkorDBTriplesStore] Disconnect failed", {
|
||||
error: error.message,
|
||||
|
|
@ -239,10 +258,8 @@ const runGraphQuery = (
|
|||
query: string,
|
||||
options?: FalkorDBStoreQueryOptions,
|
||||
): Effect.Effect<void, FalkorDBTriplesStoreError> =>
|
||||
Effect.tryPromise({
|
||||
try: () => graph.query(query, options),
|
||||
catch: (cause) => falkorDBTriplesStoreError(operation, cause),
|
||||
}).pipe(
|
||||
graph.query(query, options).pipe(
|
||||
Effect.mapError((cause) => falkorDBTriplesStoreError(operation, cause)),
|
||||
Effect.asVoid,
|
||||
);
|
||||
|
||||
|
|
@ -390,17 +407,17 @@ const withFalkorDBTriplesStore = <A>(
|
|||
export function makeFalkorDBTriplesStore(config: FalkorDBConfig = {}): FalkorDBTriplesStore {
|
||||
return {
|
||||
createNode: (uri, user, collection) =>
|
||||
Effect.runPromise(withFalkorDBTriplesStore(config, (store) => store.createNode(uri, user, collection))),
|
||||
withFalkorDBTriplesStore(config, (store) => store.createNode(uri, user, collection)),
|
||||
createLiteral: (value, user, collection) =>
|
||||
Effect.runPromise(withFalkorDBTriplesStore(config, (store) => store.createLiteral(value, user, collection))),
|
||||
withFalkorDBTriplesStore(config, (store) => store.createLiteral(value, user, collection)),
|
||||
relateNode: (src, uri, dest, user, collection) =>
|
||||
Effect.runPromise(withFalkorDBTriplesStore(config, (store) => store.relateNode(src, uri, dest, user, collection))),
|
||||
withFalkorDBTriplesStore(config, (store) => store.relateNode(src, uri, dest, user, collection)),
|
||||
relateLiteral: (src, uri, dest, user, collection) =>
|
||||
Effect.runPromise(withFalkorDBTriplesStore(config, (store) => store.relateLiteral(src, uri, dest, user, collection))),
|
||||
withFalkorDBTriplesStore(config, (store) => store.relateLiteral(src, uri, dest, user, collection)),
|
||||
storeTriples: (triples, user = "default", collection = "default") =>
|
||||
Effect.runPromise(withFalkorDBTriplesStore(config, (store) => store.storeTriples(triples, user, collection))),
|
||||
withFalkorDBTriplesStore(config, (store) => store.storeTriples(triples, user, collection)),
|
||||
deleteCollection: (user, collection) =>
|
||||
Effect.runPromise(withFalkorDBTriplesStore(config, (store) => store.deleteCollection(user, collection))),
|
||||
withFalkorDBTriplesStore(config, (store) => store.deleteCollection(user, collection)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue