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,23 @@
{
"packageJson": {
"dependencies": {
"@langchain/core": "^1.2.5",
"@langchain/langgraph": "^1.4.9",
"@langchain/openai": "^1.5.6",
"@openuidev/langchain": "latest",
"langchain": "^1.5.5",
"zod": "^4.4.3"
},
"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"
},
"removeDependencies": ["openai"]
},
"gettingStarted": "The generated LangGraph backend is a standalone Agent Server. `{{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 its native tool loop."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { tool } from "@langchain/core/tools";
import { ChatOpenAI } from "@langchain/openai";
import { generateSystemPrompt } from "@openuidev/lang-core";
import { openUIStreamTransformer } from "@openuidev/langchain/transformer";
import { createAgent } from "langchain";
import { z } from "zod";
import librarySpec from "../generated/spec.json";
import { promptOptions } from "../lib/prompt-options";
import { getWeather, WEATHER_TOOL_DESCRIPTION } from "../lib/tools/get-weather";

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

const model = new ChatOpenAI({
model: process.env.OPENAI_MODEL ?? "gpt-5.2",
streaming: true,
configuration: process.env.OPENAI_BASE_URL ? { baseURL: process.env.OPENAI_BASE_URL } : undefined,
});

/**
* A standalone LangGraph agent: LangGraph owns orchestration and tool execution,
* while the application supplies its OpenAI-compatible model provider.
*/
export const graph = createAgent({
model,
tools: [getWeatherTool],
systemPrompt: generateSystemPrompt({ library: librarySpec, promptOptions }),
streamTransformers: [openUIStreamTransformer],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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",
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";
import "@openuidev/react-ui/components.css";
import "@openuidev/react-ui/styles/index.css";

import { AgentInterface, agUIAdapter, fetchLLM } from "@openuidev/react-ui";
import { openuiLibrary } from "@openuidev/react-ui/genui-lib";

const llm = fetchLLM({
url: "/api/chat",
streamAdapter: agUIAdapter(),
});

export default function Home() {
return (
<div style={{ height: "100vh", width: "100vw", overflow: "hidden" }}>
<AgentInterface llm={llm} componentLibrary={openuiLibrary} agentName="OpenUI Self Hosted" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"packageJson": {
"dependencies": {
"@ai-sdk/openai": "3.0.90",
"ai": "6.0.244",
"zod": "^4.4.3"
},
"removeDependencies": ["openai"]
},
"gettingStarted": "The generated API route uses the Vercel AI SDK.\nAsk \"What's the weather in Berlin?\" to exercise its native tool loop."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import librarySpec from "@/generated/spec.json";
import { promptOptions } from "@/lib/prompt-options";
import { getWeather, WEATHER_TOOL_DESCRIPTION } from "@/lib/tools/get-weather";
import { createOpenAI } from "@ai-sdk/openai";
import { generateSystemPrompt } from "@openuidev/lang-core";
import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage } from "ai";
import { z } from "zod";

export const runtime = "nodejs";

const openai = createOpenAI({
baseURL: process.env.OPENAI_BASE_URL,
apiKey: process.env.OPENAI_API_KEY,
});

const tools = {
get_weather: tool({
description: WEATHER_TOOL_DESCRIPTION,
inputSchema: z.object({
location: z.string().trim().min(1).describe("City or place name, e.g. Berlin."),
}),
execute: ({ location }, { abortSignal }) => getWeather(location, { signal: abortSignal }),
}),
};

export async function POST(req: Request) {
const payload = (await req.json()) as { messages?: UIMessage[] };
if (!Array.isArray(payload.messages)) {
return Response.json({ error: "messages must be an array" }, { status: 400 });
}

const result = streamText({
model: openai.chat(process.env.OPENAI_MODEL ?? "gpt-5.2"),
system: generateSystemPrompt({ library: librarySpec, promptOptions }),
messages: await convertToModelMessages(payload.messages),
tools,
stopWhen: stepCountIs(5),
abortSignal: req.signal,
});

// Preserve the AI SDK's native UIMessage stream. The frontend adapter uses
// the AI SDK itself to decode it before mapping chunks into OpenUI events.
return result.toUIMessageStreamResponse();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";
import "@openuidev/react-ui/components.css";
import "@openuidev/react-ui/styles/index.css";

import {
AgentInterface,
fetchLLM,
vercelAIAdapter,
vercelAIMessageFormat,
} from "@openuidev/react-ui";
import { openuiLibrary } from "@openuidev/react-ui/genui-lib";

const llm = fetchLLM({
url: "/api/chat",
streamAdapter: vercelAIAdapter(),
messageFormat: vercelAIMessageFormat,
});

export default function Home() {
return (
<div style={{ height: "100vh", width: "100vw", overflow: "hidden" }}>
<AgentInterface llm={llm} componentLibrary={openuiLibrary} agentName="OpenUI Self Hosted" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/** Example app-owned tool: current weather via Open-Meteo (free, no API key). */

export const WEATHER_TOOL_DESCRIPTION =
"Get the current weather for a city or place name. Use whenever the user " +
"asks about weather, temperature, rain, or what to wear.";

export type WeatherResult =
| {
place: string;
temperature_c: number;
conditions: string;
wind_kmh: number;
}
| { error: string };

function describeWeather(code: number): string {
if (code === 0) return "clear sky";
if (code <= 3) return "partly cloudy";
if (code <= 48) return "fog";
if (code <= 57) return "drizzle";
if (code <= 67) return "rain";
if (code <= 77) return "snow";
if (code <= 82) return "rain showers";
if (code <= 86) return "snow showers";
return "thunderstorm";
}

async function fetchJson<T>(url: URL, signal?: AbortSignal): Promise<T> {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`Open-Meteo returned ${response.status}`);
}
return (await response.json()) as T;
}

export async function getWeather(
locationInput: string,
options: { signal?: AbortSignal } = {},
): Promise<WeatherResult> {
const location = locationInput.trim();
if (!location) return { error: "location is required" };

try {
const geoUrl = new URL("https://geocoding-api.open-meteo.com/v1/search");
geoUrl.searchParams.set("name", location);
geoUrl.searchParams.set("count", "1");
const geo = await fetchJson<{
results?: Array<{ name: string; country?: string; latitude: number; longitude: number }>;
}>(geoUrl, options.signal);
const place = geo.results?.[0];
if (!place) return { error: `No place found for "${location}"` };

const weatherUrl = new URL("https://api.open-meteo.com/v1/forecast");
weatherUrl.searchParams.set("latitude", String(place.latitude));
weatherUrl.searchParams.set("longitude", String(place.longitude));
weatherUrl.searchParams.set("current", "temperature_2m,weather_code,wind_speed_10m");
const weather = await fetchJson<{
current?: { temperature_2m: number; weather_code: number; wind_speed_10m: number };
}>(weatherUrl, options.signal);
if (!weather.current) return { error: "No weather data returned" };

return {
place: `${place.name}${place.country ? `, ${place.country}` : ""}`,
temperature_c: weather.current.temperature_2m,
conditions: describeWeather(weather.current.weather_code),
wind_kmh: weather.current.wind_speed_10m,
};
} catch (error) {
if (options.signal?.aborted) throw error;
return {
error: `Weather lookup failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
Loading