mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 01:19:38 +02:00
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:
parent
515fc0c264
commit
25d4227cb5
8 changed files with 147 additions and 98 deletions
|
|
@ -13,12 +13,14 @@
|
|||
"llm:openai": "tsx scripts/run-llm-openai.ts",
|
||||
"test:pipeline": "tsx scripts/test-pipeline.ts",
|
||||
"seed": "tsx scripts/seed-config.ts",
|
||||
"prompt": "tsx scripts/run-prompt.ts",
|
||||
"agent": "tsx scripts/run-agent.ts",
|
||||
"librarian": "tsx scripts/run-librarian.ts",
|
||||
"knowledge": "tsx scripts/run-knowledge.ts",
|
||||
"flow-manager": "tsx scripts/run-flow-manager.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nats": "^2.29.0",
|
||||
"tsx": "^4.21.0",
|
||||
"turbo": "^2.5.0",
|
||||
"typescript": "^5.8.0"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,11 +47,11 @@ export class AgentService extends FlowProcessor {
|
|||
|
||||
// Consumer: agent requests
|
||||
this.registerSpecification(
|
||||
new ConsumerSpec<AgentRequest>("request", this.onRequest.bind(this)),
|
||||
new ConsumerSpec<AgentRequest>("agent-request", this.onRequest.bind(this)),
|
||||
);
|
||||
|
||||
// Producer: agent responses (streaming chunks)
|
||||
this.registerSpecification(new ProducerSpec<AgentResponse>("response"));
|
||||
this.registerSpecification(new ProducerSpec<AgentResponse>("agent-response"));
|
||||
|
||||
// Request-response clients for tool execution
|
||||
this.registerSpecification(
|
||||
|
|
@ -94,7 +94,7 @@ export class AgentService extends FlowProcessor {
|
|||
const requestId = properties.id;
|
||||
if (!requestId) return;
|
||||
|
||||
const responseProducer = flowCtx.flow.producer<AgentResponse>("response");
|
||||
const responseProducer = flowCtx.flow.producer<AgentResponse>("agent-response");
|
||||
|
||||
try {
|
||||
// Build tools from flow requestors
|
||||
|
|
|
|||
|
|
@ -83,14 +83,14 @@ export class LibrarianService extends AsyncProcessor {
|
|||
while (this.running) {
|
||||
try {
|
||||
// Poll librarian requests
|
||||
const libMsg = await this.libConsumer.receive(500);
|
||||
const libMsg = await this.libConsumer.receive(2000);
|
||||
if (libMsg) {
|
||||
await this.handleLibrarianMessage(libMsg);
|
||||
await this.libConsumer.acknowledge(libMsg);
|
||||
}
|
||||
|
||||
// Poll collection management requests
|
||||
const colMsg = await this.colConsumer.receive(500);
|
||||
const colMsg = await this.colConsumer.receive(2000);
|
||||
if (colMsg) {
|
||||
await this.handleCollectionMessage(colMsg);
|
||||
await this.colConsumer.acknowledge(colMsg);
|
||||
|
|
|
|||
|
|
@ -54,11 +54,11 @@ export class PromptTemplateService extends FlowProcessor {
|
|||
|
||||
this.registerSpecification(
|
||||
new ConsumerSpec<PromptRequest>(
|
||||
"request",
|
||||
"prompt-request",
|
||||
this.onRequest.bind(this),
|
||||
),
|
||||
);
|
||||
this.registerSpecification(new ProducerSpec<PromptResponse>("response"));
|
||||
this.registerSpecification(new ProducerSpec<PromptResponse>("prompt-response"));
|
||||
|
||||
this.registerConfigHandler(this.onPromptConfig.bind(this));
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ export class PromptTemplateService extends FlowProcessor {
|
|||
const requestId = properties.id;
|
||||
if (!requestId) return;
|
||||
|
||||
const responseProducer = flowCtx.flow.producer<PromptResponse>("response");
|
||||
const responseProducer = flowCtx.flow.producer<PromptResponse>("prompt-response");
|
||||
|
||||
try {
|
||||
const template = this.templates.get(msg.name);
|
||||
|
|
|
|||
14
ts/scripts/run-prompt.ts
Normal file
14
ts/scripts/run-prompt.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Start the prompt template service.
|
||||
*
|
||||
* Usage: pnpm tsx scripts/run-prompt.ts
|
||||
*
|
||||
* Env:
|
||||
* NATS_URL (default: nats://localhost:4222)
|
||||
*/
|
||||
import { run } from "../packages/flow/src/prompt/template.js";
|
||||
|
||||
run().catch((err) => {
|
||||
console.error("Prompt service failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -96,8 +96,6 @@ async function main(): Promise<void> {
|
|||
default: {
|
||||
topics: {
|
||||
// LLM text completion
|
||||
"request": "tg.flow.text-completion-request",
|
||||
"response": "tg.flow.text-completion-response",
|
||||
"text-completion-request": "tg.flow.text-completion-request",
|
||||
"text-completion-response": "tg.flow.text-completion-response",
|
||||
// Prompt service
|
||||
|
|
@ -112,6 +110,9 @@ async function main(): Promise<void> {
|
|||
// Triples
|
||||
"triples-request": "tg.flow.triples-request",
|
||||
"triples-response": "tg.flow.triples-response",
|
||||
// Agent
|
||||
"agent-request": "tg.flow.agent-request",
|
||||
"agent-response": "tg.flow.agent-response",
|
||||
// Chunking pipeline
|
||||
"input": "tg.flow.chunk",
|
||||
"output": "tg.flow.chunk",
|
||||
|
|
|
|||
|
|
@ -127,15 +127,29 @@ async function testConfigDelete(): Promise<boolean> {
|
|||
|
||||
async function testPushFlowConfig(): Promise<boolean> {
|
||||
try {
|
||||
// Push a flow definition that LLM services will pick up
|
||||
// Push a full flow definition with all service topic mappings
|
||||
const res = await post("/api/v1/config", {
|
||||
operation: "put",
|
||||
keys: ["flows"],
|
||||
values: {
|
||||
default: {
|
||||
topics: {
|
||||
request: "tg.flow.text-completion-request",
|
||||
response: "tg.flow.text-completion-response",
|
||||
"text-completion-request": "tg.flow.text-completion-request",
|
||||
"text-completion-response": "tg.flow.text-completion-response",
|
||||
"prompt-request": "tg.flow.prompt-request",
|
||||
"prompt-response": "tg.flow.prompt-response",
|
||||
"graph-rag-request": "tg.flow.graph-rag-request",
|
||||
"graph-rag-response": "tg.flow.graph-rag-response",
|
||||
"document-rag-request": "tg.flow.document-rag-request",
|
||||
"document-rag-response": "tg.flow.document-rag-response",
|
||||
"triples-request": "tg.flow.triples-request",
|
||||
"triples-response": "tg.flow.triples-response",
|
||||
"agent-request": "tg.flow.agent-request",
|
||||
"agent-response": "tg.flow.agent-response",
|
||||
"input": "tg.flow.chunk",
|
||||
"output": "tg.flow.chunk",
|
||||
"triples": "tg.flow.triples",
|
||||
"entity-contexts": "tg.flow.entity-contexts",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue