agently-devtools 0.0.1a0__tar.gz

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.
Files changed (53) hide show
  1. agently_devtools-0.0.1a0/PKG-INFO +83 -0
  2. agently_devtools-0.0.1a0/README.md +66 -0
  3. agently_devtools-0.0.1a0/agently_devtools/__init__.py +21 -0
  4. agently_devtools-0.0.1a0/agently_devtools/_agently_compat.py +55 -0
  5. agently_devtools-0.0.1a0/agently_devtools/app.py +197 -0
  6. agently_devtools-0.0.1a0/agently_devtools/bridge.c +17233 -0
  7. agently_devtools-0.0.1a0/agently_devtools/bridge.py +194 -0
  8. agently_devtools-0.0.1a0/agently_devtools/cli.py +49 -0
  9. agently_devtools-0.0.1a0/agently_devtools/evaluations.py +850 -0
  10. agently_devtools-0.0.1a0/agently_devtools/integrations/__init__.py +1 -0
  11. agently_devtools-0.0.1a0/agently_devtools/integrations/observation.c +64439 -0
  12. agently_devtools-0.0.1a0/agently_devtools/integrations/observation.py +1625 -0
  13. agently_devtools-0.0.1a0/agently_devtools/logs/LogService.py +402 -0
  14. agently_devtools-0.0.1a0/agently_devtools/logs/Query.py +37 -0
  15. agently_devtools-0.0.1a0/agently_devtools/logs/__init__.py +2 -0
  16. agently_devtools-0.0.1a0/agently_devtools/observation/ObservationBackend.c +11553 -0
  17. agently_devtools-0.0.1a0/agently_devtools/observation/ObservationBackend.py +29 -0
  18. agently_devtools-0.0.1a0/agently_devtools/observation/ObservationService.c +27679 -0
  19. agently_devtools-0.0.1a0/agently_devtools/observation/ObservationService.py +475 -0
  20. agently_devtools-0.0.1a0/agently_devtools/observation/Query.c +5741 -0
  21. agently_devtools-0.0.1a0/agently_devtools/observation/Query.py +61 -0
  22. agently_devtools-0.0.1a0/agently_devtools/observation/RunStore.c +21197 -0
  23. agently_devtools-0.0.1a0/agently_devtools/observation/RunStore.py +324 -0
  24. agently_devtools-0.0.1a0/agently_devtools/observation/StoreBackend.py +119 -0
  25. agently_devtools-0.0.1a0/agently_devtools/observation/__init__.py +19 -0
  26. agently_devtools-0.0.1a0/agently_devtools/observation/backends/JSONLBackend.c +12351 -0
  27. agently_devtools-0.0.1a0/agently_devtools/observation/backends/JSONLBackend.py +44 -0
  28. agently_devtools-0.0.1a0/agently_devtools/observation/backends/RemoteBackend.c +12741 -0
  29. agently_devtools-0.0.1a0/agently_devtools/observation/backends/RemoteBackend.py +79 -0
  30. agently_devtools-0.0.1a0/agently_devtools/observation/backends/__init__.py +17 -0
  31. agently_devtools-0.0.1a0/agently_devtools/rust_core.py +39 -0
  32. agently_devtools-0.0.1a0/agently_devtools/static/.gitkeep +1 -0
  33. agently_devtools-0.0.1a0/agently_devtools/static/assets/index-Dnpkc26K.js +765 -0
  34. agently_devtools-0.0.1a0/agently_devtools/static/assets/index-ar1q8gZ0.css +1 -0
  35. agently_devtools-0.0.1a0/agently_devtools/static/index.html +13 -0
  36. agently_devtools-0.0.1a0/agently_devtools/ui.py +539 -0
  37. agently_devtools-0.0.1a0/agently_devtools.egg-info/PKG-INFO +83 -0
  38. agently_devtools-0.0.1a0/agently_devtools.egg-info/SOURCES.txt +51 -0
  39. agently_devtools-0.0.1a0/agently_devtools.egg-info/dependency_links.txt +1 -0
  40. agently_devtools-0.0.1a0/agently_devtools.egg-info/entry_points.txt +2 -0
  41. agently_devtools-0.0.1a0/agently_devtools.egg-info/not-zip-safe +1 -0
  42. agently_devtools-0.0.1a0/agently_devtools.egg-info/requires.txt +10 -0
  43. agently_devtools-0.0.1a0/agently_devtools.egg-info/top_level.txt +1 -0
  44. agently_devtools-0.0.1a0/pyproject.toml +40 -0
  45. agently_devtools-0.0.1a0/setup.cfg +4 -0
  46. agently_devtools-0.0.1a0/setup.py +85 -0
  47. agently_devtools-0.0.1a0/tests/test_cli.py +25 -0
  48. agently_devtools-0.0.1a0/tests/test_evaluations.py +335 -0
  49. agently_devtools-0.0.1a0/tests/test_log_service.py +237 -0
  50. agently_devtools-0.0.1a0/tests/test_observation_integration.py +533 -0
  51. agently_devtools-0.0.1a0/tests/test_observation_store_backend.py +49 -0
  52. agently_devtools-0.0.1a0/tests/test_playground.py +396 -0
  53. agently_devtools-0.0.1a0/tests/test_runtime_observation_service.py +497 -0
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.2
2
+ Name: agently-devtools
3
+ Version: 0.0.1a0
4
+ Summary: Observation and developer tooling companion package for Agently
5
+ Author-email: Agently Team <developer@agently.tech>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: agently<4.1.0,>=4.0.9
9
+ Requires-Dist: fastapi<1.0,>=0.104
10
+ Requires-Dist: httpx<0.29.0,>=0.28.1
11
+ Requires-Dist: uvicorn<1.0,>=0.30
12
+ Provides-Extra: dev
13
+ Requires-Dist: anyio<5,>=4; extra == "dev"
14
+ Requires-Dist: build<2.0,>=1.2; extra == "dev"
15
+ Requires-Dist: pytest<9.0.0,>=8.4.0; extra == "dev"
16
+ Requires-Dist: pytest-asyncio<2.0.0,>=1.0.0; extra == "dev"
17
+
18
+ # agently-devtools
19
+
20
+ `agently-devtools` is the local observation, evaluation, and playground companion package for `agently`.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install -U agently agently-devtools
26
+ ```
27
+
28
+ Python `>=3.10`
29
+
30
+ ## Start
31
+
32
+ ```bash
33
+ agently-devtools start
34
+ ```
35
+
36
+ Default local address:
37
+
38
+ - Console: `http://127.0.0.1:15596/`
39
+ - Ingest: `http://127.0.0.1:15596/observation/ingest`
40
+
41
+ ## Observation Bridge
42
+
43
+ ```python
44
+ import os
45
+
46
+ from agently import Agently
47
+ from agently_devtools import ObservationBridge
48
+
49
+ bridge = ObservationBridge(
50
+ os.environ["AGENTLY_DEVTOOLS_INGEST_URL"],
51
+ app_id="your_app_id",
52
+ group_id="your_group_id",
53
+ )
54
+ bridge.register(Agently.event_center)
55
+ ```
56
+
57
+ TriggerFlow executions automatically publish their flow definition, so the execution graph can show the full static structure, including branches that have not been hit yet.
58
+
59
+ ## Scenario Evaluations
60
+
61
+ ```python
62
+ import os
63
+
64
+ from agently_devtools import EvaluationBridge, EvaluationCase, EvaluationRunner
65
+
66
+ bridge = EvaluationBridge(
67
+ base_url=os.environ["AGENTLY_DEVTOOLS_BASE_URL"],
68
+ app_id="your_app_id",
69
+ group_id="your_group_id",
70
+ )
71
+
72
+ binding = bridge.bind_agent(
73
+ agent,
74
+ suite_id="your_suite_id",
75
+ target_name="your_target_name",
76
+ )
77
+
78
+ report = EvaluationRunner(bridge=bridge).run(
79
+ binding,
80
+ cases=[EvaluationCase(case_id="case-1", input="your scenario")],
81
+ rounds=3,
82
+ )
83
+ ```
@@ -0,0 +1,66 @@
1
+ # agently-devtools
2
+
3
+ `agently-devtools` is the local observation, evaluation, and playground companion package for `agently`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install -U agently agently-devtools
9
+ ```
10
+
11
+ Python `>=3.10`
12
+
13
+ ## Start
14
+
15
+ ```bash
16
+ agently-devtools start
17
+ ```
18
+
19
+ Default local address:
20
+
21
+ - Console: `http://127.0.0.1:15596/`
22
+ - Ingest: `http://127.0.0.1:15596/observation/ingest`
23
+
24
+ ## Observation Bridge
25
+
26
+ ```python
27
+ import os
28
+
29
+ from agently import Agently
30
+ from agently_devtools import ObservationBridge
31
+
32
+ bridge = ObservationBridge(
33
+ os.environ["AGENTLY_DEVTOOLS_INGEST_URL"],
34
+ app_id="your_app_id",
35
+ group_id="your_group_id",
36
+ )
37
+ bridge.register(Agently.event_center)
38
+ ```
39
+
40
+ TriggerFlow executions automatically publish their flow definition, so the execution graph can show the full static structure, including branches that have not been hit yet.
41
+
42
+ ## Scenario Evaluations
43
+
44
+ ```python
45
+ import os
46
+
47
+ from agently_devtools import EvaluationBridge, EvaluationCase, EvaluationRunner
48
+
49
+ bridge = EvaluationBridge(
50
+ base_url=os.environ["AGENTLY_DEVTOOLS_BASE_URL"],
51
+ app_id="your_app_id",
52
+ group_id="your_group_id",
53
+ )
54
+
55
+ binding = bridge.bind_agent(
56
+ agent,
57
+ suite_id="your_suite_id",
58
+ target_name="your_target_name",
59
+ )
60
+
61
+ report = EvaluationRunner(bridge=bridge).run(
62
+ binding,
63
+ cases=[EvaluationCase(case_id="case-1", input="your scenario")],
64
+ rounds=3,
65
+ )
66
+ ```
@@ -0,0 +1,21 @@
1
+ from .app import DEFAULT_LOCAL_API_KEY, LocalObservationBinding, create_local_observation_app
2
+ from .bridge import ObservationBridge
3
+ from .evaluations import (
4
+ EvaluationBinding,
5
+ EvaluationBridge,
6
+ EvaluationCase,
7
+ EvaluationCheckResult,
8
+ EvaluationReport,
9
+ EvaluationRoundRecord,
10
+ EvaluationRule,
11
+ EvaluationRunner,
12
+ EvaluationVariant,
13
+ )
14
+ from .logs import LogEventQuery, RuntimeLogService, RuntimeLogStats
15
+ from .observation import ObservationEventQuery, ObservationRunQuery, ObservationSubscriptionFilter, RunTreeNode
16
+ from .observation import (
17
+ RunRecord,
18
+ RuntimeObservationBackend,
19
+ RuntimeObservationService,
20
+ RuntimeObservationSubscription,
21
+ )
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ class RunContext:
7
+ run_id: str
8
+ root_run_id: str | None
9
+ parent_run_id: str | None
10
+ run_kind: str
11
+ agent_id: str | None
12
+ agent_name: str | None
13
+ session_id: str | None
14
+ response_id: str | None
15
+ execution_id: str | None
16
+ meta: dict[str, Any]
17
+
18
+ @classmethod
19
+ def create(cls, *args: Any, **kwargs: Any) -> "RunContext": ...
20
+
21
+ def create_child(self, *args: Any, **kwargs: Any) -> "RunContext": ...
22
+
23
+ def model_copy(self, *args: Any, **kwargs: Any) -> "RunContext": ...
24
+
25
+ def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
26
+
27
+
28
+ class RuntimeEvent:
29
+ event_id: str
30
+ event_type: str
31
+ source: str
32
+ level: str
33
+ message: str | None
34
+ payload: Any
35
+ error: Any
36
+ run: RunContext | None
37
+ meta: dict[str, Any]
38
+ timestamp: int
39
+
40
+ def __init__(self, *args: Any, **kwargs: Any) -> None: ...
41
+
42
+ def model_copy(self, *args: Any, **kwargs: Any) -> "RuntimeEvent": ...
43
+
44
+ def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
45
+
46
+ @classmethod
47
+ def model_validate(cls, *args: Any, **kwargs: Any) -> "RuntimeEvent": ...
48
+
49
+ else:
50
+ try:
51
+ from agently.types.data import RunContext, RuntimeEvent
52
+ except ImportError: # Compatibility for published agently wheel variants.
53
+ from agently.types.data.event import RunContext, RuntimeEvent
54
+
55
+ __all__ = ["RunContext", "RuntimeEvent"]
@@ -0,0 +1,197 @@
1
+ import threading
2
+ import time
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, Any, cast
5
+ from urllib.parse import urlparse
6
+
7
+ from agently import Agently
8
+
9
+ from ._agently_compat import RunContext
10
+ from .bridge import _create_observation_service_hooker
11
+ from .integrations import ObservationFastAPI
12
+ from .observation import RuntimeObservationService
13
+
14
+ if TYPE_CHECKING:
15
+ from agently.core import EventCenter
16
+ from agently.types.plugins import EventHooker
17
+
18
+ DEFAULT_LOCAL_API_KEY = ""
19
+ DEFAULT_HOST = "127.0.0.1"
20
+ DEFAULT_PORT = 15596
21
+ DEFAULT_ROUTE_PREFIX = "/observation"
22
+ DEFAULT_LOG_ROOT = Path.home() / ".agently-devtools" / "logs"
23
+ _LOCAL_SERVER_LOCK = threading.Lock()
24
+ _LOCAL_SERVER_THREADS: dict[tuple[str, int, str], threading.Thread] = {}
25
+
26
+
27
+ def _ensure_local_auth(api_key: str | None):
28
+ if Agently.settings.get("observation.buffer_limit", None) is None:
29
+ Agently.set_settings("observation.buffer_limit", 1200)
30
+ if Agently.settings.get("observation.channel_buffer_limit", None) is None:
31
+ Agently.set_settings("observation.channel_buffer_limit", 160)
32
+ if Agently.settings.get("logs.enabled", None) is None:
33
+ Agently.set_settings("logs.enabled", True)
34
+ if Agently.settings.get("logs.storage.path", None) is None:
35
+ Agently.set_settings("logs.storage.path", str(DEFAULT_LOG_ROOT))
36
+ if Agently.settings.get("logs.storage.segment_event_limit", None) is None:
37
+ Agently.set_settings("logs.storage.segment_event_limit", 1000)
38
+ if isinstance(api_key, str) and api_key:
39
+ cast(Any, Agently).set_api_key(api_key)
40
+ Agently.set_settings("observation.api.auth.api_keys", [api_key])
41
+ else:
42
+ Agently.set_settings("observation.api.auth.api_keys", [])
43
+ Agently.set_settings("observation.api.auth.allow_query_api_key", True)
44
+ return Agently.settings
45
+
46
+
47
+ async def emit_startup_run(
48
+ *,
49
+ source: str = "AgentlyDevtoolsCLI",
50
+ message: str = "Observation service is ready.",
51
+ ):
52
+ run = RunContext.create(
53
+ run_kind="request",
54
+ agent_name="agently-devtools",
55
+ meta={"component": "devtools"},
56
+ )
57
+ emitter = cast(Any, Agently.event_center).create_emitter(source)
58
+ await emitter.async_emit(
59
+ "observation.service_started",
60
+ message=message,
61
+ run=run,
62
+ payload={"component": "observation"},
63
+ )
64
+ await emitter.async_emit(
65
+ "observation.service_ready",
66
+ message="Observation console is listening for remote runtime uploads.",
67
+ run=run,
68
+ payload={"component": "observation"},
69
+ )
70
+
71
+
72
+ class LocalObservationBinding:
73
+ def __init__(self, observation_service: RuntimeObservationService, hooker: type["EventHooker"]):
74
+ self.observation_service = observation_service
75
+ self._hooker = hooker
76
+
77
+ def unregister(self, event_center: "EventCenter"):
78
+ event_center.unregister_hooker_plugin(self._hooker)
79
+ return self
80
+
81
+ @property
82
+ def hooker(self):
83
+ return self._hooker
84
+
85
+
86
+ def create_local_observation_app(
87
+ *,
88
+ api_key: str | None = DEFAULT_LOCAL_API_KEY,
89
+ route_prefix: str = DEFAULT_ROUTE_PREFIX,
90
+ title: str = "Agently DevTools",
91
+ fastapi_kwargs: dict[str, Any] | None = None,
92
+ register_event_sink: bool = True,
93
+ ):
94
+ settings = _ensure_local_auth(api_key)
95
+ observation_service = RuntimeObservationService(settings)
96
+ hooker = _create_observation_service_hooker(observation_service, name="LocalObservationSinkHooker")
97
+ if register_event_sink:
98
+ Agently.event_center.register_hooker_plugin(hooker)
99
+ binding = LocalObservationBinding(observation_service, hooker)
100
+ kwargs = dict(fastapi_kwargs) if fastapi_kwargs is not None else {}
101
+ app = ObservationFastAPI(
102
+ observation_service=observation_service,
103
+ settings=settings,
104
+ api_keys=[api_key] if isinstance(api_key, str) and api_key else None,
105
+ route_prefix=route_prefix,
106
+ title=title,
107
+ **kwargs,
108
+ )
109
+ return app, binding
110
+
111
+
112
+ def build_local_base_url(*, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT):
113
+ return f"http://{host}:{port}"
114
+
115
+
116
+ def build_observation_ingest_endpoint(*, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT, route_prefix: str = DEFAULT_ROUTE_PREFIX):
117
+ return f"{build_local_base_url(host=host, port=port)}{route_prefix}/ingest"
118
+
119
+
120
+ def _is_local_base_url(base_url: str):
121
+ parsed = urlparse(base_url)
122
+ return parsed.scheme in {"http", "https"} and parsed.hostname in {"127.0.0.1", "localhost"}
123
+
124
+
125
+ def _probe_local_listener(base_url: str, *, route_prefix: str = DEFAULT_ROUTE_PREFIX, api_key: str | None = None, timeout: float = 0.35):
126
+ try:
127
+ import httpx
128
+
129
+ headers = {"Authorization": f"Bearer {api_key}"} if api_key else None
130
+ response = httpx.get(f"{base_url}{route_prefix}/runs", headers=headers, timeout=timeout)
131
+ return response.status_code < 500
132
+ except Exception:
133
+ return False
134
+
135
+
136
+ def _serve_local_listener(*, host: str, port: int, api_key: str | None, route_prefix: str):
137
+ import uvicorn
138
+
139
+ app, _binding = create_local_observation_app(
140
+ api_key=api_key,
141
+ route_prefix=route_prefix,
142
+ register_event_sink=False,
143
+ )
144
+ config = uvicorn.Config(
145
+ app,
146
+ host=host,
147
+ port=port,
148
+ log_level="warning",
149
+ access_log=False,
150
+ )
151
+ server = uvicorn.Server(config)
152
+ cast(Any, server).install_signal_handlers = lambda: None
153
+ server.run()
154
+
155
+
156
+ def ensure_local_devtools_listener(
157
+ base_url: str,
158
+ *,
159
+ api_key: str | None = None,
160
+ route_prefix: str = DEFAULT_ROUTE_PREFIX,
161
+ wait_timeout: float = 5.0,
162
+ ):
163
+ if not _is_local_base_url(base_url):
164
+ return False
165
+ if _probe_local_listener(base_url, route_prefix=route_prefix, api_key=api_key):
166
+ return False
167
+
168
+ parsed = urlparse(base_url)
169
+ host = parsed.hostname or DEFAULT_HOST
170
+ port = parsed.port or DEFAULT_PORT
171
+ server_key = (host, port, route_prefix)
172
+
173
+ with _LOCAL_SERVER_LOCK:
174
+ if _probe_local_listener(base_url, route_prefix=route_prefix, api_key=api_key):
175
+ return False
176
+ thread = _LOCAL_SERVER_THREADS.get(server_key)
177
+ if thread is None or not thread.is_alive():
178
+ thread = threading.Thread(
179
+ target=_serve_local_listener,
180
+ kwargs={
181
+ "host": host,
182
+ "port": port,
183
+ "api_key": api_key,
184
+ "route_prefix": route_prefix,
185
+ },
186
+ name=f"AgentlyDevToolsListener-{host}:{port}",
187
+ daemon=True,
188
+ )
189
+ thread.start()
190
+ _LOCAL_SERVER_THREADS[server_key] = thread
191
+
192
+ deadline = time.time() + wait_timeout
193
+ while time.time() < deadline:
194
+ if _probe_local_listener(base_url, route_prefix=route_prefix, api_key=api_key):
195
+ return True
196
+ time.sleep(0.1)
197
+ raise RuntimeError(f"Timed out while starting local Agently DevTools listener at {base_url}.")