edgeone 1.6.4-beta.1 → 1.6.4-beta.2
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.
|
@@ -13,16 +13,25 @@ from typing import Any, Callable
|
|
|
13
13
|
from urllib.parse import parse_qs
|
|
14
14
|
import httpx
|
|
15
15
|
|
|
16
|
-
# --- Observability bootstrap (
|
|
17
|
-
#
|
|
18
|
-
# or
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
# --- Observability bootstrap (deferred for fast cold start) ---
|
|
17
|
+
# Moved to _ensure_observability() — called by background preload thread
|
|
18
|
+
# or lazily on first request. This avoids blocking startup with OTel imports.
|
|
19
|
+
_obs_initialized = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _ensure_observability():
|
|
23
|
+
"""Initialize observability (idempotent). Called by preload thread or first request."""
|
|
24
|
+
global _obs_initialized
|
|
25
|
+
if _obs_initialized:
|
|
26
|
+
return
|
|
27
|
+
_obs_initialized = True
|
|
28
|
+
_obs_entries_raw = os.environ.get("AGENT_OBSERVABILITY_ENTRIES", "")
|
|
29
|
+
if _obs_entries_raw:
|
|
30
|
+
try:
|
|
31
|
+
from _platform._observability import setup as _obs_setup
|
|
32
|
+
_obs_setup(json.loads(_obs_entries_raw))
|
|
33
|
+
except Exception as _obs_err:
|
|
34
|
+
print(f"[observability] setup failed: {_obs_err}", file=sys.stderr, flush=True)
|
|
26
35
|
|
|
27
36
|
from .store import InMemoryStore, BlobBackedStore
|
|
28
37
|
from .runtime import AgentRuntime, RouteEntry
|
|
@@ -455,22 +464,58 @@ _registry: dict[str, RouteEntry] = {}
|
|
|
455
464
|
# 加载失败的路由:route_path → 失败原因。404 响应时回显,避免静默 404 难以排查。
|
|
456
465
|
_failed_routes: dict[str, str] = {}
|
|
457
466
|
|
|
467
|
+
|
|
468
|
+
# --- Lazy route loading for fast cold start ---
|
|
469
|
+
# Routes are NOT imported at startup. Instead, each route module is loaded
|
|
470
|
+
# on first access (or by the background preload thread). This avoids blocking
|
|
471
|
+
# the health check with heavy imports (langchain, langgraph, pydantic, etc.).
|
|
472
|
+
|
|
473
|
+
class _LazyRouteEntry:
|
|
474
|
+
"""Lazy-loading wrapper for RouteEntry. Defers importlib.import_module() to first access."""
|
|
475
|
+
|
|
476
|
+
__slots__ = ('_module_path', '_is_index', '_handler', '_loaded', '_load_error')
|
|
477
|
+
|
|
478
|
+
def __init__(self, module_path: str, is_index: bool):
|
|
479
|
+
self._module_path = module_path
|
|
480
|
+
self._is_index = is_index
|
|
481
|
+
self._handler = None
|
|
482
|
+
self._loaded = False
|
|
483
|
+
self._load_error: str | None = None
|
|
484
|
+
|
|
485
|
+
@property
|
|
486
|
+
def handler(self):
|
|
487
|
+
if not self._loaded:
|
|
488
|
+
self._do_load()
|
|
489
|
+
if self._load_error:
|
|
490
|
+
raise ImportError(self._load_error)
|
|
491
|
+
return self._handler
|
|
492
|
+
|
|
493
|
+
@property
|
|
494
|
+
def is_index(self):
|
|
495
|
+
return self._is_index
|
|
496
|
+
|
|
497
|
+
@property
|
|
498
|
+
def module_path(self):
|
|
499
|
+
return self._module_path
|
|
500
|
+
|
|
501
|
+
def _do_load(self):
|
|
502
|
+
"""Actually import the module and resolve the handler."""
|
|
503
|
+
try:
|
|
504
|
+
mod = importlib.import_module(self._module_path)
|
|
505
|
+
handler = getattr(mod, "handler", None)
|
|
506
|
+
if handler is None:
|
|
507
|
+
self._load_error = f"module '{self._module_path}' has no 'handler' function"
|
|
508
|
+
else:
|
|
509
|
+
self._handler = handler
|
|
510
|
+
except Exception as e:
|
|
511
|
+
self._load_error = f"failed to import '{self._module_path}': {e}"
|
|
512
|
+
self._loaded = True
|
|
513
|
+
|
|
514
|
+
|
|
458
515
|
for route_path, info in _route_table_raw.items():
|
|
459
516
|
module_path = info["module"]
|
|
460
517
|
is_index = info.get("isIndex", False)
|
|
461
|
-
|
|
462
|
-
mod = importlib.import_module(module_path)
|
|
463
|
-
handler = getattr(mod, "handler", None)
|
|
464
|
-
if handler is None:
|
|
465
|
-
reason = f"module '{module_path}' has no 'handler' function"
|
|
466
|
-
print(f"[agent-python] WARNING: {reason}, skipping", file=sys.stderr, flush=True)
|
|
467
|
-
_failed_routes[route_path] = reason
|
|
468
|
-
continue
|
|
469
|
-
_registry[route_path] = RouteEntry(handler=handler, is_index=is_index, module_path=module_path)
|
|
470
|
-
except Exception as e:
|
|
471
|
-
reason = f"failed to import '{module_path}': {e}"
|
|
472
|
-
print(f"[agent-python] ERROR loading {module_path}: {e}", file=sys.stderr, flush=True)
|
|
473
|
-
_failed_routes[route_path] = reason
|
|
518
|
+
_registry[route_path] = _LazyRouteEntry(module_path, is_index)
|
|
474
519
|
|
|
475
520
|
_runtime = AgentRuntime(
|
|
476
521
|
registry=_registry,
|
|
@@ -480,13 +525,46 @@ _runtime = AgentRuntime(
|
|
|
480
525
|
failed_routes=_failed_routes,
|
|
481
526
|
)
|
|
482
527
|
|
|
483
|
-
print(f"[agent-python]
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
528
|
+
print(f"[agent-python] Registered {len(_registry)} routes (lazy), timeout={_timeout}s", file=sys.stderr, flush=True)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# --- Background preload thread ---
|
|
532
|
+
# After uvicorn binds the port and health check passes, this thread
|
|
533
|
+
# pre-imports all route modules in the background. If a request arrives
|
|
534
|
+
# before preload completes, the lazy handler property will block and
|
|
535
|
+
# import synchronously (no failure, just slower first request).
|
|
536
|
+
|
|
537
|
+
import threading
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _background_preload():
|
|
541
|
+
"""Preload routes + observability in background after startup."""
|
|
542
|
+
import time
|
|
543
|
+
time.sleep(0.05) # yield to let uvicorn finish port binding
|
|
544
|
+
|
|
545
|
+
# 1. Initialize observability first (lighter, needed for spans)
|
|
546
|
+
_ensure_observability()
|
|
547
|
+
|
|
548
|
+
# 2. Trigger lazy loading for all routes
|
|
549
|
+
for route_path, entry in _registry.items():
|
|
550
|
+
try:
|
|
551
|
+
_ = entry.handler
|
|
552
|
+
except Exception as e:
|
|
553
|
+
_failed_routes[route_path] = str(e)
|
|
554
|
+
print(f"[agent-python] ERROR loading {entry.module_path}: {e}", file=sys.stderr, flush=True)
|
|
555
|
+
|
|
556
|
+
loaded_count = sum(1 for e in _registry.values() if e._loaded and e._load_error is None)
|
|
557
|
+
if _failed_routes:
|
|
558
|
+
print(
|
|
559
|
+
f"[agent-python] Preload done: {loaded_count}/{len(_registry)} routes loaded, "
|
|
560
|
+
f"{len(_failed_routes)} failed: {', '.join(_failed_routes.keys())}",
|
|
561
|
+
file=sys.stderr, flush=True,
|
|
562
|
+
)
|
|
563
|
+
else:
|
|
564
|
+
print(f"[agent-python] Preload done: {loaded_count}/{len(_registry)} routes loaded", file=sys.stderr, flush=True)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
threading.Thread(target=_background_preload, daemon=True, name="agent-preload").start()
|
|
490
568
|
|
|
491
569
|
|
|
492
570
|
# --- ASGI app ---
|
|
@@ -511,6 +589,11 @@ async def app(scope: dict, receive, send) -> None:
|
|
|
511
589
|
if scope["type"] != "http":
|
|
512
590
|
return
|
|
513
591
|
|
|
592
|
+
# Ensure observability is initialized before processing requests.
|
|
593
|
+
# Usually already done by background preload, but if a request arrives
|
|
594
|
+
# before preload completes, this ensures instrumentation is active.
|
|
595
|
+
_ensure_observability()
|
|
596
|
+
|
|
514
597
|
# Read body
|
|
515
598
|
body_parts = []
|
|
516
599
|
while True:
|