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.
Files changed (159) hide show
  1. databricks_agentbricks/__init__.py +1 -0
  2. databricks_agentbricks/_group.py +217 -0
  3. databricks_agentbricks/agent_project.py +657 -0
  4. databricks_agentbricks/app_resources.py +139 -0
  5. databricks_agentbricks/cli/__init__.py +12 -0
  6. databricks_agentbricks/cli/app.py +108 -0
  7. databricks_agentbricks/cli/app_auth.py +198 -0
  8. databricks_agentbricks/cli/auth.py +142 -0
  9. databricks_agentbricks/cli/deploy.py +954 -0
  10. databricks_agentbricks/cli/dev.py +310 -0
  11. databricks_agentbricks/cli/endpoint.py +145 -0
  12. databricks_agentbricks/cli/endpoint_examples.py +33 -0
  13. databricks_agentbricks/cli/endpoint_output.py +53 -0
  14. databricks_agentbricks/cli/endpoint_request.py +71 -0
  15. databricks_agentbricks/cli/endpoint_transport.py +148 -0
  16. databricks_agentbricks/cli/help.py +467 -0
  17. databricks_agentbricks/cli/init.py +496 -0
  18. databricks_agentbricks/cli/mcp.py +75 -0
  19. databricks_agentbricks/cli/memory.py +520 -0
  20. databricks_agentbricks/cli/sandbox.py +823 -0
  21. databricks_agentbricks/cli/sessions.py +588 -0
  22. databricks_agentbricks/cli/tools.py +456 -0
  23. databricks_agentbricks/cli/tracing.py +724 -0
  24. databricks_agentbricks/databricks_cli.py +38 -0
  25. databricks_agentbricks/errors.py +114 -0
  26. databricks_agentbricks/lakebase_runtime_store.py +107 -0
  27. databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
  28. databricks_agentbricks/project_config.py +157 -0
  29. databricks_agentbricks/project_types.py +55 -0
  30. databricks_agentbricks/py.typed +0 -0
  31. databricks_agentbricks/render.py +349 -0
  32. databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
  33. databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
  34. databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
  35. databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
  36. databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
  37. databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
  38. databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
  39. databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
  40. databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
  41. databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
  42. databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
  43. databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
  44. databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
  45. databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
  46. databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
  47. databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
  48. databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
  49. databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
  50. databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
  51. databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
  52. databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
  53. databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
  54. databricks_agentbricks/templates/agent-openai/.env.example +37 -0
  55. databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
  56. databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
  57. databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
  58. databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
  59. databricks_agentbricks/templates/agent-openai/README.md +143 -0
  60. databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
  61. databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
  62. databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
  63. databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
  64. databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
  65. databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
  66. databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
  67. databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
  68. databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
  69. databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
  70. databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
  71. databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
  72. databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
  73. databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
  74. databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
  75. databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
  76. databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
  77. databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
  78. databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
  79. databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
  80. databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
  81. databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
  82. databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
  83. databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
  84. databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
  85. databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
  86. databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
  87. databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
  88. databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
  89. databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
  90. databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
  91. databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
  92. databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
  93. databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
  94. databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
  95. databricks_agentbricks/templates/sandbox_mcp.py +39 -0
  96. databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
  97. databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
  98. databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
  99. databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
  100. databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
  101. databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
  102. databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
  103. databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
  104. databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
  105. databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
  106. databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
  107. databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
  108. databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
  109. databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
  110. databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
  111. databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
  112. databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
  113. databricks_agentbricks/theme.py +72 -0
  114. databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
  115. databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
  116. databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
  117. databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
  118. databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
  119. databricks_agentkit/__init__.py +67 -0
  120. databricks_agentkit/_api_client.py +673 -0
  121. databricks_agentkit/_pagination.py +25 -0
  122. databricks_agentkit/client.py +31 -0
  123. databricks_agentkit/langgraph/__init__.py +98 -0
  124. databricks_agentkit/langgraph/genie.py +50 -0
  125. databricks_agentkit/langgraph/mcp.py +216 -0
  126. databricks_agentkit/langgraph/memory.py +67 -0
  127. databricks_agentkit/langgraph/session_store.py +485 -0
  128. databricks_agentkit/memory_store.py +371 -0
  129. databricks_agentkit/models.py +330 -0
  130. databricks_agentkit/openai/__init__.py +93 -0
  131. databricks_agentkit/openai/genie.py +50 -0
  132. databricks_agentkit/openai/mcp.py +189 -0
  133. databricks_agentkit/openai/memory.py +67 -0
  134. databricks_agentkit/openai/sessions.py +157 -0
  135. databricks_agentkit/py.typed +0 -0
  136. databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
  137. databricks_agentkit/runtime/README.md +184 -0
  138. databricks_agentkit/runtime/__init__.py +69 -0
  139. databricks_agentkit/runtime/app.py +316 -0
  140. databricks_agentkit/runtime/auth.py +149 -0
  141. databricks_agentkit/runtime/durability/__init__.py +1 -0
  142. databricks_agentkit/runtime/durability/execution.py +133 -0
  143. databricks_agentkit/runtime/durability/heartbeat.py +64 -0
  144. databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
  145. databricks_agentkit/runtime/durability/recovery.py +70 -0
  146. databricks_agentkit/runtime/durability/store.py +33 -0
  147. databricks_agentkit/runtime/execution.py +160 -0
  148. databricks_agentkit/runtime/genie.py +224 -0
  149. databricks_agentkit/runtime/mcp_auth.py +114 -0
  150. databricks_agentkit/runtime/model_services.py +90 -0
  151. databricks_agentkit/runtime/runtime.py +273 -0
  152. databricks_agentkit/runtime/session_store_client.py +120 -0
  153. databricks_agentkit/runtime/store.py +287 -0
  154. databricks_agentkit/runtime/tool_manifest.py +223 -0
  155. databricks_agentkit/runtime/tracing.py +71 -0
  156. databricks_agentkit/runtime/types.py +118 -0
  157. databricks_agentkit/runtime/workspace.py +27 -0
  158. databricks_agentkit/session_store.py +434 -0
  159. 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)