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,54 @@
|
|
|
1
|
+
# OpenAI Agents Chat App Overlay
|
|
2
|
+
|
|
3
|
+
`ab init --framework openai` copies this framework-specific overlay after the base `agent-openai`
|
|
4
|
+
template (it is included by default; `--disable-chat-app` opts out). It is intentionally not a
|
|
5
|
+
post-generation mutation command.
|
|
6
|
+
|
|
7
|
+
## Installed files
|
|
8
|
+
|
|
9
|
+
- `ui/` contains the zero-build chat client (shared in shape with the LangGraph template's UI).
|
|
10
|
+
- `runtime/ui.py` serves the assets and exposes demo APIs for memory and sessions.
|
|
11
|
+
- `runtime/main.py` installs the chat routes on the base FastAPI runtime.
|
|
12
|
+
- `tests/test_demo_ui.py` verifies the browser-facing routes.
|
|
13
|
+
|
|
14
|
+
## Behavior
|
|
15
|
+
|
|
16
|
+
The capability indicators are automatic. Streaming and background reflect the runtime contract;
|
|
17
|
+
Session reflects transcript history; Memory requires `AGENT_MEMORY_STORE`; Traces reflects the MLflow
|
|
18
|
+
tracing config (destination + experiment). Selecting Memory opens a pane for browsing the store's
|
|
19
|
+
entries (searchable, filterable by actor, with a modal for each entry); the other indicators expand
|
|
20
|
+
in place, and Traces links out to the MLflow experiment. The transport selector is the only manual
|
|
21
|
+
capability choice.
|
|
22
|
+
|
|
23
|
+
The composer's model picker lists the workspace's Unity Catalog AI Gateway chat model services — the
|
|
24
|
+
`system.ai.*` model services from `GET /api/2.1/unity-catalog/model-services?parent=schemas/system.ai`
|
|
25
|
+
(embeddings-only services filtered out), exposed as `GET /api/demo/models` and pinned to
|
|
26
|
+
`agent.agent.MODEL` as the default. Each request sends the selected model as `model` in the
|
|
27
|
+
invocation body; the agent is rebuilt per turn, so the picker changes the model for the next turn
|
|
28
|
+
without a restart. Discovery is best-effort: if listing is unavailable (e.g. `system.ai` isn't
|
|
29
|
+
readable), the picker falls back to just the default. Omitting `model` uses `MODEL`.
|
|
30
|
+
|
|
31
|
+
The agent calls the chosen model through the gateway (`<host>/ai-gateway/mlflow/v1`) rather than
|
|
32
|
+
`/serving-endpoints`, so `MODEL` is a `system.ai.*` model service name. The picker is capped
|
|
33
|
+
(`_MODEL_LIMIT`, 20) with the default pinned first and the rest alphabetical, so truncation never
|
|
34
|
+
drops the configured default. Transient list failures are retried in
|
|
35
|
+
`databricks_agentkit.runtime.model_services` before the fallback applies.
|
|
36
|
+
|
|
37
|
+
The UI reads local history from the agent's in-process session (`SQLiteSession`) and managed history
|
|
38
|
+
from Session Store items. It keeps a stable application session UUID in browser local storage and
|
|
39
|
+
includes it inside every durable invocation's `input`; each turn gets a separate invocation UUID.
|
|
40
|
+
The router cookie is independent and may still provide sticky replica routing.
|
|
41
|
+
|
|
42
|
+
The Sessions card creates new session UUIDs in the browser. With a managed Session Store,
|
|
43
|
+
`GET /api/demo/sessions` lists the most recent sessions for the signed-in actor and each Open action
|
|
44
|
+
calls `POST /api/demo/sessions/{session_id}/open`. Opening a session verifies ownership and reloads
|
|
45
|
+
its transcript. In local in-memory mode only the current browser session can be listed because there
|
|
46
|
+
is no shared session index.
|
|
47
|
+
|
|
48
|
+
Transcript responses include only user, assistant, tool, system, and human-decision message items;
|
|
49
|
+
non-message items remain in Session Store but are never returned to the chat UI.
|
|
50
|
+
|
|
51
|
+
Human-in-the-loop pauses are **in-process only**: a paused run (the Agents SDK `RunState`) is held in
|
|
52
|
+
memory by `agent/agent.py`, not in the session transcript, so — unlike the LangGraph template — it is
|
|
53
|
+
not durable even with a managed Session Store, and the unmanaged history path never reports pending
|
|
54
|
+
interrupts. Resume a pause on the same process that created it.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Run the OpenAI Agents SDK agent with the optional Agent Bricks chat app."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import uvicorn
|
|
7
|
+
from agent.agent import configure
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
from runtime.adapter import invoke, recover
|
|
10
|
+
from runtime.ui import install_ui
|
|
11
|
+
|
|
12
|
+
from databricks_agentkit import DurableAgentServer
|
|
13
|
+
|
|
14
|
+
# override=False so injected DATABRICKS_* (from `ab dev -p` or the deploy platform) win over a
|
|
15
|
+
# checked-in .env.
|
|
16
|
+
load_dotenv(dotenv_path=Path(__file__).parent.parent / ".env", override=False)
|
|
17
|
+
configure()
|
|
18
|
+
|
|
19
|
+
app = DurableAgentServer()
|
|
20
|
+
app.invoke(invoke)
|
|
21
|
+
app.recover(recover)
|
|
22
|
+
install_ui(app)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
uvicorn.run("runtime.main:app", host="0.0.0.0", port=int(os.getenv("PORT", "8000")))
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""Browser UI and managed-state demo controls for an Agent Bricks agent project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import uuid
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from fastapi import FastAPI, HTTPException, Query, Request
|
|
14
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
15
|
+
from fastapi.staticfiles import StaticFiles
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
|
|
18
|
+
from databricks_agentkit import workspace_client
|
|
19
|
+
from databricks_agentkit.runtime.model_services import list_ai_gateway_model_services
|
|
20
|
+
from databricks_agentkit.runtime.store import runtime_store_is_persistent_environment
|
|
21
|
+
|
|
22
|
+
_UI_ROOT = Path(__file__).resolve().parent.parent / "ui"
|
|
23
|
+
_INSTANCE_ID = uuid.uuid4().hex[:12] # identifies this process in the UI
|
|
24
|
+
_AGENTS_API = "/api/2.0/agents"
|
|
25
|
+
_MESSAGE_ROLES = {
|
|
26
|
+
"ai",
|
|
27
|
+
"assistant",
|
|
28
|
+
"developer",
|
|
29
|
+
"function",
|
|
30
|
+
"human",
|
|
31
|
+
"human_decision",
|
|
32
|
+
"system",
|
|
33
|
+
"tool",
|
|
34
|
+
"user",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MemoryEntryRequest(BaseModel):
|
|
39
|
+
path: str = Field(min_length=1, pattern=r"^/")
|
|
40
|
+
content: str = Field(min_length=1)
|
|
41
|
+
description: str | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MemorySearchRequest(BaseModel):
|
|
45
|
+
query: str = Field(min_length=1)
|
|
46
|
+
limit: int = Field(default=10, ge=1, le=100)
|
|
47
|
+
actor: str | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SessionItemsRequest(BaseModel):
|
|
51
|
+
items: list[dict[str, Any]] = Field(min_length=1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_USER_HEADERS = ("x-forwarded-email", "x-forwarded-user")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _memory_store() -> str:
|
|
58
|
+
# Same resolution the agent uses (AGENT_MEMORY_STORE env → agent.toml binding), so the demo
|
|
59
|
+
# panels reflect exactly the store the agent reads/writes.
|
|
60
|
+
from databricks_agentkit.runtime.tool_manifest import resolve_memory_store
|
|
61
|
+
|
|
62
|
+
return (resolve_memory_store() or "").strip().strip("/")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _session_store() -> str:
|
|
66
|
+
from databricks_agentkit.runtime.tool_manifest import resolve_session_store
|
|
67
|
+
|
|
68
|
+
return (resolve_session_store() or "").strip()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _request_actor(request: Request) -> str:
|
|
72
|
+
"""The actor for a demo request — the signed-in user, so the panels show that user's own data.
|
|
73
|
+
|
|
74
|
+
Mirrors how the agent resolves its actor (same forwarded-identity headers), so the memory and
|
|
75
|
+
session views here list exactly what the agent reads/writes for the current user. Falls back to
|
|
76
|
+
``"agent"`` locally / when unauthenticated.
|
|
77
|
+
"""
|
|
78
|
+
for header in _USER_HEADERS:
|
|
79
|
+
if value := request.headers.get(header):
|
|
80
|
+
return value
|
|
81
|
+
return "agent"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _request_session_id(request: Request) -> str:
|
|
85
|
+
"""Read the chat session selected by the browser, falling back to the router cookie locally."""
|
|
86
|
+
session_id = request.query_params.get("session_id") or getattr(
|
|
87
|
+
request.state, "session_id", None
|
|
88
|
+
)
|
|
89
|
+
if not session_id:
|
|
90
|
+
raise HTTPException(status_code=400, detail="session_id is required")
|
|
91
|
+
return str(session_id)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _is_deployed() -> bool:
|
|
95
|
+
app_url = os.getenv("DATABRICKS_APP_URL", "")
|
|
96
|
+
is_local = app_url.startswith(("http://localhost", "http://127.0.0.1"))
|
|
97
|
+
return bool(os.getenv("DATABRICKS_APP_NAME")) and not is_local
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Tracing turns on only with both a destination and an experiment, mirroring
|
|
101
|
+
# the runtime tracing setup so the UI card matches what the agent actually does.
|
|
102
|
+
_TRACING_DESTINATION_VARS = ("MLFLOW_TRACKING_URI", "MLFLOW_TRACING_DESTINATION")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _workspace_host() -> str:
|
|
106
|
+
"""Best-effort workspace host for building MLflow links; never raises."""
|
|
107
|
+
try:
|
|
108
|
+
host = workspace_client().config.host or ""
|
|
109
|
+
except Exception: # noqa: BLE001 - host is best-effort; a failure must not break the config route
|
|
110
|
+
host = os.getenv("DATABRICKS_HOST", "")
|
|
111
|
+
return host.rstrip("/")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _tracing() -> dict:
|
|
115
|
+
"""Whether MLflow tracing is configured, plus a best-effort link to its experiment."""
|
|
116
|
+
destination = ""
|
|
117
|
+
for var in _TRACING_DESTINATION_VARS:
|
|
118
|
+
if value := os.getenv(var):
|
|
119
|
+
destination = value
|
|
120
|
+
break
|
|
121
|
+
experiment_id = os.getenv("MLFLOW_EXPERIMENT_ID", "").strip()
|
|
122
|
+
experiment_name = os.getenv("MLFLOW_EXPERIMENT_NAME", "").strip()
|
|
123
|
+
enabled = bool(destination) and bool(experiment_id or experiment_name)
|
|
124
|
+
url: str | None = None
|
|
125
|
+
if enabled:
|
|
126
|
+
if destination.startswith(("http://", "https://")) and "databricks" not in destination:
|
|
127
|
+
# A local OSS MLflow server: link into its own UI.
|
|
128
|
+
base = destination.rstrip("/")
|
|
129
|
+
url = f"{base}/#/experiments/{experiment_id}" if experiment_id else base
|
|
130
|
+
else:
|
|
131
|
+
host = _workspace_host()
|
|
132
|
+
if host:
|
|
133
|
+
url = (
|
|
134
|
+
f"{host}/ml/experiments/{experiment_id}"
|
|
135
|
+
if experiment_id
|
|
136
|
+
else f"{host}/ml/experiments"
|
|
137
|
+
)
|
|
138
|
+
return {"enabled": enabled, "experiment_id": experiment_id or None, "url": url}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Cap the picker: the gateway's schema can list more models than belong in a dropdown.
|
|
142
|
+
_MODEL_LIMIT = 20
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _default_model() -> str:
|
|
146
|
+
"""The agent's configured default endpoint (``agent.agent.MODEL``), imported lazily.
|
|
147
|
+
|
|
148
|
+
Imported inside the function, not at module load, so the light UI import path doesn't pull in the
|
|
149
|
+
agent stack (matching ``_local_history`` below).
|
|
150
|
+
"""
|
|
151
|
+
from agent.agent import MODEL
|
|
152
|
+
|
|
153
|
+
return MODEL
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _rank_models(default: str, names: list[str]) -> list[str]:
|
|
157
|
+
"""Pin the default first, then the remaining gateway models alphabetically, without duplicates.
|
|
158
|
+
|
|
159
|
+
Truncation therefore only ever sheds models beyond the cap, never the configured default.
|
|
160
|
+
"""
|
|
161
|
+
ordered = [default, *sorted(n for n in names if n != default)]
|
|
162
|
+
seen: set[str] = set()
|
|
163
|
+
return [n for n in ordered if not (n in seen or seen.add(n))]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _discover_chat_models() -> list[str]:
|
|
167
|
+
"""The AI Gateway chat models for the picker: default first, then the rest, capped.
|
|
168
|
+
|
|
169
|
+
Best-effort: if listing fails (missing permission, repeated transient errors), fall back to just
|
|
170
|
+
the default so the picker still works. The default is always present and first.
|
|
171
|
+
"""
|
|
172
|
+
default = _default_model()
|
|
173
|
+
try:
|
|
174
|
+
names = list_ai_gateway_model_services(workspace_client())
|
|
175
|
+
except Exception: # noqa: BLE001 - a broken listing must not break the whole config endpoint
|
|
176
|
+
names = []
|
|
177
|
+
return _rank_models(default, names)[:_MODEL_LIMIT]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class _ManagedStateClient:
|
|
181
|
+
def __init__(self) -> None:
|
|
182
|
+
self._workspace = workspace_client()
|
|
183
|
+
|
|
184
|
+
def _do(
|
|
185
|
+
self,
|
|
186
|
+
method: str,
|
|
187
|
+
path: str,
|
|
188
|
+
*,
|
|
189
|
+
query: dict[str, Any] | None = None,
|
|
190
|
+
body: dict[str, Any] | None = None,
|
|
191
|
+
) -> dict:
|
|
192
|
+
result = self._workspace.api_client.do(method, path, query=query, body=body)
|
|
193
|
+
if not isinstance(result, dict):
|
|
194
|
+
raise RuntimeError(
|
|
195
|
+
f"Expected an object response from {path}, got {type(result).__name__}"
|
|
196
|
+
)
|
|
197
|
+
return result
|
|
198
|
+
|
|
199
|
+
def create_memory_entry(self, actor: str, request: MemoryEntryRequest, session_id: str) -> dict:
|
|
200
|
+
body = {
|
|
201
|
+
"actor_id": actor,
|
|
202
|
+
"path": request.path,
|
|
203
|
+
"content": request.content,
|
|
204
|
+
"session_id": session_id,
|
|
205
|
+
}
|
|
206
|
+
if request.description:
|
|
207
|
+
body["description"] = request.description
|
|
208
|
+
return self._do("POST", f"{_AGENTS_API}/memory-stores/{_memory_store()}/entries", body=body)
|
|
209
|
+
|
|
210
|
+
def list_memory_entries(self, actor: str, path_prefix: str | None = None) -> dict:
|
|
211
|
+
query = {"actor_id": actor, "page_size": 100}
|
|
212
|
+
if path_prefix:
|
|
213
|
+
query["path_prefix"] = path_prefix
|
|
214
|
+
return self._do(
|
|
215
|
+
"GET", f"{_AGENTS_API}/memory-stores/{_memory_store()}/entries", query=query
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def search_memory_entries(self, actor: str, request: MemorySearchRequest) -> dict:
|
|
219
|
+
return self._do(
|
|
220
|
+
"POST",
|
|
221
|
+
f"{_AGENTS_API}/memory-stores/{_memory_store()}/entries:search",
|
|
222
|
+
body={
|
|
223
|
+
"actor_id": actor,
|
|
224
|
+
"query": request.query,
|
|
225
|
+
"limit": request.limit,
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def ensure_session(self, actor: str, session_id: str) -> dict:
|
|
230
|
+
try:
|
|
231
|
+
return self._do(
|
|
232
|
+
"POST",
|
|
233
|
+
f"{_AGENTS_API}/session-stores/{_session_store()}/sessions",
|
|
234
|
+
query={"session_id": session_id},
|
|
235
|
+
body={
|
|
236
|
+
"actor_id": actor,
|
|
237
|
+
"metadata": {"client": "agentbricks-demo-ui"},
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
code = str(getattr(exc, "error_code", "")).upper()
|
|
242
|
+
already_exists = code in {"ALREADY_EXISTS", "RESOURCE_ALREADY_EXISTS"}
|
|
243
|
+
if not already_exists and "already exists" not in str(exc).lower():
|
|
244
|
+
raise
|
|
245
|
+
return self._do(
|
|
246
|
+
"GET",
|
|
247
|
+
f"{_AGENTS_API}/session-stores/{_session_store()}/sessions/{session_id}",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def get_session(self, session_id: str) -> dict:
|
|
251
|
+
return self._do(
|
|
252
|
+
"GET",
|
|
253
|
+
f"{_AGENTS_API}/session-stores/{_session_store()}/sessions/{session_id}",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def list_sessions(self, actor: str) -> dict:
|
|
257
|
+
return self._do(
|
|
258
|
+
"GET",
|
|
259
|
+
f"{_AGENTS_API}/session-stores/{_session_store()}/sessions",
|
|
260
|
+
query={
|
|
261
|
+
"filter": f"actor_id = {json.dumps(actor)}",
|
|
262
|
+
"order_by": "last_activity_time desc",
|
|
263
|
+
"page_size": 50,
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def append_session_items(self, session_id: str, items: list[dict[str, Any]]) -> dict:
|
|
268
|
+
return self._do(
|
|
269
|
+
"POST",
|
|
270
|
+
f"{_AGENTS_API}/session-stores/{_session_store()}/sessions/{session_id}/items:append",
|
|
271
|
+
body={"items": [{"data": item} for item in items]},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def list_session_items(self, session_id: str) -> dict:
|
|
275
|
+
return self._do(
|
|
276
|
+
"GET",
|
|
277
|
+
f"{_AGENTS_API}/session-stores/{_session_store()}/sessions/{session_id}/items",
|
|
278
|
+
query={"order_by": "create_time asc", "page_size": 100},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@lru_cache(maxsize=1)
|
|
283
|
+
def _state_client() -> _ManagedStateClient:
|
|
284
|
+
return _ManagedStateClient()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
async def _managed_call(operation, *args):
|
|
288
|
+
try:
|
|
289
|
+
return await asyncio.to_thread(operation, *args)
|
|
290
|
+
except HTTPException:
|
|
291
|
+
raise
|
|
292
|
+
except Exception as exc:
|
|
293
|
+
code = getattr(exc, "error_code", None)
|
|
294
|
+
detail = f"{code}: {exc}" if code else str(exc)
|
|
295
|
+
raise HTTPException(status_code=502, detail=detail) from exc
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _require_memory() -> None:
|
|
299
|
+
if not _memory_store():
|
|
300
|
+
raise HTTPException(
|
|
301
|
+
status_code=503,
|
|
302
|
+
detail="No memory store configured. Run `ab memory bind <store>`.",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _require_session() -> None:
|
|
307
|
+
if not _session_store():
|
|
308
|
+
raise HTTPException(
|
|
309
|
+
status_code=503,
|
|
310
|
+
detail="No session store configured. Run `ab sessions bind <store>`.",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
async def _local_history(session_id: str) -> dict[str, Any]:
|
|
315
|
+
"""Reconstruct the transcript from the in-process session (no managed Session Store).
|
|
316
|
+
|
|
317
|
+
Reads the Responses items the agent stored in its ``SQLiteSession`` for this browser session and
|
|
318
|
+
shapes each into a ``{item_id, data}`` entry the UI renders. There are no durable interrupts here:
|
|
319
|
+
a paused human-in-the-loop run is held in-process by ``agent.py`` and is not part of the session
|
|
320
|
+
transcript, so ``interrupts`` is always empty for the unmanaged path.
|
|
321
|
+
"""
|
|
322
|
+
from databricks_agentkit.openai.sessions import session_store
|
|
323
|
+
|
|
324
|
+
session = session_store(session_id)
|
|
325
|
+
items = []
|
|
326
|
+
for index, message in enumerate(await session.get_items()):
|
|
327
|
+
data = message if isinstance(message, dict) else {"content": str(message)}
|
|
328
|
+
items.append({"item_id": str(data.get("id") or index), "data": data})
|
|
329
|
+
return {"session_id": session_id, "session_items": items, "interrupts": []}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _chat_sessions(result: dict[str, Any]) -> list[dict[str, Any]]:
|
|
333
|
+
sessions = []
|
|
334
|
+
for session in result.get("sessions", []):
|
|
335
|
+
if not isinstance(session, dict):
|
|
336
|
+
continue
|
|
337
|
+
metadata = session.get("metadata")
|
|
338
|
+
metadata = metadata if isinstance(metadata, dict) else {}
|
|
339
|
+
if metadata.get("public_session_id"):
|
|
340
|
+
continue
|
|
341
|
+
sessions.append(session)
|
|
342
|
+
return sessions
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _chat_session_items(result: dict[str, Any]) -> dict[str, Any]:
|
|
346
|
+
items = []
|
|
347
|
+
for item in result.get("session_items", []):
|
|
348
|
+
if not isinstance(item, dict):
|
|
349
|
+
continue
|
|
350
|
+
data = item.get("data")
|
|
351
|
+
if not isinstance(data, dict) or data.get("event_type") or "content" not in data:
|
|
352
|
+
continue
|
|
353
|
+
role = str(data.get("role") or data.get("type") or "").lower()
|
|
354
|
+
if role in _MESSAGE_ROLES:
|
|
355
|
+
items.append(item)
|
|
356
|
+
return {**result, "session_items": items}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def install_ui(app: FastAPI) -> None:
|
|
360
|
+
"""Mount the Agent Bricks UI and its runtime control endpoints."""
|
|
361
|
+
app.mount("/ui-assets", StaticFiles(directory=_UI_ROOT), name="agentbricks-demo-ui-assets")
|
|
362
|
+
|
|
363
|
+
@app.middleware("http")
|
|
364
|
+
async def disable_ui_caching(request: Request, call_next):
|
|
365
|
+
response = await call_next(request)
|
|
366
|
+
if request.url.path == "/" or request.url.path.startswith("/ui-assets/"):
|
|
367
|
+
response.headers["Cache-Control"] = "no-store"
|
|
368
|
+
return response
|
|
369
|
+
|
|
370
|
+
@app.get("/", include_in_schema=False)
|
|
371
|
+
async def index() -> FileResponse:
|
|
372
|
+
return FileResponse(_UI_ROOT / "index.html")
|
|
373
|
+
|
|
374
|
+
@app.get("/api/ui/config", include_in_schema=False)
|
|
375
|
+
async def ui_config(request: Request) -> dict:
|
|
376
|
+
actor = _request_actor(request)
|
|
377
|
+
memory_store = _memory_store()
|
|
378
|
+
session_store = _session_store()
|
|
379
|
+
default_model = _default_model()
|
|
380
|
+
runtime_store_persistent = runtime_store_is_persistent_environment()
|
|
381
|
+
runtime_store_mode = (
|
|
382
|
+
"Runtime Store" if runtime_store_persistent else "In-process Runtime Store"
|
|
383
|
+
)
|
|
384
|
+
return {
|
|
385
|
+
"session_id": _request_session_id(request),
|
|
386
|
+
"instance_id": _INSTANCE_ID,
|
|
387
|
+
"viewer": actor if actor != "agent" else "Local developer",
|
|
388
|
+
"deployed": _is_deployed(),
|
|
389
|
+
"models": {"default": default_model, "available": [default_model]},
|
|
390
|
+
"streaming": {
|
|
391
|
+
"enabled": True,
|
|
392
|
+
"transport": "Server-sent events",
|
|
393
|
+
"persistent": runtime_store_persistent,
|
|
394
|
+
"mode": runtime_store_mode,
|
|
395
|
+
},
|
|
396
|
+
"background": {
|
|
397
|
+
"enabled": True,
|
|
398
|
+
"persistent": runtime_store_persistent,
|
|
399
|
+
"mode": runtime_store_mode,
|
|
400
|
+
},
|
|
401
|
+
"session": {
|
|
402
|
+
"durable": bool(session_store),
|
|
403
|
+
"managed": bool(session_store),
|
|
404
|
+
"history": True,
|
|
405
|
+
"mode": "Managed Session Store" if session_store else "In-process session",
|
|
406
|
+
"store": session_store or None,
|
|
407
|
+
"actor": actor,
|
|
408
|
+
},
|
|
409
|
+
"memory": {
|
|
410
|
+
"enabled": bool(memory_store),
|
|
411
|
+
"store": f"memory-stores/{memory_store}" if memory_store else None,
|
|
412
|
+
"actor": actor,
|
|
413
|
+
},
|
|
414
|
+
"tracing": _tracing(),
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
@app.get("/api/demo/models", include_in_schema=False)
|
|
418
|
+
async def demo_models() -> dict:
|
|
419
|
+
# Model discovery can take several seconds in a large workspace. Keep it separate from the
|
|
420
|
+
# runtime config so the rest of the UI becomes interactive immediately.
|
|
421
|
+
available_models = await asyncio.to_thread(_discover_chat_models)
|
|
422
|
+
return {"default": _default_model(), "available": available_models}
|
|
423
|
+
|
|
424
|
+
@app.post("/api/demo/memory/entries", include_in_schema=False)
|
|
425
|
+
async def create_memory_entry(request: Request, payload: MemoryEntryRequest) -> dict:
|
|
426
|
+
_require_memory()
|
|
427
|
+
return await _managed_call(
|
|
428
|
+
_state_client().create_memory_entry,
|
|
429
|
+
_request_actor(request),
|
|
430
|
+
payload,
|
|
431
|
+
_request_session_id(request),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
@app.get("/api/demo/memory/entries", include_in_schema=False)
|
|
435
|
+
async def list_memory_entries(
|
|
436
|
+
request: Request,
|
|
437
|
+
path_prefix: str | None = Query(default=None),
|
|
438
|
+
actor: str | None = Query(default=None),
|
|
439
|
+
) -> dict:
|
|
440
|
+
# The UI can browse another actor's memories by passing ?actor=; default to the viewer.
|
|
441
|
+
_require_memory()
|
|
442
|
+
return await _managed_call(
|
|
443
|
+
_state_client().list_memory_entries, actor or _request_actor(request), path_prefix
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
@app.post("/api/demo/memory/search", include_in_schema=False)
|
|
447
|
+
async def search_memory_entries(request: Request, payload: MemorySearchRequest) -> dict:
|
|
448
|
+
# payload.actor lets the UI search another actor's memories; default to the viewer.
|
|
449
|
+
_require_memory()
|
|
450
|
+
return await _managed_call(
|
|
451
|
+
_state_client().search_memory_entries, payload.actor or _request_actor(request), payload
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
@app.post("/api/demo/sessions", include_in_schema=False)
|
|
455
|
+
async def ensure_session(request: Request) -> dict:
|
|
456
|
+
_require_session()
|
|
457
|
+
return await _managed_call(
|
|
458
|
+
_state_client().ensure_session,
|
|
459
|
+
_request_actor(request),
|
|
460
|
+
_request_session_id(request),
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
@app.get("/api/demo/sessions", include_in_schema=False)
|
|
464
|
+
async def list_sessions(request: Request) -> dict:
|
|
465
|
+
session_id = _request_session_id(request)
|
|
466
|
+
actor = _request_actor(request)
|
|
467
|
+
if not _session_store():
|
|
468
|
+
return {
|
|
469
|
+
"sessions": [
|
|
470
|
+
{
|
|
471
|
+
"session_id": session_id,
|
|
472
|
+
"actor_id": actor,
|
|
473
|
+
"metadata": {"client": "agentbricks-demo-ui-local"},
|
|
474
|
+
}
|
|
475
|
+
],
|
|
476
|
+
"current_session_id": session_id,
|
|
477
|
+
"managed": False,
|
|
478
|
+
}
|
|
479
|
+
result = await _managed_call(_state_client().list_sessions, actor)
|
|
480
|
+
return {
|
|
481
|
+
**result,
|
|
482
|
+
"sessions": _chat_sessions(result),
|
|
483
|
+
"current_session_id": session_id,
|
|
484
|
+
"managed": True,
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@app.post("/api/demo/sessions/{session_id}/open", include_in_schema=False)
|
|
488
|
+
async def open_session(request: Request, session_id: str) -> JSONResponse:
|
|
489
|
+
_require_session()
|
|
490
|
+
session = await _managed_call(_state_client().get_session, session_id)
|
|
491
|
+
if session.get("actor_id") != _request_actor(request):
|
|
492
|
+
raise HTTPException(status_code=403, detail="Session belongs to another actor.")
|
|
493
|
+
return JSONResponse(
|
|
494
|
+
{
|
|
495
|
+
"session_id": session_id,
|
|
496
|
+
"previous_session_id": _request_session_id(request),
|
|
497
|
+
"managed": True,
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
@app.get("/api/demo/session", include_in_schema=False)
|
|
502
|
+
async def get_session(request: Request) -> dict:
|
|
503
|
+
_require_session()
|
|
504
|
+
return await _managed_call(_state_client().get_session, _request_session_id(request))
|
|
505
|
+
|
|
506
|
+
@app.post("/api/demo/session/items", include_in_schema=False)
|
|
507
|
+
async def append_session_items(request: Request, payload: SessionItemsRequest) -> dict:
|
|
508
|
+
_require_session()
|
|
509
|
+
return await _managed_call(
|
|
510
|
+
_state_client().append_session_items,
|
|
511
|
+
_request_session_id(request),
|
|
512
|
+
payload.items,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
@app.get("/api/demo/session/items", include_in_schema=False)
|
|
516
|
+
async def list_session_items(request: Request) -> dict:
|
|
517
|
+
session_id = _request_session_id(request)
|
|
518
|
+
if _session_store():
|
|
519
|
+
result = await _managed_call(_state_client().list_session_items, session_id)
|
|
520
|
+
return _chat_session_items(result)
|
|
521
|
+
return await _local_history(session_id)
|