This commit is contained in:
elpresidank 2026-05-12 08:06:58 -05:00
parent e8c7a4f6e0
commit ffd97375a8
160 changed files with 6704 additions and 1895 deletions

View file

@ -27,6 +27,7 @@ import {
type PromptRequest,
type PromptResponse,
} from "@trustgraph/base";
import { makeProcessorProgram } from "@trustgraph/base";
import { DocumentRag } from "./document-rag.js";
export class DocumentRagService extends FlowProcessor {
@ -35,7 +36,7 @@ export class DocumentRagService extends FlowProcessor {
// Consumer: document RAG requests
this.registerSpecification(
new ConsumerSpec<DocumentRagRequest>("document-rag-request", this.onRequest.bind(this)),
ConsumerSpec.fromPromise<DocumentRagRequest>("document-rag-request", this.onRequest.bind(this)),
);
// Producer: document RAG responses
@ -80,7 +81,7 @@ export class DocumentRagService extends FlowProcessor {
flowCtx: FlowContext,
): Promise<void> {
const requestId = properties.id;
if (!requestId) return;
if (requestId === undefined || requestId.length === 0) return;
const producer = flowCtx.flow.producer<DocumentRagResponse>("document-rag-response");
@ -93,7 +94,7 @@ export class DocumentRagService extends FlowProcessor {
});
const response = await documentRag.query(msg.query, {
collection: msg.collection,
...(msg.collection !== undefined ? { collection: msg.collection } : {}),
});
await producer.send(requestId, { response, endOfStream: true });
@ -107,6 +108,11 @@ export class DocumentRagService extends FlowProcessor {
}
}
export const program = makeProcessorProgram({
id: "document-rag",
make: (config) => new DocumentRagService(config),
});
export async function run(): Promise<void> {
await DocumentRagService.launch("document-rag");
}

View file

@ -8,7 +8,7 @@
*/
import type {
RequestResponse,
FlowRequestor,
TextCompletionRequest,
TextCompletionResponse,
EmbeddingsRequest,
@ -20,16 +20,20 @@ import type {
} from "@trustgraph/base";
export interface DocumentRagClients {
llm: RequestResponse<TextCompletionRequest, TextCompletionResponse>;
embeddings: RequestResponse<EmbeddingsRequest, EmbeddingsResponse>;
docEmbeddings: RequestResponse<DocumentEmbeddingsRequest, DocumentEmbeddingsResponse>;
prompt: RequestResponse<PromptRequest, PromptResponse>;
llm: FlowRequestor<TextCompletionRequest, TextCompletionResponse>;
embeddings: FlowRequestor<EmbeddingsRequest, EmbeddingsResponse>;
docEmbeddings: FlowRequestor<DocumentEmbeddingsRequest, DocumentEmbeddingsResponse>;
prompt: FlowRequestor<PromptRequest, PromptResponse>;
}
export type ChunkCallback = (text: string, endOfStream: boolean) => Promise<void>;
export class DocumentRag {
constructor(private readonly clients: DocumentRagClients) {}
private readonly clients: DocumentRagClients;
constructor(clients: DocumentRagClients) {
this.clients = clients;
}
async query(
queryText: string,
@ -57,8 +61,9 @@ export class DocumentRag {
// Step 3: Build context from chunks
const context = chunks
.filter((c) => c.content)
.map((c) => c.content)
.flatMap((c) =>
c.content !== undefined && c.content.length > 0 ? [c.content] : [],
)
.join("\n\n---\n\n");
// Step 4: Synthesize answer

View file

@ -31,6 +31,7 @@ import {
type PromptRequest,
type PromptResponse,
} from "@trustgraph/base";
import { makeProcessorProgram } from "@trustgraph/base";
import { GraphRag } from "./graph-rag.js";
export class GraphRagService extends FlowProcessor {
@ -39,7 +40,7 @@ export class GraphRagService extends FlowProcessor {
// Consumer: graph RAG requests
this.registerSpecification(
new ConsumerSpec<GraphRagRequest>("graph-rag-request", this.onRequest.bind(this)),
ConsumerSpec.fromPromise<GraphRagRequest>("graph-rag-request", this.onRequest.bind(this)),
);
// Producer: graph RAG responses
@ -91,7 +92,7 @@ export class GraphRagService extends FlowProcessor {
flowCtx: FlowContext,
): Promise<void> {
const requestId = properties.id;
if (!requestId) return;
if (requestId === undefined || requestId.length === 0) return;
const producer = flowCtx.flow.producer<GraphRagResponse>("graph-rag-response");
console.log(`[GraphRagService] Received request ${requestId}: "${msg.query?.slice(0, 60)}..." collection=${msg.collection}`);
@ -107,15 +108,17 @@ export class GraphRagService extends FlowProcessor {
prompt: flowCtx.flow.requestor<PromptRequest, PromptResponse>("prompt"),
},
{
entityLimit: msg.entityLimit,
tripleLimit: msg.tripleLimit,
maxSubgraphSize: msg.maxSubgraphSize,
maxPathLength: msg.maxPathLength,
...(msg.entityLimit !== undefined ? { entityLimit: msg.entityLimit } : {}),
...(msg.tripleLimit !== undefined ? { tripleLimit: msg.tripleLimit } : {}),
...(msg.maxSubgraphSize !== undefined
? { maxSubgraphSize: msg.maxSubgraphSize }
: {}),
...(msg.maxPathLength !== undefined ? { maxPathLength: msg.maxPathLength } : {}),
},
);
const result = await graphRag.query(msg.query, {
collection: msg.collection,
...(msg.collection !== undefined ? { collection: msg.collection } : {}),
});
// Send answer with explain data embedded in a SINGLE message.
@ -145,6 +148,11 @@ export class GraphRagService extends FlowProcessor {
}
}
export const program = makeProcessorProgram({
id: "graph-rag",
make: (config) => new GraphRagService(config),
});
export async function run(): Promise<void> {
await GraphRagService.launch("graph-rag");
}

View file

@ -16,9 +16,9 @@ import type {
EmbeddingsResponse,
GraphEmbeddingsRequest,
GraphEmbeddingsResponse,
FlowRequestor,
PromptRequest,
PromptResponse,
RequestResponse,
Term,
TextCompletionRequest,
TextCompletionResponse,
@ -37,11 +37,11 @@ export interface GraphRagConfig {
}
export interface GraphRagClients {
llm: RequestResponse<TextCompletionRequest, TextCompletionResponse>;
embeddings: RequestResponse<EmbeddingsRequest, EmbeddingsResponse>;
graphEmbeddings: RequestResponse<GraphEmbeddingsRequest, GraphEmbeddingsResponse>;
triples: RequestResponse<TriplesQueryRequest, TriplesQueryResponse>;
prompt: RequestResponse<PromptRequest, PromptResponse>;
llm: FlowRequestor<TextCompletionRequest, TextCompletionResponse>;
embeddings: FlowRequestor<EmbeddingsRequest, EmbeddingsResponse>;
graphEmbeddings: FlowRequestor<GraphEmbeddingsRequest, GraphEmbeddingsResponse>;
triples: FlowRequestor<TriplesQueryRequest, TriplesQueryResponse>;
prompt: FlowRequestor<PromptRequest, PromptResponse>;
}
export type ChunkCallback = (text: string, endOfStream: boolean) => Promise<void>;
@ -52,12 +52,14 @@ export interface GraphRagResult {
}
export class GraphRag {
private readonly clients: GraphRagClients;
private config: Required<GraphRagConfig>;
constructor(
private readonly clients: GraphRagClients,
clients: GraphRagClients,
config: GraphRagConfig = {},
) {
this.clients = clients;
this.config = {
entityLimit: config.entityLimit ?? 50,
tripleLimit: config.tripleLimit ?? 30,
@ -125,7 +127,7 @@ export class GraphRag {
return (llmResp as TextCompletionResponse).response
.split("\n")
.map((c) => c.trim())
.filter(Boolean);
.filter((c) => c.length > 0);
}
private async getVectors(concepts: string[]): Promise<number[][]> {
@ -166,11 +168,12 @@ export class GraphRag {
// Query each entity as subject to get outgoing edges
const queries = unvisited.map((entityStr) => {
const term = stringToTerm(entityStr);
return this.clients.triples.request({
const request: TriplesQueryRequest = {
s: term,
collection,
limit: this.config.tripleLimit,
});
...(collection !== undefined ? { collection } : {}),
};
return this.clients.triples.request(request);
});
const results = await Promise.all(queries);
@ -257,7 +260,12 @@ export class GraphRag {
const parsed = JSON.parse(responseText) as Array<{ id: string; score: number }>;
if (Array.isArray(parsed)) {
for (const item of parsed) {
if (item && typeof item.id === "string" && typeof item.score === "number") {
if (
typeof item === "object" &&
item !== null &&
typeof item.id === "string" &&
typeof item.score === "number"
) {
scored.push({ id: item.id, score: item.score });
}
}
@ -266,10 +274,15 @@ export class GraphRag {
// Fall back to parsing line-by-line JSON objects
for (const line of responseText.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
if (trimmed.length === 0) continue;
try {
const obj = JSON.parse(trimmed) as { id?: string; score?: number };
if (obj && typeof obj.id === "string" && typeof obj.score === "number") {
if (
typeof obj === "object" &&
obj !== null &&
typeof obj.id === "string" &&
typeof obj.score === "number"
) {
scored.push({ id: obj.id, score: obj.score });
}
} catch {
@ -281,8 +294,6 @@ export class GraphRag {
// Sort by score descending and keep top N
scored.sort((a, b) => b.score - a.score);
const topN = scored.slice(0, this.config.edgeLimit);
const selectedIds = new Set(topN.map((e) => e.id));
// Map back to triples
const result: Triple[] = [];
for (const entry of topN) {
@ -317,7 +328,7 @@ export class GraphRag {
variables: { query, context },
});
if (chunkCallback) {
if (chunkCallback !== undefined) {
// Streaming response
let fullText = "";
await this.clients.llm.request(
@ -329,11 +340,11 @@ export class GraphRag {
{
recipient: async (resp) => {
const r = resp as TextCompletionResponse;
if (r.response) {
if (r.response.length > 0) {
fullText += r.response;
await chunkCallback(r.response, !!r.endOfStream);
await chunkCallback(r.response, r.endOfStream === true);
}
return !!r.endOfStream;
return r.endOfStream === true;
},
},
);