Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"node_version": "20",
"dependencies": ["."],
"graphs": {
"agent": "./src/agent/agent.ts:graph"
},
"env": ".env"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"packageJson": {
"dependencies": {
"@langchain/core": "^1.2.5",
"@langchain/langgraph": "^1.4.9",
"@langchain/openai": "^1.5.6",
"@openuidev/langchain": "latest",
"langchain": "^1.5.5"
},
"devDependencies": {
"@langchain/langgraph-cli": "^1.4.4",
"npm-run-all2": "^9.0.2"
},
"scripts": {
"dev": "run-p dev:langgraph dev:next",
"dev:langgraph": "langgraphjs dev",
"dev:next": "next dev"
}
},
"files": {
"remove": ["src/lib/tool-loop.ts"]
},
"gettingStarted": "The generated LangGraph agent uses OpenUI Cloud as its Responses provider. `{{packageManager}} run dev` starts both the Agent Server and Next.js. Deploy the Next.js frontend to Vercel and point LANGGRAPH_API_URL at wherever the Agent Server runs.\nAsk \"What's the weather in Berlin?\" to exercise the included LangGraph tool."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { AIMessage } from "@langchain/core/messages";
import { type ServerTool, tool } from "@langchain/core/tools";
import { StateSchema } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { openUIStreamTransformer } from "@openuidev/langchain/transformer";
import { artifactTool, generateSystemPrompt } from "@openuidev/thesys-server";
import { createAgent, createMiddleware } from "langchain";
import { z } from "zod";

import { requiredEnv } from "../lib/env";
import { DEFAULT_MODEL } from "../lib/models";
import { executeGetWeather, getWeatherTool } from "../lib/tools/get-weather";

const getWeather = tool(
async ({ location }, config) =>
executeGetWeather(JSON.stringify({ location }), { signal: config.signal }),
{
name: "get_weather",
description: getWeatherTool.description,
schema: z.object({
location: z.string().trim().min(1).describe("City or place name, e.g. Berlin."),
}),
},
);

const appTools = [getWeather];
const appToolNames = new Set<string>(appTools.map(({ name }) => name));
const TOOL_CALL_BLOCK_TYPES = new Set(["tool_call", "tool_call_chunk", "tool_use"]);

function keepAppToolCallBlocks(block: unknown) {
if (typeof block !== "object" || block === null) return true;
const { type, name } = block as { type?: unknown; name?: unknown };
if (typeof type !== "string" || !TOOL_CALL_BLOCK_TYPES.has(type)) return true;
return typeof name === "string" && appToolNames.has(name);
}

// These are provider-executed tools. LangGraph sends their declarations to
// OpenUI Cloud, while Cloud runs them and stores their outputs/artifacts.
const cloudTools = [
artifactTool({ artifacts: ["slides", "report"] }),
{ type: "web_search" },
{ type: "image_search" },
// Add provider-executed MCP servers here, for example:
// { type: "mcp", server_label: "deepwiki", server_url: "https://mcp.deepwiki.com/mcp" },
] as ServerTool[];

const CloudAgentState = new StateSchema({
conversationId: z.string(),
model: z.string().default(DEFAULT_MODEL),
});

function cloudModel(model: string, conversationId?: string) {
return new ChatOpenAI({
model,
apiKey: requiredEnv("THESYS_API_KEY"),
streaming: true,
useResponsesApi: true,
configuration: { baseURL: "https://api.thesys.dev/v1/embed" },
modelKwargs: {
store: true,
...(conversationId ? { conversation: conversationId } : {}),
},
});
}

const cloudConversation = createMiddleware({
name: "OpenUICloudConversation",
stateSchema: CloudAgentState,
wrapModelCall: async (request, handler) => {
const { conversationId, model } = request.state as unknown as {
conversationId: string;
model: string;
};

const response = await handler({
...request,
model: cloudModel(model, conversationId),
// Cloud has the earlier turns. Within a LangGraph run this becomes the
// latest user message first, then each locally produced ToolMessage.
messages: request.messages.slice(-1),
});

// Cloud has already executed its provider tools. Keep only app-owned
// calls in graph state so LangGraph's ToolNode executes exactly those.
// ChatOpenAI also derives tool_calls from standard content blocks, so
// remove Cloud-owned call blocks as well as filtering response.tool_calls.
const localToolCalls = response.tool_calls?.filter(({ name }) => appToolNames.has(name));
const localContent = Array.isArray(response.content)
? response.content.filter(keepAppToolCallBlocks)
: response.content;
const contentChanged =
Array.isArray(response.content) && localContent.length !== response.content.length;
if (localToolCalls?.length === response.tool_calls?.length && !contentChanged) {
return response;
}

return new AIMessage({
id: response.id,
content: localContent,
additional_kwargs: response.additional_kwargs,
response_metadata: response.response_metadata,
tool_calls: localToolCalls,
invalid_tool_calls: response.invalid_tool_calls,
usage_metadata: response.usage_metadata,
});
},
});

/**
* A normal LangGraph agent: LangGraph owns orchestration and local
* tool execution; OpenUI Cloud is the attached Responses provider.
*/
export const graph = createAgent({
model: cloudModel(DEFAULT_MODEL),
tools: [...cloudTools, ...appTools],
systemPrompt: generateSystemPrompt(),
stateSchema: CloudAgentState,
middleware: [cloudConversation],
streamTransformers: [openUIStreamTransformer],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { resolveRequestedModel } from "@/lib/models";
import { createLangChainStreamResponse } from "@openuidev/langchain";

export const runtime = "nodejs";

const API_URL = process.env.LANGGRAPH_API_URL || "http://localhost:2024";
const ASSISTANT_ID = process.env.LANGGRAPH_ASSISTANT_ID || "agent";

/**
* Browser-to-Agent-Server proxy. The agent itself lives in src/agent/agent.ts
* and can be run locally or deployed independently.
*/
export async function POST(request: Request) {
return createLangChainStreamResponse(request, {
apiUrl: API_URL,
assistantId: ASSISTANT_ID,
apiKey: process.env.LANGSMITH_API_KEY,
debug: process.env.NODE_ENV !== "production",
prepareInput: ({ messages, requestBody }) => {
const conversationId = requestBody.threadId;
if (typeof conversationId !== "string" || !conversationId) {
throw new Error("threadId is required — create the conversation first");
}

const model = resolveRequestedModel(requestBody.model);
if (!model) throw new Error("model is not available in this agent");

return {
// OpenUI Cloud stores prior turns. LangGraph owns the current run and
// local tool loop, while only new inputs are sent to the conversation.
messages: messages.slice(-1),
conversationId,
model,
};
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use client";

import { usePersistedModel } from "@/hooks/use-persisted-model";
import { MODEL_OPTIONS } from "@/lib/models";
import { OPENUI_LOGOS, PROMPT_TEMPLATES, STARTERS } from "@/lib/starters";
import {
AgentInterface,
ModelSwitcher,
agUIAdapter,
defineArtifactCategories,
fetchLLM,
useSystemThemeMode,
} from "@openuidev/react-ui";
import {
chatLibrary,
presentationArtifactRenderer,
reportArtifactRenderer,
useOpenuiCloudStorage,
} from "@openuidev/thesys";
import { FileText, Presentation } from "lucide-react";

const { artifactRenderers, artifactCategories } = defineArtifactCategories([
{
name: "Presentations",
renderers: [presentationArtifactRenderer],
icon: <Presentation size="1em" />,
},
{
name: "Reports",
renderers: [reportArtifactRenderer],
icon: <FileText size="1em" />,
},
]);

export default function CloudChat() {
const mode = useSystemThemeMode();
const [selectedModel, setSelectedModel] = usePersistedModel();
// The LangGraph proxy emits AG-UI events, so no message format is needed:
// the Agent Server receives the conversation as graph input instead.
const llm = fetchLLM({
url: "/api/chat",
streamAdapter: agUIAdapter(),
body: { model: selectedModel },
});

const storage = useOpenuiCloudStorage({
token: "/api/frontend-token",
apiBaseUrl: "https://api.thesys.dev",
features: { artifact: true },
});

const logoPath = mode === "dark" ? OPENUI_LOGOS.DARK : OPENUI_LOGOS.LIGHT;

return (
<div className="openui-cloud-page">
<AgentInterface
storage={storage}
llm={llm}
componentLibrary={chatLibrary}
artifactRenderers={artifactRenderers}
artifactCategories={artifactCategories}
logoUrl={logoPath}
theme={{ mode }}
starters={STARTERS}
>
<AgentInterface.MobileHeader
agentName=""
actions={
<ModelSwitcher
models={MODEL_OPTIONS}
value={selectedModel}
onValueChange={setSelectedModel}
/>
}
/>
<AgentInterface.ThreadHeader className="openui-cloud-thread-header">
<ModelSwitcher
models={MODEL_OPTIONS}
value={selectedModel}
onValueChange={setSelectedModel}
/>
</AgentInterface.ThreadHeader>
<AgentInterface.Welcome
title="Good to see you"
description="What's on your mind today?"
promptTemplates={PROMPT_TEMPLATES}
glowAnimation
/>
</AgentInterface>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"packageJson": {
"dependencies": {
"@ai-sdk/openai": "^3.0.91",
"ai": "^6.0.246",
"zod": "^4.4.3"
}
},
"files": {
"remove": ["src/lib/tool-loop.ts"]
},
"gettingStarted": "The generated Vercel AI SDK route uses OpenUI Cloud as its Responses provider and is deployable as a normal Next.js app on Vercel.\nAsk \"What's the weather in Berlin?\" to exercise the included AI SDK tool."
}
Loading
Loading