databricks-agentbricks 0.2.0__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.
- databricks_agentbricks/__init__.py +1 -0
- databricks_agentbricks/_group.py +217 -0
- databricks_agentbricks/agent_project.py +657 -0
- databricks_agentbricks/app_resources.py +139 -0
- databricks_agentbricks/cli/__init__.py +12 -0
- databricks_agentbricks/cli/app.py +108 -0
- databricks_agentbricks/cli/app_auth.py +198 -0
- databricks_agentbricks/cli/auth.py +142 -0
- databricks_agentbricks/cli/deploy.py +954 -0
- databricks_agentbricks/cli/dev.py +310 -0
- databricks_agentbricks/cli/endpoint.py +145 -0
- databricks_agentbricks/cli/endpoint_examples.py +33 -0
- databricks_agentbricks/cli/endpoint_output.py +53 -0
- databricks_agentbricks/cli/endpoint_request.py +71 -0
- databricks_agentbricks/cli/endpoint_transport.py +148 -0
- databricks_agentbricks/cli/help.py +467 -0
- databricks_agentbricks/cli/init.py +496 -0
- databricks_agentbricks/cli/mcp.py +75 -0
- databricks_agentbricks/cli/memory.py +520 -0
- databricks_agentbricks/cli/sandbox.py +823 -0
- databricks_agentbricks/cli/sessions.py +588 -0
- databricks_agentbricks/cli/tools.py +456 -0
- databricks_agentbricks/cli/tracing.py +724 -0
- databricks_agentbricks/databricks_cli.py +38 -0
- databricks_agentbricks/errors.py +114 -0
- databricks_agentbricks/lakebase_runtime_store.py +107 -0
- databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
- databricks_agentbricks/project_config.py +157 -0
- databricks_agentbricks/project_types.py +55 -0
- databricks_agentbricks/py.typed +0 -0
- databricks_agentbricks/render.py +349 -0
- databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
- databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
- databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
- databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
- databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
- databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
- databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
- databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/agent-openai/.env.example +37 -0
- databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
- databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
- databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
- databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-openai/README.md +143 -0
- databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
- databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
- databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
- databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
- databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
- databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
- databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/sandbox_mcp.py +39 -0
- databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
- databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
- databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
- databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
- databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
- databricks_agentbricks/theme.py +72 -0
- databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
- databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
- databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
- databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
- databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
- databricks_agentkit/__init__.py +67 -0
- databricks_agentkit/_api_client.py +673 -0
- databricks_agentkit/_pagination.py +25 -0
- databricks_agentkit/client.py +31 -0
- databricks_agentkit/langgraph/__init__.py +98 -0
- databricks_agentkit/langgraph/genie.py +50 -0
- databricks_agentkit/langgraph/mcp.py +216 -0
- databricks_agentkit/langgraph/memory.py +67 -0
- databricks_agentkit/langgraph/session_store.py +485 -0
- databricks_agentkit/memory_store.py +371 -0
- databricks_agentkit/models.py +330 -0
- databricks_agentkit/openai/__init__.py +93 -0
- databricks_agentkit/openai/genie.py +50 -0
- databricks_agentkit/openai/mcp.py +189 -0
- databricks_agentkit/openai/memory.py +67 -0
- databricks_agentkit/openai/sessions.py +157 -0
- databricks_agentkit/py.typed +0 -0
- databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
- databricks_agentkit/runtime/README.md +184 -0
- databricks_agentkit/runtime/__init__.py +69 -0
- databricks_agentkit/runtime/app.py +316 -0
- databricks_agentkit/runtime/auth.py +149 -0
- databricks_agentkit/runtime/durability/__init__.py +1 -0
- databricks_agentkit/runtime/durability/execution.py +133 -0
- databricks_agentkit/runtime/durability/heartbeat.py +64 -0
- databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
- databricks_agentkit/runtime/durability/recovery.py +70 -0
- databricks_agentkit/runtime/durability/store.py +33 -0
- databricks_agentkit/runtime/execution.py +160 -0
- databricks_agentkit/runtime/genie.py +224 -0
- databricks_agentkit/runtime/mcp_auth.py +114 -0
- databricks_agentkit/runtime/model_services.py +90 -0
- databricks_agentkit/runtime/runtime.py +273 -0
- databricks_agentkit/runtime/session_store_client.py +120 -0
- databricks_agentkit/runtime/store.py +287 -0
- databricks_agentkit/runtime/tool_manifest.py +223 -0
- databricks_agentkit/runtime/tracing.py +71 -0
- databricks_agentkit/runtime/types.py +118 -0
- databricks_agentkit/runtime/workspace.py +27 -0
- databricks_agentkit/session_store.py +434 -0
- databricks_agentkit/timefmt.py +85 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
"""`ab tracing` — send an agent's traces to an MLflow experiment and inspect them.
|
|
2
|
+
|
|
3
|
+
``ab dev`` runs a **local, sqlite-backed MLflow server** (:func:`start_local_tracing_server`) so
|
|
4
|
+
traces stay on the machine with no workspace setup, viewable in the local MLflow UI. ``ab deploy``
|
|
5
|
+
sends traces to a per-project **workspace** experiment whose **name** lives in agent.toml (a default
|
|
6
|
+
``/Shared/agentbricks_traces/<project>`` is written at ``ab init``); deploy get-or-creates it in the active
|
|
7
|
+
workspace and wires its ``MLFLOW_EXPERIMENT_ID`` (which binds the agent, grants the deployed app via an
|
|
8
|
+
experiment app resource, reads traces, and builds the UI link). Storing the name (not the id) keeps
|
|
9
|
+
the binding valid across workspaces and profiles, since an id is workspace-local.
|
|
10
|
+
|
|
11
|
+
The bound ``experiment_name``'s presence IS the enable switch: a bound name means tracing is on for
|
|
12
|
+
deploy, its absence means off. ``ab tracing bind`` binds an experiment (by name or id, one
|
|
13
|
+
required); ``ab tracing unbind`` removes the binding; ``list`` / ``get`` read traces back.
|
|
14
|
+
|
|
15
|
+
MLflow (``mlflow-skinny``) is a base dependency, but the ``ab tracing`` commands and the deploy
|
|
16
|
+
experiment provisioning still import it lazily — ``cli.py`` imports this module at startup, so a
|
|
17
|
+
top-level import would pay mlflow's heavy import cost on every Agent Bricks CLI command.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import pathlib
|
|
23
|
+
import re
|
|
24
|
+
import socket
|
|
25
|
+
import subprocess
|
|
26
|
+
import time
|
|
27
|
+
from collections.abc import Iterator
|
|
28
|
+
from contextlib import contextmanager
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from typing import Any, Optional
|
|
31
|
+
|
|
32
|
+
import click
|
|
33
|
+
|
|
34
|
+
from databricks_agentbricks import render
|
|
35
|
+
from databricks_agentbricks.errors import AgentCliError
|
|
36
|
+
from databricks_agentkit import timefmt
|
|
37
|
+
|
|
38
|
+
_BREADCRUMB = "Agent Tracing"
|
|
39
|
+
# Per-app experiment folder under /Shared: username-free (so `ab init` can name it offline) and
|
|
40
|
+
# workspace-independent (so the same name is valid in whatever workspace the active profile targets).
|
|
41
|
+
_TRACES_DIR = "agentbricks_traces"
|
|
42
|
+
|
|
43
|
+
# The two env vars the deployed/dev agent reads to enable tracing: a destination (the workspace) and
|
|
44
|
+
# an experiment (by id). MLflow turns tracing on only when it has both.
|
|
45
|
+
TRACES_TRACKING_URI_ENV = "MLFLOW_TRACKING_URI"
|
|
46
|
+
TRACES_EXPERIMENT_ID_ENV = "MLFLOW_EXPERIMENT_ID"
|
|
47
|
+
|
|
48
|
+
# The command that binds (enables) tracing. Referenced parameter-free by deploy's "deployed without
|
|
49
|
+
# tracing" guidance and by `unbind`'s "turn it back on" step, so those hints can't go stale if the
|
|
50
|
+
# flags change; the command's own `--help` documents the flags.
|
|
51
|
+
TRACING_BIND_COMMAND = "ab tracing bind"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def default_experiment_name(project: Optional[str], token: Optional[str] = None) -> str:
|
|
55
|
+
"""The default MLflow experiment path for a project: ``/Shared/agentbricks_traces/<project>[-<token>]``.
|
|
56
|
+
|
|
57
|
+
Under ``/Shared`` (not a user home) so it needs no username — ``ab init`` writes it offline —
|
|
58
|
+
and it's workspace-independent, so the same name is valid in whatever workspace the active profile
|
|
59
|
+
targets (``ab deploy`` get-or-creates it there). ``project`` is the Agent Bricks project name (the
|
|
60
|
+
source directory's basename); an optional per-scaffold ``token`` (shared with the default store
|
|
61
|
+
names) keeps like-named projects from colliding in the shared ``/Shared`` namespace.
|
|
62
|
+
"""
|
|
63
|
+
if not project:
|
|
64
|
+
raise AgentCliError("Cannot derive the default tracing experiment without a project name.")
|
|
65
|
+
slug = re.sub(r"[^a-z0-9-]+", "-", project.lower()).strip("-") or "agent"
|
|
66
|
+
middle = f"-{token}" if token else ""
|
|
67
|
+
return f"/Shared/{_TRACES_DIR}/{slug}{middle}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def experiment_url(host: Optional[str], experiment_id: str) -> Optional[str]:
|
|
71
|
+
"""The workspace MLflow experiment Traces page, or None when the host is unavailable."""
|
|
72
|
+
if not host or host == "unknown":
|
|
73
|
+
return None
|
|
74
|
+
return f"{host.rstrip('/')}/ml/experiments/{experiment_id}?compareRunsMode=TRACES"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _mlflow():
|
|
78
|
+
"""Import mlflow lazily and return the module.
|
|
79
|
+
|
|
80
|
+
mlflow-skinny is a base dependency, so this can't fail on a correct install; the lazy import
|
|
81
|
+
exists only to keep it off the CLI startup path (``cli.py`` imports this module eagerly).
|
|
82
|
+
"""
|
|
83
|
+
import mlflow # noqa: PLC0415 - intentional lazy import (startup cost, not optionality)
|
|
84
|
+
|
|
85
|
+
return mlflow
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _workspace_uri(profile: Optional[str]) -> str:
|
|
89
|
+
"""The MLflow tracking URI for the workspace (honoring Agent Bricks' --profile)."""
|
|
90
|
+
return f"databricks://{profile}" if profile else "databricks"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _set_tracking_uri(mlflow, profile: Optional[str]) -> None:
|
|
94
|
+
"""Point MLflow at the workspace (honoring Agent Bricks' --profile)."""
|
|
95
|
+
mlflow.set_tracking_uri(_workspace_uri(profile))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# An experiment linked to a UC schema for trace storage carries this tag (the destination schema);
|
|
99
|
+
# managed experiments don't. Agent Bricks supports managed tracing only (UC support is a follow-up).
|
|
100
|
+
_UC_TRACE_TAG = "mlflow.experiment.databricksTraceDestinationPath"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _is_uc_backed(experiment) -> bool:
|
|
104
|
+
"""True if the experiment stores traces in Unity Catalog rather than the managed MLflow backend.
|
|
105
|
+
|
|
106
|
+
An MLflow ``Experiment``'s ``tags`` is always a dict (empty when it has none); ``or {}`` just
|
|
107
|
+
guards a missing attribute.
|
|
108
|
+
"""
|
|
109
|
+
return _UC_TRACE_TAG in (getattr(experiment, "tags", None) or {})
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _get_experiment_by_id(mlflow, experiment_id: str):
|
|
113
|
+
"""``mlflow.get_experiment`` but returns ``None`` for an unknown id instead of raising.
|
|
114
|
+
|
|
115
|
+
The id lookup raises ``RESOURCE_DOES_NOT_EXIST`` for a missing experiment, whereas the name lookup
|
|
116
|
+
returns ``None`` - normalize so callers treat "not found" the same either way. Other errors (auth,
|
|
117
|
+
network) still propagate.
|
|
118
|
+
"""
|
|
119
|
+
from mlflow.exceptions import MlflowException # noqa: PLC0415 - lazy import (startup cost)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
return mlflow.get_experiment(experiment_id)
|
|
123
|
+
except MlflowException as exc:
|
|
124
|
+
if getattr(exc, "error_code", "") == "RESOURCE_DOES_NOT_EXIST":
|
|
125
|
+
return None
|
|
126
|
+
raise
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def create_experiment_idempotent(profile: Optional[str], client, name: str) -> str:
|
|
130
|
+
"""Create the experiment ``name`` if missing and return its id (idempotent).
|
|
131
|
+
|
|
132
|
+
``create_experiment`` won't make the intermediate workspace folder for a nested path (e.g.
|
|
133
|
+
``/Users/<you>/agentbricks-traces/<project>``), so the parent dir is created first. Used by dev/deploy to
|
|
134
|
+
provision the managed experiment that traces log to.
|
|
135
|
+
|
|
136
|
+
Rejects a name that already resolves to a UC-backed experiment: binding one is blocked up front,
|
|
137
|
+
but a hand-edited ``agent.toml`` can point at one directly, so deploy re-checks here - Agent Bricks
|
|
138
|
+
supports managed (non-UC) tracing only.
|
|
139
|
+
"""
|
|
140
|
+
mlflow = _mlflow()
|
|
141
|
+
_set_tracking_uri(mlflow, profile)
|
|
142
|
+
experiment = mlflow.get_experiment_by_name(name)
|
|
143
|
+
if experiment:
|
|
144
|
+
if _is_uc_backed(experiment):
|
|
145
|
+
raise AgentCliError(
|
|
146
|
+
"UC-backed MLflow tracing is not supported by Agent Bricks.",
|
|
147
|
+
hint="Point this project's tracing at a managed (non-UC) experiment.",
|
|
148
|
+
)
|
|
149
|
+
return experiment.experiment_id
|
|
150
|
+
parent = name.rsplit("/", 1)[0]
|
|
151
|
+
if parent:
|
|
152
|
+
client.ensure_workspace_dir(parent)
|
|
153
|
+
return mlflow.create_experiment(name)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resolve_experiment_name(source: pathlib.Path | str) -> Optional[str]:
|
|
157
|
+
"""This project's bound ``experiment_name``, or None when tracing is unbound (off).
|
|
158
|
+
|
|
159
|
+
Resolves by name, not a stored id, so it stays correct across workspaces/profiles.
|
|
160
|
+
"""
|
|
161
|
+
from databricks_agentbricks.agent_project import (
|
|
162
|
+
AgentProject, # noqa: PLC0415 - avoid import cycle
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
project = AgentProject.load(source)
|
|
167
|
+
except AgentCliError:
|
|
168
|
+
project = None
|
|
169
|
+
return project.trace_experiment_name if project is not None else None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True)
|
|
173
|
+
class _TraceReadTarget:
|
|
174
|
+
"""Where `list`/`get` read traces from: an MLflow ``tracking_uri`` and the ``experiment_id`` in it.
|
|
175
|
+
|
|
176
|
+
``local`` marks the local ``ab dev`` store (served over a short-lived REST server) so callers can
|
|
177
|
+
label it. A field is None when there's nothing to read (no experiment resolved).
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
tracking_uri: Optional[str]
|
|
181
|
+
experiment_id: Optional[str]
|
|
182
|
+
local: bool = False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@contextmanager
|
|
186
|
+
def _with_trace_read_target(
|
|
187
|
+
source: pathlib.Path | str,
|
|
188
|
+
profile: Optional[str],
|
|
189
|
+
experiment_name: Optional[str] = None,
|
|
190
|
+
experiment_id: Optional[str] = None,
|
|
191
|
+
) -> Iterator[_TraceReadTarget]:
|
|
192
|
+
"""Yield the target `list`/`get` should read from, with MLflow's tracking URI already pointed at it.
|
|
193
|
+
|
|
194
|
+
Precedence: an explicit ``--experiment-name`` / ``--experiment-id`` (must exist), else this
|
|
195
|
+
project's workspace experiment when it's provisioned + managed, else the local ``ab dev`` store
|
|
196
|
+
(``.agentbricks/mlflow.db``). Both target fields are None when nothing has traced anywhere yet.
|
|
197
|
+
|
|
198
|
+
The local store is read over REST from a short-lived MLflow server started here (and torn down on
|
|
199
|
+
exit), not by opening the sqlite file directly: the server that wrote the schema serves it, so the
|
|
200
|
+
CLI's own mlflow-skinny version needn't match the version that wrote the store (the REST protocol is
|
|
201
|
+
compatible across a major version; a direct sqlite open must match the schema head exactly). Callers
|
|
202
|
+
must run their read inside the ``with`` block so the server is still up.
|
|
203
|
+
"""
|
|
204
|
+
mlflow = _mlflow()
|
|
205
|
+
# An explicit id/name targets the workspace directly (and must exist); it raises here, before any
|
|
206
|
+
# local server is started, so a typo never spins one up.
|
|
207
|
+
explicit = _workspace_experiment_target(profile, experiment_name, experiment_id)
|
|
208
|
+
if explicit is not None:
|
|
209
|
+
mlflow.set_tracking_uri(explicit.tracking_uri)
|
|
210
|
+
yield explicit
|
|
211
|
+
return
|
|
212
|
+
# This project's bound experiment, if it's provisioned + managed in the workspace.
|
|
213
|
+
name = _resolve_experiment_name(source)
|
|
214
|
+
if name:
|
|
215
|
+
_set_tracking_uri(mlflow, profile)
|
|
216
|
+
try:
|
|
217
|
+
experiment = mlflow.get_experiment_by_name(name)
|
|
218
|
+
except Exception: # noqa: BLE001 - workspace unreachable -> try the local dev store
|
|
219
|
+
experiment = None
|
|
220
|
+
if experiment is not None and not _is_uc_backed(experiment):
|
|
221
|
+
yield _TraceReadTarget(_workspace_uri(profile), experiment.experiment_id)
|
|
222
|
+
return
|
|
223
|
+
# Unbound, not provisioned, or unreachable -> the local dev store, if any. `ab dev` traces
|
|
224
|
+
# locally regardless of the binding, so its store is worth reading even when tracing is unbound.
|
|
225
|
+
db = pathlib.Path(source).resolve() / _AGENTBRICKS_LOCAL_DIR / "mlflow.db"
|
|
226
|
+
if not db.exists():
|
|
227
|
+
yield _TraceReadTarget(None, None)
|
|
228
|
+
return
|
|
229
|
+
server, base_url = _start_read_server(db)
|
|
230
|
+
if server is None: # best-effort: couldn't bring the local store up -> nothing to read
|
|
231
|
+
yield _TraceReadTarget(None, None, local=True)
|
|
232
|
+
return
|
|
233
|
+
try:
|
|
234
|
+
mlflow.set_tracking_uri(base_url)
|
|
235
|
+
local = mlflow.get_experiment_by_name(pathlib.Path(source).resolve().name)
|
|
236
|
+
yield _TraceReadTarget(base_url, local.experiment_id if local else None, local=True)
|
|
237
|
+
finally:
|
|
238
|
+
stop_local_tracing_server(server)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _workspace_experiment_target(
|
|
242
|
+
profile: Optional[str], experiment_name: Optional[str], experiment_id: Optional[str]
|
|
243
|
+
) -> Optional[_TraceReadTarget]:
|
|
244
|
+
"""The workspace target for an explicit ``--experiment-id`` / ``--experiment-name``, or None when
|
|
245
|
+
neither is given (the caller falls back to the project default).
|
|
246
|
+
|
|
247
|
+
An explicit identifier must exist: an unknown id or name raises rather than resolving to nothing,
|
|
248
|
+
so a typo isn't mistaken for an empty experiment.
|
|
249
|
+
"""
|
|
250
|
+
if not (experiment_id or experiment_name):
|
|
251
|
+
return None
|
|
252
|
+
mlflow = _mlflow()
|
|
253
|
+
_set_tracking_uri(mlflow, profile)
|
|
254
|
+
if experiment_id:
|
|
255
|
+
if _get_experiment_by_id(mlflow, experiment_id) is None:
|
|
256
|
+
raise AgentCliError(
|
|
257
|
+
f"No MLflow experiment found with id {experiment_id!r} in this workspace.",
|
|
258
|
+
hint="Check the id, or omit it to use this project's experiment.",
|
|
259
|
+
)
|
|
260
|
+
return _TraceReadTarget(_workspace_uri(profile), experiment_id)
|
|
261
|
+
experiment = mlflow.get_experiment_by_name(experiment_name)
|
|
262
|
+
if experiment is None:
|
|
263
|
+
raise AgentCliError(
|
|
264
|
+
f"No MLflow experiment named {experiment_name!r} in this workspace.",
|
|
265
|
+
hint="Check the name, or omit it to use this project's experiment.",
|
|
266
|
+
)
|
|
267
|
+
return _TraceReadTarget(_workspace_uri(profile), experiment.experiment_id)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _attr(obj: Any, *paths: str, default: Any = None) -> Any:
|
|
271
|
+
"""Read the first present dotted attribute path (MLflow object shapes vary by version)."""
|
|
272
|
+
for path in paths:
|
|
273
|
+
cur = obj
|
|
274
|
+
for part in path.split("."):
|
|
275
|
+
cur = getattr(cur, part, None)
|
|
276
|
+
if cur is None:
|
|
277
|
+
break
|
|
278
|
+
if cur is not None:
|
|
279
|
+
return cur
|
|
280
|
+
return default
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _status_str(status: Any) -> Optional[str]:
|
|
284
|
+
if status is None:
|
|
285
|
+
return None
|
|
286
|
+
return getattr(status, "name", None) or str(status)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _trace_to_json(trace: Any) -> dict:
|
|
290
|
+
return {
|
|
291
|
+
"trace_id": _attr(trace, "info.trace_id", "info.request_id"),
|
|
292
|
+
"status": _status_str(_attr(trace, "info.status", "info.state")),
|
|
293
|
+
"execution_time_ms": _attr(trace, "info.execution_time_ms", "info.execution_duration_ms"),
|
|
294
|
+
"timestamp_ms": _attr(trace, "info.timestamp_ms", "info.request_time"),
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# --- local dev tracing (`ab dev`) ----------------------------------------
|
|
299
|
+
|
|
300
|
+
# Local-only scratch dir (gitignored) under a dev project: holds the sqlite tracing store + artifacts.
|
|
301
|
+
_AGENTBRICKS_LOCAL_DIR = ".agentbricks"
|
|
302
|
+
# The local tracing server's MLflow: a broad 3.x range (the runtime's floor). `ab dev` writes the
|
|
303
|
+
# sqlite store with it, and `list`/`get` read that store back over REST from a short-lived server (see
|
|
304
|
+
# _with_trace_read_target) - never by opening the file with the CLI's own mlflow-skinny. So this needn't
|
|
305
|
+
# match the CLI's version, and uvx reuses ONE cached environment across agentbricks releases (fast startup
|
|
306
|
+
# after the first install) instead of cold-installing a new exact version on every mlflow bump.
|
|
307
|
+
_MLFLOW_SPEC = "mlflow>=3.10,<4"
|
|
308
|
+
# Pin the uvx server's interpreter: on Python 3.13 an older mlflow drags in a pyarrow that builds from
|
|
309
|
+
# source (slow, and fails without a C toolchain); 3.12 resolves to prebuilt wheels. uv fetches it if the
|
|
310
|
+
# machine lacks it.
|
|
311
|
+
_SERVER_PYTHON = "3.12"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _free_port() -> int:
|
|
315
|
+
"""Ask the OS for a free localhost port for the local MLflow tracking server."""
|
|
316
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
317
|
+
sock.bind(("127.0.0.1", 0))
|
|
318
|
+
return sock.getsockname()[1]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _mlflow_server_argv(db: pathlib.Path, artifacts: pathlib.Path, port: int) -> list[str]:
|
|
322
|
+
"""The ``uvx`` argv for a local, sqlite-backed MLflow tracking server on ``port``.
|
|
323
|
+
|
|
324
|
+
Shared by the `ab dev` server and the short-lived server `list`/`get` use to read the local store
|
|
325
|
+
(see _MLFLOW_SPEC / _SERVER_PYTHON for the version + interpreter pins).
|
|
326
|
+
"""
|
|
327
|
+
return [
|
|
328
|
+
"uvx",
|
|
329
|
+
"--python",
|
|
330
|
+
_SERVER_PYTHON,
|
|
331
|
+
"--from",
|
|
332
|
+
_MLFLOW_SPEC,
|
|
333
|
+
"mlflow",
|
|
334
|
+
"server",
|
|
335
|
+
"--backend-store-uri",
|
|
336
|
+
f"sqlite:///{db}",
|
|
337
|
+
"--default-artifact-root",
|
|
338
|
+
str(artifacts),
|
|
339
|
+
"--host",
|
|
340
|
+
"127.0.0.1",
|
|
341
|
+
"--port",
|
|
342
|
+
str(port),
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _wait_for_server(base_url: str, server: subprocess.Popen, timeout: float = 60.0) -> bool:
|
|
347
|
+
"""Poll the local MLflow server's health endpoint until it answers, its process exits, or timeout.
|
|
348
|
+
|
|
349
|
+
`ab dev` doesn't wait (the agent traces to the server as it comes up), but a read command queries
|
|
350
|
+
immediately and then tears the server down, so it must block until the server is live. Returns True
|
|
351
|
+
once it responds, False if the process died (e.g. install/bind failure) or it never came up.
|
|
352
|
+
"""
|
|
353
|
+
import urllib.error # noqa: PLC0415 - only needed when reading the local store
|
|
354
|
+
import urllib.request # noqa: PLC0415
|
|
355
|
+
|
|
356
|
+
deadline = time.monotonic() + timeout
|
|
357
|
+
while time.monotonic() < deadline:
|
|
358
|
+
if server.poll() is not None: # process exited before serving -> it won't come up
|
|
359
|
+
return False
|
|
360
|
+
try:
|
|
361
|
+
with urllib.request.urlopen(f"{base_url}/health", timeout=2) as resp:
|
|
362
|
+
if resp.status == 200:
|
|
363
|
+
return True
|
|
364
|
+
except (urllib.error.URLError, OSError):
|
|
365
|
+
time.sleep(0.3) # not up yet (connection refused / starting) -> retry
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _start_read_server(db: pathlib.Path) -> tuple[subprocess.Popen | None, Optional[str]]:
|
|
370
|
+
"""Start a short-lived MLflow server over the existing ``ab dev`` store and wait until it's ready.
|
|
371
|
+
|
|
372
|
+
Lets `list`/`get` read local traces over REST (the server owns the sqlite schema). Returns
|
|
373
|
+
``(server, base_url)``, or ``(None, None)`` when it can't start - best-effort, like `ab dev`, so a
|
|
374
|
+
read degrades to showing nothing rather than erroring. The caller stops the server when done.
|
|
375
|
+
"""
|
|
376
|
+
artifacts = (db.parent / "mlartifacts").resolve()
|
|
377
|
+
port = _free_port()
|
|
378
|
+
base_url = f"http://127.0.0.1:{port}"
|
|
379
|
+
try:
|
|
380
|
+
# Dup the log fd to the child and close our copy; keeps the server's output for debugging.
|
|
381
|
+
with (db.parent / "mlflow-read.log").open("w") as log:
|
|
382
|
+
server = subprocess.Popen(
|
|
383
|
+
_mlflow_server_argv(db, artifacts, port), stdout=log, stderr=subprocess.STDOUT
|
|
384
|
+
)
|
|
385
|
+
except OSError as exc:
|
|
386
|
+
render.diagnostic(
|
|
387
|
+
"warning", f"could not read local traces - {exc}", help="is `uv` installed?"
|
|
388
|
+
)
|
|
389
|
+
return None, None
|
|
390
|
+
if not _wait_for_server(base_url, server):
|
|
391
|
+
stop_local_tracing_server(server)
|
|
392
|
+
render.diagnostic(
|
|
393
|
+
"warning", "local trace store did not come up", help="see .agentbricks/mlflow-read.log"
|
|
394
|
+
)
|
|
395
|
+
return None, None
|
|
396
|
+
return server, base_url
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def start_local_tracing_server(
|
|
400
|
+
source_dir: pathlib.Path,
|
|
401
|
+
) -> tuple[subprocess.Popen | None, dict[str, str]]:
|
|
402
|
+
"""Start a local, sqlite-backed MLflow tracking server for `ab dev` tracing.
|
|
403
|
+
|
|
404
|
+
Returns ``(server_process, env)`` — ``env`` carries the ``MLFLOW_*`` vars for the dev-only manifest
|
|
405
|
+
so the agent traces locally — or ``(None, {})`` when the server can't be started (dev then runs
|
|
406
|
+
without traces). Launched via ``uvx mlflow`` so it depends on neither the skinny CLI env nor the
|
|
407
|
+
agent venv; it stores traces in ``<source>/.agentbricks/mlflow.db`` and serves the trace UI + REST API on
|
|
408
|
+
a free localhost port. Because the server owns the sqlite schema, there is no client/server
|
|
409
|
+
migration mismatch. The agent picks up the env through the same runtime gate a deployment uses (a
|
|
410
|
+
destination + an experiment), so no agent code differs between dev and deploy.
|
|
411
|
+
"""
|
|
412
|
+
agentbricks_dir = source_dir / _AGENTBRICKS_LOCAL_DIR
|
|
413
|
+
try:
|
|
414
|
+
agentbricks_dir.mkdir(exist_ok=True)
|
|
415
|
+
db = (agentbricks_dir / "mlflow.db").resolve()
|
|
416
|
+
artifacts = (agentbricks_dir / "mlartifacts").resolve()
|
|
417
|
+
port = _free_port()
|
|
418
|
+
# Passing the log file into Popen dups its fd to the child; closing our copy here is safe and
|
|
419
|
+
# keeps the server's output for debugging a failed local-tracing run.
|
|
420
|
+
with (agentbricks_dir / "mlflow-server.log").open("w") as log:
|
|
421
|
+
server = subprocess.Popen(
|
|
422
|
+
_mlflow_server_argv(db, artifacts, port), stdout=log, stderr=subprocess.STDOUT
|
|
423
|
+
)
|
|
424
|
+
except OSError as exc:
|
|
425
|
+
render.diagnostic(
|
|
426
|
+
"warning",
|
|
427
|
+
f"local tracing not started — {exc}",
|
|
428
|
+
help="running without traces (is `uv` installed?)",
|
|
429
|
+
)
|
|
430
|
+
return None, {}
|
|
431
|
+
return server, {
|
|
432
|
+
"MLFLOW_TRACKING_URI": f"http://127.0.0.1:{port}",
|
|
433
|
+
# A bare experiment name is fine for local MLflow (no workspace path / username needed).
|
|
434
|
+
"MLFLOW_EXPERIMENT_NAME": source_dir.resolve().name,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def stop_local_tracing_server(server: subprocess.Popen) -> None:
|
|
439
|
+
"""Stop the local MLflow tracking server started for `ab dev`."""
|
|
440
|
+
server.terminate()
|
|
441
|
+
try:
|
|
442
|
+
server.wait(timeout=5)
|
|
443
|
+
except subprocess.TimeoutExpired:
|
|
444
|
+
server.kill()
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# --- group ------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@click.group()
|
|
451
|
+
def tracing() -> None:
|
|
452
|
+
"""Configure MLflow tracing for your deployed agents, and inspect the traces."""
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _check_experiment_flags(
|
|
456
|
+
experiment_name: Optional[str], experiment_id: Optional[str], *, require_one: bool = False
|
|
457
|
+
) -> None:
|
|
458
|
+
"""Validate the mutually-exclusive ``--experiment-name`` / ``--experiment-id`` selectors.
|
|
459
|
+
|
|
460
|
+
Both given is always an error. ``require_one`` additionally rejects *neither* - `bind` needs one to
|
|
461
|
+
set the binding, while `list`/`get` may omit both and fall back to this project's experiment.
|
|
462
|
+
"""
|
|
463
|
+
if experiment_name and experiment_id:
|
|
464
|
+
raise AgentCliError(
|
|
465
|
+
"Pass --experiment-name or --experiment-id, not both.",
|
|
466
|
+
hint="They select the same experiment; use whichever identifier you have.",
|
|
467
|
+
)
|
|
468
|
+
if require_one and not (experiment_name or experiment_id):
|
|
469
|
+
raise AgentCliError(
|
|
470
|
+
"Pass --experiment-name or --experiment-id to bind tracing to an experiment.",
|
|
471
|
+
hint="Absence of a bound experiment means tracing is off; `ab tracing unbind` clears it.",
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _experiment_read_options(command):
|
|
476
|
+
"""Add the shared ``--experiment-name`` / ``--experiment-id`` read options to `list` and `get`."""
|
|
477
|
+
command = click.option(
|
|
478
|
+
"--experiment-id",
|
|
479
|
+
"experiment_id",
|
|
480
|
+
default=None,
|
|
481
|
+
help="MLflow experiment id to read (e.g. from the experiment URL). Mutually exclusive with "
|
|
482
|
+
"--experiment-name.",
|
|
483
|
+
)(command)
|
|
484
|
+
command = click.option(
|
|
485
|
+
"--experiment-name",
|
|
486
|
+
"experiment_name",
|
|
487
|
+
default=None,
|
|
488
|
+
help="MLflow experiment name to read (an absolute workspace path). Default: this project's "
|
|
489
|
+
"experiment.",
|
|
490
|
+
)(command)
|
|
491
|
+
return command
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# --- bind / unbind ----------------------------------------------------------
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@tracing.command("bind")
|
|
498
|
+
@click.option(
|
|
499
|
+
"--experiment-name",
|
|
500
|
+
"experiment_name",
|
|
501
|
+
default=None,
|
|
502
|
+
help="MLflow experiment name to trace to - an absolute workspace path, e.g. "
|
|
503
|
+
"/Shared/agentbricks_traces/<agent> or /Users/<you>/agentbricks_traces/<agent>. Agent Bricks creates it at "
|
|
504
|
+
"deploy. Mutually exclusive with --experiment-id.",
|
|
505
|
+
)
|
|
506
|
+
@click.option(
|
|
507
|
+
"--experiment-id",
|
|
508
|
+
"experiment_id",
|
|
509
|
+
default=None,
|
|
510
|
+
help="MLflow experiment id (e.g. copied from the experiment's workspace URL) to trace to. "
|
|
511
|
+
"Resolved to the experiment's name and stored as a name — Agent Bricks stores names, not ids, so the "
|
|
512
|
+
"binding stays valid across workspaces. Mutually exclusive with --experiment-name.",
|
|
513
|
+
)
|
|
514
|
+
@click.option(
|
|
515
|
+
"--source",
|
|
516
|
+
default=".",
|
|
517
|
+
type=click.Path(exists=True, file_okay=False),
|
|
518
|
+
help="Project directory containing agent.toml. Defaults to the current directory.",
|
|
519
|
+
)
|
|
520
|
+
@click.pass_obj
|
|
521
|
+
def tracing_bind(obj, experiment_name, experiment_id, source) -> None:
|
|
522
|
+
"""Bind tracing to an experiment, by name or id. Requires one of them (like `ab memory/sessions
|
|
523
|
+
bind`); the binding's presence is what turns tracing on.
|
|
524
|
+
|
|
525
|
+
The experiment is stored as a NAME, not an id, so the binding stays valid across
|
|
526
|
+
workspaces/profiles — Agent Bricks creates it if absent in the active workspace at deploy. ``--experiment-id``
|
|
527
|
+
(e.g. from the experiment's URL) is a convenience: it's resolved to the experiment's name and
|
|
528
|
+
stored as a name, never as an id.
|
|
529
|
+
"""
|
|
530
|
+
from databricks_agentbricks.agent_project import AgentProject # noqa: PLC0415
|
|
531
|
+
|
|
532
|
+
_check_experiment_flags(experiment_name, experiment_id, require_one=True)
|
|
533
|
+
|
|
534
|
+
# The name to store. --experiment-id is resolved to the experiment's name (Agent Bricks stores names,
|
|
535
|
+
# not ids). Either way, a UC-backed experiment is rejected up front — Agent Bricks supports managed
|
|
536
|
+
# tracing only (UC traces need a SQL warehouse to read and UC grants for the app's SP).
|
|
537
|
+
name = experiment_name
|
|
538
|
+
if experiment_id:
|
|
539
|
+
mlflow = _mlflow()
|
|
540
|
+
_set_tracking_uri(mlflow, obj.profile)
|
|
541
|
+
experiment = _get_experiment_by_id(mlflow, experiment_id)
|
|
542
|
+
if experiment is None:
|
|
543
|
+
raise AgentCliError(
|
|
544
|
+
f"No MLflow experiment found with id {experiment_id!r}.",
|
|
545
|
+
hint="Pass an existing experiment id, or use --experiment-name.",
|
|
546
|
+
)
|
|
547
|
+
if _is_uc_backed(experiment):
|
|
548
|
+
raise AgentCliError(
|
|
549
|
+
"UC-backed MLflow tracing is not supported by Agent Bricks.",
|
|
550
|
+
hint="Pass a managed (non-UC) experiment.",
|
|
551
|
+
)
|
|
552
|
+
name = experiment.name
|
|
553
|
+
elif experiment_name:
|
|
554
|
+
if not experiment_name.startswith("/"):
|
|
555
|
+
raise AgentCliError(
|
|
556
|
+
f"Experiment name must be an absolute workspace path, got {experiment_name!r}.",
|
|
557
|
+
hint="Use a path like /Shared/agentbricks_traces/<agent> or "
|
|
558
|
+
"/Users/<you>/agentbricks_traces/<agent>.",
|
|
559
|
+
)
|
|
560
|
+
# A not-yet-created name is fine (deploy creates it); only reject a name that already resolves
|
|
561
|
+
# to a UC-backed experiment.
|
|
562
|
+
mlflow = _mlflow()
|
|
563
|
+
_set_tracking_uri(mlflow, obj.profile)
|
|
564
|
+
existing = mlflow.get_experiment_by_name(experiment_name)
|
|
565
|
+
if existing is not None and _is_uc_backed(existing):
|
|
566
|
+
raise AgentCliError(
|
|
567
|
+
"UC-backed MLflow tracing is not supported by Agent Bricks.",
|
|
568
|
+
hint="Pass a managed (non-UC) experiment.",
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
project = AgentProject.load(pathlib.Path(source))
|
|
572
|
+
project.bind_tracing(name)
|
|
573
|
+
project.write()
|
|
574
|
+
|
|
575
|
+
if obj.output == "json":
|
|
576
|
+
render.emit_json({"experiment_name": name})
|
|
577
|
+
return
|
|
578
|
+
render.success(
|
|
579
|
+
f"Tracing on: experiment {name}",
|
|
580
|
+
fields={"Experiment": name},
|
|
581
|
+
next_steps=[
|
|
582
|
+
("ab dev", "Run locally with tracing on"),
|
|
583
|
+
("ab tracing list", "List traces once you have some"),
|
|
584
|
+
("ab tracing unbind", "Turn tracing off"),
|
|
585
|
+
],
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@tracing.command("unbind")
|
|
590
|
+
@click.option(
|
|
591
|
+
"--source",
|
|
592
|
+
default=".",
|
|
593
|
+
type=click.Path(exists=True, file_okay=False),
|
|
594
|
+
help="Project directory containing agent.toml. Defaults to the current directory.",
|
|
595
|
+
)
|
|
596
|
+
@click.pass_obj
|
|
597
|
+
def tracing_unbind(obj, source) -> None:
|
|
598
|
+
"""Unbind tracing: remove the experiment binding from agent.toml, turning tracing off for the
|
|
599
|
+
DEPLOYED agent (`ab deploy` then wires no MLflow env).
|
|
600
|
+
|
|
601
|
+
Deploy-only: `ab dev` still traces locally to its own MLflow server, so you keep local traces
|
|
602
|
+
while the deployed agent stays untraced.
|
|
603
|
+
"""
|
|
604
|
+
from databricks_agentbricks.agent_project import AgentProject # noqa: PLC0415
|
|
605
|
+
|
|
606
|
+
project = AgentProject.load(pathlib.Path(source))
|
|
607
|
+
project.unbind_tracing()
|
|
608
|
+
project.write()
|
|
609
|
+
|
|
610
|
+
if obj.output == "json":
|
|
611
|
+
render.emit_json({"experiment_name": None})
|
|
612
|
+
return
|
|
613
|
+
render.success(
|
|
614
|
+
"Tracing unbound (off for the deployed agent; ab dev still traces locally)",
|
|
615
|
+
next_steps=[(TRACING_BIND_COMMAND, "Turn deployed tracing back on")],
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# --- list / get -------------------------------------------------------------
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
@tracing.command("list")
|
|
623
|
+
@_experiment_read_options
|
|
624
|
+
@click.option("--limit", type=int, default=20)
|
|
625
|
+
@click.option(
|
|
626
|
+
"--source",
|
|
627
|
+
default=".",
|
|
628
|
+
type=click.Path(file_okay=False),
|
|
629
|
+
help="Project directory to resolve the default experiment from (default: current dir).",
|
|
630
|
+
)
|
|
631
|
+
@click.pass_obj
|
|
632
|
+
def tracing_list(obj, experiment_name, experiment_id, limit, source) -> None:
|
|
633
|
+
"""List recent agent traces in an experiment.
|
|
634
|
+
|
|
635
|
+
An explicit ``--experiment-name`` / ``--experiment-id`` reads that workspace experiment and must
|
|
636
|
+
name one that exists (errors otherwise, so a typo isn't mistaken for an empty experiment). With
|
|
637
|
+
neither, this project's experiment is read: the **workspace** one if it's been provisioned (by
|
|
638
|
+
`ab deploy`), otherwise the local `ab dev` store (``.agentbricks/mlflow.db``), so a not-yet-deployed
|
|
639
|
+
dev run's traces still show up here (tagged "(local dev)"). Nothing traced anywhere yet lists
|
|
640
|
+
nothing.
|
|
641
|
+
"""
|
|
642
|
+
_check_experiment_flags(experiment_name, experiment_id)
|
|
643
|
+
mlflow = _mlflow()
|
|
644
|
+
# Read inside the context manager: for a local dev store it keeps the short-lived MLflow server up
|
|
645
|
+
# for the duration of the search (return_type="list" materializes the rows before it's torn down).
|
|
646
|
+
with _with_trace_read_target(source, obj.profile, experiment_name, experiment_id) as target:
|
|
647
|
+
traces = []
|
|
648
|
+
if target.experiment_id:
|
|
649
|
+
traces = mlflow.search_traces(
|
|
650
|
+
locations=[target.experiment_id], max_results=limit, return_type="list"
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
if obj.output == "json":
|
|
654
|
+
render.emit_json([_trace_to_json(t) for t in traces])
|
|
655
|
+
return
|
|
656
|
+
rows = [
|
|
657
|
+
[
|
|
658
|
+
_attr(t, "info.trace_id", "info.request_id"),
|
|
659
|
+
render.status_pill(_status_str(_attr(t, "info.status", "info.state"))),
|
|
660
|
+
_attr(t, "info.execution_time_ms", "info.execution_duration_ms"),
|
|
661
|
+
timefmt.relative(_attr(t, "info.timestamp_ms", "info.request_time")),
|
|
662
|
+
]
|
|
663
|
+
for t in traces
|
|
664
|
+
]
|
|
665
|
+
where = " (local dev)" if target.local else ""
|
|
666
|
+
render.resource_table(
|
|
667
|
+
f"Agent Traces · {str(target.experiment_id) + where if target.experiment_id else 'no experiment yet'}",
|
|
668
|
+
[
|
|
669
|
+
("Trace ID", "left"),
|
|
670
|
+
("Status", "left"),
|
|
671
|
+
("Latency (ms)", "left"),
|
|
672
|
+
("Created", "left"),
|
|
673
|
+
],
|
|
674
|
+
rows,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@tracing.command("get")
|
|
679
|
+
@click.argument("trace_id")
|
|
680
|
+
@_experiment_read_options
|
|
681
|
+
@click.option(
|
|
682
|
+
"--source",
|
|
683
|
+
default=".",
|
|
684
|
+
type=click.Path(file_okay=False),
|
|
685
|
+
help="Project directory to resolve the experiment from (default: current dir).",
|
|
686
|
+
)
|
|
687
|
+
@click.pass_obj
|
|
688
|
+
def tracing_get(obj, trace_id, experiment_name, experiment_id, source) -> None:
|
|
689
|
+
"""Get a single trace by id (status, latency, span count, previews).
|
|
690
|
+
|
|
691
|
+
Reads from the same place as `ab tracing list`: an explicit ``--experiment-name`` /
|
|
692
|
+
``--experiment-id`` targets that workspace store and must name one that exists (errors otherwise);
|
|
693
|
+
otherwise this project's workspace experiment if provisioned, else its local `ab dev` store.
|
|
694
|
+
"""
|
|
695
|
+
_check_experiment_flags(experiment_name, experiment_id)
|
|
696
|
+
mlflow = _mlflow()
|
|
697
|
+
# Read inside the context manager so a local dev store's short-lived server stays up for the fetch.
|
|
698
|
+
with _with_trace_read_target(source, obj.profile, experiment_name, experiment_id) as target:
|
|
699
|
+
# get_trace resolves by id and needs only the tracking URI; when nothing resolved, fall back to
|
|
700
|
+
# the workspace so an id still looks there.
|
|
701
|
+
if target.tracking_uri is None:
|
|
702
|
+
mlflow.set_tracking_uri(_workspace_uri(obj.profile))
|
|
703
|
+
trace = mlflow.get_trace(trace_id)
|
|
704
|
+
if trace is None:
|
|
705
|
+
raise AgentCliError(f"No trace found with id {trace_id!r}.")
|
|
706
|
+
if obj.output == "json":
|
|
707
|
+
render.emit_json(_trace_to_json(trace))
|
|
708
|
+
return
|
|
709
|
+
spans = _attr(trace, "data.spans", default=[]) or []
|
|
710
|
+
render.detail(
|
|
711
|
+
_BREADCRUMB,
|
|
712
|
+
trace_id,
|
|
713
|
+
{
|
|
714
|
+
"Status": _status_str(_attr(trace, "info.status", "info.state")),
|
|
715
|
+
"Latency (ms)": _attr(
|
|
716
|
+
trace, "info.execution_time_ms", "info.execution_duration_ms"
|
|
717
|
+
),
|
|
718
|
+
"Spans": len(spans),
|
|
719
|
+
"Request": _attr(trace, "info.request_preview", "data.request"),
|
|
720
|
+
"Response": _attr(trace, "info.response_preview", "data.response"),
|
|
721
|
+
"Created": timefmt.absolute(_attr(trace, "info.timestamp_ms", "info.request_time")),
|
|
722
|
+
},
|
|
723
|
+
status=_status_str(_attr(trace, "info.status", "info.state")),
|
|
724
|
+
)
|