This commit is contained in:
elpresidank 2026-04-05 22:44:45 -05:00
parent c386f68743
commit b6536eca38
100 changed files with 17680 additions and 377 deletions

View file

@ -1,14 +1,70 @@
/**
* Dispatcher manager routes requests to backend services via pub/sub.
*
* Maintains a service registry mapping service names to NATS topic pairs.
* Applies wire format translation on requests (client internal) and
* reverse translation on responses (internal client).
*
* Python reference: trustgraph-flow/trustgraph/gateway/dispatch/manager.py
*/
import { NatsBackend, RequestResponse, type PubSubBackend } from "@trustgraph/base";
import type { GatewayConfig } from "../server.js";
import { translateRequest, translateResponse } from "./serialize.js";
export type Responder = (response: unknown, complete: boolean) => Promise<void>;
// ---------- Service registry ----------
/**
* Flow-scoped request/response services.
* These are resolved within a specific flow's interface definitions.
* Topic pattern: tg.flow.<name>-request / tg.flow.<name>-response
*/
const FLOW_SERVICES: ReadonlyMap<string, { request: string; response: string }> = new Map([
["agent", { request: "agent-request", response: "agent-response" }],
["text-completion", { request: "text-completion-request", response: "text-completion-response" }],
["prompt", { request: "prompt-request", response: "prompt-response" }],
["graph-rag", { request: "graph-rag-request", response: "graph-rag-response" }],
["document-rag", { request: "document-rag-request", response: "document-rag-response" }],
["embeddings", { request: "embeddings-request", response: "embeddings-response" }],
["graph-embeddings", { request: "graph-embeddings-request", response: "graph-embeddings-response" }],
["document-embeddings", { request: "doc-embeddings-request", response: "doc-embeddings-response" }],
["triples", { request: "triples-request", response: "triples-response" }],
]);
/**
* Global services (not flow-scoped).
* These always use fixed topics regardless of which flow is active.
*/
const GLOBAL_SERVICES: ReadonlyMap<string, { request: string; response: string }> = new Map([
["config", { request: "config-request", response: "config-response" }],
["flow", { request: "flow-request", response: "flow-response" }],
["librarian", { request: "librarian-request", response: "librarian-response" }],
["knowledge", { request: "knowledge-request", response: "knowledge-response" }],
["collection-management", { request: "collection-management-request", response: "collection-management-response" }],
]);
/**
* Services that support streaming responses (multiple messages per request).
* The completion flag is determined by checking for end-of-stream markers.
*/
const STREAMING_SERVICES = new Set([
"agent",
"text-completion",
"graph-rag",
"document-rag",
"triples",
"knowledge",
"librarian",
]);
function topicName(name: string): string {
return `tg.flow.${name}`;
}
// ---------- Manager ----------
export class DispatcherManager {
private pubsub: PubSubBackend;
private requestors = new Map<string, RequestResponse<unknown, unknown>>();
@ -18,8 +74,7 @@ export class DispatcherManager {
}
async start(): Promise<void> {
// Pre-create requestors for known global services
// Flow-specific requestors are created on demand
// Requestors are created on demand when first accessed
}
async stop(): Promise<void> {
@ -29,6 +84,8 @@ export class DispatcherManager {
await this.pubsub.close();
}
// ---------- Internal helpers ----------
private async getRequestor(
requestTopic: string,
responseTopic: string,
@ -48,25 +105,71 @@ export class DispatcherManager {
return rr;
}
private resolveGlobalTopics(
kind: string,
): { requestTopic: string; responseTopic: string } {
const entry = GLOBAL_SERVICES.get(kind);
if (entry) {
return {
requestTopic: topicName(entry.request),
responseTopic: topicName(entry.response),
};
}
// Fallback: derive from kind name directly
return {
requestTopic: topicName(`${kind}-request`),
responseTopic: topicName(`${kind}-response`),
};
}
private resolveFlowTopics(
kind: string,
): { requestTopic: string; responseTopic: string } {
const entry = FLOW_SERVICES.get(kind);
if (entry) {
return {
requestTopic: topicName(entry.request),
responseTopic: topicName(entry.response),
};
}
// Fallback: derive from kind name directly
return {
requestTopic: topicName(`${kind}-request`),
responseTopic: topicName(`${kind}-response`),
};
}
/**
* Determine whether a response is the final one in a streaming sequence.
* Checks for various end-of-stream markers used by different services.
*/
private isComplete(response: unknown): boolean {
if (typeof response !== "object" || response === null) return true;
const res = response as Record<string, unknown>;
return (
!!res.complete ||
!!res.endOfStream ||
!!res.endOfSession ||
!!res.end_of_stream ||
!!res.end_of_session ||
!!res.eos ||
// error responses are always final
!!res.error
);
}
// ---------- Global service dispatch ----------
async dispatchGlobalService(
kind: string,
request: Record<string, unknown>,
): Promise<unknown> {
const requestTopic = `tg.flow.${kind}-request`;
const responseTopic = `tg.flow.${kind}-response`;
const { requestTopic, responseTopic } = this.resolveGlobalTopics(kind);
const rr = await this.getRequestor(requestTopic, responseTopic, `global:${kind}`);
return rr.request(request);
}
async dispatchFlowService(
flow: string,
kind: string,
request: Record<string, unknown>,
): Promise<unknown> {
const requestTopic = `tg.flow.${kind}-request`;
const responseTopic = `tg.flow.${kind}-response`;
const rr = await this.getRequestor(requestTopic, responseTopic, `flow:${flow}:${kind}`);
return rr.request(request);
const translated = translateRequest(kind, request);
const response = await rr.request(translated);
return translateResponse(kind, response);
}
async dispatchGlobalServiceStreaming(
@ -74,37 +177,74 @@ export class DispatcherManager {
request: Record<string, unknown>,
responder: Responder,
): Promise<void> {
const requestTopic = `tg.flow.${kind}-request`;
const responseTopic = `tg.flow.${kind}-response`;
const { requestTopic, responseTopic } = this.resolveGlobalTopics(kind);
const rr = await this.getRequestor(requestTopic, responseTopic, `global:${kind}`);
const translated = translateRequest(kind, request);
await rr.request(request, {
await rr.request(translated, {
recipient: async (response) => {
const res = response as Record<string, unknown>;
const complete = !!res.complete || !!res.endOfStream || !!res.endOfSession;
await responder(res, complete);
const translatedRes = translateResponse(kind, response);
const complete = this.isComplete(translatedRes);
await responder(translatedRes, complete);
return complete;
},
});
}
// ---------- Flow-scoped service dispatch ----------
async dispatchFlowService(
flow: string,
kind: string,
request: Record<string, unknown>,
): Promise<unknown> {
const { requestTopic, responseTopic } = this.resolveFlowTopics(kind);
const rr = await this.getRequestor(
requestTopic,
responseTopic,
`flow:${flow}:${kind}`,
);
const translated = translateRequest(kind, request);
const response = await rr.request(translated);
return translateResponse(kind, response);
}
async dispatchFlowServiceStreaming(
flow: string,
kind: string,
request: Record<string, unknown>,
responder: Responder,
): Promise<void> {
const requestTopic = `tg.flow.${kind}-request`;
const responseTopic = `tg.flow.${kind}-response`;
const rr = await this.getRequestor(requestTopic, responseTopic, `flow:${flow}:${kind}`);
const { requestTopic, responseTopic } = this.resolveFlowTopics(kind);
const rr = await this.getRequestor(
requestTopic,
responseTopic,
`flow:${flow}:${kind}`,
);
const translated = translateRequest(kind, request);
await rr.request(request, {
await rr.request(translated, {
recipient: async (response) => {
const res = response as Record<string, unknown>;
const complete = !!res.complete || !!res.endOfStream || !!res.endOfSession;
await responder(res, complete);
const translatedRes = translateResponse(kind, response);
const complete = this.isComplete(translatedRes);
await responder(translatedRes, complete);
return complete;
},
});
}
// ---------- Static introspection ----------
static get flowServiceNames(): readonly string[] {
return [...FLOW_SERVICES.keys()];
}
static get globalServiceNames(): readonly string[] {
return [...GLOBAL_SERVICES.keys()];
}
static isStreamingService(kind: string): boolean {
return STREAMING_SERVICES.has(kind);
}
}