langhost 0.11.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
langhost/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Minimal CLI for self-hosted LangGraph Agent Server (edition=pg)."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("langhost")
7
+ except PackageNotFoundError: # pragma: no cover
8
+ __version__ = "0.0.0"
9
+
10
+ __all__ = ["__version__"]
langhost/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from langhost.cli import cli
2
+
3
+ if __name__ == "__main__":
4
+ cli()
langhost/cli.py ADDED
@@ -0,0 +1,357 @@
1
+ """langhost CLI — thin wrappers around langgraph-cli + langgraph_api.cli.run_server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import pathlib
8
+ import sys
9
+ from collections.abc import Sequence
10
+ from typing import Any
11
+
12
+ import click
13
+ from dotenv import load_dotenv
14
+ from langgraph_api.cli import run_server
15
+ from langgraph_cli.config import validate_config_file
16
+ from langgraph_cli.constants import DEFAULT_CONFIG
17
+ from pyfiglet import figlet_format
18
+
19
+ from langhost import __version__
20
+
21
+ # Always the open Postgres+Redis runtime.
22
+ RUNTIME_EDITION = "pg"
23
+ DEFAULT_PORT = 31296
24
+ LOG_LEVELS = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
25
+ DEFAULT_STUDIO_ORIGIN = "https://smith.langchain.com"
26
+
27
+ # Upstream run_server always logs an inmem-oriented welcome; replace it for langhost.
28
+ _UPSTREAM_WELCOME_MARKER = "This in-memory server is designed for development and testing"
29
+
30
+
31
+ def _display_host(host: str) -> str:
32
+ if host in {"0.0.0.0", "::"}:
33
+ return "127.0.0.1"
34
+ if ":" in host and not host.startswith("["):
35
+ return f"[{host}]"
36
+ return host
37
+
38
+
39
+ def _server_base_url(
40
+ host: str,
41
+ port: int,
42
+ *,
43
+ ssl: bool,
44
+ mount_prefix: str | None,
45
+ ) -> str:
46
+ scheme = "https" if ssl else "http"
47
+ url = f"{scheme}://{_display_host(host)}:{port}"
48
+ if mount_prefix:
49
+ url += mount_prefix
50
+ return url
51
+
52
+
53
+ def _langhost_welcome(
54
+ *,
55
+ host: str,
56
+ port: int,
57
+ ssl: bool,
58
+ studio_origin: str | None,
59
+ mount_prefix: str | None,
60
+ ) -> str:
61
+ api_url = _server_base_url(host, port, ssl=ssl, mount_prefix=mount_prefix)
62
+ origin = (studio_origin or DEFAULT_STUDIO_ORIGIN).rstrip("/")
63
+ studio_url = f"{origin}/studio/?baseUrl={api_url}"
64
+ agent_chat_url = f"https://agentchat.vercel.app/?apiUrl={api_url}&assistantId=agent"
65
+ title = figlet_format("langhost", font="standard").rstrip()
66
+ return f"""
67
+
68
+ {title}
69
+
70
+ - 🚀 API: \033[36m{api_url}\033[0m
71
+ - 🎨 Studio UI: \033[36m{studio_url}\033[0m
72
+ - 📚 API Docs: \033[36m{api_url}/docs\033[0m
73
+ - 💬 Agent Chat UI: \033[36m{agent_chat_url}\033[0m
74
+
75
+ Self-hosted, production-grade LangGraph Agent Server (Postgres + Redis).
76
+ langhost {__version__}
77
+
78
+ """
79
+
80
+
81
+ class _ReplaceWelcomeBanner(logging.Filter):
82
+ """Swap the upstream inmem welcome for a prebuilt langhost banner."""
83
+
84
+ def __init__(self, welcome: str) -> None:
85
+ super().__init__()
86
+ self._welcome = welcome
87
+
88
+ def filter(self, record: logging.LogRecord) -> bool:
89
+ if _UPSTREAM_WELCOME_MARKER not in record.getMessage():
90
+ return True
91
+ record.msg = self._welcome
92
+ record.args = ()
93
+ return True
94
+
95
+
96
+ @click.group()
97
+ @click.version_option(version=__version__, prog_name="langhost")
98
+ def cli() -> None:
99
+ """Self-hosted LangGraph Agent Server on Postgres + Redis (edition=pg)."""
100
+
101
+
102
+ @cli.command(
103
+ "serve",
104
+ help=(
105
+ "Run the Agent Server (LANGGRAPH_RUNTIME_EDITION=pg). "
106
+ "Use --reload for local development; --workers for production."
107
+ ),
108
+ )
109
+ @click.option(
110
+ "--host",
111
+ default="127.0.0.1",
112
+ show_default=True,
113
+ help=(
114
+ "Bind address. Prefer 127.0.0.1 for local work; use 0.0.0.0 only on "
115
+ "trusted networks / behind a reverse proxy."
116
+ ),
117
+ )
118
+ @click.option(
119
+ "--port",
120
+ "-p",
121
+ default=DEFAULT_PORT,
122
+ show_default=True,
123
+ type=int,
124
+ help="Port to bind the Agent Server to.",
125
+ )
126
+ @click.option(
127
+ "--config",
128
+ "-c",
129
+ default=DEFAULT_CONFIG,
130
+ show_default=True,
131
+ type=click.Path(exists=True, dir_okay=False, path_type=pathlib.Path),
132
+ help="Path to langgraph.json (graphs, deps, env, auth, http, …).",
133
+ )
134
+ @click.option(
135
+ "--env-file",
136
+ type=click.Path(exists=True, dir_okay=False, path_type=pathlib.Path),
137
+ default=None,
138
+ help="Optional dotenv file to load before starting (default: .env if present).",
139
+ )
140
+ @click.option(
141
+ "--database-uri",
142
+ default=None,
143
+ help="Postgres URI (overrides DATABASE_URI). Required unless set in the environment.",
144
+ )
145
+ @click.option(
146
+ "--redis-uri",
147
+ default=None,
148
+ help="Redis URI (overrides REDIS_URI). Required unless set in the environment.",
149
+ )
150
+ @click.option(
151
+ "--reload",
152
+ is_flag=True,
153
+ help="Auto-reload on code changes (dev). Incompatible with --workers > 1.",
154
+ )
155
+ @click.option(
156
+ "--reload-include",
157
+ "reload_includes",
158
+ multiple=True,
159
+ help="Glob(s) to watch when --reload is set (repeatable).",
160
+ )
161
+ @click.option(
162
+ "--reload-exclude",
163
+ "reload_excludes",
164
+ multiple=True,
165
+ help="Glob(s) to ignore when --reload is set (repeatable).",
166
+ )
167
+ @click.option(
168
+ "--workers",
169
+ default=1,
170
+ show_default=True,
171
+ type=click.IntRange(min=1),
172
+ help="Uvicorn worker processes (prod). Incompatible with --reload.",
173
+ )
174
+ @click.option(
175
+ "--n-jobs-per-worker",
176
+ default=None,
177
+ type=int,
178
+ help=(
179
+ "Max concurrent background jobs per server process "
180
+ "(sets N_JOBS_PER_WORKER; default inside run_server is 1)."
181
+ ),
182
+ )
183
+ @click.option(
184
+ "--browser/--no-browser",
185
+ default=False,
186
+ help="Open LangSmith Studio in the browser when the server is ready.",
187
+ )
188
+ @click.option(
189
+ "--studio-url",
190
+ default=None,
191
+ help="LangSmith Studio base URL (default: https://smith.langchain.com).",
192
+ )
193
+ @click.option(
194
+ "--tunnel",
195
+ is_flag=True,
196
+ help="Expose the server via a Cloudflare tunnel (remote Studio access).",
197
+ )
198
+ @click.option(
199
+ "--debug-port",
200
+ default=None,
201
+ type=int,
202
+ help="Listen for a remote debugger (debugpy) on this port.",
203
+ )
204
+ @click.option(
205
+ "--wait-for-client",
206
+ is_flag=True,
207
+ help="With --debug-port, block until a debugger attaches.",
208
+ )
209
+ @click.option(
210
+ "--allow-blocking",
211
+ is_flag=True,
212
+ help="Do not raise on synchronous/blocking I/O in graph code.",
213
+ )
214
+ @click.option(
215
+ "--server-log-level",
216
+ default="INFO",
217
+ show_default=True,
218
+ type=click.Choice(LOG_LEVELS, case_sensitive=False),
219
+ help="Log level for uvicorn / langgraph_api.server.",
220
+ )
221
+ @click.option(
222
+ "--ssl-certfile",
223
+ type=click.Path(exists=True, dir_okay=False, path_type=pathlib.Path),
224
+ default=None,
225
+ help="TLS certificate file (serve over HTTPS).",
226
+ )
227
+ @click.option(
228
+ "--ssl-keyfile",
229
+ type=click.Path(exists=True, dir_okay=False, path_type=pathlib.Path),
230
+ default=None,
231
+ help="TLS private key file (serve over HTTPS).",
232
+ )
233
+ def serve(
234
+ host: str,
235
+ port: int,
236
+ config: pathlib.Path,
237
+ env_file: pathlib.Path | None,
238
+ database_uri: str | None,
239
+ redis_uri: str | None,
240
+ reload: bool,
241
+ reload_includes: tuple[str, ...],
242
+ reload_excludes: tuple[str, ...],
243
+ workers: int,
244
+ n_jobs_per_worker: int | None,
245
+ browser: bool,
246
+ studio_url: str | None,
247
+ tunnel: bool,
248
+ debug_port: int | None,
249
+ wait_for_client: bool,
250
+ allow_blocking: bool,
251
+ server_log_level: str,
252
+ ssl_certfile: pathlib.Path | None,
253
+ ssl_keyfile: pathlib.Path | None,
254
+ ) -> None:
255
+ if reload and workers > 1:
256
+ raise click.UsageError("Cannot combine --reload with --workers > 1.")
257
+ if (ssl_certfile is None) != (ssl_keyfile is None):
258
+ raise click.UsageError("Both --ssl-certfile and --ssl-keyfile are required for HTTPS.")
259
+ if ssl_certfile and ssl_keyfile and tunnel:
260
+ raise click.UsageError("Cannot combine --tunnel with SSL options.")
261
+ if wait_for_client and debug_port is None:
262
+ raise click.UsageError("--wait-for-client requires --debug-port.")
263
+
264
+ load_dotenv(env_file or ".env", override=False)
265
+ os.environ["LANGGRAPH_RUNTIME_EDITION"] = RUNTIME_EDITION
266
+
267
+ database_uri = database_uri or os.environ.get("DATABASE_URI")
268
+ redis_uri = redis_uri or os.environ.get("REDIS_URI")
269
+ if not database_uri:
270
+ raise click.UsageError(
271
+ "DATABASE_URI is required. Please set it in the environment or pass it to the command via --database-uri."
272
+ )
273
+ if not redis_uri:
274
+ raise click.UsageError(
275
+ "REDIS_URI is required. Please set it in the environment or pass it to the command via --redis-uri."
276
+ )
277
+ if n_jobs_per_worker is None:
278
+ raw = os.environ.get("N_JOBS_PER_WORKER")
279
+ if raw:
280
+ n_jobs_per_worker = int(raw)
281
+
282
+ config_json = validate_config_file(config)
283
+ if config_json.get("node_version"):
284
+ raise click.UsageError(
285
+ "JS graphs are not supported by langhost. Remove the 'node_version' field from the config."
286
+ )
287
+
288
+ cwd = pathlib.Path.cwd()
289
+ sys.path.append(str(cwd))
290
+ for dep in config_json.get("dependencies", []):
291
+ dep_path = cwd / dep
292
+ if dep_path.is_dir() and dep_path.exists():
293
+ sys.path.append(str(dep_path))
294
+
295
+ includes: Sequence[str] | None = list(reload_includes) or None
296
+ excludes: Sequence[str] | None = list(reload_excludes) or None
297
+ uvicorn_kwargs: dict[str, Any] = {}
298
+ if workers > 1:
299
+ uvicorn_kwargs["workers"] = workers
300
+
301
+ http_cfg = config_json.get("http")
302
+ mount_prefix = None
303
+ if isinstance(http_cfg, dict):
304
+ mount_prefix = http_cfg.get("mount_prefix")
305
+ mount_prefix = (
306
+ os.environ.get("MOUNT_PREFIX") or os.environ.get("LANGGRAPH_MOUNT_PREFIX") or mount_prefix
307
+ )
308
+
309
+ welcome = _langhost_welcome(
310
+ host=host,
311
+ port=port,
312
+ ssl=ssl_certfile is not None and ssl_keyfile is not None,
313
+ studio_origin=studio_url,
314
+ mount_prefix=mount_prefix,
315
+ )
316
+ api_cli_logger = logging.getLogger("langgraph_api.cli")
317
+ banner_filter = _ReplaceWelcomeBanner(welcome)
318
+ api_cli_logger.addFilter(banner_filter)
319
+ try:
320
+ run_server(
321
+ host,
322
+ port,
323
+ reload,
324
+ config_json.get("graphs", {}),
325
+ n_jobs_per_worker=n_jobs_per_worker,
326
+ open_browser=browser,
327
+ tunnel=tunnel,
328
+ debug_port=debug_port,
329
+ wait_for_client=wait_for_client,
330
+ env=config_json.get("env"),
331
+ reload_includes=includes,
332
+ reload_excludes=excludes,
333
+ store=config_json.get("store"),
334
+ auth=config_json.get("auth"),
335
+ http=http_cfg,
336
+ ui=config_json.get("ui"),
337
+ ui_config=config_json.get("ui_config"),
338
+ webhooks=config_json.get("webhooks"),
339
+ checkpointer=config_json.get("checkpointer"),
340
+ studio_url=studio_url,
341
+ disable_persistence=config_json.get("disable_persistence", False),
342
+ allow_blocking=allow_blocking,
343
+ server_level=server_log_level,
344
+ ssl_certfile=ssl_certfile,
345
+ ssl_keyfile=ssl_keyfile,
346
+ runtime_edition=RUNTIME_EDITION,
347
+ __database_uri__=database_uri,
348
+ __redis_uri__=redis_uri,
349
+ __migrations_path__=None,
350
+ **uvicorn_kwargs,
351
+ )
352
+ finally:
353
+ api_cli_logger.removeFilter(banner_filter)
354
+
355
+
356
+ if __name__ == "__main__":
357
+ cli()
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: langhost
3
+ Version: 0.11.1
4
+ Summary: Minimal CLI for self-hosted LangGraph Agent Server on langgraph-runtime-pg
5
+ Project-URL: Homepage, https://github.com/langhost/langhost
6
+ Project-URL: Repository, https://github.com/langhost/langhost
7
+ Project-URL: Issues, https://github.com/langhost/langhost/issues
8
+ Project-URL: Changelog, https://github.com/langhost/langhost/releases
9
+ Author-email: Mohankumar Ramachandran <mail@mohanram.dev>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent-protocol,agent-server,agents,ai-agents,cli,langchain,langgraph,langgraph-api,langgraph-sdk,langsmith,llm,open-source,postgres,redis,self-hosted,uvicorn
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.11
28
+ Requires-Dist: click>=8.1.7
29
+ Requires-Dist: langgraph-cli<0.5,>=0.4.0
30
+ Requires-Dist: langgraph-runtime-pg==0.11.1
31
+ Requires-Dist: pyfiglet>=1.0.0
32
+ Requires-Dist: python-dotenv>=0.8.0
33
+ Requires-Dist: uvicorn[standard]>=0.51.0
34
+ Description-Content-Type: text/markdown
35
+
36
+ # langhost
37
+
38
+ Minimal CLI for running the official LangGraph Agent Server on the open
39
+ [`langgraph-runtime-pg`](https://pypi.org/project/langgraph-runtime-pg/) backend
40
+ (Postgres + Redis).
41
+
42
+ ## Command
43
+
44
+ ```bash
45
+ langhost serve [OPTIONS]
46
+ ```
47
+
48
+ Requires a `langgraph.json` in the working directory (same format as
49
+ [`langgraph-cli`](https://github.com/langchain-ai/langgraph/tree/main/libs/cli))
50
+ and `DATABASE_URI` / `REDIS_URI` (see repo `.env.example`). Bring your own
51
+ Postgres + Redis (for example via the repo `docker-compose.yml`).
52
+
53
+ ```bash
54
+ cp .env.example .env
55
+ docker compose up -d postgres redis
56
+
57
+ # Dev (hot reload)
58
+ langhost serve --reload -c langgraph.json
59
+
60
+ # Prod (multi-process)
61
+ langhost serve --host 0.0.0.0 --workers 4 -c langgraph.json
62
+ ```
63
+
64
+ Run `langhost serve --help` for the full option list.
@@ -0,0 +1,8 @@
1
+ langhost/__init__.py,sha256=sikKdH4Ndot6yowFCsnKy_umgW6qiG1aIZWn8gPbVyc,279
2
+ langhost/__main__.py,sha256=Z_8g0qzCSaEiYYX5KPZ_0Q3YHN83PaSNYPW1op38meM,67
3
+ langhost/cli.py,sha256=5-hiDEddirPsj_Pzoy8avojtUWRk3DcwxrO8figUWbY,10774
4
+ langhost-0.11.1.dist-info/METADATA,sha256=GuzF3kUYJ2C25yybDrn59sxwZzynmeapmYFSd-0VHJ4,2442
5
+ langhost-0.11.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
6
+ langhost-0.11.1.dist-info/entry_points.txt,sha256=dYV6IfsUZmU1pCBJPoEszZzU76O1uYMd_mjpQNjW_KE,46
7
+ langhost-0.11.1.dist-info/licenses/LICENSE,sha256=VOzjTuqsyEVtwd7ylP1eoLjPAAvdB6iC9a7JBGFJUgE,1080
8
+ langhost-0.11.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ langhost = langhost.cli:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohankumar Ramachandran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.