Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cockpit/langgraph/client-tools/node/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
.env
package-lock.json

# LangGraph API
.langgraph_api
29 changes: 29 additions & 0 deletions cockpit/langgraph/client-tools/node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# LangGraph Client Tools — Node backend

The TypeScript/LangGraph.js twin of [`../python`](../python). Same graph behavior,
built on [`@threadplane/middleware/langgraph`](https://www.npmjs.com/package/@threadplane/middleware) —
the JS twin of the Python `threadplane-middleware`. It binds the browser-declared
client tools onto the model and ends the turn on a client-tool call so the browser
executes it and re-runs with a `ToolMessage`.

The browser declares the tools and `@threadplane/langgraph` ships the catalog as
`input.client_tools`; the graph's state channels (`clientToolsChannel()`) retain it
across the turn.

## Run

```bash
npm install
OPENAI_API_KEY=... npm run dev # langgraphjs dev --port 5308
```

Then serve the **shared** Angular frontend — it already targets this graph
(`assistantId: "client-tools"`, `http://localhost:4308/api` → proxied to `:5308`):

```bash
npx nx serve cockpit-langgraph-client-tools-angular
```

This backend serves the LangGraph Platform API on the same port (`5308`) and graph
id (`client-tools`) as the Python backend, so the Angular app connects unchanged —
only the backend runtime (Node vs Python) differs.
7 changes: 7 additions & 0 deletions cockpit/langgraph/client-tools/node/langgraph.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"graphs": {
"client-tools": "./src/graph.ts:graph"
},
"node_version": "20",
"env": ".env"
}
19 changes: 19 additions & 0 deletions cockpit/langgraph/client-tools/node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "cockpit-langgraph-client-tools-node",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "LangGraph.js client-tools backend — the TypeScript twin of ../python, built on @threadplane/middleware/langgraph.",
"scripts": {
"dev": "langgraphjs dev --port 5308 --no-browser"
},
"dependencies": {
"@langchain/core": "^1.1.49",
"@langchain/langgraph": "^1.4.2",
"@langchain/openai": "^1.4.7",
"@threadplane/middleware": "^0.0.1"
},
"devDependencies": {
"@langchain/langgraph-cli": "^1.3.1"
}
}
71 changes: 71 additions & 0 deletions cockpit/langgraph/client-tools/node/src/graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
/**
* LangGraph client-tools graph (LangGraph.js / TypeScript path).
*
* The JS twin of `../python/src/graph.py`. The browser declares the tools
* (get_weather / weather_card / confirm_booking) and the `@threadplane/langgraph`
* adapter ships the catalog as `input.client_tools`. This graph declares the
* client-tools state channels so the catalog is retained across the turn, binds
* those client stubs onto the model (no server implementation), and ends the
* turn when the model calls one — the browser executes it and re-runs with a
* ToolMessage, which the model then summarizes.
*
* The bind/route logic comes entirely from `@threadplane/middleware/langgraph`.
*/
import { ChatOpenAI } from '@langchain/openai';
import { Annotation, MessagesAnnotation, StateGraph, END } from '@langchain/langgraph';
import { SystemMessage } from '@langchain/core/messages';
import {
bindClientTools,
clientToolsChannel,
clientToolsRouter,
} from '@threadplane/middleware/langgraph';

// `clientToolsChannel()` declares the `tools` (primary) and `client_tools`
// (fallback) channels. The @threadplane/langgraph adapter ships the catalog as
// `client_tools`; bindClientTools reads `tools` then falls back to it.
const State = Annotation.Root({
...MessagesAnnotation.spec,
...clientToolsChannel(),
});

// Inlined so the graph has no filesystem/ESM-path dependency under langgraphjs dev.
const SYSTEM_PROMPT = `# Client Tools Assistant

You are a demo assistant showing browser-executed tools over LangGraph. You have
three client tools — call the right one and do not answer in prose first:

- When the user asks about the weather for a place, call \`get_weather\` with the
location. After it returns, give a one-sentence summary using the data.
- When the user asks to *show* or *display* a weather card, call \`weather_card\`
with the location and plausible readings (temperatureF, conditions, humidity,
windMph). After it renders, briefly confirm.
- When the user asks to book or reserve something, call \`confirm_booking\` with a
one-line \`summary\` of what they're booking. After the user responds, confirm
if they accepted or acknowledge if they cancelled.

Keep replies to one short sentence; the components carry the detail.`;

const baseLlm = new ChatOpenAI({ model: 'gpt-5-mini', streaming: true });

function buildClientToolsGraph() {
const agent = async (state: typeof State.State) => {
// bindClientTools reads state.tools then falls back to state.client_tools.
const llm = bindClientTools(baseLlm, [], state);
const response = await llm.invoke([new SystemMessage(SYSTEM_PROMPT), ...state.messages]);
return { messages: [response] };
};

// No server tools: a client tool call ends the run (the browser executes it),
// and so does a final text turn. clientToolsRouter([]) routes both to END.
return new StateGraph(State)
.addNode('agent', agent)
.addEdge('__start__', 'agent')
.addConditionalEdges('agent', clientToolsRouter([]), [END])
.compile();
}

// The graph instance referenced by langgraph.json. For `langgraphjs dev` the
// platform runtime provides the checkpointer, so we compile without one
// (mirrors the Python graph).
export const graph = buildClientToolsGraph();
15 changes: 15 additions & 0 deletions cockpit/langgraph/client-tools/node/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["node"],
"noEmit": true
},
"include": ["src/**/*.ts"]
}
Loading