mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 09:29:38 +02:00
init
This commit is contained in:
parent
c386f68743
commit
b6536eca38
100 changed files with 17680 additions and 377 deletions
|
|
@ -15,6 +15,7 @@
|
|||
"prom-client": "^15.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^3.1.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ import {
|
|||
type NatsConnection,
|
||||
type JetStreamClient,
|
||||
type JetStreamManager,
|
||||
type ConsumerMessages,
|
||||
type Consumer as NatsJsConsumer,
|
||||
type JsMsg,
|
||||
StringCodec,
|
||||
AckPolicy,
|
||||
DeliverPolicy,
|
||||
} from "nats";
|
||||
|
||||
import type {
|
||||
|
|
@ -31,17 +32,22 @@ import type {
|
|||
const sc = StringCodec();
|
||||
|
||||
class NatsMessage<T> implements Message<T> {
|
||||
/** Exposed so acknowledge/negativeAcknowledge can access the raw JsMsg */
|
||||
readonly _jsMsg: JsMsg;
|
||||
|
||||
constructor(
|
||||
private readonly msg: JsMsg,
|
||||
msg: JsMsg,
|
||||
private readonly decoded: T,
|
||||
) {}
|
||||
) {
|
||||
this._jsMsg = msg;
|
||||
}
|
||||
|
||||
value(): T {
|
||||
return this.decoded;
|
||||
}
|
||||
|
||||
properties(): Record<string, string> {
|
||||
const headers = this.msg.headers;
|
||||
const headers = this._jsMsg.headers;
|
||||
const props: Record<string, string> = {};
|
||||
if (headers) {
|
||||
for (const [key, values] of headers) {
|
||||
|
|
@ -84,7 +90,7 @@ class NatsProducer<T> implements BackendProducer<T> {
|
|||
}
|
||||
|
||||
class NatsConsumer<T> implements BackendConsumer<T> {
|
||||
private messages: ConsumerMessages | null = null;
|
||||
private consumer: NatsJsConsumer | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly js: JetStreamClient,
|
||||
|
|
@ -106,43 +112,57 @@ class NatsConsumer<T> implements BackendConsumer<T> {
|
|||
});
|
||||
}
|
||||
|
||||
// Create or bind to durable consumer
|
||||
const consumer = await this.js.consumers.get(streamName, this.subscription);
|
||||
this.messages = await consumer.consume();
|
||||
// Create or bind to durable consumer.
|
||||
// Try to get an existing durable consumer first; if it doesn't exist, create it.
|
||||
try {
|
||||
this.consumer = await this.js.consumers.get(streamName, this.subscription);
|
||||
} catch {
|
||||
const deliverPolicy =
|
||||
this.initialPosition === "earliest"
|
||||
? DeliverPolicy.All
|
||||
: DeliverPolicy.New;
|
||||
|
||||
await this.jsm.consumers.add(streamName, {
|
||||
durable_name: this.subscription,
|
||||
ack_policy: AckPolicy.Explicit,
|
||||
deliver_policy: deliverPolicy,
|
||||
filter_subject: this.subject,
|
||||
});
|
||||
|
||||
this.consumer = await this.js.consumers.get(streamName, this.subscription);
|
||||
}
|
||||
}
|
||||
|
||||
async receive(timeoutMs = 2000): Promise<Message<T> | null> {
|
||||
if (!this.messages) throw new Error("Consumer not initialized");
|
||||
if (!this.consumer) throw new Error("Consumer not initialized");
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
for await (const msg of this.messages) {
|
||||
const decoded = JSON.parse(sc.decode(msg.data)) as T;
|
||||
return new NatsMessage(msg, decoded);
|
||||
}
|
||||
// Pull a single message with a timeout using the pull-based API.
|
||||
// consumer.next() returns a JsMsg or null when the timeout expires.
|
||||
const msg = await this.consumer.next({ expires: timeoutMs });
|
||||
if (!msg) return null;
|
||||
|
||||
if (Date.now() >= deadline) return null;
|
||||
return null;
|
||||
const decoded = JSON.parse(sc.decode(msg.data)) as T;
|
||||
return new NatsMessage(msg, decoded);
|
||||
}
|
||||
|
||||
async acknowledge(message: Message<T>): Promise<void> {
|
||||
const natsMsg = message as NatsMessage<T>;
|
||||
// Access internal JsMsg for ack — in practice we'd store the ref
|
||||
// This is a simplified version; real impl tracks msg refs
|
||||
void natsMsg;
|
||||
natsMsg._jsMsg.ack();
|
||||
}
|
||||
|
||||
async negativeAcknowledge(message: Message<T>): Promise<void> {
|
||||
void message;
|
||||
const natsMsg = message as NatsMessage<T>;
|
||||
natsMsg._jsMsg.nak();
|
||||
}
|
||||
|
||||
async unsubscribe(): Promise<void> {
|
||||
// Drain and close consumer
|
||||
// The pull-based consumer does not have a persistent subscription to drain.
|
||||
// Clearing the reference is sufficient; the durable consumer persists server-side.
|
||||
this.consumer = null;
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.messages) {
|
||||
this.messages.stop();
|
||||
}
|
||||
this.consumer = null;
|
||||
}
|
||||
|
||||
private streamNameFromSubject(subject: string): string {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import type { PubSubBackend, BackendConsumer, Message } from "../backend/types.js";
|
||||
import type { Flow } from "../processor/flow.js";
|
||||
import { TooManyRequestsError } from "../errors.js";
|
||||
|
||||
export type MessageHandler<T> = (
|
||||
|
|
@ -16,6 +17,8 @@ export type MessageHandler<T> = (
|
|||
export interface FlowContext {
|
||||
id: string;
|
||||
name: string;
|
||||
/** Reference to the owning Flow instance, giving handlers access to producers and parameters. */
|
||||
flow: Flow;
|
||||
}
|
||||
|
||||
export interface ConsumerOptions<T> {
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ export class Flow {
|
|||
await spec.add(this, this.pubsub, this.definition);
|
||||
}
|
||||
|
||||
// Start all consumers
|
||||
// Start all consumers, passing this Flow instance via FlowContext
|
||||
for (const consumer of this.consumers.values()) {
|
||||
consumer.start({ id: this.processorId, name: this.name }).catch((err) => {
|
||||
consumer.start({ id: this.processorId, name: this.name, flow: this }).catch((err) => {
|
||||
console.error(`[Flow:${this.name}] Consumer error:`, err);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,16 +29,24 @@ export abstract class EmbeddingsService extends FlowProcessor {
|
|||
private async onRequest(
|
||||
msg: EmbeddingsRequest,
|
||||
properties: Record<string, string>,
|
||||
_flowCtx: FlowContext,
|
||||
flowCtx: FlowContext,
|
||||
): Promise<void> {
|
||||
const requestId = properties.id;
|
||||
if (!requestId) return;
|
||||
|
||||
const responseProducer = flowCtx.flow.producer<EmbeddingsResponse>("response");
|
||||
|
||||
try {
|
||||
const vectors = await this.onEmbeddings(msg.text, msg.model);
|
||||
void vectors; // Producer send would go here
|
||||
await responseProducer.send(requestId, { vectors });
|
||||
} catch (err) {
|
||||
console.error(`[EmbeddingsService] Error processing request:`, err);
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await responseProducer.send(requestId, {
|
||||
vectors: [],
|
||||
error: { type: "embeddings-error", message },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { ProducerSpec } from "../spec/producer-spec.js";
|
|||
import { ParameterSpec } from "../spec/parameter-spec.js";
|
||||
import type { ProcessorConfig } from "../processor/async-processor.js";
|
||||
import type { FlowContext } from "../messaging/consumer.js";
|
||||
import type { Flow } from "../processor/flow.js";
|
||||
import type {
|
||||
TextCompletionRequest,
|
||||
TextCompletionResponse,
|
||||
|
|
@ -37,12 +36,11 @@ export abstract class LlmService extends FlowProcessor {
|
|||
properties: Record<string, string>,
|
||||
flowCtx: FlowContext,
|
||||
): Promise<void> {
|
||||
// We need the actual flow instance to access producers/parameters.
|
||||
// In the full implementation, FlowContext would carry a flow reference.
|
||||
// For now this shows the pattern.
|
||||
const requestId = properties.id;
|
||||
if (!requestId) return;
|
||||
|
||||
const responseProducer = flowCtx.flow.producer<TextCompletionResponse>("response");
|
||||
|
||||
try {
|
||||
if (msg.streaming && this.supportsStreaming()) {
|
||||
for await (const chunk of this.generateContentStream(
|
||||
|
|
@ -51,8 +49,13 @@ export abstract class LlmService extends FlowProcessor {
|
|||
msg.model,
|
||||
msg.temperature,
|
||||
)) {
|
||||
// Send each chunk as a response with the same request ID
|
||||
void chunk; // Producer send would go here
|
||||
await responseProducer.send(requestId, {
|
||||
response: chunk.text,
|
||||
model: chunk.model,
|
||||
inToken: chunk.inToken ?? undefined,
|
||||
outToken: chunk.outToken ?? undefined,
|
||||
endOfStream: chunk.isFinal,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const result = await this.generateContent(
|
||||
|
|
@ -61,10 +64,24 @@ export abstract class LlmService extends FlowProcessor {
|
|||
msg.model,
|
||||
msg.temperature,
|
||||
);
|
||||
void result; // Producer send would go here
|
||||
|
||||
await responseProducer.send(requestId, {
|
||||
response: result.text,
|
||||
model: result.model,
|
||||
inToken: result.inToken,
|
||||
outToken: result.outToken,
|
||||
endOfStream: true,
|
||||
});
|
||||
}
|
||||
} 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
"rootDir": "src",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue