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,42 @@
1
+ [project]
2
+ name = "agent-openai"
3
+ version = "0.1.0"
4
+ description = "OpenAI Agents SDK agent on the Agent Bricks durable runtime for Databricks Apps"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Agent Developer", email = "developer@example.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ # Agent Bricks includes the HTTP runtime; this extra adds the openai framework adapter.
12
+ "databricks-agentbricks[openai]>=0.2.0",
13
+ "uvicorn>=0.41.0",
14
+ "python-dotenv>=1.2.1",
15
+ "databricks-sdk>=0.124.0",
16
+ "databricks-agents>=1.9.3",
17
+ ]
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["agent", "runtime"]
25
+
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "hatchling>=1.28.0",
30
+ "pytest>=9.0.2",
31
+ "pytest-asyncio>=1.3.0",
32
+ ]
33
+
34
+ [tool.uv]
35
+ default-groups = ["dev"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+ asyncio_mode = "auto"
40
+
41
+ [project.scripts]
42
+ start-server = "runtime.main:main"
@@ -0,0 +1,188 @@
1
+ """Translate between Agent Bricks invocations and the framework-native agent entrypoint."""
2
+
3
+ from collections.abc import AsyncGenerator
4
+ from typing import Any
5
+
6
+ from agent.agent import resume_agent, run_agent
7
+ from agents import RunResultStreaming
8
+ from agents.items import ToolApprovalItem
9
+ from openai.types.responses import ResponseTextDeltaEvent
10
+
11
+ from databricks_agentkit import InvocationContext
12
+ from databricks_agentkit.runtime.auth import AuthError
13
+
14
+ _RECOVERY_INSTRUCTION = (
15
+ "This is a recovery attempt after a previous worker stopped before completing this invocation. "
16
+ "Continue the same task, but assume some tool calls or external side effects may already have "
17
+ "completed or may still be in progress. Check current state before repeating side effects and "
18
+ "avoid duplicate actions when possible."
19
+ )
20
+
21
+
22
+ def _payload(value: Any) -> dict[str, Any]:
23
+ if isinstance(value, list):
24
+ return {"messages": value}
25
+ if not isinstance(value, dict):
26
+ raise ValueError("input must be a message list or an object")
27
+ return value
28
+
29
+
30
+ def _session_id(payload: dict[str, Any], context: InvocationContext) -> str:
31
+ value = payload.get("session_id") or context.session_id
32
+ if not isinstance(value, str) or not value:
33
+ raise ValueError("session_id must be a non-empty string")
34
+ return value
35
+
36
+
37
+ def _actor(payload: dict[str, Any], session_id: str) -> str:
38
+ value = payload.get("actor") or session_id
39
+ if not isinstance(value, str) or not value:
40
+ raise ValueError("actor must be a non-empty string")
41
+ return value
42
+
43
+
44
+ def _agent_input(
45
+ payload: dict[str, Any],
46
+ session_id: str,
47
+ *,
48
+ recovery: bool = False,
49
+ ) -> list[Any] | Any:
50
+ if (resume := payload.get("resume")) is not None:
51
+ if not isinstance(resume, dict):
52
+ raise ValueError("resume must be an object")
53
+ return resume_agent(session_id, resume)
54
+ messages = payload.get("messages") or []
55
+ if not isinstance(messages, list):
56
+ raise ValueError("messages must be a list")
57
+ if recovery:
58
+ return [{"role": "developer", "content": _RECOVERY_INSTRUCTION}, *messages]
59
+ return messages
60
+
61
+
62
+ async def invoke(value: Any, context: InvocationContext) -> dict:
63
+ return await _invoke_agent(_payload(value), context)
64
+
65
+
66
+ async def recover(value: Any, context: InvocationContext) -> dict:
67
+ return await _invoke_agent(_payload(value), context, recovery=True)
68
+
69
+
70
+ async def _invoke_agent(
71
+ payload: dict[str, Any],
72
+ context: InvocationContext,
73
+ *,
74
+ recovery: bool = False,
75
+ ) -> dict:
76
+ session_id = _session_id(payload, context)
77
+ actor = _actor(payload, session_id)
78
+ auth = getattr(context, "request_auth", None)
79
+ user_auth = auth is not None
80
+ if user_auth and any(
81
+ payload.get(key) is not None for key in ("resume", "approval", "approvals")
82
+ ):
83
+ raise AuthError(
84
+ "MCP_USER_AUTH_HITL_UNSUPPORTED",
85
+ "Request-user invocations do not support approval or resume input.",
86
+ 400,
87
+ )
88
+ if user_auth and recovery:
89
+ raise AuthError(
90
+ "MCP_USER_AUTH_BACKGROUND_UNSUPPORTED",
91
+ "Request-user invocations cannot be recovered in the background.",
92
+ 400,
93
+ )
94
+ internal_session_id = (
95
+ auth.namespace("session", session_id) if auth and payload.get("session_id") else session_id
96
+ )
97
+ actor = auth.namespace("actor", actor) if auth else actor
98
+ auth_kwargs = {"workspace_client_for": auth.client_for} if user_auth else {}
99
+ model = payload.get("model")
100
+ outputs = []
101
+ async with run_agent(
102
+ _agent_input(payload, internal_session_id, recovery=recovery),
103
+ session_id=internal_session_id,
104
+ actor=actor,
105
+ model=model if isinstance(model, str) else None,
106
+ **auth_kwargs,
107
+ ) as result:
108
+ async for event in _serialize_events(result):
109
+ if user_auth and event.get("type") == "interrupt":
110
+ raise AuthError(
111
+ "MCP_USER_AUTH_HITL_UNSUPPORTED",
112
+ "Request-user invocations do not support paused approvals.",
113
+ 400,
114
+ )
115
+ await context.emit(event)
116
+ if event.get("type") in ("message", "interrupt"):
117
+ outputs.append(event)
118
+
119
+ interrupted = bool(outputs and outputs[-1].get("type") == "interrupt")
120
+ return {
121
+ "output": [event["message"] if event["type"] == "message" else event for event in outputs],
122
+ **({"session_id": session_id} if not user_auth or payload.get("session_id") else {}),
123
+ "status": "interrupted" if interrupted else "completed",
124
+ }
125
+
126
+
127
+ async def _serialize_events(result: RunResultStreaming) -> AsyncGenerator[dict, None]:
128
+ async for event in result.stream_events():
129
+ if event.type == "raw_response_event":
130
+ if isinstance(event.data, ResponseTextDeltaEvent) and event.data.delta:
131
+ yield {"type": "delta", "content": event.data.delta, "id": event.data.item_id}
132
+ elif event.type == "run_item_stream_event":
133
+ if message := _normalize_item(event.item):
134
+ yield {"type": "message", "message": message}
135
+
136
+ for item in result.interruptions:
137
+ yield {"type": "interrupt", "id": item.call_id, "value": _approval_value(item)}
138
+
139
+
140
+ def _approval_value(item: ToolApprovalItem) -> dict:
141
+ return {"action_requests": [{"name": item.tool_name, "args": _tool_args(item)}]}
142
+
143
+
144
+ def _tool_args(item: ToolApprovalItem) -> Any:
145
+ import json
146
+
147
+ args = item.arguments
148
+ if isinstance(args, str):
149
+ try:
150
+ return json.loads(args)
151
+ except json.JSONDecodeError:
152
+ return {"arguments": args}
153
+ return args or {}
154
+
155
+
156
+ def _normalize_item(item: Any) -> dict | None:
157
+ from agents import ItemHelpers
158
+ from agents.items import MessageOutputItem, ToolCallItem, ToolCallOutputItem
159
+
160
+ if isinstance(item, MessageOutputItem):
161
+ return {"role": "assistant", "content": ItemHelpers.text_message_output(item)}
162
+ if isinstance(item, ToolCallItem):
163
+ return {
164
+ "role": "assistant",
165
+ "content": "",
166
+ "tool_calls": [{"name": item.tool_name, "args": _tool_args_from_call(item)}],
167
+ }
168
+ if isinstance(item, ToolCallOutputItem):
169
+ return {"role": "tool", "name": _tool_call_name(item), "content": str(item.output)}
170
+ return None
171
+
172
+
173
+ def _tool_args_from_call(item: Any) -> Any:
174
+ import json
175
+
176
+ raw = item.raw_item
177
+ args = raw.get("arguments") if isinstance(raw, dict) else getattr(raw, "arguments", None)
178
+ if isinstance(args, str):
179
+ try:
180
+ return json.loads(args)
181
+ except json.JSONDecodeError:
182
+ return {"arguments": args}
183
+ return args or {}
184
+
185
+
186
+ def _tool_call_name(item: Any) -> str | None:
187
+ raw = item.raw_item
188
+ return raw.get("name") if isinstance(raw, dict) else getattr(raw, "name", None)
@@ -0,0 +1,25 @@
1
+ """Run the OpenAI Agents SDK agent through Agent Bricks Runtime."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ # Importing the agent is side-effect-free (no env is read until configure()), so it sits up top.
7
+ import uvicorn
8
+ from agent.agent import configure
9
+ from dotenv import load_dotenv
10
+
11
+ from databricks_agentkit import DurableAgentServer
12
+ from runtime.adapter import invoke, recover
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
+
23
+
24
+ def main() -> None:
25
+ uvicorn.run("runtime.main:app", host="0.0.0.0", port=int(os.getenv("PORT", "8000")))
@@ -0,0 +1,326 @@
1
+ """Smoke tests for the agent.
2
+
3
+ Hermetic tests import only the leaf modules (tools, session store, event serialization) — no
4
+ Databricks auth needed, so they run anywhere. The live test builds the full agent and calls the
5
+ model; it is skipped unless a workspace profile is configured.
6
+ """
7
+
8
+ import os
9
+ from contextlib import asynccontextmanager
10
+ from types import SimpleNamespace
11
+ from unittest.mock import AsyncMock, MagicMock
12
+
13
+ import pytest
14
+ from agent.agent import resume_agent
15
+ from agent.tools import all_tools
16
+ from agents import FunctionTool
17
+ from runtime.adapter import _normalize_item, _serialize_events
18
+
19
+
20
+ def test_tools_autoregister():
21
+ tools = all_tools()
22
+ assert tools, "expected the sample tool to auto-register"
23
+ assert all(isinstance(t, FunctionTool) for t in tools)
24
+ assert {"get_current_time", "send_message"} <= {t.name for t in tools}
25
+
26
+
27
+ def test_gated_tool_needs_approval():
28
+ # The gated demo tool must exist, be listed for approval, and declare needs_approval, or the HITL
29
+ # demo does nothing.
30
+ from agent.agent import REQUIRE_APPROVAL
31
+
32
+ assert "send_message" in REQUIRE_APPROVAL
33
+ send = next(t for t in all_tools() if t.name == "send_message")
34
+ assert send.needs_approval is True
35
+
36
+
37
+ class _FakeItem:
38
+ """Stand-in for an Agents SDK run item, matched by _normalize_item's isinstance checks."""
39
+
40
+
41
+ def test_normalize_message_item():
42
+ from agents.items import MessageOutputItem
43
+
44
+ item = object.__new__(MessageOutputItem)
45
+ # ItemHelpers.text_message_output reads raw_item.content; give it a text part.
46
+ from openai.types.responses import ResponseOutputMessage, ResponseOutputText
47
+
48
+ item.raw_item = ResponseOutputMessage(
49
+ id="m1",
50
+ type="message",
51
+ role="assistant",
52
+ status="completed",
53
+ content=[ResponseOutputText(type="output_text", text="hello", annotations=[])],
54
+ )
55
+ assert _normalize_item(item) == {"role": "assistant", "content": "hello"}
56
+
57
+
58
+ class _FakeToolApproval:
59
+ def __init__(self, name, args, call_id):
60
+ self.tool_name, self.arguments, self.call_id = name, args, call_id
61
+
62
+
63
+ class _FakeStreamResult:
64
+ """Minimal RunResultStreaming stand-in: a delta, a message, then a pending interruption."""
65
+
66
+ def __init__(self, events, interruptions, state):
67
+ self._events, self.interruptions, self._state = events, interruptions, state
68
+ self.final_output = None
69
+
70
+ async def stream_events(self):
71
+ for event in self._events:
72
+ yield event
73
+
74
+ def to_state(self):
75
+ return self._state
76
+
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_agent_events_omit_unavailable_mcp_servers(monkeypatch):
80
+ import agent.agent as agent_module
81
+
82
+ def server(name, *, connect_error=None, list_error=None, cleanup_error=None):
83
+ value = MagicMock(name=name)
84
+ value.name = name
85
+ value.tool_filter = lambda *_args: True
86
+ value.cache_tools_list = False
87
+ value.connect = AsyncMock(side_effect=connect_error)
88
+ value.cleanup = AsyncMock(side_effect=cleanup_error)
89
+ value.list_tools = AsyncMock(return_value=[], side_effect=list_error)
90
+ return value
91
+
92
+ healthy = server("healthy")
93
+ unavailable = [
94
+ server("connect-failure", connect_error=PermissionError("HTTP error 403")),
95
+ server(
96
+ "list-failure",
97
+ list_error=RuntimeError("tool discovery failed"),
98
+ cleanup_error=RuntimeError("cleanup failed"),
99
+ ),
100
+ ]
101
+ all_servers = [healthy, *unavailable]
102
+ tool_filters = [server.tool_filter for server in all_servers]
103
+
104
+ async def mcp_servers(_extra):
105
+ return [healthy, *unavailable]
106
+
107
+ create_agent = MagicMock(return_value=object())
108
+
109
+ monkeypatch.setattr(agent_module, "mcp_servers", mcp_servers)
110
+ monkeypatch.setattr(agent_module, "build_mcp_servers", lambda: [])
111
+ monkeypatch.setattr(agent_module, "create_agent", create_agent)
112
+ monkeypatch.setattr(agent_module, "session_store", lambda _session_id, _actor: None)
113
+ monkeypatch.setattr(
114
+ agent_module.Runner,
115
+ "run_streamed",
116
+ lambda *_args, **_kwargs: _FakeStreamResult([], [], None),
117
+ )
118
+
119
+ async with agent_module.run_agent([], session_id="s", actor="actor") as result:
120
+ assert [event async for event in result.stream_events()] == []
121
+ # create_agent(actor, mcp) — the healthy servers are the second positional arg.
122
+ assert create_agent.call_args.args[1] == [healthy]
123
+ assert healthy.cache_tools_list is True
124
+ assert all(
125
+ server.tool_filter is tool_filter
126
+ for server, tool_filter in zip(all_servers, tool_filters, strict=True)
127
+ )
128
+ for server in all_servers:
129
+ server.cleanup.assert_awaited_once()
130
+
131
+
132
+ @pytest.mark.asyncio
133
+ async def test_agent_events_propagate_request_user_mcp_failure(monkeypatch):
134
+ import agent.agent as agent_module
135
+
136
+ server = MagicMock(name="request-user")
137
+ server.name = "request-user"
138
+ server._agentbricks_request_user = True
139
+ server.connect = AsyncMock(side_effect=PermissionError("request-user denied"))
140
+ server.cleanup = AsyncMock()
141
+
142
+ async def mcp_servers(_extra):
143
+ return [server]
144
+
145
+ monkeypatch.setattr(agent_module, "mcp_servers", mcp_servers)
146
+ monkeypatch.setattr(agent_module, "build_mcp_servers", lambda: [])
147
+ monkeypatch.setattr(agent_module, "create_agent", MagicMock(return_value=object()))
148
+ monkeypatch.setattr(agent_module, "session_store", lambda _session_id, _actor: None)
149
+ monkeypatch.setattr(
150
+ agent_module.Runner,
151
+ "run_streamed",
152
+ lambda *_args, **_kwargs: _FakeStreamResult([], [], None),
153
+ )
154
+
155
+ with pytest.raises(PermissionError, match="request-user denied"):
156
+ async with agent_module.run_agent([], session_id="s", actor="actor"):
157
+ pass
158
+
159
+
160
+ @pytest.mark.asyncio
161
+ async def test_serialize_events_relays_interrupt_as_native_event():
162
+ approval = _FakeToolApproval("send_message", '{"recipient": "x", "body": "y"}', "call-1")
163
+ sentinel_state = object()
164
+ result = _FakeStreamResult([], [approval], sentinel_state)
165
+
166
+ events = [e async for e in _serialize_events(result)]
167
+
168
+ assert events == [
169
+ {
170
+ "type": "interrupt",
171
+ "id": "call-1",
172
+ "value": {
173
+ "action_requests": [
174
+ {"name": "send_message", "args": {"recipient": "x", "body": "y"}}
175
+ ]
176
+ },
177
+ }
178
+ ]
179
+
180
+
181
+ def test_resume_agent_approves_pending_run(monkeypatch):
182
+ from agent.agent import _pending_runs
183
+
184
+ approved = []
185
+
186
+ class _State:
187
+ def get_interruptions(self):
188
+ return ["item-a"]
189
+
190
+ def approve(self, item):
191
+ approved.append(item)
192
+
193
+ def reject(self, item, rejection_message=None):
194
+ raise AssertionError("should not reject on approve")
195
+
196
+ _pending_runs["sess-2"] = _State()
197
+ resume_agent("sess-2", {"decisions": [{"type": "approve"}]})
198
+ assert approved == ["item-a"]
199
+ assert "sess-2" not in _pending_runs # popped so it can't be resumed twice
200
+
201
+
202
+ def test_resume_agent_without_pending_run_raises():
203
+ with pytest.raises(RuntimeError, match="No paused run"):
204
+ resume_agent("never-started", {"decisions": [{"type": "approve"}]})
205
+
206
+
207
+ def test_configure_raises_clear_error_without_auth(monkeypatch):
208
+ from agent.agent import configure
209
+
210
+ monkeypatch.delenv("DATABRICKS_CONFIG_PROFILE", raising=False)
211
+ monkeypatch.delenv("DATABRICKS_HOST", raising=False)
212
+ monkeypatch.delenv("DATABRICKS_TOKEN", raising=False)
213
+ monkeypatch.setenv("DATABRICKS_CONFIG_FILE", "/nonexistent-databrickscfg")
214
+ with pytest.raises(RuntimeError, match="Databricks auth is not configured"):
215
+ configure()
216
+
217
+
218
+ def test_configure_routes_openai_client_to_workspace(monkeypatch):
219
+ import agent.agent as agent_module
220
+ import agents
221
+
222
+ workspace = object()
223
+ created = object()
224
+ client = MagicMock(return_value=created)
225
+ set_client = MagicMock()
226
+ set_api = MagicMock()
227
+
228
+ monkeypatch.setattr(agent_module, "workspace_client", lambda: workspace)
229
+ monkeypatch.setattr(
230
+ agent_module,
231
+ "workspace_headers",
232
+ lambda: {"X-Databricks-Org-Id": "123"},
233
+ )
234
+ monkeypatch.setattr(agent_module, "AsyncDatabricksOpenAI", client)
235
+ monkeypatch.setattr(agent_module, "configure_tracing", lambda: None)
236
+ monkeypatch.setattr(agents, "set_default_openai_client", set_client)
237
+ monkeypatch.setattr(agents, "set_default_openai_api", set_api)
238
+
239
+ agent_module.configure()
240
+
241
+ client.assert_called_once_with(
242
+ workspace_client=workspace,
243
+ default_headers={"X-Databricks-Org-Id": "123"},
244
+ use_ai_gateway=True,
245
+ )
246
+ set_client.assert_called_once_with(created)
247
+ set_api.assert_called_once_with("chat_completions")
248
+
249
+
250
+ def test_session_store_defaults_to_in_process(monkeypatch):
251
+ import databricks_agentkit.openai.sessions as ss
252
+
253
+ monkeypatch.delenv("AGENT_SESSION_STORE", raising=False)
254
+ ss._local_sessions.clear()
255
+ # In-process default: same session id returns the same cached SQLiteSession (multi-turn works).
256
+ assert ss.session_store("abc-123") is ss.session_store("abc-123")
257
+
258
+
259
+ def test_session_store_selects_durable_store(monkeypatch):
260
+ import databricks_agentkit.openai.sessions as ss
261
+
262
+ monkeypatch.setenv("AGENT_SESSION_STORE", "my-store")
263
+ monkeypatch.setattr(ss, "SessionStoreClient", lambda *a, **k: _FakeStoreClient())
264
+ store = ss.session_store("abc-123")
265
+ assert isinstance(store, ss.DatabricksSessionStore)
266
+
267
+
268
+ class _FakeStoreClient:
269
+ def set_session_store(self, name):
270
+ return self
271
+
272
+
273
+ @pytest.mark.asyncio
274
+ async def test_adapter_recovery_marks_replayed_agent_input(monkeypatch):
275
+ import runtime.adapter as adapter
276
+
277
+ calls = []
278
+
279
+ @asynccontextmanager
280
+ async def fake_run_agent(agent_input, **kwargs):
281
+ calls.append((agent_input, kwargs))
282
+ yield _FakeStreamResult([], [], None)
283
+
284
+ monkeypatch.setattr(adapter, "run_agent", fake_run_agent)
285
+ payload = {"session_id": "session-1", "messages": [{"role": "user", "content": "hi"}]}
286
+ context = SimpleNamespace(session_id="runtime-session", emit=AsyncMock())
287
+
288
+ await adapter.invoke(payload, context)
289
+ await adapter.recover(payload, context)
290
+
291
+ assert calls == [
292
+ (payload["messages"], {"session_id": "session-1", "actor": "session-1", "model": None}),
293
+ (
294
+ [
295
+ {"role": "developer", "content": adapter._RECOVERY_INSTRUCTION},
296
+ *payload["messages"],
297
+ ],
298
+ {"session_id": "session-1", "actor": "session-1", "model": None},
299
+ ),
300
+ ]
301
+ assert payload["messages"] == [{"role": "user", "content": "hi"}]
302
+
303
+
304
+ def _has_workspace_auth() -> bool:
305
+ return bool(
306
+ os.getenv("DATABRICKS_CONFIG_PROFILE")
307
+ or (os.getenv("DATABRICKS_HOST") and os.getenv("DATABRICKS_TOKEN"))
308
+ )
309
+
310
+
311
+ @pytest.mark.skipif(
312
+ not _has_workspace_auth(),
313
+ reason="no Databricks profile configured; skipping live model call",
314
+ )
315
+ @pytest.mark.asyncio
316
+ async def test_agent_responds_end_to_end():
317
+ from agent.agent import configure, run_agent
318
+
319
+ configure()
320
+ async with run_agent(
321
+ [{"role": "user", "content": "Reply with the single word: pong"}],
322
+ session_id="test-e2e",
323
+ actor="test-actor",
324
+ ) as result:
325
+ _ = [event async for event in result.stream_events()]
326
+ assert result.final_output
@@ -0,0 +1,19 @@
1
+ from databricks_agentkit import DurableAgentServer
2
+ from databricks_agentkit.runtime.store import InMemoryRuntimeStore
3
+
4
+
5
+ async def _invoke(request, context):
6
+ return request
7
+
8
+
9
+ def test_runtime_exposes_invocation_routes() -> None:
10
+ app = DurableAgentServer(runtime_store=InMemoryRuntimeStore())
11
+ app.invoke(_invoke)
12
+ app.recover(_invoke)
13
+ paths = app.openapi()["paths"]
14
+
15
+ assert paths["/api/invocations"]["post"]
16
+ assert paths["/api/invocations/{invocation_id}"]["get"]
17
+ assert paths["/api/invocations/{invocation_id}/events"]["get"]
18
+ assert "/invocations" not in paths
19
+ assert "/api/health" not in paths
@@ -0,0 +1,28 @@
1
+ from databricks_agentkit.runtime import workspace
2
+
3
+
4
+ class _FakeWorkspaceClient:
5
+ def __init__(self, **kwargs):
6
+ self.kwargs = kwargs
7
+
8
+
9
+ def test_workspace_client_adds_account_routing_header(monkeypatch):
10
+ monkeypatch.setenv("DATABRICKS_WORKSPACE_ID", " 123456 ")
11
+ monkeypatch.setattr(workspace, "WorkspaceClient", _FakeWorkspaceClient)
12
+
13
+ client = workspace.workspace_client()
14
+
15
+ assert client.kwargs == {
16
+ "custom_headers": {"X-Databricks-Org-Id": "123456"},
17
+ }
18
+ assert workspace.workspace_headers() == {"X-Databricks-Org-Id": "123456"}
19
+
20
+
21
+ def test_workspace_client_uses_default_sdk_resolution_without_workspace_id(monkeypatch):
22
+ monkeypatch.delenv("DATABRICKS_WORKSPACE_ID", raising=False)
23
+ monkeypatch.setattr(workspace, "WorkspaceClient", _FakeWorkspaceClient)
24
+
25
+ client = workspace.workspace_client()
26
+
27
+ assert client.kwargs == {}
28
+ assert workspace.workspace_headers() == {}
@@ -0,0 +1 @@
1
+ DATABRICKS_CONFIG_PROFILE=DEFAULT
@@ -0,0 +1,11 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .pytest_cache/
6
+ dist/
7
+ *.egg-info/
8
+
9
+ # Agent Bricks local dev artifacts (`agentbricks dev`)
10
+ .agentbricks/
11
+ app.agentbricksdev.yaml
@@ -0,0 +1,22 @@
1
+ # Custom LangGraph server
2
+
3
+ This template shows how to serve a LangGraph agent with an ordinary FastAPI application. It does
4
+ not use the managed `DurableAgentServer` HTTP server or durable runtime.
5
+
6
+ This template does not load managed tool bindings from `agent.toml`, so `ab tools add` is not
7
+ supported. Wire framework-native Python tools and MCP servers directly in `agent/agent.py`.
8
+
9
+ ```bash
10
+ ab dev
11
+ ```
12
+
13
+ Call its single foreground endpoint:
14
+
15
+ ```bash
16
+ curl -sS http://localhost:8000/invocations \
17
+ -H 'Content-Type: application/json' \
18
+ -d '{"input":[{"role":"user","content":"Hello"}]}'
19
+ ```
20
+
21
+ Edit `runtime/main.py` to define your own HTTP contract. Edit `agent/agent.py` to change the model
22
+ or agent behavior. Deploy with `ab --profile <profile> deploy custom-agent-langgraph`.