databricks-agentbricks 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- databricks_agentbricks/__init__.py +1 -0
- databricks_agentbricks/_group.py +217 -0
- databricks_agentbricks/agent_project.py +657 -0
- databricks_agentbricks/app_resources.py +139 -0
- databricks_agentbricks/cli/__init__.py +12 -0
- databricks_agentbricks/cli/app.py +108 -0
- databricks_agentbricks/cli/app_auth.py +198 -0
- databricks_agentbricks/cli/auth.py +142 -0
- databricks_agentbricks/cli/deploy.py +954 -0
- databricks_agentbricks/cli/dev.py +310 -0
- databricks_agentbricks/cli/endpoint.py +145 -0
- databricks_agentbricks/cli/endpoint_examples.py +33 -0
- databricks_agentbricks/cli/endpoint_output.py +53 -0
- databricks_agentbricks/cli/endpoint_request.py +71 -0
- databricks_agentbricks/cli/endpoint_transport.py +148 -0
- databricks_agentbricks/cli/help.py +467 -0
- databricks_agentbricks/cli/init.py +496 -0
- databricks_agentbricks/cli/mcp.py +75 -0
- databricks_agentbricks/cli/memory.py +520 -0
- databricks_agentbricks/cli/sandbox.py +823 -0
- databricks_agentbricks/cli/sessions.py +588 -0
- databricks_agentbricks/cli/tools.py +456 -0
- databricks_agentbricks/cli/tracing.py +724 -0
- databricks_agentbricks/databricks_cli.py +38 -0
- databricks_agentbricks/errors.py +114 -0
- databricks_agentbricks/lakebase_runtime_store.py +107 -0
- databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
- databricks_agentbricks/project_config.py +157 -0
- databricks_agentbricks/project_types.py +55 -0
- databricks_agentbricks/py.typed +0 -0
- databricks_agentbricks/render.py +349 -0
- databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
- databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
- databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
- databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
- databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
- databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
- databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
- databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/agent-openai/.env.example +37 -0
- databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
- databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
- databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
- databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-openai/README.md +143 -0
- databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
- databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
- databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
- databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
- databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
- databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
- databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/sandbox_mcp.py +39 -0
- databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
- databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
- databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
- databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
- databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
- databricks_agentbricks/theme.py +72 -0
- databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
- databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
- databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
- databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
- databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
- databricks_agentkit/__init__.py +67 -0
- databricks_agentkit/_api_client.py +673 -0
- databricks_agentkit/_pagination.py +25 -0
- databricks_agentkit/client.py +31 -0
- databricks_agentkit/langgraph/__init__.py +98 -0
- databricks_agentkit/langgraph/genie.py +50 -0
- databricks_agentkit/langgraph/mcp.py +216 -0
- databricks_agentkit/langgraph/memory.py +67 -0
- databricks_agentkit/langgraph/session_store.py +485 -0
- databricks_agentkit/memory_store.py +371 -0
- databricks_agentkit/models.py +330 -0
- databricks_agentkit/openai/__init__.py +93 -0
- databricks_agentkit/openai/genie.py +50 -0
- databricks_agentkit/openai/mcp.py +189 -0
- databricks_agentkit/openai/memory.py +67 -0
- databricks_agentkit/openai/sessions.py +157 -0
- databricks_agentkit/py.typed +0 -0
- databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
- databricks_agentkit/runtime/README.md +184 -0
- databricks_agentkit/runtime/__init__.py +69 -0
- databricks_agentkit/runtime/app.py +316 -0
- databricks_agentkit/runtime/auth.py +149 -0
- databricks_agentkit/runtime/durability/__init__.py +1 -0
- databricks_agentkit/runtime/durability/execution.py +133 -0
- databricks_agentkit/runtime/durability/heartbeat.py +64 -0
- databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
- databricks_agentkit/runtime/durability/recovery.py +70 -0
- databricks_agentkit/runtime/durability/store.py +33 -0
- databricks_agentkit/runtime/execution.py +160 -0
- databricks_agentkit/runtime/genie.py +224 -0
- databricks_agentkit/runtime/mcp_auth.py +114 -0
- databricks_agentkit/runtime/model_services.py +90 -0
- databricks_agentkit/runtime/runtime.py +273 -0
- databricks_agentkit/runtime/session_store_client.py +120 -0
- databricks_agentkit/runtime/store.py +287 -0
- databricks_agentkit/runtime/tool_manifest.py +223 -0
- databricks_agentkit/runtime/tracing.py +71 -0
- databricks_agentkit/runtime/types.py +118 -0
- databricks_agentkit/runtime/workspace.py +27 -0
- databricks_agentkit/session_store.py +434 -0
- databricks_agentkit/timefmt.py +85 -0
|
@@ -0,0 +1,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")))
|
|
File without changes
|
|
@@ -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
|