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,144 @@
1
+ """Translate between Agent Bricks invocations and the framework-native agent entrypoint."""
2
+
3
+ from collections.abc import AsyncGenerator, AsyncIterator
4
+ from typing import Any
5
+
6
+ from agent.agent import recovery_input, run_agent
7
+ from langchain.messages import AIMessageChunk
8
+ from langgraph.types import Command
9
+
10
+ from databricks_agentkit import InvocationContext
11
+ from databricks_agentkit.runtime.auth import AuthError
12
+
13
+
14
+ def _payload(value: Any) -> dict[str, Any]:
15
+ if isinstance(value, list):
16
+ return {"messages": value}
17
+ if not isinstance(value, dict):
18
+ raise ValueError("input must be a message list or an object")
19
+ return value
20
+
21
+
22
+ def _session_id(payload: dict[str, Any], context: InvocationContext) -> str:
23
+ value = payload.get("session_id") or context.session_id
24
+ if not isinstance(value, str) or not value:
25
+ raise ValueError("session_id must be a non-empty string")
26
+ return value
27
+
28
+
29
+ def _actor(payload: dict[str, Any], session_id: str) -> str:
30
+ value = payload.get("actor") or session_id
31
+ if not isinstance(value, str) or not value:
32
+ raise ValueError("actor must be a non-empty string")
33
+ return value
34
+
35
+
36
+ def _agent_input(payload: dict[str, Any]) -> Any:
37
+ if (resume := payload.get("resume")) is not None:
38
+ return Command(resume=resume)
39
+ messages = payload.get("messages") or []
40
+ if not isinstance(messages, list):
41
+ raise ValueError("messages must be a list")
42
+ return {"messages": messages}
43
+
44
+
45
+ async def invoke(value: Any, context: InvocationContext) -> dict:
46
+ payload = _payload(value)
47
+ _check_user_input(payload, context)
48
+ return await _invoke_agent(_agent_input(payload), payload, context)
49
+
50
+
51
+ async def recover(value: Any, context: InvocationContext) -> dict:
52
+ payload = _payload(value)
53
+ _check_user_input(payload, context)
54
+ if getattr(context, "request_auth", None) is not None:
55
+ raise AuthError(
56
+ "MCP_USER_AUTH_BACKGROUND_UNSUPPORTED",
57
+ "Request-user invocations cannot be recovered in the background.",
58
+ 400,
59
+ )
60
+ session_id = _session_id(payload, context)
61
+ actor = _actor(payload, session_id)
62
+ agent_input = await recovery_input(
63
+ _agent_input(payload),
64
+ session_id=session_id,
65
+ actor=actor,
66
+ invocation_id=context.invocation_id,
67
+ )
68
+ return await _invoke_agent(agent_input, payload, context)
69
+
70
+
71
+ def _check_user_input(payload: dict[str, Any], context: InvocationContext) -> None:
72
+ auth = getattr(context, "request_auth", None)
73
+ if auth is not None and any(
74
+ payload.get(key) is not None for key in ("resume", "approval", "approvals")
75
+ ):
76
+ raise AuthError(
77
+ "MCP_USER_AUTH_HITL_UNSUPPORTED",
78
+ "Request-user invocations do not support approval or resume input.",
79
+ 400,
80
+ )
81
+
82
+
83
+ async def _invoke_agent(
84
+ agent_input: Any,
85
+ payload: dict[str, Any],
86
+ context: InvocationContext,
87
+ ) -> dict:
88
+ session_id = _session_id(payload, context)
89
+ actor = _actor(payload, session_id)
90
+ auth = getattr(context, "request_auth", None)
91
+ internal_session_id = (
92
+ auth.namespace("session", session_id) if auth and payload.get("session_id") else session_id
93
+ )
94
+ actor = auth.namespace("actor", actor) if auth else actor
95
+ user_auth = auth is not None
96
+ auth_kwargs = {"workspace_client_for": auth.client_for} if user_auth else {}
97
+ model = payload.get("model")
98
+ outputs = []
99
+ async for event in _serialize_events(
100
+ run_agent(
101
+ agent_input,
102
+ session_id=internal_session_id,
103
+ actor=actor,
104
+ model=model if isinstance(model, str) else None,
105
+ invocation_id=context.invocation_id,
106
+ **auth_kwargs,
107
+ )
108
+ ):
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(async_stream: AsyncIterator[Any]) -> AsyncGenerator[dict, None]:
128
+ async for mode, payload in async_stream:
129
+ if mode == "updates":
130
+ if interrupts := payload.get("__interrupt__"):
131
+ for item in interrupts:
132
+ yield {"type": "interrupt", "id": item.id, "value": item.value}
133
+ continue
134
+ for node_data in payload.values():
135
+ messages = node_data.get("messages", []) if isinstance(node_data, dict) else []
136
+ for message in messages:
137
+ yield {"type": "message", "message": message.model_dump()}
138
+ elif mode == "messages":
139
+ try:
140
+ chunk = payload[0]
141
+ if isinstance(chunk, AIMessageChunk) and (content := chunk.content):
142
+ yield {"type": "delta", "content": content, "id": chunk.id}
143
+ except (KeyError, IndexError, TypeError):
144
+ continue
@@ -0,0 +1,25 @@
1
+ """Run the LangGraph 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,211 @@
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 types import SimpleNamespace
10
+ from unittest.mock import AsyncMock
11
+
12
+ import pytest
13
+ from agent.tools import all_tools
14
+ from langchain_core.tools import BaseTool
15
+ from runtime.adapter import _serialize_events
16
+
17
+ from databricks_agentkit.langgraph.session_store import (
18
+ checkpointer,
19
+ invocation_metadata,
20
+ thread_config,
21
+ )
22
+
23
+
24
+ def test_tools_autoregister():
25
+ tools = all_tools()
26
+ assert tools, "expected the sample tool to auto-register"
27
+ assert all(isinstance(t, BaseTool) for t in tools)
28
+ assert {"get_current_time", "send_message"} <= {t.name for t in tools}
29
+
30
+
31
+ def test_gated_tool_is_in_require_approval():
32
+ # The gated demo tool must exist and be listed for approval, or the HITL demo does nothing.
33
+ from agent.agent import REQUIRE_APPROVAL
34
+
35
+ assert REQUIRE_APPROVAL.get("send_message")
36
+ assert "send_message" in {t.name for t in all_tools()}
37
+
38
+
39
+ class _FakeInterrupt:
40
+ def __init__(self, value, id): # mirrors langgraph.types.Interrupt's `.value` / `.id`
41
+ self.value, self.id = value, id
42
+
43
+
44
+ async def _aiter(events):
45
+ for e in events:
46
+ yield e
47
+
48
+
49
+ @pytest.mark.asyncio
50
+ async def test_serialize_events_relays_interrupt_as_native_event():
51
+ hitl = {"action_requests": [{"name": "send_message", "args": {"recipient": "x", "body": "y"}}]}
52
+ stream = _aiter([("updates", {"__interrupt__": (_FakeInterrupt(hitl, "int-1"),)})])
53
+ events = [e async for e in _serialize_events(stream)]
54
+ assert events == [{"type": "interrupt", "id": "int-1", "value": hitl}]
55
+
56
+
57
+ def test_configure_raises_clear_error_without_auth(monkeypatch):
58
+ from agent.agent import configure
59
+
60
+ monkeypatch.delenv("DATABRICKS_CONFIG_PROFILE", raising=False)
61
+ monkeypatch.delenv("DATABRICKS_HOST", raising=False)
62
+ monkeypatch.delenv("DATABRICKS_TOKEN", raising=False)
63
+ monkeypatch.setenv("DATABRICKS_CONFIG_FILE", "/nonexistent-databrickscfg")
64
+ with pytest.raises(RuntimeError, match="Databricks auth is not configured"):
65
+ configure()
66
+
67
+
68
+ def test_chat_model_forwards_account_routing_header(monkeypatch):
69
+ from agent.agent import _RoutedChatDatabricks
70
+
71
+ monkeypatch.setenv("DATABRICKS_WORKSPACE_ID", "123456")
72
+ model = _RoutedChatDatabricks(endpoint="test-endpoint")
73
+
74
+ assert model._get_client_kwargs()["default_headers"] == {"X-Databricks-Org-Id": "123456"}
75
+
76
+
77
+ def test_thread_config_from_session_id():
78
+ # actor_id rides alongside thread_id — the durable saver maps it onto the Session's actor.
79
+ assert thread_config("abc-123") == {
80
+ "configurable": {"thread_id": "abc-123", "actor_id": "abc-123"}
81
+ }
82
+
83
+
84
+ def test_thread_config_uses_supplied_actor():
85
+ # A caller-supplied actor (e.g. the signed-in user) partitions the durable store per user.
86
+ assert thread_config("abc-123", "alice") == {
87
+ "configurable": {"thread_id": "abc-123", "actor_id": "alice"}
88
+ }
89
+
90
+
91
+ def test_checkpointer_is_shared(monkeypatch):
92
+ # In-memory by default (no AGENT_SESSION_STORE); built once and shared so multi-turn works.
93
+ import databricks_agentkit.langgraph.session_store as ss
94
+
95
+ monkeypatch.setattr(ss, "_saver", None) # reset the process-wide saver
96
+ assert checkpointer() is checkpointer()
97
+
98
+
99
+ def test_session_store_selects_durable_saver(monkeypatch):
100
+ # AGENT_SESSION_STORE must route to the durable Session Store saver, not stay in-memory. Stub the
101
+ # REST client so it stays hermetic (no network); the saver builds without touching the API.
102
+ import databricks_agentkit.langgraph.session_store as ss
103
+
104
+ monkeypatch.setattr(ss, "_saver", None)
105
+ monkeypatch.setenv("AGENT_SESSION_STORE", "my-store")
106
+ monkeypatch.setattr(ss, "SessionStoreClient", lambda *a, **k: _FakeStoreClient())
107
+ saver = checkpointer()
108
+ assert isinstance(saver, ss.DatabricksSessionStoreSaver)
109
+
110
+
111
+ class _FakeStoreClient:
112
+ def set_session_store(self, name):
113
+ return self
114
+
115
+
116
+ @pytest.mark.asyncio
117
+ async def test_recovery_input_resumes_current_checkpoint(monkeypatch):
118
+ import agent.agent as agent_module
119
+
120
+ class Saver:
121
+ async def aget_tuple(self, config):
122
+ return SimpleNamespace(metadata=invocation_metadata("inv-1"))
123
+
124
+ monkeypatch.setattr(agent_module, "checkpointer", lambda: Saver())
125
+ original = {"messages": [{"role": "user", "content": "hi"}]}
126
+ recovered = await agent_module.recovery_input(
127
+ original,
128
+ session_id="session-1",
129
+ actor="actor-1",
130
+ invocation_id="inv-1",
131
+ )
132
+
133
+ assert recovered is None
134
+
135
+
136
+ @pytest.mark.asyncio
137
+ async def test_recovery_replays_input_without_current_checkpoint(monkeypatch):
138
+ import agent.agent as agent_module
139
+
140
+ class Saver:
141
+ async def aget_tuple(self, config):
142
+ return None
143
+
144
+ monkeypatch.setattr(agent_module, "checkpointer", lambda: Saver())
145
+ original = {"messages": [{"role": "user", "content": "hi"}]}
146
+ recovered = await agent_module.recovery_input(
147
+ original,
148
+ session_id="session-1",
149
+ actor="actor-1",
150
+ invocation_id="inv-1",
151
+ )
152
+
153
+ assert recovered is original
154
+
155
+
156
+ @pytest.mark.asyncio
157
+ async def test_adapter_calls_same_run_agent_for_invoke_and_recovery(monkeypatch):
158
+ import runtime.adapter as adapter
159
+
160
+ calls = []
161
+
162
+ async def fake_run_agent(agent_input, **kwargs):
163
+ calls.append((agent_input, kwargs))
164
+ if False:
165
+ yield None
166
+
167
+ async def fake_recovery_input(agent_input, **_kwargs):
168
+ return None
169
+
170
+ monkeypatch.setattr(adapter, "run_agent", fake_run_agent)
171
+ monkeypatch.setattr(adapter, "recovery_input", fake_recovery_input)
172
+ context = SimpleNamespace(
173
+ invocation_id="inv-1",
174
+ session_id="runtime-session",
175
+ emit=AsyncMock(),
176
+ )
177
+ payload = {"session_id": "session-1", "messages": [{"role": "user", "content": "hi"}]}
178
+
179
+ await adapter.invoke(payload, context)
180
+ await adapter.recover(payload, context)
181
+
182
+ assert calls[0][0] == {"messages": payload["messages"]}
183
+ assert calls[1][0] is None
184
+ assert calls[0][1] == calls[1][1]
185
+
186
+
187
+ def _has_workspace_auth() -> bool:
188
+ return bool(
189
+ os.getenv("DATABRICKS_CONFIG_PROFILE")
190
+ or (os.getenv("DATABRICKS_HOST") and os.getenv("DATABRICKS_TOKEN"))
191
+ )
192
+
193
+
194
+ @pytest.mark.skipif(
195
+ not _has_workspace_auth(),
196
+ reason="no Databricks profile configured; skipping live model call",
197
+ )
198
+ @pytest.mark.asyncio
199
+ async def test_agent_responds_end_to_end():
200
+ from agent.agent import configure, run_agent
201
+
202
+ configure()
203
+ events = [
204
+ event
205
+ async for event in run_agent(
206
+ {"messages": [{"role": "user", "content": "Reply with the single word: pong"}]},
207
+ session_id="test-e2e",
208
+ actor="test-actor",
209
+ )
210
+ ]
211
+ assert events
@@ -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,37 @@
1
+ # Copy to .env for local development: cp .env.example .env
2
+ #
3
+ # The only required setting for local dev is a Databricks auth profile (used to call the model).
4
+ # Everything else — MLflow tracing, long-term memory — is optional and off by default. Invocation
5
+ # durability uses an in-memory store locally; conversation history uses an in-process SQLite session.
6
+
7
+ # Databricks auth profile (from `databricks auth profiles`). Used to call the model endpoint.
8
+ DATABRICKS_CONFIG_PROFILE=DEFAULT
9
+ # Or use explicit host/token instead of a profile:
10
+ # DATABRICKS_HOST=https://<your-workspace>.databricks.com
11
+ # DATABRICKS_TOKEN=dapi...
12
+
13
+ # --- Optional: MLflow tracing ---
14
+ # Leave UNSET to skip tracing (local dev). To enable, set a destination AND an experiment (either
15
+ # form of each works): destination = MLFLOW_TRACKING_URI or MLFLOW_TRACING_DESTINATION;
16
+ # experiment = MLFLOW_EXPERIMENT_ID or MLFLOW_EXPERIMENT_NAME.
17
+ # MLFLOW_TRACKING_URI="databricks"
18
+ # MLFLOW_EXPERIMENT_ID=
19
+ # MLFLOW_EXPERIMENT_NAME=
20
+ # For fully-local tracing (no workspace), point MLFLOW_TRACKING_URI at a local OSS MLflow server
21
+ # (`mlflow server`), e.g. MLFLOW_TRACKING_URI="http://localhost:5000".
22
+
23
+ # --- Optional: long-term memory tools ---
24
+ # Leave UNSET to skip. Set to a managed memory store ID to register the remember/recall tools
25
+ # (persist/search facts across conversations); see databricks_agentkit/openai/memory.py. Memory is
26
+ # partitioned per user: the agent uses the signed-in user as the actor (falls back to "agent"
27
+ # locally); edit `_actor` in agent/agent.py to change that.
28
+ # AGENT_MEMORY_STORE=<memory-store-id>
29
+
30
+ # --- Optional: durable state (managed Session Store) ---
31
+ # Leave UNSET to keep the in-process session (SQLiteSession in :memory:): multi-turn works within one
32
+ # process but does not survive restarts/replicas. Set to an existing managed Session Store name to
33
+ # persist the transcript through its REST API (DatabricksSessionStore) — durable conversation history
34
+ # over RPCs (no DB connection). Access uses your normal Databricks auth (must have access to the
35
+ # store). NOTE: this persists the transcript only; paused human-in-the-loop runs are in-process even
36
+ # when this is set. See databricks_agentkit/openai/sessions.py.
37
+ # AGENT_SESSION_STORE=my-agent-sessions
@@ -0,0 +1,199 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python
3
+
4
+ ### Python ###
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ share/python-wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+ MANIFEST
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ *.manifest
36
+ *.spec
37
+
38
+ # Installer logs
39
+ pip-log.txt
40
+ pip-delete-this-directory.txt
41
+
42
+ # Unit test / coverage reports
43
+ htmlcov/
44
+ .tox/
45
+ .nox/
46
+ .coverage
47
+ .coverage.*
48
+ .cache
49
+ nosetests.xml
50
+ coverage.xml
51
+ *.cover
52
+ *.py,cover
53
+ .hypothesis/
54
+ .pytest_cache/
55
+ cover/
56
+
57
+ # Translations
58
+ *.mo
59
+ *.pot
60
+
61
+ # Django stuff:
62
+ *.log
63
+ local_settings.py
64
+ db.sqlite3
65
+ db.sqlite3-journal
66
+
67
+ # Flask stuff:
68
+ instance/
69
+ .webassets-cache
70
+
71
+ # Scrapy stuff:
72
+ .scrapy
73
+
74
+ # Sphinx documentation
75
+ docs/_build/
76
+
77
+ # PyBuilder
78
+ .pybuilder/
79
+ target/
80
+
81
+ # Jupyter Notebook
82
+ .ipynb_checkpoints
83
+
84
+ # IPython
85
+ profile_default/
86
+ ipython_config.py
87
+
88
+ # pyenv
89
+ # For a library or package, you might want to ignore these files since the code is
90
+ # intended to run in multiple environments; otherwise, check them in:
91
+ # .python-version
92
+
93
+ # pipenv
94
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
96
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
97
+ # install all needed dependencies.
98
+ #Pipfile.lock
99
+
100
+ # poetry
101
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
102
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
103
+ # commonly ignored for libraries.
104
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
105
+ #poetry.lock
106
+
107
+ # pdm
108
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
109
+ #pdm.lock
110
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
111
+ # in version control.
112
+ # https://pdm.fming.dev/#use-with-ide
113
+ .pdm.toml
114
+
115
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
116
+ __pypackages__/
117
+
118
+ # Celery stuff
119
+ celerybeat-schedule
120
+ celerybeat.pid
121
+
122
+ # SageMath parsed files
123
+ *.sage.py
124
+
125
+ # Environments
126
+ .env
127
+ .venv
128
+ env/
129
+ venv/
130
+ ENV/
131
+ env.bak/
132
+ venv.bak/
133
+
134
+ # Spyder project settings
135
+ .spyderproject
136
+ .spyproject
137
+
138
+ # Rope project settings
139
+ .ropeproject
140
+
141
+ # mkdocs documentation
142
+ /site
143
+
144
+ # mypy
145
+ .mypy_cache/
146
+ .dmypy.json
147
+ dmypy.json
148
+
149
+ # Pyre type checker
150
+ .pyre/
151
+
152
+ # pytype static type analyzer
153
+ .pytype/
154
+
155
+ # Cython debug symbols
156
+ cython_debug/
157
+
158
+ # PyCharm
159
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
160
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
161
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
162
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163
+ #.idea/
164
+
165
+ # VS Code
166
+ .vscode/
167
+
168
+ ### Python Patch ###
169
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
170
+ poetry.toml
171
+
172
+ # ruff
173
+ .ruff_cache/
174
+
175
+ # LSP config files
176
+ pyrightconfig.json
177
+
178
+ # End of https://www.toptal.com/developers/gitignore/api/python
179
+
180
+ .DS_*
181
+
182
+ # Databricks / MLflow local artifacts
183
+ **/mlruns/
184
+ mlflow.db
185
+ **/.databricks
186
+ .claude/
187
+
188
+ # Environment files
189
+ **/.env
190
+ **/.env.local
191
+
192
+ # Local (Lakebase-free) SQLite session store
193
+ local_agent_sessions.db
194
+ local_agent_sessions.db-*
195
+ *.db-journal
196
+
197
+ # Agent Bricks local dev artifacts (`agentbricks dev`)
198
+ .agentbricks/
199
+ app.agentbricksdev.yaml