fix: resolve FlowProcessor topic collisions, librarian timeout, tests

Fix critical bug where all FlowProcessor services shared the same spec
names ("request"/"response"), causing them to steal each other's NATS
topics. Now each service uses unique spec names matching the flow config
topic keys (e.g., "text-completion-request", "prompt-request",
"agent-request").

Fix librarian NATS consumer timeout (500ms → 2000ms, below NATS minimum).

Update seed-config and test-pipeline with correct flow topic mappings.
Add prompt template runner script.

Smoke test results: 11/11 passing (config CRUD, WebSocket, LLM,
librarian CRUD). Agent routing verified via manual curl test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
elpresidank 2026-04-06 01:02:10 -05:00
parent 515fc0c264
commit 25d4227cb5
8 changed files with 147 additions and 98 deletions

View file

@ -4,102 +4,120 @@
* Python reference: trustgraph-base/trustgraph/base/llm_service.py
*/
import { FlowProcessor } from "../processor/flow-processor.js";
import { ConsumerSpec } from "../spec/consumer-spec.js";
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 {FlowProcessor} from "../processor/index.js";
import {
ConsumerSpec, ProducerSpec,
ParameterSpec
} from "../spec/index.js";
import type {ProcessorConfig} from "../processor/index.js";
import type {FlowContext} from "../messaging/consumer.js";
import type {
TextCompletionRequest,
TextCompletionResponse,
TextCompletionRequest,
TextCompletionResponse,
} from "../schema/messages.js";
import type { LlmResult, LlmChunk } from "../schema/primitives.js";
import type {LlmResult, LlmChunk} from "../schema/index.js";
export abstract class LlmService extends FlowProcessor {
constructor(config: ProcessorConfig) {
super(config);
protected constructor(config: ProcessorConfig) {
super(config);
this.registerSpecification(
new ConsumerSpec<TextCompletionRequest>(
"request",
this.onRequest.bind(this),
),
);
this.registerSpecification(new ProducerSpec<TextCompletionResponse>("response"));
this.registerSpecification(new ParameterSpec("model"));
this.registerSpecification(new ParameterSpec("temperature"));
}
this.registerSpecification(
new ConsumerSpec<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) return;
private async onRequest(
msg: TextCompletionRequest,
properties: Record<string, string>,
flowCtx: FlowContext,
): Promise<void> {
const requestId = properties.id;
if (!requestId) return;
const responseProducer = flowCtx.flow.producer<TextCompletionResponse>("response");
const responseProducer = flowCtx.flow.producer<TextCompletionResponse>("text-completion-response");
try {
if (msg.streaming && this.supportsStreaming()) {
for await (const chunk of this.generateContentStream(
msg.system,
msg.prompt,
msg.model,
msg.temperature,
)) {
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(
msg.system,
msg.prompt,
msg.model,
msg.temperature,
);
try {
if (msg.streaming && this.supportsStreaming()) {
for await (const chunk of this.generateContentStream(
msg.system,
msg.prompt,
msg.model,
msg.temperature,
)) {
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(
msg.system,
msg.prompt,
msg.model,
msg.temperature,
);
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);
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,
});
}
}
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 generateContent(
system: string,
prompt: string,
model?: string,
temperature?: number,
): Promise<LlmResult>;
abstract generateContentStream(
system: string,
prompt: string,
model?: string,
temperature?: number,
): AsyncGenerator<LlmChunk>;
abstract generateContentStream(
system: string,
prompt: string,
model?: string,
temperature?: number,
): AsyncGenerator<LlmChunk>;
supportsStreaming(): boolean {
return false;
}
supportsStreaming(): boolean {
return false;
}
}