acp-sdk 0.4.0__py3-none-any.whl → 0.6.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.
- acp_sdk/client/client.py +83 -18
- acp_sdk/server/app.py +5 -3
- acp_sdk/server/bundle.py +16 -8
- acp_sdk/server/server.py +4 -1
- acp_sdk/server/types.py +4 -1
- {acp_sdk-0.4.0.dist-info → acp_sdk-0.6.0.dist-info}/METADATA +8 -6
- {acp_sdk-0.4.0.dist-info → acp_sdk-0.6.0.dist-info}/RECORD +8 -8
- {acp_sdk-0.4.0.dist-info → acp_sdk-0.6.0.dist-info}/WHEEL +0 -0
acp_sdk/client/client.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
import ssl
|
2
|
+
import typing
|
1
3
|
import uuid
|
2
4
|
from collections.abc import AsyncGenerator, AsyncIterator
|
3
5
|
from contextlib import asynccontextmanager
|
@@ -31,20 +33,58 @@ from acp_sdk.models import (
|
|
31
33
|
RunResumeResponse,
|
32
34
|
SessionId,
|
33
35
|
)
|
36
|
+
from acp_sdk.models.models import MessagePart
|
37
|
+
|
38
|
+
Input = list[Message] | Message | list[MessagePart] | MessagePart | list[str] | str
|
34
39
|
|
35
40
|
|
36
41
|
class Client:
|
37
42
|
def __init__(
|
38
43
|
self,
|
39
44
|
*,
|
40
|
-
base_url: httpx.URL | str = "",
|
41
|
-
timeout: httpx.Timeout | None = None,
|
42
45
|
session_id: SessionId | None = None,
|
43
46
|
client: httpx.AsyncClient | None = None,
|
44
47
|
instrument: bool = True,
|
48
|
+
auth: httpx._types.AuthTypes | None = None,
|
49
|
+
params: httpx._types.QueryParamTypes | None = None,
|
50
|
+
headers: httpx._types.HeaderTypes | None = None,
|
51
|
+
cookies: httpx._types.CookieTypes | None = None,
|
52
|
+
timeout: httpx._types.TimeoutTypes = None,
|
53
|
+
verify: ssl.SSLContext | str | bool = True,
|
54
|
+
cert: httpx._types.CertTypes | None = None,
|
55
|
+
http1: bool = True,
|
56
|
+
http2: bool = False,
|
57
|
+
proxy: httpx._types.ProxyTypes | None = None,
|
58
|
+
mounts: None | (typing.Mapping[str, httpx.AsyncBaseTransport | None]) = None,
|
59
|
+
follow_redirects: bool = False,
|
60
|
+
limits: httpx.Limits = httpx._config.DEFAULT_LIMITS,
|
61
|
+
max_redirects: int = httpx._config.DEFAULT_MAX_REDIRECTS,
|
62
|
+
event_hooks: None | (typing.Mapping[str, list[httpx._client.EventHook]]) = None,
|
63
|
+
base_url: httpx.URL | str = "",
|
64
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
65
|
+
trust_env: bool = True,
|
45
66
|
) -> None:
|
46
67
|
self._session_id = session_id
|
47
|
-
self._client = client or httpx.AsyncClient(
|
68
|
+
self._client = client or httpx.AsyncClient(
|
69
|
+
auth=auth,
|
70
|
+
params=params,
|
71
|
+
headers=headers,
|
72
|
+
cookies=cookies,
|
73
|
+
timeout=timeout,
|
74
|
+
verify=verify,
|
75
|
+
cert=cert,
|
76
|
+
http1=http1,
|
77
|
+
http2=http2,
|
78
|
+
proxy=proxy,
|
79
|
+
mounts=mounts,
|
80
|
+
follow_redirects=follow_redirects,
|
81
|
+
limits=limits,
|
82
|
+
max_redirects=max_redirects,
|
83
|
+
event_hooks=event_hooks,
|
84
|
+
base_url=base_url,
|
85
|
+
transport=transport,
|
86
|
+
trust_env=trust_env,
|
87
|
+
)
|
48
88
|
if instrument:
|
49
89
|
HTTPXClientInstrumentor.instrument_client(self._client)
|
50
90
|
|
@@ -79,14 +119,15 @@ class Client:
|
|
79
119
|
async def agent(self, *, name: AgentName) -> Agent:
|
80
120
|
response = await self._client.get(f"/agents/{name}")
|
81
121
|
self._raise_error(response)
|
82
|
-
|
122
|
+
response = AgentReadResponse.model_validate(response.json())
|
123
|
+
return Agent(**response.model_dump())
|
83
124
|
|
84
|
-
async def run_sync(self, *, agent: AgentName
|
125
|
+
async def run_sync(self, input: Input, *, agent: AgentName) -> Run:
|
85
126
|
response = await self._client.post(
|
86
127
|
"/runs",
|
87
128
|
content=RunCreateRequest(
|
88
129
|
agent_name=agent,
|
89
|
-
inputs=
|
130
|
+
inputs=self._unify_inputs(input),
|
90
131
|
mode=RunMode.SYNC,
|
91
132
|
session_id=self._session_id,
|
92
133
|
).model_dump_json(),
|
@@ -94,14 +135,14 @@ class Client:
|
|
94
135
|
self._raise_error(response)
|
95
136
|
response = RunCreateResponse.model_validate(response.json())
|
96
137
|
self._set_session(response)
|
97
|
-
return response
|
138
|
+
return Run(**response.model_dump())
|
98
139
|
|
99
|
-
async def run_async(self, *, agent: AgentName
|
140
|
+
async def run_async(self, input: Input, *, agent: AgentName) -> Run:
|
100
141
|
response = await self._client.post(
|
101
142
|
"/runs",
|
102
143
|
content=RunCreateRequest(
|
103
144
|
agent_name=agent,
|
104
|
-
inputs=
|
145
|
+
inputs=self._unify_inputs(input),
|
105
146
|
mode=RunMode.ASYNC,
|
106
147
|
session_id=self._session_id,
|
107
148
|
).model_dump_json(),
|
@@ -109,16 +150,16 @@ class Client:
|
|
109
150
|
self._raise_error(response)
|
110
151
|
response = RunCreateResponse.model_validate(response.json())
|
111
152
|
self._set_session(response)
|
112
|
-
return response
|
153
|
+
return Run(**response.model_dump())
|
113
154
|
|
114
|
-
async def run_stream(self, *, agent: AgentName
|
155
|
+
async def run_stream(self, input: Input, *, agent: AgentName) -> AsyncIterator[Event]:
|
115
156
|
async with aconnect_sse(
|
116
157
|
self._client,
|
117
158
|
"POST",
|
118
159
|
"/runs",
|
119
160
|
content=RunCreateRequest(
|
120
161
|
agent_name=agent,
|
121
|
-
inputs=
|
162
|
+
inputs=self._unify_inputs(input),
|
122
163
|
mode=RunMode.STREAM,
|
123
164
|
session_id=self._session_id,
|
124
165
|
).model_dump_json(),
|
@@ -136,25 +177,28 @@ class Client:
|
|
136
177
|
async def run_cancel(self, *, run_id: RunId) -> Run:
|
137
178
|
response = await self._client.post(f"/runs/{run_id}/cancel")
|
138
179
|
self._raise_error(response)
|
139
|
-
|
180
|
+
response = RunCancelResponse.model_validate(response.json())
|
181
|
+
return Run(**response.model_dump())
|
140
182
|
|
141
|
-
async def run_resume_sync(self, *, run_id: RunId
|
183
|
+
async def run_resume_sync(self, await_resume: AwaitResume, *, run_id: RunId) -> Run:
|
142
184
|
response = await self._client.post(
|
143
185
|
f"/runs/{run_id}",
|
144
186
|
content=RunResumeRequest(await_resume=await_resume, mode=RunMode.SYNC).model_dump_json(),
|
145
187
|
)
|
146
188
|
self._raise_error(response)
|
147
|
-
|
189
|
+
response = RunResumeResponse.model_validate(response.json())
|
190
|
+
return Run(**response.model_dump())
|
148
191
|
|
149
|
-
async def run_resume_async(self, *, run_id: RunId
|
192
|
+
async def run_resume_async(self, await_resume: AwaitResume, *, run_id: RunId) -> Run:
|
150
193
|
response = await self._client.post(
|
151
194
|
f"/runs/{run_id}",
|
152
195
|
content=RunResumeRequest(await_resume=await_resume, mode=RunMode.ASYNC).model_dump_json(),
|
153
196
|
)
|
154
197
|
self._raise_error(response)
|
155
|
-
|
198
|
+
response = RunResumeResponse.model_validate(response.json())
|
199
|
+
return Run(**response.model_dump())
|
156
200
|
|
157
|
-
async def run_resume_stream(self, *, run_id: RunId
|
201
|
+
async def run_resume_stream(self, await_resume: AwaitResume, *, run_id: RunId) -> AsyncIterator[Event]:
|
158
202
|
async with aconnect_sse(
|
159
203
|
self._client,
|
160
204
|
"POST",
|
@@ -183,3 +227,24 @@ class Client:
|
|
183
227
|
|
184
228
|
def _set_session(self, run: Run) -> None:
|
185
229
|
self._session_id = run.session_id
|
230
|
+
|
231
|
+
def _unify_inputs(self, input: Input) -> list[Message]:
|
232
|
+
if isinstance(input, list):
|
233
|
+
if len(input) == 0:
|
234
|
+
return []
|
235
|
+
if all(isinstance(item, Message) for item in input):
|
236
|
+
return input
|
237
|
+
elif all(isinstance(item, MessagePart) for item in input):
|
238
|
+
return [Message(parts=input)]
|
239
|
+
elif all(isinstance(item, str) for item in input):
|
240
|
+
return [Message(parts=[MessagePart(content=content) for content in input])]
|
241
|
+
else:
|
242
|
+
raise RuntimeError("List with mixed types is not supported")
|
243
|
+
else:
|
244
|
+
if isinstance(input, str):
|
245
|
+
input = MessagePart(content=input)
|
246
|
+
if isinstance(input, MessagePart):
|
247
|
+
input = Message(parts=[input])
|
248
|
+
if isinstance(input, Message):
|
249
|
+
input = [input]
|
250
|
+
return input
|
acp_sdk/server/app.py
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
from collections.abc import AsyncGenerator
|
2
2
|
from concurrent.futures import ThreadPoolExecutor
|
3
3
|
from contextlib import asynccontextmanager
|
4
|
+
from datetime import datetime, timedelta
|
4
5
|
from enum import Enum
|
5
6
|
|
7
|
+
from cachetools import TTLCache
|
6
8
|
from fastapi import FastAPI, HTTPException, status
|
7
9
|
from fastapi.encoders import jsonable_encoder
|
8
10
|
from fastapi.responses import JSONResponse, StreamingResponse
|
@@ -45,7 +47,7 @@ class Headers(str, Enum):
|
|
45
47
|
RUN_ID = "Run-ID"
|
46
48
|
|
47
49
|
|
48
|
-
def create_app(*agents: Agent) -> FastAPI:
|
50
|
+
def create_app(*agents: Agent, run_limit: int = 1000, run_ttl: timedelta = timedelta(hours=1)) -> FastAPI:
|
49
51
|
executor: ThreadPoolExecutor
|
50
52
|
|
51
53
|
@asynccontextmanager
|
@@ -60,8 +62,8 @@ def create_app(*agents: Agent) -> FastAPI:
|
|
60
62
|
FastAPIInstrumentor.instrument_app(app)
|
61
63
|
|
62
64
|
agents: dict[AgentName, Agent] = {agent.name: agent for agent in agents}
|
63
|
-
runs:
|
64
|
-
sessions:
|
65
|
+
runs: TTLCache[RunId, RunBundle] = TTLCache(maxsize=run_limit, ttl=run_ttl, timer=datetime.now)
|
66
|
+
sessions: TTLCache[SessionId, Session] = TTLCache(maxsize=run_limit, ttl=run_ttl, timer=datetime.now)
|
65
67
|
|
66
68
|
app.exception_handler(ACPError)(acp_error_handler)
|
67
69
|
app.exception_handler(StarletteHTTPException)(http_exception_handler)
|
acp_sdk/server/bundle.py
CHANGED
@@ -3,7 +3,7 @@ import logging
|
|
3
3
|
from collections.abc import AsyncGenerator
|
4
4
|
from concurrent.futures import ThreadPoolExecutor
|
5
5
|
|
6
|
-
from pydantic import ValidationError
|
6
|
+
from pydantic import BaseModel, ValidationError
|
7
7
|
|
8
8
|
from acp_sdk.instrumentation import get_tracer
|
9
9
|
from acp_sdk.models import (
|
@@ -88,6 +88,13 @@ class RunBundle:
|
|
88
88
|
run_logger = logging.LoggerAdapter(logger, {"run_id": str(self.run.run_id)})
|
89
89
|
|
90
90
|
in_message = False
|
91
|
+
|
92
|
+
async def flush_message() -> None:
|
93
|
+
nonlocal in_message
|
94
|
+
if in_message:
|
95
|
+
await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
|
96
|
+
in_message = False
|
97
|
+
|
91
98
|
try:
|
92
99
|
await self.emit(RunCreatedEvent(run=self.run))
|
93
100
|
|
@@ -103,7 +110,9 @@ class RunBundle:
|
|
103
110
|
while True:
|
104
111
|
next = await generator.asend(await_resume)
|
105
112
|
|
106
|
-
if isinstance(next, MessagePart):
|
113
|
+
if isinstance(next, (MessagePart, str)):
|
114
|
+
if isinstance(next, str):
|
115
|
+
next = MessagePart(content=next)
|
107
116
|
if not in_message:
|
108
117
|
self.run.outputs.append(Message(parts=[]))
|
109
118
|
in_message = True
|
@@ -111,9 +120,7 @@ class RunBundle:
|
|
111
120
|
self.run.outputs[-1].parts.append(next)
|
112
121
|
await self.emit(MessagePartEvent(part=next))
|
113
122
|
elif isinstance(next, Message):
|
114
|
-
|
115
|
-
await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
|
116
|
-
in_message = False
|
123
|
+
await flush_message()
|
117
124
|
self.run.outputs.append(next)
|
118
125
|
await self.emit(MessageCreatedEvent(message=next))
|
119
126
|
for part in next.parts:
|
@@ -130,7 +137,9 @@ class RunBundle:
|
|
130
137
|
elif isinstance(next, Error):
|
131
138
|
raise ACPError(error=next)
|
132
139
|
elif next is None:
|
133
|
-
|
140
|
+
await flush_message()
|
141
|
+
elif isinstance(next, BaseModel):
|
142
|
+
await self.emit(GenericEvent(generic=AnyModel(**next.model_dump())))
|
134
143
|
else:
|
135
144
|
try:
|
136
145
|
generic = AnyModel.model_validate(next)
|
@@ -138,8 +147,7 @@ class RunBundle:
|
|
138
147
|
except ValidationError:
|
139
148
|
raise TypeError("Invalid yield")
|
140
149
|
except StopAsyncIteration:
|
141
|
-
|
142
|
-
await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
|
150
|
+
await flush_message()
|
143
151
|
self.run.status = RunStatus.COMPLETED
|
144
152
|
await self.emit(RunCompletedEvent(run=self.run))
|
145
153
|
run_logger.info("Run completed")
|
acp_sdk/server/server.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import asyncio
|
2
2
|
import os
|
3
3
|
from collections.abc import Awaitable
|
4
|
+
from datetime import timedelta
|
4
5
|
from typing import Any, Callable
|
5
6
|
|
6
7
|
import uvicorn
|
@@ -42,6 +43,8 @@ class Server:
|
|
42
43
|
self,
|
43
44
|
configure_logger: bool = True,
|
44
45
|
configure_telemetry: bool = False,
|
46
|
+
run_limit: int = 1000,
|
47
|
+
run_ttl: timedelta = timedelta(hours=1),
|
45
48
|
host: str = "127.0.0.1",
|
46
49
|
port: int = 8000,
|
47
50
|
uds: str | None = None,
|
@@ -105,7 +108,7 @@ class Server:
|
|
105
108
|
configure_telemetry_func()
|
106
109
|
|
107
110
|
config = uvicorn.Config(
|
108
|
-
create_app(*self._agents),
|
111
|
+
create_app(*self._agents, run_limit=run_limit, run_ttl=run_ttl),
|
109
112
|
host,
|
110
113
|
port,
|
111
114
|
uds,
|
acp_sdk/server/types.py
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
from typing import Any
|
2
2
|
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
3
5
|
from acp_sdk.models import AwaitRequest, AwaitResume, Message
|
6
|
+
from acp_sdk.models.models import MessagePart
|
4
7
|
|
5
|
-
RunYield = Message | AwaitRequest | dict[str | Any] | None
|
8
|
+
RunYield = Message | MessagePart | str | AwaitRequest | BaseModel | dict[str | Any] | None
|
6
9
|
RunYieldResume = AwaitResume | None
|
@@ -1,14 +1,15 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: acp-sdk
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.0
|
4
4
|
Summary: Agent Communication Protocol SDK
|
5
5
|
Author: IBM Corp.
|
6
6
|
Maintainer-email: Tomas Pilar <thomas7pilar@gmail.com>
|
7
7
|
License-Expression: Apache-2.0
|
8
8
|
Requires-Python: <4.0,>=3.11
|
9
|
+
Requires-Dist: cachetools>=5.5.2
|
9
10
|
Requires-Dist: fastapi[standard]>=0.115.8
|
10
11
|
Requires-Dist: httpx-sse>=0.4.0
|
11
|
-
Requires-Dist: httpx>=0.
|
12
|
+
Requires-Dist: httpx>=0.26.0
|
12
13
|
Requires-Dist: janus>=2.0.0
|
13
14
|
Requires-Dist: opentelemetry-api>=1.31.1
|
14
15
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.31.1
|
@@ -28,11 +29,12 @@ Agent Communication Protocol SDK for Python provides allows developers to serve
|
|
28
29
|
|
29
30
|
## Installation
|
30
31
|
|
31
|
-
Install
|
32
|
+
Install according to your Python package manager:
|
32
33
|
|
33
|
-
|
34
|
-
pip install acp-sdk
|
35
|
-
|
34
|
+
- `uv add acp-sdk`
|
35
|
+
- `pip install acp-sdk`
|
36
|
+
- `poetry add acp-sdk`
|
37
|
+
- ...
|
36
38
|
|
37
39
|
## Quickstart
|
38
40
|
|
@@ -3,23 +3,23 @@ acp_sdk/instrumentation.py,sha256=JqSyvILN3sGAfOZrmckQq4-M_4_5alyPn95DK0o5lfA,16
|
|
3
3
|
acp_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
4
|
acp_sdk/version.py,sha256=Niy83rgvigB4hL_rR-O4ySvI7dj6xnqkyOe_JTymi9s,73
|
5
5
|
acp_sdk/client/__init__.py,sha256=Bca1DORrswxzZsrR2aUFpATuNG2xNSmYvF1Z2WJaVbc,51
|
6
|
-
acp_sdk/client/client.py,sha256=
|
6
|
+
acp_sdk/client/client.py,sha256=CtH8dBEEKLjhPqCvxhEOPGnmNMMYsG22AH0OC5pWbpE,9310
|
7
7
|
acp_sdk/models/__init__.py,sha256=numSDBDT1QHx7n_Y3Deb5VOvKWcUBxbOEaMwQBSRHxc,151
|
8
8
|
acp_sdk/models/errors.py,sha256=rEyaMVvQuBi7fwWe_d0PGGySYsD3FZTluQ-SkC0yhAs,444
|
9
9
|
acp_sdk/models/models.py,sha256=eOoOfftPKbTaLlqZlE4gE1ZwDkrQ_FdRiDx4wSJILP4,6539
|
10
10
|
acp_sdk/models/schemas.py,sha256=Kj7drJSR8d-N3KHzu_qTnLdagrMtAyhid5swluuhHTw,645
|
11
11
|
acp_sdk/server/__init__.py,sha256=mxBBBFaZuMEUENRMLwp1XZkuLeT9QghcFmNvjnqvAAU,377
|
12
12
|
acp_sdk/server/agent.py,sha256=DhcPDPDL9jpDST40K_bInvDXfpF1cwxIhqXzu8z0blU,6203
|
13
|
-
acp_sdk/server/app.py,sha256=
|
14
|
-
acp_sdk/server/bundle.py,sha256=
|
13
|
+
acp_sdk/server/app.py,sha256=YcW5kEwmRER9LKRsLt2vNqFso4_nnbWabdjiN-4_0-0,6601
|
14
|
+
acp_sdk/server/bundle.py,sha256=nLxQMUCSVzlmEgSUYRunnpD3jO2yE2w1XVvKW6NOTPE,6564
|
15
15
|
acp_sdk/server/context.py,sha256=MgnLV6qcDIhc_0BjW7r4Jj1tHts4ZuwpdTGIBnz2Mgo,1036
|
16
16
|
acp_sdk/server/errors.py,sha256=GSO8yYIqEeX8Y4Lz86ks35dMTHiQiXuOrLYYx0eXsbI,2110
|
17
17
|
acp_sdk/server/logging.py,sha256=Oc8yZigCsuDnHHPsarRzu0RX3NKaLEgpELM2yovGKDI,411
|
18
|
-
acp_sdk/server/server.py,sha256
|
18
|
+
acp_sdk/server/server.py,sha256=6mIVyURgJvcZCPad1CV8TcgHC2mqyEFCGgGATGyP34Q,5548
|
19
19
|
acp_sdk/server/session.py,sha256=0cDr924HC5x2bBNbK9NSKVHAt5A_mi5dK8P4jP_ugq0,629
|
20
20
|
acp_sdk/server/telemetry.py,sha256=1BUxNg-xL_Vqgs27PDWNc3HikrQW2lidAtT_FKlp_Qk,1833
|
21
|
-
acp_sdk/server/types.py,sha256=
|
21
|
+
acp_sdk/server/types.py,sha256=teBNRWSks8XP1SCQKGEtbNWQahVD3RAOPnysTxcQPxI,292
|
22
22
|
acp_sdk/server/utils.py,sha256=EfrF9VCyVk3AM_ao-BIB9EzGbfTrh4V2Bz-VFr6f6Sg,351
|
23
|
-
acp_sdk-0.
|
24
|
-
acp_sdk-0.
|
25
|
-
acp_sdk-0.
|
23
|
+
acp_sdk-0.6.0.dist-info/METADATA,sha256=K2qABevuRD1ydsvoqniFrrNVu_abbf5I3IQDS7TuAUM,1654
|
24
|
+
acp_sdk-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
25
|
+
acp_sdk-0.6.0.dist-info/RECORD,,
|
File without changes
|