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.
@@ -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
+