edgeone 1.6.2 → 1.6.3-beta.1
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.
- package/edgeone-bin/edgeone.js +3 -3
- package/edgeone-dist/cli.js +2365 -1158
- package/edgeone-dist/pages/dev/runner-worker.js +2422 -1189
- package/edgeone-dist/pages/templates/agent-python/__pycache__/memory.cpython-313.pyc +0 -0
- package/edgeone-dist/pages/templates/agent-python/adapter.py +164 -32
- package/edgeone-dist/pages/templates/agent-python/context.py +15 -0
- package/edgeone-dist/pages/templates/agent-python/memory.py +272 -101
- package/edgeone-dist/pages/templates/agent-python/runtime.py +19 -13
- package/edgeone-dist/studio/ui/assets/agent-obs-BpTIot8x.js +4 -0
- package/edgeone-dist/studio/ui/assets/agent-obs-CEOY4DZz.css +1 -0
- package/edgeone-dist/studio/ui/assets/{index-DD3d108t.js → index-DN8YhmEp.js} +1 -1
- package/edgeone-dist/studio/ui/index.html +3 -3
- package/package.json +1 -1
- package/edgeone-dist/studio/ui/assets/agent-obs-Dvi4IpEy.js +0 -4
- package/edgeone-dist/studio/ui/assets/agent-obs-qDJCE0TQ.css +0 -1
|
Binary file
|
|
@@ -13,20 +13,76 @@ 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
|
|
29
|
-
from .context import StreamResponse
|
|
38
|
+
from .context import EoContext, StreamResponse
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# --- Geo parsing (aligns with Node agent-node.ts parseEoConnectingGeo) ---
|
|
42
|
+
|
|
43
|
+
def _parse_eo(headers: dict) -> EoContext:
|
|
44
|
+
"""Parse eo-connecting-geo header into structured EoContext.
|
|
45
|
+
|
|
46
|
+
Format: key=value key="quoted value"
|
|
47
|
+
Aligns with Node Agent parseEoConnectingGeo logic.
|
|
48
|
+
"""
|
|
49
|
+
import urllib.parse as _up
|
|
50
|
+
|
|
51
|
+
geo_str = ""
|
|
52
|
+
if isinstance(headers, dict):
|
|
53
|
+
geo_str = _up.unquote(headers.get("eo-connecting-geo", ""))
|
|
54
|
+
if not geo_str:
|
|
55
|
+
return EoContext(geo={}, client_ip=str(headers.get("eo-connecting-ip", "")) if isinstance(headers, dict) else "")
|
|
56
|
+
|
|
57
|
+
result = {}
|
|
58
|
+
matches = __import__("re").findall(r'[a-z_]+="[^"]*"|[a-z_]+=[A-Za-z0-9.-]+', geo_str)
|
|
59
|
+
for m in matches:
|
|
60
|
+
key, val = m.split("=", 1)
|
|
61
|
+
result[key] = val.strip('"')
|
|
62
|
+
|
|
63
|
+
geo = {}
|
|
64
|
+
if "asn" in result:
|
|
65
|
+
geo["asn"] = result["asn"]
|
|
66
|
+
if "nation_name" in result:
|
|
67
|
+
geo["countryName"] = result["nation_name"]
|
|
68
|
+
if "region_code" in result:
|
|
69
|
+
geo["regionCode"] = result["region_code"]
|
|
70
|
+
geo["countryCodeAlpha2"] = result["region_code"].split("-")[0]
|
|
71
|
+
if "nation_numeric" in result:
|
|
72
|
+
geo["countryCodeNumeric"] = result["nation_numeric"]
|
|
73
|
+
if "region_name" in result:
|
|
74
|
+
geo["regionName"] = result["region_name"]
|
|
75
|
+
if "city_name" in result:
|
|
76
|
+
geo["cityName"] = result["city_name"]
|
|
77
|
+
if "latitude" in result:
|
|
78
|
+
geo["latitude"] = result["latitude"]
|
|
79
|
+
if "longitude" in result:
|
|
80
|
+
geo["longitude"] = result["longitude"]
|
|
81
|
+
if "network_operator" in result:
|
|
82
|
+
geo["cisp"] = result["network_operator"]
|
|
83
|
+
|
|
84
|
+
client_ip = str(headers.get("eo-connecting-ip", "")) if isinstance(headers, dict) else ""
|
|
85
|
+
return EoContext(geo=geo, client_ip=client_ip)
|
|
30
86
|
|
|
31
87
|
|
|
32
88
|
# --- Platform response headers (align with Node agent + python-function) ---
|
|
@@ -408,22 +464,58 @@ _registry: dict[str, RouteEntry] = {}
|
|
|
408
464
|
# 加载失败的路由:route_path → 失败原因。404 响应时回显,避免静默 404 难以排查。
|
|
409
465
|
_failed_routes: dict[str, str] = {}
|
|
410
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
|
+
|
|
411
515
|
for route_path, info in _route_table_raw.items():
|
|
412
516
|
module_path = info["module"]
|
|
413
517
|
is_index = info.get("isIndex", False)
|
|
414
|
-
|
|
415
|
-
mod = importlib.import_module(module_path)
|
|
416
|
-
handler = getattr(mod, "handler", None)
|
|
417
|
-
if handler is None:
|
|
418
|
-
reason = f"module '{module_path}' has no 'handler' function"
|
|
419
|
-
print(f"[agent-python] WARNING: {reason}, skipping", file=sys.stderr, flush=True)
|
|
420
|
-
_failed_routes[route_path] = reason
|
|
421
|
-
continue
|
|
422
|
-
_registry[route_path] = RouteEntry(handler=handler, is_index=is_index, module_path=module_path)
|
|
423
|
-
except Exception as e:
|
|
424
|
-
reason = f"failed to import '{module_path}': {e}"
|
|
425
|
-
print(f"[agent-python] ERROR loading {module_path}: {e}", file=sys.stderr, flush=True)
|
|
426
|
-
_failed_routes[route_path] = reason
|
|
518
|
+
_registry[route_path] = _LazyRouteEntry(module_path, is_index)
|
|
427
519
|
|
|
428
520
|
_runtime = AgentRuntime(
|
|
429
521
|
registry=_registry,
|
|
@@ -433,13 +525,46 @@ _runtime = AgentRuntime(
|
|
|
433
525
|
failed_routes=_failed_routes,
|
|
434
526
|
)
|
|
435
527
|
|
|
436
|
-
print(f"[agent-python]
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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()
|
|
443
568
|
|
|
444
569
|
|
|
445
570
|
# --- ASGI app ---
|
|
@@ -464,6 +589,11 @@ async def app(scope: dict, receive, send) -> None:
|
|
|
464
589
|
if scope["type"] != "http":
|
|
465
590
|
return
|
|
466
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
|
+
|
|
467
597
|
# Read body
|
|
468
598
|
body_parts = []
|
|
469
599
|
while True:
|
|
@@ -571,7 +701,9 @@ async def app(scope: dict, receive, send) -> None:
|
|
|
571
701
|
except (ImportError, Exception):
|
|
572
702
|
pass
|
|
573
703
|
|
|
574
|
-
dispatch_task = asyncio.create_task(
|
|
704
|
+
dispatch_task = asyncio.create_task(
|
|
705
|
+
_runtime.handle(path, method, headers, query, body, eo=_parse_eo(headers))
|
|
706
|
+
)
|
|
575
707
|
|
|
576
708
|
# 用 Event 让 watcher 区分「流被业务自己 cancel 了」和「client 真的断开」:
|
|
577
709
|
# 流式 generator 内部 await asyncio.sleep 时如果业务调了 task.cancel,
|
|
@@ -105,12 +105,24 @@ def _resolve_sandbox_api_env(env: Mapping[str, str], configured_api_env: str = "
|
|
|
105
105
|
return ""
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
@dataclass
|
|
109
|
+
class EoContext:
|
|
110
|
+
"""EdgeOne 请求元数据,与 Node Agent 的 AgentEoContext 对齐。
|
|
111
|
+
|
|
112
|
+
包含从 eo-connecting-geo / eo-connecting-ip 请求头解析的地理位置信息。
|
|
113
|
+
在 adapter 层解析后传入 runtime,挂载到 ctx.eo 和 ctx.request.eo。
|
|
114
|
+
"""
|
|
115
|
+
geo: dict = field(default_factory=dict)
|
|
116
|
+
client_ip: str = ""
|
|
117
|
+
|
|
118
|
+
|
|
108
119
|
@dataclass
|
|
109
120
|
class RequestInfo:
|
|
110
121
|
body: dict
|
|
111
122
|
headers: dict
|
|
112
123
|
query: dict # URL query parameters, e.g. ?foo=bar → {"foo": "bar"}
|
|
113
124
|
signal: asyncio.Event # is_set() → cancelled
|
|
125
|
+
eo: EoContext = field(default_factory=EoContext)
|
|
114
126
|
|
|
115
127
|
@property
|
|
116
128
|
def is_cancelled(self) -> bool:
|
|
@@ -143,6 +155,9 @@ class AgentContext:
|
|
|
143
155
|
env: dict
|
|
144
156
|
kv: Any # Per-route KV store (BlobBackedStore or InMemoryStore)
|
|
145
157
|
agents: Any # AgentsApi (avoid circular import)
|
|
158
|
+
# EdgeOne 请求元数据,与 Node Agent ctx.eo 对齐
|
|
159
|
+
# 包含 geo(地理位置)和 client_ip(客户端 IP),从 eo-connecting-geo/ip 请求头解析
|
|
160
|
+
eo: EoContext = field(default_factory=EoContext)
|
|
146
161
|
# 同 conversation 的活跃 run_id(如果有)。供业务自管并发场景使用:
|
|
147
162
|
# 对齐 Node `AgentContext.active_run_id`,业务在自定义 stop / cancel handler 中
|
|
148
163
|
# 可以根据这个字段判断是否需要主动 abort 旧 run;非 index 路由不会自动 409。
|