Advance TS port Effect workbench

This commit is contained in:
elpresidank 2026-06-01 16:22:25 -05:00
parent 92dae8c374
commit 3515106670
116 changed files with 12286 additions and 9584 deletions

View file

@ -18,6 +18,7 @@ import type { EmbeddingsRequest, EmbeddingsResponse } from "../schema/messages.j
import { ConsumerSpec } from "../spec/consumer-spec.js";
import { ParameterSpec } from "../spec/parameter-spec.js";
import { ProducerSpec } from "../spec/producer-spec.js";
import type { Spec } from "../spec/types.js";
export interface EmbeddingsServiceShape {
readonly embed: (
@ -30,53 +31,55 @@ export class Embeddings extends Context.Service<Embeddings, EmbeddingsServiceSha
"@trustgraph/base/services/embeddings-service/Embeddings",
) {}
const onEmbeddingsRequest = Effect.fn("EmbeddingsService.onRequest")(function* (
msg: EmbeddingsRequest,
properties: Record<string, string>,
flowCtx: FlowContext<Embeddings>,
): Effect.fn.Return<void, FlowResourceNotFoundError | MessagingDeliveryError, Embeddings> {
const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) {
return;
}
const responseProducer = yield* flowCtx.flow.producerEffect<EmbeddingsResponse>("embeddings-response");
const embeddings = yield* Embeddings;
const response = yield* embeddings.embed(msg.text, msg.model).pipe(
Effect.map((vectors) => ({ vectors }) satisfies EmbeddingsResponse),
Effect.catch((error) =>
Effect.logError("[EmbeddingsService] Error processing request", {
error: errorMessage(error),
operation: error.operation,
provider: error.provider ?? "unknown",
}).pipe(
Effect.as({
vectors: [],
error: {
type: "embeddings-error",
message: errorMessage(error),
},
} satisfies EmbeddingsResponse),
),
),
);
yield* responseProducer.send(requestId, response);
});
export const makeEmbeddingsSpecs = (): ReadonlyArray<Spec<Embeddings>> => [
new ConsumerSpec<EmbeddingsRequest, FlowResourceNotFoundError | MessagingDeliveryError, Embeddings>(
"embeddings-request",
onEmbeddingsRequest,
),
new ProducerSpec<EmbeddingsResponse>("embeddings-response"),
new ParameterSpec("model"),
];
export class EmbeddingsService extends FlowProcessor<Embeddings> {
constructor(config: ProcessorConfig) {
super(config);
this.registerSpecification(
new ConsumerSpec<EmbeddingsRequest, FlowResourceNotFoundError | MessagingDeliveryError, Embeddings>(
"embeddings-request",
this.onRequestEffect.bind(this),
),
);
this.registerSpecification(new ProducerSpec<EmbeddingsResponse>("embeddings-response"));
this.registerSpecification(new ParameterSpec("model"));
}
private onRequestEffect(
msg: EmbeddingsRequest,
properties: Record<string, string>,
flowCtx: FlowContext<Embeddings>,
): Effect.Effect<void, FlowResourceNotFoundError | MessagingDeliveryError, Embeddings> {
const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) {
return Effect.void;
for (const spec of makeEmbeddingsSpecs()) {
this.registerSpecification(spec);
}
return Effect.gen(function* () {
const responseProducer = yield* flowCtx.flow.producerEffect<EmbeddingsResponse>("embeddings-response");
const embeddings = yield* Embeddings;
const response = yield* embeddings.embed(msg.text, msg.model).pipe(
Effect.map((vectors) => ({ vectors }) satisfies EmbeddingsResponse),
Effect.catch((error) =>
Effect.logError("[EmbeddingsService] Error processing request", {
error: errorMessage(error),
operation: error.operation,
provider: error.provider ?? "unknown",
}).pipe(
Effect.as({
vectors: [],
error: {
type: "embeddings-error",
message: errorMessage(error),
},
} satisfies EmbeddingsResponse),
),
),
);
yield* responseProducer.send(requestId, response);
});
}
}

View file

@ -1,6 +1,15 @@
export { LlmService } from "./llm-service.js";
export {
Llm,
LlmService,
LlmServiceError,
makeLlmServiceShape,
makeLlmSpecs,
type LlmProvider,
type LlmServiceShape,
} from "./llm-service.js";
export {
Embeddings,
EmbeddingsService,
makeEmbeddingsSpecs,
type EmbeddingsServiceShape,
} from "./embeddings-service.js";

View file

@ -1,125 +1,247 @@
/**
* Base LLM service handles message plumbing, subclasses implement the LLM call.
* Base LLM capability contract and message-bus adapter.
*
* Python reference: trustgraph-base/trustgraph/base/llm_service.py
*/
import {FlowProcessor} from "../processor/index.js";
import { Context, Effect } from "effect";
import * as S from "effect/Schema";
import {
ConsumerSpec, ProducerSpec,
ParameterSpec
} from "../spec/index.js";
import type {ProcessorConfig} from "../processor/index.js";
import type {FlowContext} from "../messaging/consumer.js";
errorMessage,
type FlowResourceNotFoundError,
type MessagingDeliveryError,
} from "../errors.js";
import type { FlowContext } from "../messaging/consumer.js";
import { FlowProcessor } from "../processor/flow-processor.js";
import type { ProcessorConfig } from "../processor/async-processor.js";
import type {
TextCompletionRequest,
TextCompletionResponse,
TextCompletionRequest,
TextCompletionResponse,
} from "../schema/messages.js";
import type {LlmResult, LlmChunk} from "../schema/index.js";
import type { LlmChunk, LlmResult } from "../schema/primitives.js";
import { ConsumerSpec } from "../spec/consumer-spec.js";
import { ParameterSpec } from "../spec/parameter-spec.js";
import { ProducerSpec } from "../spec/producer-spec.js";
import type { Spec } from "../spec/types.js";
export abstract class LlmService extends FlowProcessor {
protected constructor(config: ProcessorConfig) {
super(config);
export class LlmServiceError extends S.TaggedErrorClass<LlmServiceError>()(
"LlmServiceError",
{
message: S.String,
operation: S.String,
},
) {}
this.registerSpecification(
ConsumerSpec.fromPromise<TextCompletionRequest>(
"text-completion-request",
this.onRequest.bind(this),
),
);
this.registerSpecification(new ProducerSpec<TextCompletionResponse>("text-completion-response"));
this.registerSpecification(new ParameterSpec("model"));
this.registerSpecification(new ParameterSpec("temperature"));
}
private async onRequest(
msg: TextCompletionRequest,
properties: Record<string, string>,
flowCtx: FlowContext,
): Promise<void> {
const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return;
const responseProducer = flowCtx.flow.producer<TextCompletionResponse>("text-completion-response");
try {
if (msg.streaming === true && this.supportsStreaming()) {
for await (const chunk of this.generateContentStream(
msg.system,
msg.prompt,
msg.model,
msg.temperature,
)) {
const response = {
response: chunk.text,
...(chunk.model !== undefined ? { model: chunk.model } : {}),
...(chunk.inToken !== null ? { inToken: chunk.inToken } : {}),
...(chunk.outToken !== null ? { outToken: chunk.outToken } : {}),
endOfStream: chunk.isFinal,
};
await responseProducer.send(
requestId,
response
);
}
} else {
const result = await this.generateContent(
msg.system,
msg.prompt,
msg.model,
msg.temperature,
);
const response = {
response: result.text,
...(result.model !== undefined ? { model: result.model } : {}),
...(result.inToken !== undefined ? { inToken: result.inToken } : {}),
...(result.outToken !== undefined ? { outToken: result.outToken } : {}),
endOfStream: true,
};
await responseProducer.send(
requestId,
response
);
}
} catch (err) {
console.error(
`[LlmService] Error processing request:`,
err
);
const message = err instanceof Error
? err.message
: String(err);
await responseProducer.send(
requestId,
{
response: "",
error: {
type: "llm-error",
message
},
endOfStream: true,
}
);
}
}
abstract generateContent(
system: string,
prompt: string,
model?: string,
temperature?: number,
): Promise<LlmResult>;
abstract generateContentStream(
system: string,
prompt: string,
model?: string,
temperature?: number,
): AsyncGenerator<LlmChunk>;
supportsStreaming(): boolean {
return false;
}
export interface LlmProvider {
readonly generateContent: (
system: string,
prompt: string,
model?: string,
temperature?: number,
) => Promise<LlmResult>;
readonly generateContentStream: (
system: string,
prompt: string,
model?: string,
temperature?: number,
) => AsyncGenerator<LlmChunk>;
readonly supportsStreaming: () => boolean;
}
export interface LlmServiceShape {
readonly generateContent: (
system: string,
prompt: string,
model?: string,
temperature?: number,
) => Effect.Effect<LlmResult, LlmServiceError>;
readonly generateContentStream: (
system: string,
prompt: string,
model?: string,
temperature?: number,
) => AsyncGenerator<LlmChunk>;
readonly supportsStreaming: () => boolean;
}
export class Llm extends Context.Service<Llm, LlmServiceShape>()(
"@trustgraph/base/services/llm-service/Llm",
) {}
const llmServiceError = (operation: string, cause: unknown) =>
new LlmServiceError({
operation,
message: errorMessage(cause),
});
export const makeLlmServiceShape = (provider: LlmProvider): LlmServiceShape => ({
generateContent: Effect.fn("Llm.generateContent")((
system,
prompt,
model,
temperature,
) =>
Effect.tryPromise({
try: () => provider.generateContent(system, prompt, model, temperature),
catch: (cause) => llmServiceError("generate-content", cause),
}),
),
generateContentStream: (
system,
prompt,
model,
temperature,
) => provider.generateContentStream(system, prompt, model, temperature),
supportsStreaming: () => provider.supportsStreaming(),
});
type LlmHandlerError =
| FlowResourceNotFoundError
| MessagingDeliveryError;
const resultToResponse = (result: LlmResult): TextCompletionResponse => ({
response: result.text,
model: result.model,
inToken: result.inToken,
outToken: result.outToken,
endOfStream: true,
});
const chunkToResponse = (chunk: LlmChunk): TextCompletionResponse => ({
response: chunk.text,
model: chunk.model,
...(chunk.inToken !== null ? { inToken: chunk.inToken } : {}),
...(chunk.outToken !== null ? { outToken: chunk.outToken } : {}),
endOfStream: chunk.isFinal,
});
const llmErrorResponse = (error: LlmServiceError): TextCompletionResponse => ({
response: "",
error: {
type: "llm-error",
message: error.message,
},
endOfStream: true,
});
const sendStreamingResponse = Effect.fn("LlmService.sendStreamingResponse")(function* (
llm: LlmServiceShape,
requestId: string,
msg: TextCompletionRequest,
responseProducer: {
readonly send: (
id: string,
message: TextCompletionResponse,
) => Effect.Effect<void, MessagingDeliveryError>;
},
) {
const context = yield* Effect.context<never>();
yield* Effect.tryPromise({
try: async () => {
for await (const chunk of llm.generateContentStream(
msg.system,
msg.prompt,
msg.model,
msg.temperature,
)) {
await Effect.runPromiseWith(context)(
responseProducer.send(requestId, chunkToResponse(chunk)),
);
}
},
catch: (cause) => llmServiceError("generate-content-stream", cause),
});
});
const onLlmRequest = Effect.fn("LlmService.onRequest")(function* (
msg: TextCompletionRequest,
properties: Record<string, string>,
flowCtx: FlowContext<Llm>,
): Effect.fn.Return<void, LlmHandlerError, Llm> {
const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return;
const responseProducer = yield* flowCtx.flow.producerEffect<TextCompletionResponse>(
"text-completion-response",
);
const llm = yield* Llm;
if (msg.streaming === true && llm.supportsStreaming()) {
yield* sendStreamingResponse(llm, requestId, msg, responseProducer).pipe(
Effect.catch((error) =>
Effect.logError("[LlmService] Error processing streaming request", {
error: error.message,
operation: error.operation,
}).pipe(
Effect.flatMap(() =>
responseProducer.send(requestId, llmErrorResponse(error)),
),
),
),
);
return;
}
const response = yield* llm.generateContent(
msg.system,
msg.prompt,
msg.model,
msg.temperature,
).pipe(
Effect.map(resultToResponse),
Effect.catch((error) =>
Effect.logError("[LlmService] Error processing request", {
error: error.message,
operation: error.operation,
}).pipe(
Effect.as(llmErrorResponse(error)),
),
),
);
yield* responseProducer.send(requestId, response);
});
export const makeLlmSpecs = (): ReadonlyArray<Spec<Llm>> => [
new ConsumerSpec<TextCompletionRequest, LlmHandlerError, Llm>(
"text-completion-request",
onLlmRequest,
),
new ProducerSpec<TextCompletionResponse>("text-completion-response"),
new ParameterSpec("model"),
new ParameterSpec("temperature"),
];
export abstract class LlmService extends FlowProcessor<Llm> implements LlmProvider {
protected constructor(config: ProcessorConfig) {
super(config);
for (const spec of makeLlmSpecs()) {
this.registerSpecification(spec);
}
}
override startEffect() {
return super.startEffect().pipe(
Effect.provideService(Llm, Llm.of(makeLlmServiceShape(this))),
);
}
abstract generateContent(
system: string,
prompt: string,
model?: string,
temperature?: number,
): Promise<LlmResult>;
abstract generateContentStream(
system: string,
prompt: string,
model?: string,
temperature?: number,
): AsyncGenerator<LlmChunk>;
supportsStreaming(): boolean {
return false;
}
}