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,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"
|
|
File without changes
|
|
@@ -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")))
|
|
File without changes
|
|
@@ -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,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`.
|
|
File without changes
|