mirror of
https://github.com/langchain-ai/langgraphjs-api.git
synced 2026-07-19 13:25:54 -04:00
feat(api): custom routes
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { Hono } from "hono";
|
||||
|
||||
const api = new Hono();
|
||||
|
||||
api.get("/info", (c) => c.json({ flags: { assistants: true, crons: false } }));
|
||||
|
||||
api.get("/ok", (c) => c.json({ ok: true }));
|
||||
|
||||
export default api;
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
declare module "hono" {
|
||||
interface ContextVariableMap {
|
||||
auth: AuthContext | undefined;
|
||||
auth?: AuthContext | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,23 @@ export async function spawnServer(
|
||||
ui?: Record<string, string>;
|
||||
ui_config?: { shared?: string[] };
|
||||
auth?: { path?: string; disable_studio_auth?: boolean };
|
||||
http?: {
|
||||
app?: string;
|
||||
disable_assistants?: boolean;
|
||||
disable_threads?: boolean;
|
||||
disable_runs?: boolean;
|
||||
disable_store?: boolean;
|
||||
disable_meta?: boolean;
|
||||
cors?: {
|
||||
allow_origins?: string[];
|
||||
allow_methods?: string[];
|
||||
allow_headers?: string[];
|
||||
allow_credentials?: boolean;
|
||||
allow_origin_regex?: string;
|
||||
expose_headers?: string[];
|
||||
max_age?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
env: NodeJS.ProcessEnv;
|
||||
hostUrl: string;
|
||||
@@ -61,6 +78,7 @@ For production use, please use LangGraph Cloud.
|
||||
ui: context.config.ui,
|
||||
ui_config: context.config.ui_config,
|
||||
cwd: options.projectCwd,
|
||||
http: context.config.http,
|
||||
}),
|
||||
],
|
||||
{
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Hono } from "hono";
|
||||
import * as path from "node:path";
|
||||
import * as url from "node:url";
|
||||
|
||||
export async function registerHttp(appPath: string, options: { cwd: string }) {
|
||||
const [userFile, exportSymbol] = appPath.split(":", 2);
|
||||
const sourceFile = path.resolve(options.cwd, userFile);
|
||||
|
||||
const user = (await import(url.pathToFileURL(sourceFile).toString()).then(
|
||||
(module) => module[exportSymbol || "default"],
|
||||
)) as Hono | undefined;
|
||||
|
||||
if (!user) throw new Error(`Failed to load HTTP app: ${appPath}`);
|
||||
return { api: user };
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { MiddlewareHandler } from "hono";
|
||||
import { cors as honoCors } from "hono/cors";
|
||||
|
||||
export const cors = (
|
||||
cors:
|
||||
| {
|
||||
allow_origins?: string[];
|
||||
allow_origin_regex?: string;
|
||||
allow_methods?: string[];
|
||||
allow_headers?: string[];
|
||||
allow_credentials?: boolean;
|
||||
expose_headers?: string[];
|
||||
max_age?: number;
|
||||
}
|
||||
| undefined,
|
||||
): MiddlewareHandler => {
|
||||
if (cors == null) return honoCors();
|
||||
|
||||
const originRegex = cors.allow_origin_regex
|
||||
? new RegExp(cors.allow_origin_regex)
|
||||
: undefined;
|
||||
|
||||
const origin = originRegex
|
||||
? (origin: string) => {
|
||||
originRegex.lastIndex = 0; // reset regex in case it's a global regex
|
||||
if (originRegex.test(origin)) return origin;
|
||||
return undefined;
|
||||
}
|
||||
: (cors.allow_origins ?? []);
|
||||
|
||||
// TODO: handle `cors.allow_credentials`
|
||||
return honoCors({
|
||||
origin,
|
||||
maxAge: cors.max_age,
|
||||
allowMethods: cors.allow_methods,
|
||||
allowHeaders: cors.allow_headers,
|
||||
credentials: cors.allow_credentials,
|
||||
exposeHeaders: cors.expose_headers,
|
||||
});
|
||||
};
|
||||
|
||||
// This is used to match the behavior of the original LangGraph API
|
||||
// where the content-type is not being validated. Might be nice
|
||||
// to warn about this in the future and throw an error instead.
|
||||
export const ensureContentType = (): MiddlewareHandler => {
|
||||
return async (c, next) => {
|
||||
if (
|
||||
c.req.header("content-type")?.startsWith("text/plain") &&
|
||||
c.req.method !== "GET" &&
|
||||
c.req.method !== "OPTIONS"
|
||||
) {
|
||||
c.req.raw.headers.set("content-type", "application/json");
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { serve } from "@hono/node-server";
|
||||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
|
||||
import { registerFromEnv } from "./graph/load.mjs";
|
||||
|
||||
@@ -8,6 +7,7 @@ import runs from "./api/runs.mjs";
|
||||
import threads from "./api/threads.mjs";
|
||||
import assistants from "./api/assistants.mjs";
|
||||
import store from "./api/store.mjs";
|
||||
import meta from "./api/meta.mjs";
|
||||
|
||||
import { truncate, conn as opsConn } from "./storage/ops.mjs";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
@@ -18,54 +18,8 @@ import { checkpointer } from "./storage/checkpoint.mjs";
|
||||
import { store as graphStore } from "./storage/store.mjs";
|
||||
import { auth } from "./auth/custom.mjs";
|
||||
import { registerAuth } from "./auth/index.mjs";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// This is used to match the behavior of the original LangGraph API
|
||||
// where the content-type is not being validated. Might be nice
|
||||
// to warn about this in the future and throw an error instead.
|
||||
app.use(async (c, next) => {
|
||||
if (
|
||||
c.req.header("content-type")?.startsWith("text/plain") &&
|
||||
c.req.method !== "GET" &&
|
||||
c.req.method !== "OPTIONS"
|
||||
) {
|
||||
c.req.raw.headers.set("content-type", "application/json");
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(requestLogger());
|
||||
app.get("/info", (c) => c.json({ flags: { assistants: true, crons: false } }));
|
||||
|
||||
app.post(
|
||||
"/internal/truncate",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
runs: z.boolean().optional(),
|
||||
threads: z.boolean().optional(),
|
||||
assistants: z.boolean().optional(),
|
||||
checkpointer: z.boolean().optional(),
|
||||
store: z.boolean().optional(),
|
||||
}),
|
||||
),
|
||||
(c) => {
|
||||
const { runs, threads, assistants, checkpointer, store } =
|
||||
c.req.valid("json");
|
||||
|
||||
truncate({ runs, threads, assistants, checkpointer, store });
|
||||
return c.json({ ok: true });
|
||||
},
|
||||
);
|
||||
|
||||
app.use(auth());
|
||||
app.route("/", assistants);
|
||||
app.route("/", runs);
|
||||
app.route("/", threads);
|
||||
app.route("/", store);
|
||||
import { registerHttp } from "./http/custom.mjs";
|
||||
import { cors, ensureContentType } from "./http/middleware.mjs";
|
||||
|
||||
export const StartServerSchema = z.object({
|
||||
port: z.number(),
|
||||
@@ -81,6 +35,27 @@ export const StartServerSchema = z.object({
|
||||
.optional(),
|
||||
ui: z.record(z.string()).optional(),
|
||||
ui_config: z.object({ shared: z.array(z.string()).optional() }).optional(),
|
||||
http: z
|
||||
.object({
|
||||
app: z.string().optional(),
|
||||
disable_assistants: z.boolean().default(false),
|
||||
disable_threads: z.boolean().default(false),
|
||||
disable_runs: z.boolean().default(false),
|
||||
disable_store: z.boolean().default(false),
|
||||
disable_meta: z.boolean().default(false),
|
||||
cors: z
|
||||
.object({
|
||||
allow_origins: z.array(z.string()).optional(),
|
||||
allow_methods: z.array(z.string()).optional(),
|
||||
allow_headers: z.array(z.string()).optional(),
|
||||
allow_credentials: z.boolean().optional(),
|
||||
allow_origin_regex: z.string().optional(),
|
||||
expose_headers: z.array(z.string()).optional(),
|
||||
max_age: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export async function startServer(options: z.infer<typeof StartServerSchema>) {
|
||||
@@ -99,21 +74,59 @@ export async function startServer(options: z.infer<typeof StartServerSchema>) {
|
||||
logger.info(`Registering graphs from ${options.cwd}`);
|
||||
await registerFromEnv(options.graphs, { cwd: options.cwd });
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
if (options.auth?.path) {
|
||||
logger.info(`Loading auth from ${options.auth.path}`);
|
||||
await registerAuth(options.auth, { cwd: options.cwd });
|
||||
app.use(auth());
|
||||
}
|
||||
|
||||
if (options.ui) {
|
||||
logger.info(`Loading UI`);
|
||||
const { api, registerGraphUi } = await import("./ui/load.mjs");
|
||||
if (options.http?.app) {
|
||||
logger.info(`Loading HTTP app from ${options.http.app}`);
|
||||
const { api } = await registerHttp(options.http.app, { cwd: options.cwd });
|
||||
app.route("/", api);
|
||||
}
|
||||
|
||||
app.use(cors(options.http?.cors));
|
||||
app.use(requestLogger());
|
||||
app.use(ensureContentType());
|
||||
|
||||
app.post(
|
||||
"/internal/truncate",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
runs: z.boolean().optional(),
|
||||
threads: z.boolean().optional(),
|
||||
assistants: z.boolean().optional(),
|
||||
checkpointer: z.boolean().optional(),
|
||||
store: z.boolean().optional(),
|
||||
}),
|
||||
),
|
||||
(c) => {
|
||||
const { runs, threads, assistants, checkpointer, store } =
|
||||
c.req.valid("json");
|
||||
|
||||
truncate({ runs, threads, assistants, checkpointer, store });
|
||||
return c.json({ ok: true });
|
||||
},
|
||||
);
|
||||
|
||||
if (!options.http?.disable_meta) app.route("/", meta);
|
||||
if (!options.http?.disable_assistants) app.route("/", assistants);
|
||||
if (!options.http?.disable_runs) app.route("/", runs);
|
||||
if (!options.http?.disable_threads) app.route("/", threads);
|
||||
if (!options.http?.disable_store) app.route("/", store);
|
||||
|
||||
if (options.ui) {
|
||||
logger.info(`Registering UI from ${options.cwd}`);
|
||||
const { api, registerGraphUi } = await import("./ui/load.mjs");
|
||||
await registerGraphUi(options.ui, {
|
||||
cwd: options.cwd,
|
||||
config: options.ui_config,
|
||||
});
|
||||
app.route("/", api);
|
||||
}
|
||||
|
||||
logger.info(`Starting ${options.nWorkers} workers`);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Hono } from "hono";
|
||||
|
||||
export const app = new Hono().get("/info", (c) => c.json({ random: true }));
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {
|
||||
"agent": "./agent.mts:graph",
|
||||
"agent_simple": "./agent_simple.mts:graph"
|
||||
},
|
||||
"ui": {
|
||||
"agent": "./agent/ui.tsx"
|
||||
},
|
||||
"http": {
|
||||
"app": "./http.mts:app"
|
||||
},
|
||||
"env": {
|
||||
"LANGGRAPH_CONFIG": "{\"agent\": {\"configurable\": {\"model_name\": \"openai\"}}}"
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,27 @@ const BaseConfigSchema = z.object({
|
||||
disable_studio_auth: z.boolean().default(false),
|
||||
})
|
||||
.optional(),
|
||||
http: z
|
||||
.object({
|
||||
app: z.string().optional(),
|
||||
disable_assistants: z.boolean().default(false),
|
||||
disable_threads: z.boolean().default(false),
|
||||
disable_runs: z.boolean().default(false),
|
||||
disable_store: z.boolean().default(false),
|
||||
disable_meta: z.boolean().default(false),
|
||||
cors: z
|
||||
.object({
|
||||
allow_origins: z.array(z.string()).optional(),
|
||||
allow_methods: z.array(z.string()).optional(),
|
||||
allow_headers: z.array(z.string()).optional(),
|
||||
allow_credentials: z.boolean().optional(),
|
||||
allow_origin_regex: z.string().optional(),
|
||||
expose_headers: z.array(z.string()).optional(),
|
||||
max_age: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const DEFAULT_PYTHON_VERSION = "3.11" as const;
|
||||
|
||||
Reference in New Issue
Block a user