mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-03 15:01:00 +02:00
saving
This commit is contained in:
parent
e8c7a4f6e0
commit
ffd97375a8
160 changed files with 6704 additions and 1895 deletions
|
|
@ -22,6 +22,7 @@ import {
|
|||
type ToolRequest,
|
||||
type ToolResponse,
|
||||
} from "@trustgraph/base";
|
||||
import { makeProcessorProgram } from "@trustgraph/base";
|
||||
|
||||
interface McpServiceConfig {
|
||||
url: string;
|
||||
|
|
@ -36,7 +37,7 @@ export class McpToolService extends FlowProcessor {
|
|||
super(config);
|
||||
|
||||
this.registerSpecification(
|
||||
new ConsumerSpec<ToolRequest>("mcp-tool-request", this.onRequest.bind(this)),
|
||||
ConsumerSpec.fromPromise<ToolRequest>("mcp-tool-request", this.onRequest.bind(this)),
|
||||
);
|
||||
this.registerSpecification(new ProducerSpec<ToolResponse>("mcp-tool-response"));
|
||||
|
||||
|
|
@ -77,14 +78,16 @@ export class McpToolService extends FlowProcessor {
|
|||
flowCtx: FlowContext,
|
||||
): Promise<void> {
|
||||
const requestId = properties.id;
|
||||
if (!requestId) return;
|
||||
if (requestId === undefined || requestId.length === 0) return;
|
||||
|
||||
const responseProducer = flowCtx.flow.producer<ToolResponse>("mcp-tool-response");
|
||||
|
||||
try {
|
||||
const result = await this.invokeTool(
|
||||
msg.name,
|
||||
msg.parameters ? JSON.parse(msg.parameters) : {},
|
||||
msg.parameters !== undefined && msg.parameters.length > 0
|
||||
? JSON.parse(msg.parameters) as Record<string, unknown>
|
||||
: {},
|
||||
);
|
||||
|
||||
if (typeof result === "string") {
|
||||
|
|
@ -110,7 +113,7 @@ export class McpToolService extends FlowProcessor {
|
|||
}
|
||||
|
||||
const svcConfig = this.mcpServices[name];
|
||||
if (!svcConfig.url) {
|
||||
if (svcConfig.url.length === 0) {
|
||||
throw new Error(`MCP service "${name}" URL not defined`);
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +121,7 @@ export class McpToolService extends FlowProcessor {
|
|||
|
||||
// Build headers with optional bearer token
|
||||
const headers: Record<string, string> = {};
|
||||
if (svcConfig["auth-token"]) {
|
||||
if (svcConfig["auth-token"] !== undefined && svcConfig["auth-token"].length > 0) {
|
||||
headers["Authorization"] = `Bearer ${svcConfig["auth-token"]}`;
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +136,7 @@ export class McpToolService extends FlowProcessor {
|
|||
const client = new Client({ name: "trustgraph-mcp-client", version: "1.0.0" });
|
||||
|
||||
try {
|
||||
await client.connect(transport);
|
||||
await client.connect(transport as unknown as Parameters<Client["connect"]>[0]);
|
||||
|
||||
const result = await client.callTool({
|
||||
name: remoteName,
|
||||
|
|
@ -141,11 +144,11 @@ export class McpToolService extends FlowProcessor {
|
|||
});
|
||||
|
||||
// Extract response — prefer structured content, fall back to text
|
||||
if (result.structuredContent) {
|
||||
if (result.structuredContent !== undefined && result.structuredContent !== null) {
|
||||
return result.structuredContent;
|
||||
}
|
||||
|
||||
if (result.content && Array.isArray(result.content)) {
|
||||
if (result.content !== undefined && Array.isArray(result.content)) {
|
||||
return result.content
|
||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
|
|
@ -158,3 +161,8 @@ export class McpToolService extends FlowProcessor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const program = makeProcessorProgram({
|
||||
id: "mcp-tool",
|
||||
make: (config) => new McpToolService(config),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,13 +25,22 @@ const MAX_MARKER_LEN = Math.max(...MARKERS.map((m) => m.prefix.length));
|
|||
export class StreamingReActParser {
|
||||
private state: ReActState = "initial";
|
||||
private buffer = "";
|
||||
private onThought: (text: string) => void;
|
||||
private onAction: (name: string) => void;
|
||||
private onActionInput: (input: string) => void;
|
||||
private onFinalAnswer: (text: string) => void;
|
||||
|
||||
constructor(
|
||||
private onThought: (text: string) => void,
|
||||
private onAction: (name: string) => void,
|
||||
private onActionInput: (input: string) => void,
|
||||
private onFinalAnswer: (text: string) => void,
|
||||
) {}
|
||||
onThought: (text: string) => void,
|
||||
onAction: (name: string) => void,
|
||||
onActionInput: (input: string) => void,
|
||||
onFinalAnswer: (text: string) => void,
|
||||
) {
|
||||
this.onThought = onThought;
|
||||
this.onAction = onAction;
|
||||
this.onActionInput = onActionInput;
|
||||
this.onFinalAnswer = onFinalAnswer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed a chunk of LLM output text into the parser.
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import {
|
|||
type ToolRequest,
|
||||
type ToolResponse,
|
||||
} from "@trustgraph/base";
|
||||
import { makeProcessorProgram } from "@trustgraph/base";
|
||||
|
||||
import {
|
||||
createKnowledgeQueryTool,
|
||||
|
|
@ -45,7 +46,7 @@ import {
|
|||
type ExplainData,
|
||||
} from "./tools.js";
|
||||
import { buildReActPrompt } from "./prompt.js";
|
||||
import { filterToolsByGroupAndState, getNextState } from "../tool-filter.js";
|
||||
import { filterToolsByGroupAndState } from "../tool-filter.js";
|
||||
import type { AgentTool, ToolArg } from "./types.js";
|
||||
|
||||
const MAX_ITERATIONS = 10;
|
||||
|
|
@ -59,7 +60,7 @@ export class AgentService extends FlowProcessor {
|
|||
|
||||
// Consumer: agent requests
|
||||
this.registerSpecification(
|
||||
new ConsumerSpec<AgentRequest>("agent-request", this.onRequest.bind(this)),
|
||||
ConsumerSpec.fromPromise<AgentRequest>("agent-request", this.onRequest.bind(this)),
|
||||
);
|
||||
|
||||
// Producer: agent responses (streaming chunks)
|
||||
|
|
@ -132,11 +133,12 @@ export class AgentService extends FlowProcessor {
|
|||
for (const [_toolId, toolValue] of Object.entries(toolConfig)) {
|
||||
try {
|
||||
const data = JSON.parse(toolValue) as Record<string, unknown>;
|
||||
const implType = data["type"] as string;
|
||||
const name = data["name"] as string;
|
||||
const description = data["description"] as string ?? "";
|
||||
const implType = typeof data["type"] === "string" ? data["type"] : "";
|
||||
const name = typeof data["name"] === "string" ? data["name"] : "";
|
||||
const description =
|
||||
typeof data["description"] === "string" ? data["description"] : "";
|
||||
|
||||
if (!name) {
|
||||
if (name.length === 0) {
|
||||
console.warn(`[AgentService] Skipping tool with no name: ${_toolId}`);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -148,7 +150,10 @@ export class AgentService extends FlowProcessor {
|
|||
// Will be wired to requestor at request time
|
||||
tool = {
|
||||
name,
|
||||
description: description || "Query the knowledge graph for information about entities and their relationships.",
|
||||
description:
|
||||
description.length > 0
|
||||
? description
|
||||
: "Query the knowledge graph for information about entities and their relationships.",
|
||||
args: [{ name: "question", type: "string", description: "The question to ask" }],
|
||||
config: data,
|
||||
execute: async () => "", // placeholder — wired at request time
|
||||
|
|
@ -158,7 +163,10 @@ export class AgentService extends FlowProcessor {
|
|||
case "document-query":
|
||||
tool = {
|
||||
name,
|
||||
description: description || "Search documents for relevant information.",
|
||||
description:
|
||||
description.length > 0
|
||||
? description
|
||||
: "Search documents for relevant information.",
|
||||
args: [{ name: "question", type: "string", description: "The question to search for" }],
|
||||
config: data,
|
||||
execute: async () => "",
|
||||
|
|
@ -168,7 +176,10 @@ export class AgentService extends FlowProcessor {
|
|||
case "triples-query":
|
||||
tool = {
|
||||
name,
|
||||
description: description || "Query for specific triples in the knowledge graph.",
|
||||
description:
|
||||
description.length > 0
|
||||
? description
|
||||
: "Query for specific triples in the knowledge graph.",
|
||||
args: [
|
||||
{ name: "subject", type: "string", description: "Subject entity (optional)" },
|
||||
{ name: "predicate", type: "string", description: "Predicate/relationship (optional)" },
|
||||
|
|
@ -203,7 +214,7 @@ export class AgentService extends FlowProcessor {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (tool) {
|
||||
if (tool !== null) {
|
||||
tools.push(tool);
|
||||
console.log(`[AgentService] Registered tool: ${name} (${implType})`);
|
||||
}
|
||||
|
|
@ -276,7 +287,7 @@ export class AgentService extends FlowProcessor {
|
|||
flowCtx: FlowContext,
|
||||
): Promise<void> {
|
||||
const requestId = properties.id;
|
||||
if (!requestId) return;
|
||||
if (requestId === undefined || requestId.length === 0) return;
|
||||
|
||||
const responseProducer = flowCtx.flow.producer<AgentResponse>("agent-response");
|
||||
|
||||
|
|
@ -290,7 +301,7 @@ export class AgentService extends FlowProcessor {
|
|||
// Build tools — config-driven or hardcoded fallback
|
||||
let tools: AgentTool[];
|
||||
|
||||
if (this.configuredTools) {
|
||||
if (this.configuredTools !== null) {
|
||||
tools = this.wireTools(this.configuredTools, flowCtx, msg.collection, onExplain);
|
||||
} else {
|
||||
// Hardcoded fallback (backward compat)
|
||||
|
|
@ -339,7 +350,7 @@ export class AgentService extends FlowProcessor {
|
|||
prompt: conversation,
|
||||
});
|
||||
|
||||
if (llmResponse.error) {
|
||||
if (llmResponse.error !== undefined) {
|
||||
await responseProducer.send(requestId, {
|
||||
chunk_type: "error",
|
||||
content: `LLM error: ${llmResponse.error.message}`,
|
||||
|
|
@ -354,7 +365,7 @@ export class AgentService extends FlowProcessor {
|
|||
const parsed = parseReActResponse(text);
|
||||
|
||||
// Send thought chunk
|
||||
if (parsed.thought) {
|
||||
if (parsed.thought.length > 0) {
|
||||
await responseProducer.send(requestId, {
|
||||
chunk_type: "thought",
|
||||
content: parsed.thought,
|
||||
|
|
@ -363,7 +374,7 @@ export class AgentService extends FlowProcessor {
|
|||
}
|
||||
|
||||
// If we got a final answer, emit explain events then send the answer
|
||||
if (parsed.finalAnswer) {
|
||||
if (parsed.finalAnswer.length > 0) {
|
||||
// Emit explain events collected from tool calls
|
||||
for (const explain of explainEvents) {
|
||||
await responseProducer.send(requestId, {
|
||||
|
|
@ -384,11 +395,11 @@ export class AgentService extends FlowProcessor {
|
|||
}
|
||||
|
||||
// Execute tool if action was specified
|
||||
if (parsed.action && parsed.actionInput) {
|
||||
if (parsed.action.length > 0 && parsed.actionInput.length > 0) {
|
||||
const tool = tools.find((t) => t.name === parsed.action);
|
||||
let observation: string;
|
||||
|
||||
if (tool) {
|
||||
if (tool !== undefined) {
|
||||
try {
|
||||
observation = await tool.execute(parsed.actionInput);
|
||||
} catch (err) {
|
||||
|
|
@ -407,7 +418,7 @@ export class AgentService extends FlowProcessor {
|
|||
|
||||
// Append the full exchange to conversation for the next iteration
|
||||
conversation += `\n${text}\nObservation: ${observation}\n`;
|
||||
} else if (!parsed.finalAnswer) {
|
||||
} else if (parsed.finalAnswer.length === 0) {
|
||||
// LLM didn't produce a valid action or final answer -- nudge it
|
||||
conversation += `\n${text}\nObservation: You must either use a tool (Action + Action Input) or provide a Final Answer.\n`;
|
||||
}
|
||||
|
|
@ -464,30 +475,31 @@ function parseReActResponse(text: string): {
|
|||
// Everything from "Final Answer:" to end of text is the answer
|
||||
const firstLine = trimmed.slice("Final Answer:".length).trim();
|
||||
const remainingLines = lines.slice(i + 1).join("\n").trim();
|
||||
finalAnswer = firstLine + (remainingLines ? "\n" + remainingLines : "");
|
||||
finalAnswer =
|
||||
firstLine + (remainingLines.length > 0 ? "\n" + remainingLines : "");
|
||||
break;
|
||||
} else if (trimmed.startsWith("Thought:")) {
|
||||
currentSection = "thought";
|
||||
const content = trimmed.slice("Thought:".length).trim();
|
||||
if (content) {
|
||||
thought += (thought ? "\n" : "") + content;
|
||||
if (content.length > 0) {
|
||||
thought += (thought.length > 0 ? "\n" : "") + content;
|
||||
}
|
||||
} else if (trimmed.startsWith("Action Input:")) {
|
||||
currentSection = "action_input";
|
||||
const content = trimmed.slice("Action Input:".length).trim();
|
||||
if (content) {
|
||||
if (content.length > 0) {
|
||||
actionInput += content;
|
||||
}
|
||||
} else if (trimmed.startsWith("Action:")) {
|
||||
currentSection = "action";
|
||||
const content = trimmed.slice("Action:".length).trim();
|
||||
if (content) {
|
||||
if (content.length > 0) {
|
||||
action = content;
|
||||
}
|
||||
} else if (trimmed.startsWith("Observation:")) {
|
||||
// Stop processing -- observations are injected by us, not the LLM
|
||||
currentSection = null;
|
||||
} else if (trimmed.length > 0 && currentSection) {
|
||||
} else if (trimmed.length > 0 && currentSection !== null) {
|
||||
// Continuation line for current section
|
||||
switch (currentSection) {
|
||||
case "thought":
|
||||
|
|
@ -512,6 +524,11 @@ function parseReActResponse(text: string): {
|
|||
};
|
||||
}
|
||||
|
||||
export const program = makeProcessorProgram({
|
||||
id: "agent",
|
||||
make: (config) => new AgentService(config),
|
||||
});
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
await AgentService.launch("agent");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type {
|
||||
RequestResponse,
|
||||
FlowRequestor,
|
||||
GraphRagRequest,
|
||||
GraphRagResponse,
|
||||
DocumentRagRequest,
|
||||
|
|
@ -68,7 +68,7 @@ export interface ExplainData {
|
|||
* Query the knowledge graph for information about entities and their relationships.
|
||||
*/
|
||||
export function createKnowledgeQueryTool(
|
||||
client: RequestResponse<GraphRagRequest, GraphRagResponse>,
|
||||
client: FlowRequestor<GraphRagRequest, GraphRagResponse>,
|
||||
collection?: string,
|
||||
onExplain?: (data: ExplainData) => void,
|
||||
): AgentTool {
|
||||
|
|
@ -86,19 +86,27 @@ export function createKnowledgeQueryTool(
|
|||
async execute(input: string): Promise<string> {
|
||||
const question = parseQuestion(input);
|
||||
console.log(`[KnowledgeQuery] Executing: "${question.slice(0, 60)}..." collection=${collection}`);
|
||||
const res = await client.request({ query: question, collection });
|
||||
console.log(`[KnowledgeQuery] Response (${res.response?.length ?? 0} chars): ${res.error ? `ERROR: ${res.error.message}` : `${res.response?.slice(0, 300)}...`}`);
|
||||
const request: GraphRagRequest = {
|
||||
query: question,
|
||||
...(collection !== undefined ? { collection } : {}),
|
||||
};
|
||||
const res = await client.request(request);
|
||||
console.log(`[KnowledgeQuery] Response (${res.response?.length ?? 0} chars): ${res.error !== undefined ? `ERROR: ${res.error.message}` : `${res.response?.slice(0, 300)}...`}`);
|
||||
|
||||
// Extract explain data if embedded in the response
|
||||
const rawRes = res as Record<string, unknown>;
|
||||
if (rawRes.message_type === "explain" && rawRes.explain_triples && onExplain) {
|
||||
if (
|
||||
rawRes.message_type === "explain" &&
|
||||
rawRes.explain_triples !== undefined &&
|
||||
onExplain !== undefined
|
||||
) {
|
||||
onExplain({
|
||||
explainId: (rawRes.explain_id as string) ?? "",
|
||||
triples: rawRes.explain_triples as Triple[],
|
||||
});
|
||||
}
|
||||
|
||||
if (res.error) return `Error: ${res.error.message}`;
|
||||
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||
return res.response;
|
||||
},
|
||||
};
|
||||
|
|
@ -108,7 +116,7 @@ export function createKnowledgeQueryTool(
|
|||
* Search documents for relevant information.
|
||||
*/
|
||||
export function createDocumentQueryTool(
|
||||
client: RequestResponse<DocumentRagRequest, DocumentRagResponse>,
|
||||
client: FlowRequestor<DocumentRagRequest, DocumentRagResponse>,
|
||||
collection?: string,
|
||||
): AgentTool {
|
||||
return {
|
||||
|
|
@ -124,8 +132,12 @@ export function createDocumentQueryTool(
|
|||
],
|
||||
async execute(input: string): Promise<string> {
|
||||
const question = parseQuestion(input);
|
||||
const res = await client.request({ query: question, collection });
|
||||
if (res.error) return `Error: ${res.error.message}`;
|
||||
const request: DocumentRagRequest = {
|
||||
query: question,
|
||||
...(collection !== undefined ? { collection } : {}),
|
||||
};
|
||||
const res = await client.request(request);
|
||||
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||
return res.response;
|
||||
},
|
||||
};
|
||||
|
|
@ -153,13 +165,20 @@ function parseTriplesInput(input: string): {
|
|||
return undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
s: toTerm(parsed.subject ?? parsed.s),
|
||||
p: toTerm(parsed.predicate ?? parsed.p),
|
||||
o: toTerm(parsed.object ?? parsed.o),
|
||||
limit:
|
||||
typeof parsed.limit === "number" ? parsed.limit : undefined,
|
||||
};
|
||||
const result: {
|
||||
s?: Term;
|
||||
p?: Term;
|
||||
o?: Term;
|
||||
limit?: number;
|
||||
} = {};
|
||||
const s = toTerm(parsed.subject ?? parsed.s);
|
||||
const p = toTerm(parsed.predicate ?? parsed.p);
|
||||
const o = toTerm(parsed.object ?? parsed.o);
|
||||
if (s !== undefined) result.s = s;
|
||||
if (p !== undefined) result.p = p;
|
||||
if (o !== undefined) result.o = o;
|
||||
if (typeof parsed.limit === "number") result.limit = parsed.limit;
|
||||
return result;
|
||||
} catch {
|
||||
// If not valid JSON, treat as a subject search
|
||||
return {
|
||||
|
|
@ -172,7 +191,7 @@ function parseTriplesInput(input: string): {
|
|||
* Query for specific triples (subject-predicate-object relationships) in the knowledge graph.
|
||||
*/
|
||||
export function createTriplesQueryTool(
|
||||
client: RequestResponse<TriplesQueryRequest, TriplesQueryResponse>,
|
||||
client: FlowRequestor<TriplesQueryRequest, TriplesQueryResponse>,
|
||||
collection?: string,
|
||||
): AgentTool {
|
||||
return {
|
||||
|
|
@ -199,17 +218,18 @@ export function createTriplesQueryTool(
|
|||
],
|
||||
async execute(input: string): Promise<string> {
|
||||
const { s, p, o, limit } = parseTriplesInput(input);
|
||||
const res = await client.request({
|
||||
s,
|
||||
p,
|
||||
o,
|
||||
collection,
|
||||
const request: TriplesQueryRequest = {
|
||||
limit: limit ?? 20,
|
||||
});
|
||||
...(s !== undefined ? { s } : {}),
|
||||
...(p !== undefined ? { p } : {}),
|
||||
...(o !== undefined ? { o } : {}),
|
||||
...(collection !== undefined ? { collection } : {}),
|
||||
};
|
||||
const res = await client.request(request);
|
||||
|
||||
if (res.error) return `Error: ${res.error.message}`;
|
||||
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||
|
||||
if (!res.triples || res.triples.length === 0) {
|
||||
if (res.triples === undefined || res.triples.length === 0) {
|
||||
return "No triples found matching the query.";
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +249,7 @@ export function createTriplesQueryTool(
|
|||
* this function just wraps it as an AgentTool the ReAct agent can invoke.
|
||||
*/
|
||||
export function createMcpTool(
|
||||
client: RequestResponse<ToolRequest, ToolResponse>,
|
||||
client: FlowRequestor<ToolRequest, ToolResponse>,
|
||||
toolName: string,
|
||||
description: string,
|
||||
args: ToolArg[],
|
||||
|
|
@ -240,9 +260,9 @@ export function createMcpTool(
|
|||
args,
|
||||
async execute(input: string): Promise<string> {
|
||||
const res = await client.request({ name: toolName, parameters: input });
|
||||
if (res.error) return `Error: ${res.error.message}`;
|
||||
if (res.text) return res.text;
|
||||
if (res.object) return res.object;
|
||||
if (res.error !== undefined) return `Error: ${res.error.message}`;
|
||||
if (res.text !== undefined) return res.text;
|
||||
if (res.object !== undefined) return res.object;
|
||||
return "No content";
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function filterToolsByGroupAndState(
|
|||
currentState?: string,
|
||||
): AgentTool[] {
|
||||
const groups = requestedGroups ?? ["default"];
|
||||
const state = currentState || "undefined";
|
||||
const state = currentState ?? "undefined";
|
||||
|
||||
return tools.filter((tool) => isToolAvailable(tool, groups, state));
|
||||
}
|
||||
|
|
@ -31,12 +31,12 @@ function isToolAvailable(
|
|||
|
||||
// Get tool groups (default to ["default"])
|
||||
let toolGroups = config["group"] as string[] | string | undefined;
|
||||
if (!toolGroups) toolGroups = ["default"];
|
||||
if (toolGroups === undefined) toolGroups = ["default"];
|
||||
if (!Array.isArray(toolGroups)) toolGroups = [toolGroups];
|
||||
|
||||
// Get tool applicable states (default to ["*"] = all states)
|
||||
let applicableStates = config["applicable-states"] as string[] | string | undefined;
|
||||
if (!applicableStates) applicableStates = ["*"];
|
||||
if (applicableStates === undefined) applicableStates = ["*"];
|
||||
if (!Array.isArray(applicableStates)) applicableStates = [applicableStates];
|
||||
|
||||
// Group match: wildcard in requested groups, or intersection non-empty
|
||||
|
|
@ -57,5 +57,5 @@ function isToolAvailable(
|
|||
*/
|
||||
export function getNextState(tool: AgentTool, currentState: string): string {
|
||||
const nextState = tool.config?.["state"] as string | undefined;
|
||||
return nextState || currentState;
|
||||
return nextState ?? currentState;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue