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,118 @@
|
|
|
1
|
+
"""Contracts shared by Agent Bricks Runtime, stores, and agent applications.
|
|
2
|
+
|
|
3
|
+
The runtime returns :class:`Invocation` snapshots and ordered :class:`InvocationEvent` records.
|
|
4
|
+
An executor receives an :class:`InvocationAttemptContext` for attempt fencing and event emission.
|
|
5
|
+
``DurableAgentServer`` adapts that lower-level context into :class:`InvocationContext` for functions
|
|
6
|
+
registered with ``@app.invoke`` and ``@app.recover``.
|
|
7
|
+
|
|
8
|
+
All request, response, and event payloads use the recursive ``JsonValue`` / ``JsonObject`` aliases,
|
|
9
|
+
so Runtime Store values can be stored consistently by in-memory and Lakebase implementations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections.abc import Awaitable, Callable
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import TYPE_CHECKING, TypeAlias
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from databricks_agentkit.runtime.auth import RequestAuthContext
|
|
19
|
+
|
|
20
|
+
JsonValue: TypeAlias = None | bool | int | float | str | list["JsonValue"] | dict[str, "JsonValue"]
|
|
21
|
+
JsonObject = dict[str, JsonValue]
|
|
22
|
+
InvocationEventEmitter = Callable[[JsonObject], Awaitable[int]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InvocationStatus(str, Enum):
|
|
26
|
+
"""Lifecycle states stored by Agent Bricks Runtime."""
|
|
27
|
+
|
|
28
|
+
QUEUED = "QUEUED"
|
|
29
|
+
ACTIVE = "ACTIVE"
|
|
30
|
+
COMPLETED = "COMPLETED"
|
|
31
|
+
FAILED = "FAILED"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Invocation:
|
|
36
|
+
"""A Runtime Store snapshot for one idempotent invocation and its owning attempt."""
|
|
37
|
+
|
|
38
|
+
invocation_id: str
|
|
39
|
+
status: InvocationStatus
|
|
40
|
+
attempt: int
|
|
41
|
+
request: JsonValue
|
|
42
|
+
response: JsonValue
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def is_terminal(self) -> bool:
|
|
46
|
+
"""Whether this invocation can no longer transition or emit events."""
|
|
47
|
+
return self.status in {
|
|
48
|
+
InvocationStatus.COMPLETED,
|
|
49
|
+
InvocationStatus.FAILED,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class InvocationEvent:
|
|
55
|
+
"""One persisted event emitted by an invocation attempt."""
|
|
56
|
+
|
|
57
|
+
sequence_number: int
|
|
58
|
+
invocation_id: str
|
|
59
|
+
attempt: int
|
|
60
|
+
event: JsonObject
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class InvocationAttemptContext:
|
|
65
|
+
"""Attempt metadata and event emission passed to the runtime's executor function."""
|
|
66
|
+
|
|
67
|
+
invocation_id: str
|
|
68
|
+
attempt: int
|
|
69
|
+
_emit: InvocationEventEmitter | None = field(default=None, repr=False, compare=False)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def is_recovery(self) -> bool:
|
|
73
|
+
"""Whether this is a replacement attempt after an earlier worker stopped heartbeating."""
|
|
74
|
+
return self.attempt > 1
|
|
75
|
+
|
|
76
|
+
async def emit(self, event: JsonObject) -> int:
|
|
77
|
+
"""Persist an ordered event and return its replay cursor."""
|
|
78
|
+
if self._emit is None:
|
|
79
|
+
raise RuntimeError("event emission is not available for this invocation context")
|
|
80
|
+
return await self._emit(event)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
InvocationExecutorFn = Callable[[JsonValue, InvocationAttemptContext], Awaitable[JsonValue]]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class InvocationContext:
|
|
88
|
+
"""Invocation/session metadata and event emission for a decorated agent function."""
|
|
89
|
+
|
|
90
|
+
invocation_id: str
|
|
91
|
+
session_id: str
|
|
92
|
+
attempt: int
|
|
93
|
+
_attempt_context: InvocationAttemptContext = field(repr=False, compare=False)
|
|
94
|
+
request_auth: "RequestAuthContext | None" = field(default=None, repr=False, compare=False)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def is_recovery(self) -> bool:
|
|
98
|
+
"""Whether ``@app.recover`` is handling a replacement attempt."""
|
|
99
|
+
return self.attempt > 1
|
|
100
|
+
|
|
101
|
+
async def emit(self, event: JsonObject) -> int:
|
|
102
|
+
"""Persist an ordered application event and return its replay cursor."""
|
|
103
|
+
return await self._attempt_context.emit(event)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
InvocationHook = Callable[[JsonValue, InvocationContext], Awaitable[JsonValue]]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class InvocationConflictError(ValueError):
|
|
110
|
+
"""Raised when an invocation ID is reused with a different request."""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class InvocationNotFoundError(LookupError):
|
|
114
|
+
"""Raised when waiting for an unknown invocation ID."""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class InvocationFailedError(RuntimeError):
|
|
118
|
+
"""Raised when an invocation reaches the failed state."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Construct Databricks SDK clients with workspace routing when required."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from databricks.sdk import WorkspaceClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def workspace_headers() -> dict[str, str]:
|
|
11
|
+
"""Return headers required to route an account-host request to its workspace."""
|
|
12
|
+
workspace_id = os.getenv("DATABRICKS_WORKSPACE_ID", "").strip()
|
|
13
|
+
return {"X-Databricks-Org-Id": workspace_id} if workspace_id else {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def workspace_client() -> WorkspaceClient:
|
|
17
|
+
"""Return the environment-authenticated client for the active workspace.
|
|
18
|
+
|
|
19
|
+
``databricks apps run-local`` can authenticate through an account-level vanity host while
|
|
20
|
+
exposing the target workspace through ``DATABRICKS_WORKSPACE_ID``. The SDK needs the same
|
|
21
|
+
routing header as the Databricks CLI for those profiles. Ordinary workspace hosts and deployed
|
|
22
|
+
Apps continue to use the SDK's default authentication chain.
|
|
23
|
+
"""
|
|
24
|
+
headers = workspace_headers()
|
|
25
|
+
if not headers:
|
|
26
|
+
return WorkspaceClient()
|
|
27
|
+
return WorkspaceClient(custom_headers=headers)
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""Typed session-store resources and operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Sequence
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import TYPE_CHECKING, Any, List, Optional
|
|
9
|
+
|
|
10
|
+
from databricks_agentkit._pagination import validate_page_size
|
|
11
|
+
from databricks_agentkit.timefmt import parse_timestamp
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from databricks_agentkit._api_client import _AgentBricksApiClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class SessionItem:
|
|
19
|
+
item_id: str
|
|
20
|
+
data: Any
|
|
21
|
+
create_time: Optional[datetime] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, kw_only=True)
|
|
25
|
+
class ExtractedMemory:
|
|
26
|
+
"""A memory entry produced by `Session.extract_memories` (a read-only result view)."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
actor_id: Optional[str] = None
|
|
30
|
+
path: Optional[str] = None
|
|
31
|
+
content: Optional[str] = None
|
|
32
|
+
description: Optional[str] = None
|
|
33
|
+
session_id: Optional[str] = None
|
|
34
|
+
source_type: Optional[str] = None
|
|
35
|
+
create_time: Optional[datetime] = None
|
|
36
|
+
update_time: Optional[datetime] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, kw_only=True)
|
|
40
|
+
class Session:
|
|
41
|
+
store_name: str
|
|
42
|
+
session_id: str
|
|
43
|
+
actor_id: str
|
|
44
|
+
parent_session_id: Optional[str] = None
|
|
45
|
+
root_session_id: Optional[str] = None
|
|
46
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
47
|
+
create_time: Optional[datetime] = None
|
|
48
|
+
update_time: Optional[datetime] = None
|
|
49
|
+
last_activity_time: Optional[datetime] = None
|
|
50
|
+
_client: SessionStores = field(repr=False, compare=False)
|
|
51
|
+
|
|
52
|
+
def update(self, *, metadata: dict[str, str]) -> Session:
|
|
53
|
+
return self._client._update_session(self, metadata=metadata)
|
|
54
|
+
|
|
55
|
+
def delete(self, *, force: bool = False) -> None:
|
|
56
|
+
self._client._delete_session(self, force=force)
|
|
57
|
+
|
|
58
|
+
def fork(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
actor_id: str,
|
|
62
|
+
up_to_item_id: Optional[str] = None,
|
|
63
|
+
session_id: Optional[str] = None,
|
|
64
|
+
metadata: Optional[dict[str, str]] = None,
|
|
65
|
+
) -> Session:
|
|
66
|
+
return self._client._fork_session(
|
|
67
|
+
self,
|
|
68
|
+
actor_id=actor_id,
|
|
69
|
+
up_to_item_id=up_to_item_id,
|
|
70
|
+
session_id=session_id,
|
|
71
|
+
metadata=metadata,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def list_items(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
page_size: Optional[int] = None,
|
|
78
|
+
order_by: Optional[str] = None,
|
|
79
|
+
) -> Iterator[SessionItem]:
|
|
80
|
+
return self._client._list_items(
|
|
81
|
+
self,
|
|
82
|
+
page_size=page_size,
|
|
83
|
+
order_by=order_by,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def append_items(self, items: Sequence[Any]) -> List[SessionItem]:
|
|
87
|
+
return self._client._append_items(self, items=items)
|
|
88
|
+
|
|
89
|
+
def pop_item(self) -> Optional[SessionItem]:
|
|
90
|
+
return self._client._pop_item(self)
|
|
91
|
+
|
|
92
|
+
def clear_items(self) -> None:
|
|
93
|
+
self._client._clear_items(self)
|
|
94
|
+
|
|
95
|
+
def extract_memories(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
memory_store: str,
|
|
99
|
+
instructions: Optional[str] = None,
|
|
100
|
+
dry_run: bool = False,
|
|
101
|
+
) -> List[ExtractedMemory]:
|
|
102
|
+
"""Synchronously distill this session's transcript into memory entries.
|
|
103
|
+
|
|
104
|
+
Writes the entries to `memory_store` and returns them; pass `dry_run=True` to return the
|
|
105
|
+
extracted entries without persisting them.
|
|
106
|
+
"""
|
|
107
|
+
return self._client._extract_memories(
|
|
108
|
+
self, memory_store=memory_store, instructions=instructions, dry_run=dry_run
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True, kw_only=True)
|
|
113
|
+
class SessionStore:
|
|
114
|
+
name: str
|
|
115
|
+
session_store_id: Optional[str] = None
|
|
116
|
+
creator_user_id: Optional[str] = None
|
|
117
|
+
create_time: Optional[datetime] = None
|
|
118
|
+
update_time: Optional[datetime] = None
|
|
119
|
+
description: Optional[str] = None
|
|
120
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
121
|
+
_client: SessionStores = field(repr=False, compare=False)
|
|
122
|
+
|
|
123
|
+
def update(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
description: Optional[str] = None,
|
|
127
|
+
metadata: Optional[dict[str, str]] = None,
|
|
128
|
+
) -> SessionStore:
|
|
129
|
+
return self._client._update_store(
|
|
130
|
+
self,
|
|
131
|
+
description=description,
|
|
132
|
+
metadata=metadata,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def delete(self) -> None:
|
|
136
|
+
self._client._delete_store(self)
|
|
137
|
+
|
|
138
|
+
def add(
|
|
139
|
+
self,
|
|
140
|
+
*,
|
|
141
|
+
actor_id: str,
|
|
142
|
+
session_id: Optional[str] = None,
|
|
143
|
+
parent_session_id: Optional[str] = None,
|
|
144
|
+
metadata: Optional[dict[str, str]] = None,
|
|
145
|
+
) -> Session:
|
|
146
|
+
return self._client._add_session(
|
|
147
|
+
self,
|
|
148
|
+
actor_id=actor_id,
|
|
149
|
+
session_id=session_id,
|
|
150
|
+
parent_session_id=parent_session_id,
|
|
151
|
+
metadata=metadata,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def list(
|
|
155
|
+
self,
|
|
156
|
+
*,
|
|
157
|
+
page_size: Optional[int] = None,
|
|
158
|
+
filter: Optional[str] = None,
|
|
159
|
+
order_by: Optional[str] = None,
|
|
160
|
+
) -> Iterator[Session]:
|
|
161
|
+
return self._client._list_sessions(
|
|
162
|
+
self,
|
|
163
|
+
page_size=page_size,
|
|
164
|
+
filter=filter,
|
|
165
|
+
order_by=order_by,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def get(self, session_id: str) -> Session:
|
|
169
|
+
return self._client._get_session(self, session_id=session_id)
|
|
170
|
+
|
|
171
|
+
def grant_permission(self, principal_id: str, *, permission: str = "WRITE") -> None:
|
|
172
|
+
"""Grant a service principal READ/WRITE access to this session store.
|
|
173
|
+
|
|
174
|
+
``principal_id`` is the service principal's application (client) id. The grant is applied
|
|
175
|
+
server-side, so the caller needs neither store ownership nor Lakebase MANAGE.
|
|
176
|
+
"""
|
|
177
|
+
self._client._grant_permission(self, principal_id, permission=permission)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SessionStores:
|
|
181
|
+
def __init__(self, api: _AgentBricksApiClient):
|
|
182
|
+
self._api = api
|
|
183
|
+
|
|
184
|
+
def create(
|
|
185
|
+
self,
|
|
186
|
+
name: str,
|
|
187
|
+
*,
|
|
188
|
+
description: Optional[str] = None,
|
|
189
|
+
metadata: Optional[dict[str, str]] = None,
|
|
190
|
+
) -> SessionStore:
|
|
191
|
+
response = self._api.create_session_store(
|
|
192
|
+
name,
|
|
193
|
+
description,
|
|
194
|
+
metadata,
|
|
195
|
+
)
|
|
196
|
+
return self._store_from_response(response)
|
|
197
|
+
|
|
198
|
+
def list(self, *, page_size: Optional[int] = None) -> Iterator[SessionStore]:
|
|
199
|
+
validate_page_size(page_size)
|
|
200
|
+
page_token = None
|
|
201
|
+
while True:
|
|
202
|
+
response = self._api.list_session_stores(
|
|
203
|
+
page_size=page_size,
|
|
204
|
+
page_token=page_token,
|
|
205
|
+
)
|
|
206
|
+
for store in response.get("session_stores", []):
|
|
207
|
+
yield self._store_from_response(store)
|
|
208
|
+
page_token = response.get("next_page_token")
|
|
209
|
+
if not page_token:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
def get(self, name: str) -> SessionStore:
|
|
213
|
+
return self._store_from_response(self._api.get_session_store(name))
|
|
214
|
+
|
|
215
|
+
def _update_store(
|
|
216
|
+
self,
|
|
217
|
+
store: SessionStore,
|
|
218
|
+
*,
|
|
219
|
+
description: Optional[str] = None,
|
|
220
|
+
metadata: Optional[dict[str, str]] = None,
|
|
221
|
+
) -> SessionStore:
|
|
222
|
+
if description is None and metadata is None:
|
|
223
|
+
raise ValueError("at least one of description and metadata is required")
|
|
224
|
+
response = self._api.update_session_store(
|
|
225
|
+
store.name,
|
|
226
|
+
description=description,
|
|
227
|
+
metadata=metadata,
|
|
228
|
+
)
|
|
229
|
+
return self._store_from_response(response)
|
|
230
|
+
|
|
231
|
+
def _delete_store(self, store: SessionStore) -> None:
|
|
232
|
+
self._api.delete_session_store(store.name)
|
|
233
|
+
|
|
234
|
+
def _grant_permission(self, store: SessionStore, principal_id: str, *, permission: str) -> None:
|
|
235
|
+
self._api.grant_session_store_permission(store.name, principal_id, permission=permission)
|
|
236
|
+
|
|
237
|
+
def _add_session(
|
|
238
|
+
self,
|
|
239
|
+
store: SessionStore,
|
|
240
|
+
*,
|
|
241
|
+
actor_id: str,
|
|
242
|
+
session_id: Optional[str] = None,
|
|
243
|
+
parent_session_id: Optional[str] = None,
|
|
244
|
+
metadata: Optional[dict[str, str]] = None,
|
|
245
|
+
) -> Session:
|
|
246
|
+
response = self._api.create_session(
|
|
247
|
+
store.name,
|
|
248
|
+
actor_id,
|
|
249
|
+
session_id=session_id,
|
|
250
|
+
parent_session_id=parent_session_id,
|
|
251
|
+
metadata=metadata,
|
|
252
|
+
)
|
|
253
|
+
return self._session_from_response(response, store.name)
|
|
254
|
+
|
|
255
|
+
def _list_sessions(
|
|
256
|
+
self,
|
|
257
|
+
store: SessionStore,
|
|
258
|
+
*,
|
|
259
|
+
page_size: Optional[int] = None,
|
|
260
|
+
filter: Optional[str] = None,
|
|
261
|
+
order_by: Optional[str] = None,
|
|
262
|
+
) -> Iterator[Session]:
|
|
263
|
+
validate_page_size(page_size)
|
|
264
|
+
if order_by is None:
|
|
265
|
+
order_by = "create_time desc"
|
|
266
|
+
page_token = None
|
|
267
|
+
while True:
|
|
268
|
+
response = self._api.list_sessions(
|
|
269
|
+
store.name,
|
|
270
|
+
filter=filter,
|
|
271
|
+
order_by=order_by,
|
|
272
|
+
page_size=page_size,
|
|
273
|
+
page_token=page_token,
|
|
274
|
+
)
|
|
275
|
+
for session in response.get("sessions", []):
|
|
276
|
+
yield self._session_from_response(session, store.name)
|
|
277
|
+
page_token = response.get("next_page_token")
|
|
278
|
+
if not page_token:
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
def _get_session(
|
|
282
|
+
self,
|
|
283
|
+
store: SessionStore,
|
|
284
|
+
*,
|
|
285
|
+
session_id: str,
|
|
286
|
+
) -> Session:
|
|
287
|
+
response = self._api.get_session(session_id, store.name)
|
|
288
|
+
return self._session_from_response(response, store.name)
|
|
289
|
+
|
|
290
|
+
def _update_session(self, session: Session, *, metadata: dict[str, str]) -> Session:
|
|
291
|
+
response = self._api.update_session(
|
|
292
|
+
session.store_name,
|
|
293
|
+
session.session_id,
|
|
294
|
+
metadata,
|
|
295
|
+
)
|
|
296
|
+
return self._session_from_response(response, session.store_name)
|
|
297
|
+
|
|
298
|
+
def _delete_session(self, session: Session, *, force: bool = False) -> None:
|
|
299
|
+
self._api.delete_session(session.store_name, session.session_id, force=force)
|
|
300
|
+
|
|
301
|
+
def _fork_session(
|
|
302
|
+
self,
|
|
303
|
+
session: Session,
|
|
304
|
+
*,
|
|
305
|
+
actor_id: str,
|
|
306
|
+
up_to_item_id: Optional[str] = None,
|
|
307
|
+
session_id: Optional[str] = None,
|
|
308
|
+
metadata: Optional[dict[str, str]] = None,
|
|
309
|
+
) -> Session:
|
|
310
|
+
response = self._api.fork_session(
|
|
311
|
+
session.store_name,
|
|
312
|
+
session.session_id,
|
|
313
|
+
actor_id,
|
|
314
|
+
up_to_item_id=up_to_item_id,
|
|
315
|
+
session_id=session_id,
|
|
316
|
+
metadata=metadata,
|
|
317
|
+
)
|
|
318
|
+
return self._session_from_response(
|
|
319
|
+
response.get("session", response),
|
|
320
|
+
session.store_name,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def _list_items(
|
|
324
|
+
self,
|
|
325
|
+
session: Session,
|
|
326
|
+
*,
|
|
327
|
+
page_size: Optional[int] = None,
|
|
328
|
+
order_by: Optional[str] = None,
|
|
329
|
+
) -> Iterator[SessionItem]:
|
|
330
|
+
validate_page_size(page_size)
|
|
331
|
+
page_token = None
|
|
332
|
+
while True:
|
|
333
|
+
response = self._api.list_session_items(
|
|
334
|
+
session.store_name,
|
|
335
|
+
session.session_id,
|
|
336
|
+
order_by=order_by,
|
|
337
|
+
page_size=page_size,
|
|
338
|
+
page_token=page_token,
|
|
339
|
+
)
|
|
340
|
+
for item in response.get("session_items", []):
|
|
341
|
+
yield self._item_from_response(item)
|
|
342
|
+
page_token = response.get("next_page_token")
|
|
343
|
+
if not page_token:
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
def _append_items(self, session: Session, *, items: Sequence[Any]) -> List[SessionItem]:
|
|
347
|
+
if not items:
|
|
348
|
+
raise ValueError("at least one item is required")
|
|
349
|
+
response = self._api.append_session_items(
|
|
350
|
+
session.store_name,
|
|
351
|
+
session.session_id,
|
|
352
|
+
list(items),
|
|
353
|
+
)
|
|
354
|
+
return [self._item_from_response(item) for item in response.get("session_items", [])]
|
|
355
|
+
|
|
356
|
+
def _pop_item(self, session: Session) -> Optional[SessionItem]:
|
|
357
|
+
response = self._api.pop_session_item(session.store_name, session.session_id)
|
|
358
|
+
item = response.get("item")
|
|
359
|
+
return self._item_from_response(item) if item is not None else None
|
|
360
|
+
|
|
361
|
+
def _clear_items(self, session: Session) -> None:
|
|
362
|
+
self._api.clear_session_items(session.store_name, session.session_id)
|
|
363
|
+
|
|
364
|
+
def _extract_memories(
|
|
365
|
+
self,
|
|
366
|
+
session: Session,
|
|
367
|
+
*,
|
|
368
|
+
memory_store: str,
|
|
369
|
+
instructions: Optional[str],
|
|
370
|
+
dry_run: bool,
|
|
371
|
+
) -> List[ExtractedMemory]:
|
|
372
|
+
response = self._api.extract_memories(
|
|
373
|
+
session.store_name,
|
|
374
|
+
session.session_id,
|
|
375
|
+
memory_store,
|
|
376
|
+
instructions=instructions,
|
|
377
|
+
dry_run=dry_run,
|
|
378
|
+
)
|
|
379
|
+
return [self._extracted_from_response(e) for e in response.get("entries") or []]
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
def _extracted_from_response(response: dict[str, Any]) -> ExtractedMemory:
|
|
383
|
+
return ExtractedMemory(
|
|
384
|
+
name=response["name"],
|
|
385
|
+
actor_id=response.get("actor_id"),
|
|
386
|
+
path=response.get("path"),
|
|
387
|
+
content=response.get("content"),
|
|
388
|
+
description=response.get("description"),
|
|
389
|
+
session_id=response.get("session_id"),
|
|
390
|
+
source_type=response.get("source_type"),
|
|
391
|
+
create_time=parse_timestamp(response.get("create_time")),
|
|
392
|
+
update_time=parse_timestamp(response.get("update_time")),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def _store_from_response(self, response: dict[str, Any]) -> SessionStore:
|
|
396
|
+
name = response.get("session_store_name")
|
|
397
|
+
if name is None:
|
|
398
|
+
name = response["name"].removeprefix("session-stores/")
|
|
399
|
+
return SessionStore(
|
|
400
|
+
name=name,
|
|
401
|
+
session_store_id=response.get("session_store_id"),
|
|
402
|
+
creator_user_id=response.get("creator_user_id"),
|
|
403
|
+
create_time=parse_timestamp(response.get("create_time")),
|
|
404
|
+
update_time=parse_timestamp(response.get("update_time")),
|
|
405
|
+
description=response.get("description"),
|
|
406
|
+
metadata=dict(response.get("metadata", {})),
|
|
407
|
+
_client=self,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def _session_from_response(
|
|
411
|
+
self,
|
|
412
|
+
response: dict[str, Any],
|
|
413
|
+
session_store_name: str,
|
|
414
|
+
) -> Session:
|
|
415
|
+
return Session(
|
|
416
|
+
store_name=response.get("session_store_name", session_store_name),
|
|
417
|
+
session_id=response["session_id"],
|
|
418
|
+
actor_id=response["actor_id"],
|
|
419
|
+
parent_session_id=response.get("parent_session_id"),
|
|
420
|
+
root_session_id=response.get("root_session_id"),
|
|
421
|
+
metadata=dict(response.get("metadata", {})),
|
|
422
|
+
create_time=parse_timestamp(response.get("create_time")),
|
|
423
|
+
update_time=parse_timestamp(response.get("update_time")),
|
|
424
|
+
last_activity_time=parse_timestamp(response.get("last_activity_time")),
|
|
425
|
+
_client=self,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _item_from_response(response: dict[str, Any]) -> SessionItem:
|
|
430
|
+
return SessionItem(
|
|
431
|
+
item_id=response["item_id"],
|
|
432
|
+
data=response["data"],
|
|
433
|
+
create_time=parse_timestamp(response.get("create_time")),
|
|
434
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Timestamp parsing and humanization for the Agent Bricks CLI.
|
|
2
|
+
|
|
3
|
+
The 2.0 agents APIs return timestamps as `google.protobuf.Timestamp`, which
|
|
4
|
+
serializes to an RFC 3339 string (`2026-08-15T01:29:00Z`) under the fields
|
|
5
|
+
`create_time`/`update_time`. Older memory-store responses used epoch-millis
|
|
6
|
+
int64 (`created_at`/`updated_at`); `parse_timestamp` accepts either form.
|
|
7
|
+
|
|
8
|
+
`relative` returns concise phrases such as "13 days ago" and "An hour ago".
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from typing import Optional, Union
|
|
15
|
+
|
|
16
|
+
TimestampValue = Union[int, float, str, datetime, None]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_timestamp(value: TimestampValue) -> Optional[datetime]:
|
|
20
|
+
"""Parse a datetime, an epoch-millis number, a numeric string, or an RFC 3339 string.
|
|
21
|
+
|
|
22
|
+
Returns a timezone-aware UTC datetime, or None if the value is empty/unparseable.
|
|
23
|
+
"""
|
|
24
|
+
if value is None or value == "":
|
|
25
|
+
return None
|
|
26
|
+
if isinstance(value, datetime):
|
|
27
|
+
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
28
|
+
if isinstance(value, (int, float)):
|
|
29
|
+
return datetime.fromtimestamp(value / 1000.0, tz=timezone.utc)
|
|
30
|
+
if isinstance(value, str):
|
|
31
|
+
if value.isdigit():
|
|
32
|
+
return datetime.fromtimestamp(int(value) / 1000.0, tz=timezone.utc)
|
|
33
|
+
iso = value.replace("Z", "+00:00")
|
|
34
|
+
try:
|
|
35
|
+
dt = datetime.fromisoformat(iso)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return None
|
|
38
|
+
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def relative(value: TimestampValue, *, now: Optional[datetime] = None) -> str:
|
|
43
|
+
"""Humanize a timestamp as an "N units ago" string."""
|
|
44
|
+
dt = parse_timestamp(value)
|
|
45
|
+
if dt is None:
|
|
46
|
+
return "—"
|
|
47
|
+
now = now or datetime.now(tz=timezone.utc)
|
|
48
|
+
seconds = (now - dt).total_seconds()
|
|
49
|
+
if seconds < 0:
|
|
50
|
+
return "just now"
|
|
51
|
+
|
|
52
|
+
minutes = seconds / 60
|
|
53
|
+
hours = minutes / 60
|
|
54
|
+
days = hours / 24
|
|
55
|
+
|
|
56
|
+
if seconds < 45:
|
|
57
|
+
return "just now"
|
|
58
|
+
if minutes < 2:
|
|
59
|
+
return "A minute ago"
|
|
60
|
+
if minutes < 60:
|
|
61
|
+
return f"{round(minutes)} minutes ago"
|
|
62
|
+
if hours < 2:
|
|
63
|
+
return "An hour ago"
|
|
64
|
+
if hours < 24:
|
|
65
|
+
return f"{round(hours)} hours ago"
|
|
66
|
+
if days < 2:
|
|
67
|
+
return "A day ago"
|
|
68
|
+
if days < 30:
|
|
69
|
+
return f"{round(days)} days ago"
|
|
70
|
+
if days < 60:
|
|
71
|
+
return "A month ago"
|
|
72
|
+
if days < 365:
|
|
73
|
+
return f"{round(days / 30)} months ago"
|
|
74
|
+
if days < 730:
|
|
75
|
+
return "A year ago"
|
|
76
|
+
return f"{round(days / 365)} years ago"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def absolute(value: TimestampValue) -> str:
|
|
80
|
+
"""Format a timestamp like the mock's detail rail: "May 19, 2026, 05:00 PM"."""
|
|
81
|
+
dt = parse_timestamp(value)
|
|
82
|
+
if dt is None:
|
|
83
|
+
return "—"
|
|
84
|
+
local = dt.astimezone()
|
|
85
|
+
return f"{local.strftime('%b')} {local.day}, {local.strftime('%Y, %I:%M %p')}"
|