acp-sdk 0.10.1__tar.gz → 0.11.0__tar.gz
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.
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/PKG-INFO +2 -1
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/pyproject.toml +2 -1
- acp_sdk-0.11.0/src/acp_sdk/client/client.py +324 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/models/__init__.py +1 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/models/models.py +75 -9
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/models/schemas.py +18 -3
- acp_sdk-0.11.0/src/acp_sdk/models/types.py +9 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/__init__.py +5 -1
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/agent.py +8 -84
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/app.py +87 -29
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/context.py +11 -3
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/executor.py +139 -14
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/server.py +17 -4
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/memory_store.py +4 -4
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/utils.py +1 -1
- acp_sdk-0.11.0/src/acp_sdk/shared/__init__.py +2 -0
- acp_sdk-0.11.0/src/acp_sdk/shared/resources.py +46 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/fixtures/server.py +44 -3
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/test_suites/test_discovery.py +4 -4
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/test_suites/test_runs.py +3 -11
- acp_sdk-0.11.0/tests/e2e/test_suites/test_sessions.py +45 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/client/test_client.py +42 -7
- acp_sdk-0.10.1/src/acp_sdk/client/client.py +0 -233
- acp_sdk-0.10.1/src/acp_sdk/server/session.py +0 -24
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/.gitignore +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/.python-version +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/README.md +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/docs/.gitignore +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/docs/Makefile +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/docs/conf.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/docs/index.rst +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/docs/make.bat +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/pytest.ini +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/__init__.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/client/__init__.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/client/types.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/client/utils.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/instrumentation.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/models/errors.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/py.typed +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/errors.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/logging.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/__init__.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/postgresql_store.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/redis_store.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/store.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/utils.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/telemetry.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/types.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/version.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/conftest.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/__init__.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/config.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/fixtures/__init__.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/fixtures/client.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/test_suites/__init__.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/client/test_utils.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/models/__init__.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/models/test_models.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/server/__init__.py +0 -0
- {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/server/test_server.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: acp-sdk
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.11.0
|
4
4
|
Summary: Agent Communication Protocol SDK
|
5
5
|
Author: IBM Corp.
|
6
6
|
Maintainer-email: Tomas Pilar <thomas7pilar@gmail.com>
|
@@ -11,6 +11,7 @@ Requires-Dist: fastapi[standard]>=0.115
|
|
11
11
|
Requires-Dist: httpx-sse>=0.4
|
12
12
|
Requires-Dist: httpx>=0.26
|
13
13
|
Requires-Dist: janus>=2.0
|
14
|
+
Requires-Dist: obstore>=0.6
|
14
15
|
Requires-Dist: opentelemetry-api>=1.31
|
15
16
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.31
|
16
17
|
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.52b1
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "acp-sdk"
|
3
|
-
version = "0.
|
3
|
+
version = "0.11.0"
|
4
4
|
description = "Agent Communication Protocol SDK"
|
5
5
|
license = "Apache-2.0"
|
6
6
|
readme = "README.md"
|
@@ -21,6 +21,7 @@ dependencies = [
|
|
21
21
|
"cachetools>=5.5",
|
22
22
|
"redis>=6.1",
|
23
23
|
"psycopg[binary]>=3.2",
|
24
|
+
"obstore>=0.6",
|
24
25
|
]
|
25
26
|
|
26
27
|
[build-system]
|
@@ -0,0 +1,324 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
import ssl
|
4
|
+
import typing
|
5
|
+
from collections.abc import AsyncIterator
|
6
|
+
from types import TracebackType
|
7
|
+
from typing import Self
|
8
|
+
|
9
|
+
import httpx
|
10
|
+
from httpx_sse import EventSource, aconnect_sse
|
11
|
+
from pydantic import TypeAdapter
|
12
|
+
|
13
|
+
from acp_sdk.client.types import Input
|
14
|
+
from acp_sdk.client.utils import input_to_messages
|
15
|
+
from acp_sdk.instrumentation import get_tracer
|
16
|
+
from acp_sdk.models import (
|
17
|
+
ACPError,
|
18
|
+
AgentManifest,
|
19
|
+
AgentName,
|
20
|
+
AgentReadResponse,
|
21
|
+
AgentsListResponse,
|
22
|
+
AwaitResume,
|
23
|
+
Error,
|
24
|
+
ErrorCode,
|
25
|
+
ErrorEvent,
|
26
|
+
Event,
|
27
|
+
PingResponse,
|
28
|
+
Run,
|
29
|
+
RunCancelResponse,
|
30
|
+
RunCreateRequest,
|
31
|
+
RunCreateResponse,
|
32
|
+
RunEventsListResponse,
|
33
|
+
RunId,
|
34
|
+
RunMode,
|
35
|
+
RunResumeRequest,
|
36
|
+
RunResumeResponse,
|
37
|
+
Session,
|
38
|
+
SessionReadResponse,
|
39
|
+
)
|
40
|
+
|
41
|
+
logger = logging.getLogger(__name__)
|
42
|
+
|
43
|
+
|
44
|
+
class Client:
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
*,
|
48
|
+
session: Session | None = None,
|
49
|
+
client: httpx.AsyncClient | None = None,
|
50
|
+
manage_client: bool = True,
|
51
|
+
auth: httpx._types.AuthTypes | None = None,
|
52
|
+
params: httpx._types.QueryParamTypes | None = None,
|
53
|
+
headers: httpx._types.HeaderTypes | None = None,
|
54
|
+
cookies: httpx._types.CookieTypes | None = None,
|
55
|
+
timeout: httpx._types.TimeoutTypes = None,
|
56
|
+
verify: ssl.SSLContext | str | bool = True,
|
57
|
+
cert: httpx._types.CertTypes | None = None,
|
58
|
+
http1: bool = True,
|
59
|
+
http2: bool = False,
|
60
|
+
proxy: httpx._types.ProxyTypes | None = None,
|
61
|
+
mounts: None | (typing.Mapping[str, httpx.AsyncBaseTransport | None]) = None,
|
62
|
+
follow_redirects: bool = False,
|
63
|
+
limits: httpx.Limits = httpx._config.DEFAULT_LIMITS,
|
64
|
+
max_redirects: int = httpx._config.DEFAULT_MAX_REDIRECTS,
|
65
|
+
event_hooks: None | (typing.Mapping[str, list[httpx._client.EventHook]]) = None,
|
66
|
+
base_url: httpx.URL | str = "",
|
67
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
68
|
+
trust_env: bool = True,
|
69
|
+
) -> None:
|
70
|
+
self._session = session
|
71
|
+
self._session_last_refresh_base_url: httpx.URL | None = None
|
72
|
+
self._session_refresh_lock = asyncio.Lock()
|
73
|
+
|
74
|
+
self._client = client or httpx.AsyncClient(
|
75
|
+
auth=auth,
|
76
|
+
params=params,
|
77
|
+
headers=headers,
|
78
|
+
cookies=cookies,
|
79
|
+
timeout=timeout,
|
80
|
+
verify=verify,
|
81
|
+
cert=cert,
|
82
|
+
http1=http1,
|
83
|
+
http2=http2,
|
84
|
+
proxy=proxy,
|
85
|
+
mounts=mounts,
|
86
|
+
follow_redirects=follow_redirects,
|
87
|
+
limits=limits,
|
88
|
+
max_redirects=max_redirects,
|
89
|
+
event_hooks=event_hooks,
|
90
|
+
base_url=base_url,
|
91
|
+
transport=transport,
|
92
|
+
trust_env=trust_env,
|
93
|
+
)
|
94
|
+
self._manage_client = manage_client
|
95
|
+
|
96
|
+
@property
|
97
|
+
def client(self) -> httpx.AsyncClient:
|
98
|
+
return self._client
|
99
|
+
|
100
|
+
async def __aenter__(self) -> Self:
|
101
|
+
if self._manage_client:
|
102
|
+
await self._client.__aenter__()
|
103
|
+
self._session_span_manager = (
|
104
|
+
(
|
105
|
+
get_tracer()
|
106
|
+
.start_as_current_span("session", attributes={"acp.session": str(self._session.id)})
|
107
|
+
.__enter__()
|
108
|
+
)
|
109
|
+
if self._session
|
110
|
+
else None
|
111
|
+
)
|
112
|
+
return self
|
113
|
+
|
114
|
+
async def __aexit__(
|
115
|
+
self,
|
116
|
+
exc_type: type[BaseException] | None = None,
|
117
|
+
exc_value: BaseException | None = None,
|
118
|
+
traceback: TracebackType | None = None,
|
119
|
+
) -> None:
|
120
|
+
if self._session_span_manager:
|
121
|
+
self._session_span_manager.__exit__(exc_type, exc_value, traceback)
|
122
|
+
if self._manage_client:
|
123
|
+
await self._client.__aexit__(exc_type, exc_value, traceback)
|
124
|
+
|
125
|
+
def session(self, session: Session | None = None) -> Self:
|
126
|
+
return Client(client=self._client, manage_client=False, session=session or Session())
|
127
|
+
|
128
|
+
async def agents(self, *, base_url: httpx.URL | str | None = None) -> AsyncIterator[AgentManifest]:
|
129
|
+
response = await self._client.get(self._create_url("/agents", base_url=base_url))
|
130
|
+
self._raise_error(response)
|
131
|
+
for agent in AgentsListResponse.model_validate(response.json()).agents:
|
132
|
+
yield agent
|
133
|
+
|
134
|
+
async def agent(self, *, name: AgentName, base_url: httpx.URL | str | None = None) -> AgentManifest:
|
135
|
+
response = await self._client.get(self._create_url(f"/agents/{name}", base_url=base_url))
|
136
|
+
self._raise_error(response)
|
137
|
+
response = AgentReadResponse.model_validate(response.json())
|
138
|
+
return AgentManifest(**response.model_dump())
|
139
|
+
|
140
|
+
async def ping(self, *, base_url: httpx.URL | str | None = None) -> bool:
|
141
|
+
response = await self._client.get(self._create_url("/ping", base_url=base_url))
|
142
|
+
self._raise_error(response)
|
143
|
+
PingResponse.model_validate(response.json())
|
144
|
+
return
|
145
|
+
|
146
|
+
async def run_sync(self, input: Input, *, agent: AgentName, base_url: httpx.URL | str | None = None) -> Run:
|
147
|
+
response = await self._client.post(
|
148
|
+
self._create_url("/runs", base_url=base_url),
|
149
|
+
content=RunCreateRequest(
|
150
|
+
agent_name=agent,
|
151
|
+
input=input_to_messages(input),
|
152
|
+
mode=RunMode.SYNC,
|
153
|
+
**(await self._prepare_session_for_run(base_url=base_url)),
|
154
|
+
).model_dump_json(),
|
155
|
+
)
|
156
|
+
self._raise_error(response)
|
157
|
+
response = RunCreateResponse.model_validate(response.json())
|
158
|
+
return Run(**response.model_dump())
|
159
|
+
|
160
|
+
async def run_async(self, input: Input, *, agent: AgentName, base_url: httpx.URL | str | None = None) -> Run:
|
161
|
+
response = await self._client.post(
|
162
|
+
self._create_url("/runs", base_url=base_url),
|
163
|
+
content=RunCreateRequest(
|
164
|
+
agent_name=agent,
|
165
|
+
input=input_to_messages(input),
|
166
|
+
mode=RunMode.ASYNC,
|
167
|
+
**(await self._prepare_session_for_run(base_url=base_url)),
|
168
|
+
).model_dump_json(),
|
169
|
+
)
|
170
|
+
self._raise_error(response)
|
171
|
+
response = RunCreateResponse.model_validate(response.json())
|
172
|
+
return Run(**response.model_dump())
|
173
|
+
|
174
|
+
async def run_stream(
|
175
|
+
self, input: Input, *, agent: AgentName, base_url: httpx.URL | str | None = None
|
176
|
+
) -> AsyncIterator[Event]:
|
177
|
+
async with aconnect_sse(
|
178
|
+
self._client,
|
179
|
+
"POST",
|
180
|
+
self._create_url("/runs", base_url=base_url),
|
181
|
+
content=RunCreateRequest(
|
182
|
+
agent_name=agent,
|
183
|
+
input=input_to_messages(input),
|
184
|
+
mode=RunMode.STREAM,
|
185
|
+
session=await self._prepare_session_for_run(base_url=base_url),
|
186
|
+
).model_dump_json(),
|
187
|
+
) as event_source:
|
188
|
+
async for event in self._validate_stream(event_source):
|
189
|
+
yield event
|
190
|
+
|
191
|
+
async def run_status(self, *, run_id: RunId, base_url: httpx.URL | str | None = None) -> Run:
|
192
|
+
response = await self._client.get(self._create_url(f"/runs/{run_id}", base_url=base_url))
|
193
|
+
self._raise_error(response)
|
194
|
+
return Run.model_validate(response.json())
|
195
|
+
|
196
|
+
async def run_events(self, *, run_id: RunId, base_url: httpx.URL | str | None = None) -> AsyncIterator[Event]:
|
197
|
+
response = await self._client.get(self._create_url(f"/runs/{run_id}/events", base_url=base_url))
|
198
|
+
self._raise_error(response)
|
199
|
+
response = RunEventsListResponse.model_validate(response.json())
|
200
|
+
for event in response.events:
|
201
|
+
yield event
|
202
|
+
|
203
|
+
async def run_cancel(self, *, run_id: RunId, base_url: httpx.URL | str | None = None) -> Run:
|
204
|
+
response = await self._client.post(self._create_url(f"/runs/{run_id}/cancel", base_url=base_url))
|
205
|
+
self._raise_error(response)
|
206
|
+
response = RunCancelResponse.model_validate(response.json())
|
207
|
+
return Run(**response.model_dump())
|
208
|
+
|
209
|
+
async def run_resume_sync(
|
210
|
+
self, await_resume: AwaitResume, *, run_id: RunId, base_url: httpx.URL | str | None = None
|
211
|
+
) -> Run:
|
212
|
+
response = await self._client.post(
|
213
|
+
self._create_url(f"/runs/{run_id}", base_url=base_url),
|
214
|
+
content=RunResumeRequest(await_resume=await_resume, mode=RunMode.SYNC).model_dump_json(),
|
215
|
+
)
|
216
|
+
self._raise_error(response)
|
217
|
+
response = RunResumeResponse.model_validate(response.json())
|
218
|
+
return Run(**response.model_dump())
|
219
|
+
|
220
|
+
async def run_resume_async(
|
221
|
+
self, await_resume: AwaitResume, *, run_id: RunId, base_url: httpx.URL | str | None = None
|
222
|
+
) -> Run:
|
223
|
+
response = await self._client.post(
|
224
|
+
self._create_url(f"/runs/{run_id}", base_url=base_url),
|
225
|
+
content=RunResumeRequest(await_resume=await_resume, mode=RunMode.ASYNC).model_dump_json(),
|
226
|
+
)
|
227
|
+
self._raise_error(response)
|
228
|
+
response = RunResumeResponse.model_validate(response.json())
|
229
|
+
return Run(**response.model_dump())
|
230
|
+
|
231
|
+
async def run_resume_stream(
|
232
|
+
self, await_resume: AwaitResume, *, run_id: RunId, base_url: httpx.URL | str | None = None
|
233
|
+
) -> AsyncIterator[Event]:
|
234
|
+
async with aconnect_sse(
|
235
|
+
self._client,
|
236
|
+
"POST",
|
237
|
+
self._create_url(f"/runs/{run_id}", base_url=base_url),
|
238
|
+
content=RunResumeRequest(await_resume=await_resume, mode=RunMode.STREAM).model_dump_json(),
|
239
|
+
) as event_source:
|
240
|
+
async for event in self._validate_stream(event_source):
|
241
|
+
yield event
|
242
|
+
|
243
|
+
async def refresh_session(
|
244
|
+
self, *, base_url: httpx.URL | str | None = None, timeout: httpx._types.TimeoutTypes = 5000
|
245
|
+
) -> Session:
|
246
|
+
if not self._session:
|
247
|
+
raise RuntimeError("Client is not in a session")
|
248
|
+
|
249
|
+
async with self._session_refresh_lock:
|
250
|
+
url = self._create_url(
|
251
|
+
f"/sessions/{self._session.id}",
|
252
|
+
base_url=base_url or self._session_last_refresh_base_url,
|
253
|
+
)
|
254
|
+
|
255
|
+
try:
|
256
|
+
response = await self._client.get(url, timeout=timeout)
|
257
|
+
response = SessionReadResponse.model_validate(response.json())
|
258
|
+
self._session = Session(**response.model_dump())
|
259
|
+
except ACPError as e:
|
260
|
+
if e.error.code == ErrorCode.NOT_FOUND:
|
261
|
+
pass
|
262
|
+
raise e
|
263
|
+
|
264
|
+
return self._session
|
265
|
+
|
266
|
+
async def _validate_stream(
|
267
|
+
self,
|
268
|
+
event_source: EventSource,
|
269
|
+
) -> AsyncIterator[Event]:
|
270
|
+
if event_source.response.is_error:
|
271
|
+
await event_source.response.aread()
|
272
|
+
self._raise_error(event_source.response)
|
273
|
+
async for event in event_source.aiter_sse():
|
274
|
+
event: Event = TypeAdapter(Event).validate_json(event.data)
|
275
|
+
if isinstance(event, ErrorEvent):
|
276
|
+
raise ACPError(error=event.error)
|
277
|
+
yield event
|
278
|
+
|
279
|
+
def _raise_error(self, response: httpx.Response) -> None:
|
280
|
+
try:
|
281
|
+
response.raise_for_status()
|
282
|
+
except httpx.HTTPError:
|
283
|
+
raise ACPError(Error.model_validate(response.json()))
|
284
|
+
|
285
|
+
def _create_base_url(self, base_url: httpx.URL | str | None) -> httpx.URL:
|
286
|
+
base_url = httpx.URL(base_url or self._client.base_url)
|
287
|
+
if not base_url.raw_path.endswith(b"/"):
|
288
|
+
base_url = base_url.copy_with(raw_path=base_url.raw_path + b"/")
|
289
|
+
return base_url
|
290
|
+
|
291
|
+
def _create_url(self, endpoint: str, base_url: httpx.URL | str | None) -> httpx.URL:
|
292
|
+
merge_url = httpx.URL(endpoint)
|
293
|
+
|
294
|
+
if not merge_url.is_relative_url:
|
295
|
+
raise ValueError("Endpoint must be a relative URL")
|
296
|
+
|
297
|
+
base_url = self._create_base_url(base_url)
|
298
|
+
merge_raw_path = base_url.raw_path + merge_url.raw_path.lstrip(b"/")
|
299
|
+
return base_url.copy_with(raw_path=merge_raw_path)
|
300
|
+
|
301
|
+
async def _prepare_session_for_run(self, *, base_url: httpx.URL | str | None) -> dict:
|
302
|
+
if not self._session:
|
303
|
+
return {}
|
304
|
+
|
305
|
+
target_base_url = self._create_base_url(base_url=base_url)
|
306
|
+
try:
|
307
|
+
if not self._session_last_refresh_base_url:
|
308
|
+
return {"session": self._session}
|
309
|
+
if self._session_last_refresh_base_url == target_base_url:
|
310
|
+
# Same server, no need to forward session
|
311
|
+
return {"session_id": self._session.id}
|
312
|
+
|
313
|
+
session = await self.refresh_session()
|
314
|
+
return {"session": session}
|
315
|
+
except ACPError as e:
|
316
|
+
if e.error.code == ErrorCode.NOT_FOUND:
|
317
|
+
return {"session": self._session}
|
318
|
+
raise e
|
319
|
+
finally:
|
320
|
+
await self._update_session_refresh_url(target_base_url)
|
321
|
+
|
322
|
+
async def _update_session_refresh_url(self, url: httpx.URL) -> None:
|
323
|
+
async with self._session_refresh_lock:
|
324
|
+
self._session_last_refresh_base_url = url
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import asyncio
|
2
2
|
import uuid
|
3
|
+
from collections.abc import AsyncIterator
|
3
4
|
from datetime import datetime, timezone
|
4
5
|
from enum import Enum
|
5
6
|
from typing import Any, Literal, Optional, Union
|
@@ -7,6 +8,8 @@ from typing import Any, Literal, Optional, Union
|
|
7
8
|
from pydantic import AnyUrl, BaseModel, ConfigDict, Field
|
8
9
|
|
9
10
|
from acp_sdk.models.errors import ACPError, Error
|
11
|
+
from acp_sdk.models.types import AgentName, ResourceId, ResourceUrl, RunId, SessionId
|
12
|
+
from acp_sdk.shared import ResourceLoader, ResourceStore
|
10
13
|
|
11
14
|
|
12
15
|
class AnyModel(BaseModel):
|
@@ -74,6 +77,40 @@ class Metadata(BaseModel):
|
|
74
77
|
model_config = ConfigDict(extra="allow")
|
75
78
|
|
76
79
|
|
80
|
+
class CitationMetadata(BaseModel):
|
81
|
+
"""
|
82
|
+
Represents an inline citation, providing info about information source. This
|
83
|
+
is supposed to be rendered as an inline icon, optionally marking a text
|
84
|
+
range it belongs to.
|
85
|
+
|
86
|
+
If CitationMetadata is included together with content in the message part,
|
87
|
+
the citation belongs to that content and renders at the MessagePart position.
|
88
|
+
This way may be used for non-text content, like images and files.
|
89
|
+
|
90
|
+
Alternatively, `start_index` and `end_index` may define a text range,
|
91
|
+
counting characters in the current Message across all MessageParts with
|
92
|
+
content type `text/*`, where the citation will be rendered. If one of
|
93
|
+
`start_index` and `end_index` is missing or their values are equal, the
|
94
|
+
citation renders only as an inline icon at that position.
|
95
|
+
|
96
|
+
If both `start_index` and `end_index` are not present and MessagePart has no
|
97
|
+
content, the citation renders as inline icon only at the MessagePart position.
|
98
|
+
|
99
|
+
Properties:
|
100
|
+
- url: URL of the source document.
|
101
|
+
- title: Title of the source document.
|
102
|
+
- description: Accompanying text, which may be a general description of the
|
103
|
+
source document, or a specific snippet.
|
104
|
+
"""
|
105
|
+
|
106
|
+
kind: Literal["citation"] = "citation"
|
107
|
+
start_index: Optional[int]
|
108
|
+
end_index: Optional[int]
|
109
|
+
url: Optional[str]
|
110
|
+
title: Optional[str]
|
111
|
+
description: Optional[str]
|
112
|
+
|
113
|
+
|
77
114
|
class MessagePart(BaseModel):
|
78
115
|
name: Optional[str] = None
|
79
116
|
content_type: Optional[str] = "text/plain"
|
@@ -83,9 +120,9 @@ class MessagePart(BaseModel):
|
|
83
120
|
|
84
121
|
model_config = ConfigDict(extra="allow")
|
85
122
|
|
123
|
+
metadata: Optional[CitationMetadata] = Field(discriminator="kind", default=None)
|
124
|
+
|
86
125
|
def model_post_init(self, __context: Any) -> None:
|
87
|
-
if self.content is None and self.content_url is None:
|
88
|
-
raise ValueError("Either content or content_url must be provided")
|
89
126
|
if self.content is not None and self.content_url is not None:
|
90
127
|
raise ValueError("Only one of content or content_url can be provided")
|
91
128
|
|
@@ -95,6 +132,7 @@ class Artifact(MessagePart):
|
|
95
132
|
|
96
133
|
|
97
134
|
class Message(BaseModel):
|
135
|
+
role: Literal["user"] | Literal["agent"] | str = Field("user", pattern=r"^(user|agent(\/[a-zA-Z0-9_\-]+)?)$")
|
98
136
|
parts: list[MessagePart]
|
99
137
|
created_at: datetime | None = Field(default_factory=lambda: datetime.now(timezone.utc))
|
100
138
|
completed_at: datetime | None = Field(default_factory=lambda: datetime.now(timezone.utc))
|
@@ -102,7 +140,10 @@ class Message(BaseModel):
|
|
102
140
|
def __add__(self, other: "Message") -> "Message":
|
103
141
|
if not isinstance(other, Message):
|
104
142
|
raise TypeError(f"Cannot concatenate Message with {type(other).__name__}")
|
143
|
+
if self.role != other.role:
|
144
|
+
raise ValueError("Cannot concatenate messages with different roles")
|
105
145
|
return Message(
|
146
|
+
role=self.role,
|
106
147
|
parts=self.parts + other.parts,
|
107
148
|
created_at=min(self.created_at, other.created_at) if self.created_at and other.created_at else None,
|
108
149
|
completed_at=max(self.completed_at, other.completed_at)
|
@@ -146,11 +187,6 @@ class Message(BaseModel):
|
|
146
187
|
return Message(parts=parts, created_at=self.created_at, completed_at=self.completed_at)
|
147
188
|
|
148
189
|
|
149
|
-
AgentName = str
|
150
|
-
SessionId = uuid.UUID
|
151
|
-
RunId = uuid.UUID
|
152
|
-
|
153
|
-
|
154
190
|
class RunMode(str, Enum):
|
155
191
|
SYNC = "sync"
|
156
192
|
ASYNC = "async"
|
@@ -280,11 +316,41 @@ Event = Union[
|
|
280
316
|
RunCancelledEvent,
|
281
317
|
RunFailedEvent,
|
282
318
|
RunCompletedEvent,
|
283
|
-
MessagePartEvent,
|
284
319
|
]
|
285
320
|
|
286
321
|
|
287
|
-
class
|
322
|
+
class AgentManifest(BaseModel):
|
288
323
|
name: str
|
289
324
|
description: str | None = None
|
290
325
|
metadata: Metadata = Metadata()
|
326
|
+
|
327
|
+
|
328
|
+
class Session(BaseModel):
|
329
|
+
id: SessionId = Field(default_factory=uuid.uuid4)
|
330
|
+
history: list[ResourceUrl] = Field(default_factory=list)
|
331
|
+
state: ResourceUrl | None = None
|
332
|
+
|
333
|
+
loader: ResourceLoader | None = Field(None, exclude=True)
|
334
|
+
store: ResourceStore | None = Field(None, exclude=True)
|
335
|
+
|
336
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
337
|
+
|
338
|
+
async def load_history(self, *, loader: ResourceLoader | None = None) -> AsyncIterator[Message]:
|
339
|
+
loader = loader or self.loader or ResourceLoader()
|
340
|
+
for url in self.history:
|
341
|
+
data = await loader.load(url)
|
342
|
+
yield Message.model_validate_json(data)
|
343
|
+
|
344
|
+
async def load_state(self, *, loader: ResourceLoader | None = None) -> bytes:
|
345
|
+
loader = loader or self.loader or ResourceLoader()
|
346
|
+
data = await loader.load(self.state)
|
347
|
+
return data
|
348
|
+
|
349
|
+
async def store_state(self, data: bytes, *, store: ResourceStore | None = None) -> ResourceUrl:
|
350
|
+
store = store or self.store
|
351
|
+
if not store:
|
352
|
+
raise ValueError("Store must be specified")
|
353
|
+
|
354
|
+
id = ResourceId()
|
355
|
+
await store.store(id, data)
|
356
|
+
return await store.url(id)
|
@@ -1,6 +1,16 @@
|
|
1
1
|
from pydantic import BaseModel
|
2
2
|
|
3
|
-
from acp_sdk.models.models import
|
3
|
+
from acp_sdk.models.models import (
|
4
|
+
AgentManifest,
|
5
|
+
AgentName,
|
6
|
+
AwaitResume,
|
7
|
+
Event,
|
8
|
+
Message,
|
9
|
+
Run,
|
10
|
+
RunMode,
|
11
|
+
Session,
|
12
|
+
SessionId,
|
13
|
+
)
|
4
14
|
|
5
15
|
|
6
16
|
class PingResponse(BaseModel):
|
@@ -8,16 +18,17 @@ class PingResponse(BaseModel):
|
|
8
18
|
|
9
19
|
|
10
20
|
class AgentsListResponse(BaseModel):
|
11
|
-
agents: list[
|
21
|
+
agents: list[AgentManifest]
|
12
22
|
|
13
23
|
|
14
|
-
class AgentReadResponse(
|
24
|
+
class AgentReadResponse(AgentManifest):
|
15
25
|
pass
|
16
26
|
|
17
27
|
|
18
28
|
class RunCreateRequest(BaseModel):
|
19
29
|
agent_name: AgentName
|
20
30
|
session_id: SessionId | None = None
|
31
|
+
session: Session | None = None
|
21
32
|
input: list[Message]
|
22
33
|
mode: RunMode = RunMode.SYNC
|
23
34
|
|
@@ -45,3 +56,7 @@ class RunCancelResponse(Run):
|
|
45
56
|
|
46
57
|
class RunEventsListResponse(BaseModel):
|
47
58
|
events: list[Event]
|
59
|
+
|
60
|
+
|
61
|
+
class SessionReadResponse(Session):
|
62
|
+
pass
|
@@ -1,7 +1,11 @@
|
|
1
|
-
from acp_sdk.server.agent import
|
1
|
+
from acp_sdk.server.agent import AgentManifest as AgentManifest
|
2
2
|
from acp_sdk.server.agent import agent as agent
|
3
3
|
from acp_sdk.server.app import create_app as create_app
|
4
4
|
from acp_sdk.server.context import Context as Context
|
5
5
|
from acp_sdk.server.server import Server as Server
|
6
|
+
from acp_sdk.server.store import MemoryStore as MemoryStore
|
7
|
+
from acp_sdk.server.store import PostgreSQLStore as PostgreSQLStore
|
8
|
+
from acp_sdk.server.store import RedisStore as RedisStore
|
9
|
+
from acp_sdk.server.store import Store as Store
|
6
10
|
from acp_sdk.server.types import RunYield as RunYield
|
7
11
|
from acp_sdk.server.types import RunYieldResume as RunYieldResume
|