mirror of
https://github.com/langchain-ai/langgraphjs.git
synced 2026-07-22 17:15:25 -04:00
feat(supervisor): Supervisor with RemoteGraph assistants (#1604)
Co-authored-by: Tat Dat Duong <david@duong.cz>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@langchain/langgraph-supervisor": patch
|
||||
---
|
||||
|
||||
Add support for RemoteGraph agents in supervisor (#1461)
|
||||
@@ -1,6 +1,9 @@
|
||||
import { LanguageModelLike } from "@langchain/core/language_models/base";
|
||||
import { StructuredToolInterface, DynamicTool } from "@langchain/core/tools";
|
||||
import { RunnableToolLike } from "@langchain/core/runnables";
|
||||
import type {
|
||||
RunnableConfig,
|
||||
RunnableToolLike,
|
||||
} from "@langchain/core/runnables";
|
||||
import { InteropZodType } from "@langchain/core/utils/types";
|
||||
import {
|
||||
START,
|
||||
@@ -20,6 +23,8 @@ import {
|
||||
BaseChatModel,
|
||||
BindToolsInput,
|
||||
} from "@langchain/core/language_models/chat_models";
|
||||
import type { RemoteGraph } from "@langchain/langgraph/remote";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import { createHandoffTool, createHandoffBackMessages } from "./handoff.js";
|
||||
|
||||
export type { AgentNameMode };
|
||||
@@ -58,6 +63,14 @@ function isChatModelWithParallelToolCallsParam(
|
||||
return llm.bindTools.length >= 2;
|
||||
}
|
||||
|
||||
function isRemoteGraph(agent: unknown): agent is RemoteGraph {
|
||||
if (agent == null || typeof agent !== "object") return false;
|
||||
if (!("lc_id" in agent)) return false;
|
||||
if (!Array.isArray(agent.lc_id)) return false;
|
||||
|
||||
return agent.lc_id.join(".") === "langgraph.pregel.RemoteGraph";
|
||||
}
|
||||
|
||||
const makeCallAgent = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
agent: any, // TODO: agent should not be `any`
|
||||
@@ -71,8 +84,23 @@ const makeCallAgent = (
|
||||
);
|
||||
}
|
||||
|
||||
return async (state: Record<string, unknown>) => {
|
||||
const output = await agent.invoke(state);
|
||||
return async (state: Record<string, unknown>, config?: RunnableConfig) => {
|
||||
let conf = config;
|
||||
|
||||
if (isRemoteGraph(agent)) {
|
||||
const threadId = config?.configurable?.thread_id;
|
||||
const agentThreadId =
|
||||
threadId && agent.name ? uuidv5(agent.name, threadId) : null;
|
||||
|
||||
conf = {
|
||||
...(config ?? {}),
|
||||
configurable: {
|
||||
...(config?.configurable ?? {}),
|
||||
...{ thread_id: agentThreadId },
|
||||
},
|
||||
};
|
||||
}
|
||||
const output = await agent.invoke(state, conf);
|
||||
let { messages } = output;
|
||||
|
||||
if (outputMode === "last_message") {
|
||||
@@ -95,13 +123,16 @@ export type CreateSupervisorParams<
|
||||
/**
|
||||
* List of agents to manage
|
||||
*/
|
||||
agents: CompiledStateGraph<
|
||||
AnnotationRootT["State"],
|
||||
AnnotationRootT["Update"],
|
||||
string,
|
||||
AnnotationRootT["spec"],
|
||||
AnnotationRootT["spec"]
|
||||
>[];
|
||||
agents: (
|
||||
| CompiledStateGraph<
|
||||
AnnotationRootT["State"],
|
||||
AnnotationRootT["Update"],
|
||||
string,
|
||||
AnnotationRootT["spec"],
|
||||
AnnotationRootT["spec"]
|
||||
>
|
||||
| RemoteGraph
|
||||
)[];
|
||||
|
||||
/**
|
||||
* Language model to use for the supervisor
|
||||
@@ -323,9 +354,15 @@ const createSupervisor = <
|
||||
agentNames.add(agent.name);
|
||||
}
|
||||
|
||||
const handoffTools = agents.map(({ name, description }) =>
|
||||
createHandoffTool({ agentName: name!, agentDescription: description })
|
||||
);
|
||||
const handoffTools = agents.map((agent) => {
|
||||
const agentName = agent.name!;
|
||||
const agentDescription =
|
||||
"description" in agent && typeof agent.description === "string"
|
||||
? agent.description
|
||||
: undefined;
|
||||
|
||||
return createHandoffTool({ agentName, agentDescription });
|
||||
});
|
||||
const allTools = [...(tools ?? []), ...handoffTools];
|
||||
|
||||
let supervisorLLM = llm;
|
||||
@@ -389,7 +426,7 @@ const createSupervisor = <
|
||||
builder = builder.addNode(
|
||||
agent.name!,
|
||||
makeCallAgent(agent, outputMode, addHandoffBackMessages, supervisorName),
|
||||
{ subgraphs: [agent] }
|
||||
{ subgraphs: isRemoteGraph(agent) ? undefined : [agent] }
|
||||
);
|
||||
builder = builder.addEdge(agent.name!, supervisorAgent.name!);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { HumanMessage, AIMessage } from "@langchain/core/messages";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import { RemoteGraph } from "@langchain/langgraph/remote";
|
||||
import { createReactAgent, ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
import { createSupervisor } from "../supervisor.js";
|
||||
import { FakeToolCallingChatModel } from "./utils.js";
|
||||
|
||||
class FakeRemoteGraph extends RemoteGraph {
|
||||
public receivedThreadIds: Array<string | undefined> = [];
|
||||
|
||||
get lc_id(): string[] {
|
||||
return ["langgraph", "pregel", "RemoteGraph"];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override async invoke(_state: any, config?: any) {
|
||||
this.receivedThreadIds.push(config?.configurable?.thread_id);
|
||||
return { messages: [new AIMessage({ content: "remote result" })] };
|
||||
}
|
||||
}
|
||||
|
||||
describe("Supervisor with RemoteGraph agents", () => {
|
||||
it("propagates per-agent thread_id and exposes handoff tool description", async () => {
|
||||
const ROOT_THREAD_ID = "123e4567-e89b-12d3-a456-426614174000";
|
||||
|
||||
// Supervisor will transfer to the remote agent, then produce a final answer
|
||||
const supervisorMessages = [
|
||||
new AIMessage({
|
||||
content: "",
|
||||
tool_calls: [
|
||||
{
|
||||
name: "transfer_to_remote_expert",
|
||||
args: {},
|
||||
id: "call_remote_handoff",
|
||||
type: "tool_call",
|
||||
},
|
||||
],
|
||||
}),
|
||||
new AIMessage({ content: "done" }),
|
||||
];
|
||||
|
||||
const supervisorModel = new FakeToolCallingChatModel({
|
||||
responses: supervisorMessages,
|
||||
});
|
||||
|
||||
// Prepare RemoteGraph agent
|
||||
const remote = new FakeRemoteGraph({ graphId: "dummy" });
|
||||
(remote as unknown as { name?: string }).name = "remote_expert";
|
||||
(remote as unknown as { description?: string }).description =
|
||||
"Remote expert doing remote things.";
|
||||
|
||||
// Build supervisor workflow
|
||||
const workflow = createSupervisor({
|
||||
agents: [remote],
|
||||
llm: supervisorModel,
|
||||
prompt: "You are a supervisor managing a remote expert.",
|
||||
});
|
||||
|
||||
// Assert handoff tool includes remote description
|
||||
const toolNode = (
|
||||
workflow.nodes.supervisor.runnable as ReturnType<typeof createReactAgent>
|
||||
).nodes.tools.bound as ToolNode;
|
||||
|
||||
expect(toolNode.tools).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "transfer_to_remote_expert",
|
||||
description: "Remote expert doing remote things.",
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
// Compile and invoke with a root thread id
|
||||
const app = workflow.compile();
|
||||
const result = await app.invoke(
|
||||
{ messages: [new HumanMessage({ content: "hi" })] },
|
||||
{ configurable: { thread_id: ROOT_THREAD_ID } }
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// Verify per-agent thread id derivation is passed to the RemoteGraph
|
||||
const expectedAgentThreadId = uuidv5("remote_expert", ROOT_THREAD_ID);
|
||||
expect(remote.receivedThreadIds[0]).toBe(expectedAgentThreadId);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user