managed-deepagents 0.1.1__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.
- managed_deepagents/__init__.py +25 -0
- managed_deepagents/_utils.py +90 -0
- managed_deepagents/client.py +236 -0
- managed_deepagents/errors.py +30 -0
- managed_deepagents/py.typed +1 -0
- managed_deepagents/resources.py +613 -0
- managed_deepagents/streaming.py +139 -0
- managed_deepagents/types.py +119 -0
- managed_deepagents-0.1.1.dist-info/METADATA +140 -0
- managed_deepagents-0.1.1.dist-info/RECORD +12 -0
- managed_deepagents-0.1.1.dist-info/WHEEL +4 -0
- managed_deepagents-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Python SDK for LangSmith Managed Deep Agents."""
|
|
2
|
+
|
|
3
|
+
from managed_deepagents.client import (
|
|
4
|
+
AsyncClient,
|
|
5
|
+
AsyncManagedDeepAgentsClient,
|
|
6
|
+
Client,
|
|
7
|
+
ManagedDeepAgentsClient,
|
|
8
|
+
)
|
|
9
|
+
from managed_deepagents.errors import (
|
|
10
|
+
ManagedDeepAgentsAPIError,
|
|
11
|
+
ManagedDeepAgentsConfigError,
|
|
12
|
+
ManagedDeepAgentsError,
|
|
13
|
+
)
|
|
14
|
+
from managed_deepagents.streaming import SSEEvent
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AsyncClient",
|
|
18
|
+
"AsyncManagedDeepAgentsClient",
|
|
19
|
+
"Client",
|
|
20
|
+
"ManagedDeepAgentsAPIError",
|
|
21
|
+
"ManagedDeepAgentsClient",
|
|
22
|
+
"ManagedDeepAgentsConfigError",
|
|
23
|
+
"ManagedDeepAgentsError",
|
|
24
|
+
"SSEEvent",
|
|
25
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
DEFAULT_API_URL = "https://api.smith.langchain.com"
|
|
8
|
+
MANAGED_DEEPAGENTS_PATH = "/v1/deepagents"
|
|
9
|
+
FLEET_PATH = "/v1/fleet"
|
|
10
|
+
MANAGED_AGENT_BASE_PATHS = (
|
|
11
|
+
MANAGED_DEEPAGENTS_PATH,
|
|
12
|
+
FLEET_PATH,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_default_api_key() -> str | None:
|
|
17
|
+
return os.getenv("LANGSMITH_API_KEY")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_default_api_url() -> str:
|
|
21
|
+
return os.getenv("LANGSMITH_ENDPOINT") or DEFAULT_API_URL
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def normalize_api_url(api_url: str) -> str:
|
|
25
|
+
base_url = api_url.rstrip("/")
|
|
26
|
+
if base_url.endswith(MANAGED_AGENT_BASE_PATHS):
|
|
27
|
+
return base_url
|
|
28
|
+
if base_url.endswith("/v1"):
|
|
29
|
+
return f"{base_url}/deepagents"
|
|
30
|
+
return f"{base_url}{MANAGED_DEEPAGENTS_PATH}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def clean_mapping(values: Mapping[str, Any]) -> dict[str, Any]:
|
|
34
|
+
return {key: value for key, value in values.items() if value is not None}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def merge_body_fields(
|
|
38
|
+
body: Mapping[str, Any] | None,
|
|
39
|
+
fields: Mapping[str, Any],
|
|
40
|
+
) -> dict[str, Any]:
|
|
41
|
+
payload = dict(body or {})
|
|
42
|
+
payload.update(clean_mapping(fields))
|
|
43
|
+
return payload
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def prepare_agent_payload(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
47
|
+
next_payload = dict(payload)
|
|
48
|
+
files = next_payload.get("files")
|
|
49
|
+
if isinstance(files, Mapping):
|
|
50
|
+
next_payload["files"] = {
|
|
51
|
+
path: {"content": value} if isinstance(value, str) else value
|
|
52
|
+
for path, value in files.items()
|
|
53
|
+
}
|
|
54
|
+
return next_payload
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_stream_body(
|
|
58
|
+
*,
|
|
59
|
+
agent_id: str,
|
|
60
|
+
messages: list[Mapping[str, Any]],
|
|
61
|
+
stream_mode: list[str] | None = None,
|
|
62
|
+
stream_subgraphs: bool | None = None,
|
|
63
|
+
user_timezone: str | None = None,
|
|
64
|
+
extra: Mapping[str, Any] | None = None,
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
body = dict(extra or {})
|
|
67
|
+
body.update(
|
|
68
|
+
clean_mapping(
|
|
69
|
+
{
|
|
70
|
+
"agent_id": agent_id,
|
|
71
|
+
"messages": messages,
|
|
72
|
+
"stream_mode": stream_mode,
|
|
73
|
+
"stream_subgraphs": stream_subgraphs,
|
|
74
|
+
"user_timezone": user_timezone,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
return body
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_invoke_body(
|
|
82
|
+
*,
|
|
83
|
+
agent_id: str,
|
|
84
|
+
messages: list[Mapping[str, Any]],
|
|
85
|
+
extra: Mapping[str, Any] | None = None,
|
|
86
|
+
) -> dict[str, Any]:
|
|
87
|
+
body = dict(extra or {})
|
|
88
|
+
body["assistant_id"] = agent_id
|
|
89
|
+
body["input"] = {"messages": messages}
|
|
90
|
+
return body
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterable, Iterable, Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from managed_deepagents._utils import (
|
|
9
|
+
get_default_api_key,
|
|
10
|
+
get_default_api_url,
|
|
11
|
+
normalize_api_url,
|
|
12
|
+
)
|
|
13
|
+
from managed_deepagents.errors import ManagedDeepAgentsAPIError
|
|
14
|
+
from managed_deepagents.resources import (
|
|
15
|
+
AgentsResource,
|
|
16
|
+
AsyncAgentsResource,
|
|
17
|
+
AsyncAuthSessionsResource,
|
|
18
|
+
AsyncMcpServersResource,
|
|
19
|
+
AsyncThreadsResource,
|
|
20
|
+
AuthSessionsResource,
|
|
21
|
+
McpServersResource,
|
|
22
|
+
ThreadsResource,
|
|
23
|
+
)
|
|
24
|
+
from managed_deepagents.streaming import SSEEvent, aiter_sse_events, iter_sse_events
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Client:
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
api_url: str | None = None,
|
|
32
|
+
api_key: str | None = None,
|
|
33
|
+
workspace_id: str | None = None,
|
|
34
|
+
timeout: float | httpx.Timeout = 30.0,
|
|
35
|
+
headers: Mapping[str, str] | None = None,
|
|
36
|
+
http_client: httpx.Client | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.api_url = normalize_api_url(api_url or get_default_api_url())
|
|
39
|
+
self.api_key = api_key if api_key is not None else get_default_api_key()
|
|
40
|
+
self.workspace_id = workspace_id
|
|
41
|
+
self._headers = dict(headers or {})
|
|
42
|
+
self._owns_client = http_client is None
|
|
43
|
+
self._client = http_client or httpx.Client(timeout=timeout)
|
|
44
|
+
|
|
45
|
+
self.agents = AgentsResource(self)
|
|
46
|
+
self.threads = ThreadsResource(self)
|
|
47
|
+
self.mcp_servers = McpServersResource(self)
|
|
48
|
+
self.auth_sessions = AuthSessionsResource(self)
|
|
49
|
+
|
|
50
|
+
def close(self) -> None:
|
|
51
|
+
if self._owns_client:
|
|
52
|
+
self._client.close()
|
|
53
|
+
|
|
54
|
+
def __enter__(self) -> Client:
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def __exit__(self, *_exc: object) -> None:
|
|
58
|
+
self.close()
|
|
59
|
+
|
|
60
|
+
def request(
|
|
61
|
+
self,
|
|
62
|
+
method: str,
|
|
63
|
+
path: str,
|
|
64
|
+
*,
|
|
65
|
+
json: Mapping[str, Any] | None = None,
|
|
66
|
+
params: Mapping[str, Any] | None = None,
|
|
67
|
+
headers: Mapping[str, str] | None = None,
|
|
68
|
+
) -> Any:
|
|
69
|
+
response = self._client.request(
|
|
70
|
+
method,
|
|
71
|
+
self._url(path),
|
|
72
|
+
json=json,
|
|
73
|
+
params=params,
|
|
74
|
+
headers=self._request_headers(headers),
|
|
75
|
+
)
|
|
76
|
+
return _decode_response(response)
|
|
77
|
+
|
|
78
|
+
def stream(
|
|
79
|
+
self,
|
|
80
|
+
path: str,
|
|
81
|
+
*,
|
|
82
|
+
json: Mapping[str, Any],
|
|
83
|
+
headers: Mapping[str, str] | None = None,
|
|
84
|
+
) -> Iterable[SSEEvent]:
|
|
85
|
+
with self._client.stream(
|
|
86
|
+
"POST",
|
|
87
|
+
self._url(path),
|
|
88
|
+
json=json,
|
|
89
|
+
headers=self._request_headers(
|
|
90
|
+
{"Accept": "text/event-stream", **dict(headers or {})}
|
|
91
|
+
),
|
|
92
|
+
) as response:
|
|
93
|
+
if response.status_code >= 400:
|
|
94
|
+
response.read()
|
|
95
|
+
_raise_for_status(response)
|
|
96
|
+
yield from iter_sse_events(response.iter_bytes())
|
|
97
|
+
|
|
98
|
+
def _url(self, path: str) -> str:
|
|
99
|
+
return f"{self.api_url}/{path.lstrip('/')}"
|
|
100
|
+
|
|
101
|
+
def _request_headers(
|
|
102
|
+
self,
|
|
103
|
+
headers: Mapping[str, str] | None = None,
|
|
104
|
+
) -> dict[str, str]:
|
|
105
|
+
request_headers = {"Accept": "application/json", **self._headers}
|
|
106
|
+
if self.api_key:
|
|
107
|
+
request_headers["X-Api-Key"] = self.api_key
|
|
108
|
+
if self.workspace_id:
|
|
109
|
+
request_headers["X-Tenant-Id"] = self.workspace_id
|
|
110
|
+
request_headers.update(headers or {})
|
|
111
|
+
return request_headers
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AsyncClient:
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
api_url: str | None = None,
|
|
119
|
+
api_key: str | None = None,
|
|
120
|
+
workspace_id: str | None = None,
|
|
121
|
+
timeout: float | httpx.Timeout = 30.0,
|
|
122
|
+
headers: Mapping[str, str] | None = None,
|
|
123
|
+
http_client: httpx.AsyncClient | None = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
self.api_url = normalize_api_url(api_url or get_default_api_url())
|
|
126
|
+
self.api_key = api_key if api_key is not None else get_default_api_key()
|
|
127
|
+
self.workspace_id = workspace_id
|
|
128
|
+
self._headers = dict(headers or {})
|
|
129
|
+
self._owns_client = http_client is None
|
|
130
|
+
self._client = http_client or httpx.AsyncClient(timeout=timeout)
|
|
131
|
+
|
|
132
|
+
self.agents = AsyncAgentsResource(self)
|
|
133
|
+
self.threads = AsyncThreadsResource(self)
|
|
134
|
+
self.mcp_servers = AsyncMcpServersResource(self)
|
|
135
|
+
self.auth_sessions = AsyncAuthSessionsResource(self)
|
|
136
|
+
|
|
137
|
+
async def close(self) -> None:
|
|
138
|
+
if self._owns_client:
|
|
139
|
+
await self._client.aclose()
|
|
140
|
+
|
|
141
|
+
async def __aenter__(self) -> AsyncClient:
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
async def __aexit__(self, *_exc: object) -> None:
|
|
145
|
+
await self.close()
|
|
146
|
+
|
|
147
|
+
async def request(
|
|
148
|
+
self,
|
|
149
|
+
method: str,
|
|
150
|
+
path: str,
|
|
151
|
+
*,
|
|
152
|
+
json: Mapping[str, Any] | None = None,
|
|
153
|
+
params: Mapping[str, Any] | None = None,
|
|
154
|
+
headers: Mapping[str, str] | None = None,
|
|
155
|
+
) -> Any:
|
|
156
|
+
response = await self._client.request(
|
|
157
|
+
method,
|
|
158
|
+
self._url(path),
|
|
159
|
+
json=json,
|
|
160
|
+
params=params,
|
|
161
|
+
headers=self._request_headers(headers),
|
|
162
|
+
)
|
|
163
|
+
return _decode_response(response)
|
|
164
|
+
|
|
165
|
+
async def stream(
|
|
166
|
+
self,
|
|
167
|
+
path: str,
|
|
168
|
+
*,
|
|
169
|
+
json: Mapping[str, Any],
|
|
170
|
+
headers: Mapping[str, str] | None = None,
|
|
171
|
+
) -> AsyncIterable[SSEEvent]:
|
|
172
|
+
async with self._client.stream(
|
|
173
|
+
"POST",
|
|
174
|
+
self._url(path),
|
|
175
|
+
json=json,
|
|
176
|
+
headers=self._request_headers(
|
|
177
|
+
{"Accept": "text/event-stream", **dict(headers or {})}
|
|
178
|
+
),
|
|
179
|
+
) as response:
|
|
180
|
+
if response.status_code >= 400:
|
|
181
|
+
await response.aread()
|
|
182
|
+
_raise_for_status(response)
|
|
183
|
+
async for event in aiter_sse_events(response.aiter_bytes()):
|
|
184
|
+
yield event
|
|
185
|
+
|
|
186
|
+
def _url(self, path: str) -> str:
|
|
187
|
+
return f"{self.api_url}/{path.lstrip('/')}"
|
|
188
|
+
|
|
189
|
+
def _request_headers(
|
|
190
|
+
self,
|
|
191
|
+
headers: Mapping[str, str] | None = None,
|
|
192
|
+
) -> dict[str, str]:
|
|
193
|
+
request_headers = {"Accept": "application/json", **self._headers}
|
|
194
|
+
if self.api_key:
|
|
195
|
+
request_headers["X-Api-Key"] = self.api_key
|
|
196
|
+
if self.workspace_id:
|
|
197
|
+
request_headers["X-Tenant-Id"] = self.workspace_id
|
|
198
|
+
request_headers.update(headers or {})
|
|
199
|
+
return request_headers
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _decode_response(response: httpx.Response) -> Any:
|
|
203
|
+
_raise_for_status(response)
|
|
204
|
+
if response.status_code == 204 or not response.content:
|
|
205
|
+
return None
|
|
206
|
+
content_type = response.headers.get("content-type", "")
|
|
207
|
+
if "application/json" not in content_type:
|
|
208
|
+
return response.text
|
|
209
|
+
return response.json()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
213
|
+
if response.status_code < 400:
|
|
214
|
+
return
|
|
215
|
+
body = _response_body(response)
|
|
216
|
+
detail = body.get("detail") if isinstance(body, dict) else None
|
|
217
|
+
code = body.get("code") if isinstance(body, dict) else None
|
|
218
|
+
message = detail or response.reason_phrase or "Managed Deep Agents API error"
|
|
219
|
+
raise ManagedDeepAgentsAPIError(
|
|
220
|
+
message,
|
|
221
|
+
status_code=response.status_code,
|
|
222
|
+
code=code,
|
|
223
|
+
detail=detail,
|
|
224
|
+
body=body,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _response_body(response: httpx.Response) -> Any:
|
|
229
|
+
try:
|
|
230
|
+
return response.json()
|
|
231
|
+
except ValueError:
|
|
232
|
+
return response.text
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
ManagedDeepAgentsClient = Client
|
|
236
|
+
AsyncManagedDeepAgentsClient = AsyncClient
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ManagedDeepAgentsError(Exception):
|
|
7
|
+
"""Base exception raised by the Managed Deep Agents SDK."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ManagedDeepAgentsConfigError(ManagedDeepAgentsError):
|
|
11
|
+
"""Raised when client configuration is invalid."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ManagedDeepAgentsAPIError(ManagedDeepAgentsError):
|
|
15
|
+
"""Raised when the API returns a non-2xx response."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
message: str,
|
|
20
|
+
*,
|
|
21
|
+
status_code: int,
|
|
22
|
+
code: str | None = None,
|
|
23
|
+
detail: str | None = None,
|
|
24
|
+
body: Any | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.status_code = status_code
|
|
28
|
+
self.code = code
|
|
29
|
+
self.detail = detail
|
|
30
|
+
self.body = body
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|