merge: bring upstream v0.3.0 (72 commits) into Ai4Sci fork
Merged upstream/main (418abca, release v0.3.0) into our fork on a
dedicated branch. 21 conflicting files resolved; main worktree untouched.
Resolution policy and key decisions:
- Keep Ai4Sci runtime endpoints, durable dispatch, workspace scopes and
the HITL/DynamicReview approval chain (approval path is product-critical).
- Adopt upstream model registry (llm/registry.py): our 136 model entries
are a strict subset of upstream's 180, so dropping our inline table
loses nothing and gains 44 new models.
- Adopt upstream native EvoChatDeepSeek; drop our obsolete
_patch_deepseek_reasoning_passback monkey patch.
- Keep our six patches.py additions, ported onto upstream's new
_OpenAICompatContent class: stable tool-call ids, tool-history
sanitization, drop_reasoning_metadata, empty-SSE keepalive,
extracted-document-text patch, _has_assistant_tool_protocol.
- Keep our skill-budget middleware path (skills=None) instead of passing
skills through, to avoid double loading.
- Keep sanitized error labels (_safe_error_label) while adopting
upstream's injected MiddlewareEventSink for fallback narration.
- Keep port 3076 and the LANGGRAPH_SERVER_URL override; adopt upstream's
host/probe-host handling and CONFIG_DRIFT_SINCE_LAUNCH.
- Adopt upstream dependency stack: deepagents 0.7.6, langchain-quickjs
0.3.7, langgraph-api 0.14; keep our extra deps (rfc8785, pillow,
firecrawl-anydoc, nest-asyncio).
- Align call sites with upstream APIs: create_tool_selector_middleware
now takes events= instead of track_stream_selection=.
This commit is contained in:
@@ -46,6 +46,13 @@ def deploy(
|
||||
"--port",
|
||||
help="Port for langgraph dev (default: config.langgraph_dev_port = 3076)",
|
||||
),
|
||||
host: str | None = typer.Option(
|
||||
None,
|
||||
"--host",
|
||||
help="Interface to bind (default: config.langgraph_dev_host = "
|
||||
"127.0.0.1, i.e. this machine only — pass 0.0.0.0 to reach it from "
|
||||
"other machines, but note the server has no auth)",
|
||||
),
|
||||
tunnel: bool = typer.Option(
|
||||
False,
|
||||
"--tunnel",
|
||||
@@ -66,9 +73,15 @@ def deploy(
|
||||
"""
|
||||
from ..config import apply_config_to_env, get_effective_config
|
||||
from ..langgraph_dev.manager import (
|
||||
_DEFAULT_HOST,
|
||||
_DEFAULT_PORT,
|
||||
RUNTIME,
|
||||
_base_url,
|
||||
_is_loopback_host,
|
||||
_is_port_occupied,
|
||||
_pid_serves_port,
|
||||
_read_workspace_sidecar,
|
||||
_server_config_fingerprint,
|
||||
is_langgraph_dev_running,
|
||||
read_tunnel_url,
|
||||
start_langgraph_dev,
|
||||
@@ -114,20 +127,46 @@ def deploy(
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# A blank ``--host`` means "not passed" (matching serve), so it can never
|
||||
# discard the configured bind. Both branches strip: whitespace reaching
|
||||
# socket.bind() surfaces as an opaque gaierror, and duck-typed configs
|
||||
# handed to this function never ran ``__post_init__`` normalization.
|
||||
cli_host = host.strip() if host is not None else ""
|
||||
effective_host = (
|
||||
cli_host
|
||||
or str(getattr(config, "langgraph_dev_host", _DEFAULT_HOST) or "").strip()
|
||||
or _DEFAULT_HOST
|
||||
)
|
||||
|
||||
# 4. Pre-flight port check — refuse to start if a non-EvoSci process is
|
||||
# holding the port. If an existing EvoSci langgraph dev is already up,
|
||||
# also refuse (deploy is the "primary server" — running multiple on the
|
||||
# same port is a configuration error).
|
||||
if _is_port_occupied(effective_port):
|
||||
if is_langgraph_dev_running(port=effective_port):
|
||||
if _is_port_occupied(effective_port, effective_host):
|
||||
if is_langgraph_dev_running(port=effective_port, host=effective_host):
|
||||
console.print(
|
||||
f"[red]Port {effective_port} is already serving a langgraph dev "
|
||||
f"instance.[/red]"
|
||||
)
|
||||
console.print(
|
||||
"[dim]Stop the existing EvoSci/serve session first, or use "
|
||||
"[bold]--port[/bold] to deploy on a different port.[/dim]"
|
||||
)
|
||||
sidecar = _read_workspace_sidecar()
|
||||
if sidecar is not None and _pid_serves_port(
|
||||
sidecar.get("pid"), effective_port
|
||||
):
|
||||
# Surface what we know about the occupant — with keepalive it
|
||||
# may be an ownerless leftover rather than a live session.
|
||||
# Only when the recorded pid verifiably serves THIS port, so a
|
||||
# stale or other-port record is never blamed.
|
||||
console.print(
|
||||
f"[dim]It serves workspace {sidecar.get('workspace')} "
|
||||
f"(pid {sidecar.get('pid')}). Stop it with "
|
||||
f"[bold]EvoSci server stop[/bold], or use "
|
||||
f"[bold]--port[/bold] to deploy on a different port.[/dim]"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
"[dim]Stop the existing EvoSci/serve session first, or use "
|
||||
"[bold]--port[/bold] to deploy on a different port.[/dim]"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
f"[red]Port {effective_port} is occupied by another process.[/red]"
|
||||
@@ -144,6 +183,7 @@ def deploy(
|
||||
Panel(
|
||||
Text.from_markup(
|
||||
f"[bold]Workspace:[/bold] {_shorten(ws)}\n"
|
||||
f"[bold]Host:[/bold] {effective_host}\n"
|
||||
f"[bold]Port:[/bold] {effective_port}\n"
|
||||
f"[bold]Auth:[/bold] {_auth_label}"
|
||||
),
|
||||
@@ -162,6 +202,13 @@ def deploy(
|
||||
f"[bold red]{DANGEROUS_BANNER_MESSAGE}[/bold red]"
|
||||
)
|
||||
|
||||
if not _is_loopback_host(effective_host):
|
||||
console.print(
|
||||
"[bold white on red] ⚠ PUBLIC BIND [/bold white on red] "
|
||||
f"[bold red]Listening on {effective_host} — no auth, and the agent "
|
||||
f"can run shell. Trusted networks only.[/bold red]"
|
||||
)
|
||||
|
||||
if tunnel:
|
||||
console.print(
|
||||
"[bold white on red] ⚠ PUBLIC TUNNEL [/bold white on red] "
|
||||
@@ -197,10 +244,12 @@ def deploy(
|
||||
proc = start_langgraph_dev(
|
||||
workspace_dir=Path(ws),
|
||||
port=effective_port,
|
||||
host=effective_host,
|
||||
file_persistence=file_persistence,
|
||||
jobs_per_worker=jobs_per_worker,
|
||||
deploy_mode=True,
|
||||
tunnel=tunnel,
|
||||
config_fingerprint=_server_config_fingerprint(config),
|
||||
)
|
||||
atexit.register(stop_langgraph_dev, proc)
|
||||
except Exception as exc:
|
||||
@@ -236,7 +285,7 @@ def deploy(
|
||||
Panel(
|
||||
Text.from_markup(
|
||||
f"[bold]Endpoint:[/bold] "
|
||||
f"http://localhost:{effective_port}\n"
|
||||
f"{_base_url(effective_port, effective_host)}\n"
|
||||
f"{public_line}"
|
||||
f"[bold]Assistant ID:[/bold] EvoScientist\n"
|
||||
f"[bold]Connect via:[/bold] any LangChain SDK / "
|
||||
|
||||
@@ -42,6 +42,7 @@ from ..stream.console import console
|
||||
# Front-end npm package + spec. ``@latest`` → always the newest published UI.
|
||||
_WEBUI_PACKAGE = "@evoscientist/webui@latest"
|
||||
_DEFAULT_WEBUI_PORT = 4716
|
||||
_DEFAULT_WEBUI_HOST = "127.0.0.1"
|
||||
|
||||
|
||||
def run_webui(config: Any, workspace_dir: str | None = None) -> None:
|
||||
@@ -58,10 +59,15 @@ def run_webui(config: Any, workspace_dir: str | None = None) -> None:
|
||||
"""
|
||||
from ..config import apply_config_to_env
|
||||
from ..langgraph_dev.manager import (
|
||||
_DEFAULT_HOST,
|
||||
_DEFAULT_PORT,
|
||||
RUNTIME,
|
||||
_base_url,
|
||||
_format_hostport,
|
||||
_is_loopback_host,
|
||||
_is_port_occupied,
|
||||
_read_workspace_sidecar,
|
||||
_server_config_fingerprint,
|
||||
is_langgraph_dev_running,
|
||||
start_langgraph_dev,
|
||||
stop_langgraph_dev,
|
||||
@@ -84,6 +90,14 @@ def run_webui(config: Any, workspace_dir: str | None = None) -> None:
|
||||
# webui_port = the local Next.js server the browser actually opens.
|
||||
backend_port = int(getattr(config, "langgraph_dev_port", _DEFAULT_PORT))
|
||||
webui_port = int(getattr(config, "webui_port", _DEFAULT_WEBUI_PORT))
|
||||
# ...and their bind interfaces, both loopback by default — the front-end
|
||||
# carries workspace/skill APIs of its own (see config.webui_host).
|
||||
backend_host = (
|
||||
str(getattr(config, "langgraph_dev_host", _DEFAULT_HOST) or _DEFAULT_HOST)
|
||||
).strip() or _DEFAULT_HOST
|
||||
webui_host = (
|
||||
str(getattr(config, "webui_host", _DEFAULT_WEBUI_HOST) or _DEFAULT_WEBUI_HOST)
|
||||
).strip() or _DEFAULT_WEBUI_HOST
|
||||
for label, p in (("langgraph dev", backend_port), ("WebUI", webui_port)):
|
||||
if not (1 <= p <= 65535):
|
||||
console.print(
|
||||
@@ -128,8 +142,8 @@ def run_webui(config: Any, workspace_dir: str | None = None) -> None:
|
||||
# else start a fresh deploy-mode one (full MCP + async). Refuse a foreign
|
||||
# occupant — that's a configuration error, not something to silently share.
|
||||
started_proc = None
|
||||
if _is_port_occupied(backend_port):
|
||||
if is_langgraph_dev_running(port=backend_port):
|
||||
if _is_port_occupied(backend_port, backend_host):
|
||||
if is_langgraph_dev_running(port=backend_port, host=backend_host):
|
||||
# Reuse an existing EvoSci server only when it serves THIS workspace
|
||||
# — mirror the sidecar guard in ensure_langgraph_dev so WebUI started
|
||||
# from workspace B never silently binds to a server pinned to
|
||||
@@ -150,6 +164,31 @@ def run_webui(config: Any, workspace_dir: str | None = None) -> None:
|
||||
f"[/dim]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
if sidecar is not None and sidecar.get("deploy_mode") is False:
|
||||
# A stripped (CLI-started) server has no MCP and no async
|
||||
# sub-agents — silently reusing it would degrade the WebUI
|
||||
# with no visible cause. Refuse; never auto-kill.
|
||||
console.print(
|
||||
f"[red]Port {backend_port} is serving a stripped "
|
||||
f"(CLI-mode) langgraph dev — the WebUI needs the full "
|
||||
f"deploy-mode server (MCP + async sub-agents).[/red]"
|
||||
)
|
||||
console.print(
|
||||
"[dim]Stop it with [bold]EvoSci server stop[/bold], then "
|
||||
"re-run [bold]EvoSci[/bold].[/dim]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
if sidecar is not None:
|
||||
recorded_fp = sidecar.get("config_fingerprint")
|
||||
if isinstance(
|
||||
recorded_fp, str
|
||||
) and recorded_fp != _server_config_fingerprint(config):
|
||||
console.print(
|
||||
"[yellow]⚠ Config changed since this server was "
|
||||
"launched — it still serves the old settings. Apply "
|
||||
"them with [bold]EvoSci server stop[/bold], then "
|
||||
"re-run EvoSci.[/yellow]"
|
||||
)
|
||||
console.print(
|
||||
f"[green]✓[/green] Reusing langgraph dev already serving "
|
||||
f"port {backend_port}"
|
||||
@@ -174,17 +213,28 @@ def run_webui(config: Any, workspace_dir: str | None = None) -> None:
|
||||
started_proc = start_langgraph_dev(
|
||||
workspace_dir=Path(ws),
|
||||
port=backend_port,
|
||||
host=backend_host,
|
||||
file_persistence=file_persistence,
|
||||
jobs_per_worker=jobs_per_worker,
|
||||
deploy_mode=True,
|
||||
config_fingerprint=_server_config_fingerprint(config),
|
||||
)
|
||||
atexit.register(stop_langgraph_dev, started_proc)
|
||||
if getattr(config, "langgraph_dev_keepalive", False):
|
||||
# Keepalive: the deploy-mode backend outlives this session so
|
||||
# the next same-workspace launch reuses it instantly. The npx
|
||||
# front-end below still stops on exit as usual.
|
||||
console.print(
|
||||
"[dim]keepalive: backend server stays up after exit — "
|
||||
"stop it with [bold]EvoSci server stop[/bold].[/dim]"
|
||||
)
|
||||
else:
|
||||
atexit.register(stop_langgraph_dev, started_proc)
|
||||
except Exception as exc:
|
||||
console.print(f"[red]langgraph dev startup failed:[/red] {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
console.print("[green]✓[/green] langgraph dev ready")
|
||||
|
||||
if _is_port_occupied(webui_port):
|
||||
if _is_port_occupied(webui_port, webui_host):
|
||||
console.print(
|
||||
f"[yellow]⚠ Port {webui_port} is already in use; the WebUI server "
|
||||
f"may fail to start. Change it with "
|
||||
@@ -197,20 +247,37 @@ def run_webui(config: Any, workspace_dir: str | None = None) -> None:
|
||||
# inherited so it all shows in THIS terminal. EVOSCIENTIST_LANGGRAPH_DEV_PORT
|
||||
# lets the UI's config prefill point at our backend automatically. Secrets
|
||||
# are scrubbed — the browser UI never needs LLM provider API keys.
|
||||
#
|
||||
# HOSTNAME is the front-end's only bind knob: the package has no --host
|
||||
# flag; its launcher forwards `HOSTNAME || "127.0.0.1"` to the Next server.
|
||||
webui_env = _scrubbed_env(
|
||||
{
|
||||
"EVOSCIENTIST_LANGGRAPH_DEV_PORT": str(backend_port),
|
||||
"PORT": str(webui_port),
|
||||
"HOSTNAME": webui_host,
|
||||
}
|
||||
)
|
||||
# The UI reaches the backend from the BROWSER; when only the front-end is
|
||||
# exposed, remote pages load but every request fails — say so.
|
||||
remote_backend_hint = ""
|
||||
if not _is_loopback_host(webui_host) and _is_loopback_host(backend_host):
|
||||
remote_backend_hint = (
|
||||
f"\n[yellow]Note:[/yellow] the UI connects to the backend from the "
|
||||
f"browser. Remote visitors cannot reach a loopback backend — run "
|
||||
f"[bold]EvoSci config set langgraph_dev_host 0.0.0.0[/bold] and "
|
||||
f"point the UI at [bold]http://<this-machine-ip>:{backend_port}"
|
||||
f"[/bold].\n"
|
||||
)
|
||||
console.print(
|
||||
Panel(
|
||||
Text.from_markup(
|
||||
f"[bold]Backend:[/bold] http://localhost:{backend_port} "
|
||||
f"[bold]Backend:[/bold] {_base_url(backend_port, backend_host)} "
|
||||
f"[dim](langgraph dev — Assistant: EvoScientist)[/dim]\n"
|
||||
f"[bold]WebUI:[/bold] http://localhost:{webui_port} "
|
||||
f"[bold]WebUI:[/bold] "
|
||||
f"http://{_format_hostport(webui_host, webui_port)} "
|
||||
f"[dim](opens in your browser)[/dim]\n"
|
||||
f"[bold]Logs:[/bold] {_shorten(str(RUNTIME.log_file))}\n\n"
|
||||
f"[bold]Logs:[/bold] {_shorten(str(RUNTIME.log_file))}\n"
|
||||
f"{remote_backend_hint}\n"
|
||||
f"[dim]Fetching {_WEBUI_PACKAGE} via npx (first run may take a "
|
||||
f"moment)… Press Ctrl+C to stop.[/dim]"
|
||||
),
|
||||
@@ -218,6 +285,19 @@ def run_webui(config: Any, workspace_dir: str | None = None) -> None:
|
||||
border_style="green",
|
||||
)
|
||||
)
|
||||
if not _is_loopback_host(backend_host):
|
||||
console.print(
|
||||
"[bold white on red] ⚠ PUBLIC BIND [/bold white on red] "
|
||||
f"[bold red]Backend listening on {backend_host} — no auth, and the "
|
||||
f"agent can run shell. Trusted networks only.[/bold red]"
|
||||
)
|
||||
if not _is_loopback_host(webui_host):
|
||||
console.print(
|
||||
"[bold white on red] ⚠ PUBLIC BIND [/bold white on red] "
|
||||
f"[bold red]WebUI listening on {webui_host} — its API reads, writes "
|
||||
f"and uploads workspace files and installs skills, with no auth. "
|
||||
f"Trusted networks only.[/bold red]"
|
||||
)
|
||||
|
||||
popen_kwargs: dict[str, Any] = {"env": webui_env}
|
||||
if os.name == "posix":
|
||||
|
||||
Reference in New Issue
Block a user