acp-sdk 0.4.0__tar.gz → 0.5.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.4.0 → acp_sdk-0.5.0}/PKG-INFO +8 -6
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/README.md +5 -4
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/pyproject.toml +6 -2
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/client/client.py +43 -15
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/server/app.py +5 -3
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/server/bundle.py +16 -8
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/server/server.py +4 -1
- acp_sdk-0.5.0/src/acp_sdk/server/types.py +9 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/tests/e2e/fixtures/server.py +5 -3
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/tests/e2e/test_suites/test_runs.py +47 -15
- acp_sdk-0.5.0/tests/unit/client/test_client.py +36 -0
- acp_sdk-0.4.0/src/acp_sdk/server/types.py +0 -6
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/.gitignore +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/.python-version +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/docs/_sidebar.md +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/docs/client.md +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/docs/index.html +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/docs/models.md +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/docs/server.md +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/pytest.ini +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/__init__.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/client/__init__.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/instrumentation.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/models/__init__.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/models/errors.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/models/models.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/models/schemas.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/py.typed +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/server/__init__.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/server/agent.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/server/context.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/server/errors.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/server/logging.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/server/session.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/server/telemetry.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/server/utils.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/src/acp_sdk/version.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/tests/conftest.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/tests/e2e/__init__.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/tests/e2e/config.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/tests/e2e/fixtures/__init__.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/tests/e2e/fixtures/client.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/tests/e2e/test_suites/__init__.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/tests/unit/models/__init__.py +0 -0
- {acp_sdk-0.4.0 → acp_sdk-0.5.0}/tests/unit/models/test_models.py +0 -0
@@ -1,14 +1,15 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: acp-sdk
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.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
|
|
@@ -8,11 +8,12 @@ Agent Communication Protocol SDK for Python provides allows developers to serve
|
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
11
|
-
Install
|
11
|
+
Install according to your Python package manager:
|
12
12
|
|
13
|
-
|
14
|
-
pip install acp-sdk
|
15
|
-
|
13
|
+
- `uv add acp-sdk`
|
14
|
+
- `pip install acp-sdk`
|
15
|
+
- `poetry add acp-sdk`
|
16
|
+
- ...
|
16
17
|
|
17
18
|
## Quickstart
|
18
19
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "acp-sdk"
|
3
|
-
version = "0.
|
3
|
+
version = "0.5.0"
|
4
4
|
description = "Agent Communication Protocol SDK"
|
5
5
|
license = "Apache-2.0"
|
6
6
|
readme = "README.md"
|
@@ -10,7 +10,7 @@ requires-python = ">=3.11, <4.0"
|
|
10
10
|
dependencies = [
|
11
11
|
"opentelemetry-api>=1.31.1",
|
12
12
|
"pydantic>=2.11.1",
|
13
|
-
"httpx>=0.
|
13
|
+
"httpx>=0.26.0",
|
14
14
|
"httpx-sse>=0.4.0",
|
15
15
|
"opentelemetry-instrumentation-httpx>=0.52b1",
|
16
16
|
"fastapi[standard]>=0.115.8",
|
@@ -18,8 +18,12 @@ dependencies = [
|
|
18
18
|
"opentelemetry-instrumentation-fastapi>=0.52b1",
|
19
19
|
"opentelemetry-sdk>=1.31.1",
|
20
20
|
"janus>=2.0.0",
|
21
|
+
"cachetools>=5.5.2",
|
21
22
|
]
|
22
23
|
|
23
24
|
[build-system]
|
24
25
|
requires = ["hatchling"]
|
25
26
|
build-backend = "hatchling.build"
|
27
|
+
|
28
|
+
[dependency-groups]
|
29
|
+
dev = ["pytest-httpx>=0.35.0"]
|
@@ -31,6 +31,9 @@ from acp_sdk.models import (
|
|
31
31
|
RunResumeResponse,
|
32
32
|
SessionId,
|
33
33
|
)
|
34
|
+
from acp_sdk.models.models import MessagePart
|
35
|
+
|
36
|
+
Input = list[Message] | Message | list[MessagePart] | MessagePart | list[str] | str
|
34
37
|
|
35
38
|
|
36
39
|
class Client:
|
@@ -79,14 +82,15 @@ class Client:
|
|
79
82
|
async def agent(self, *, name: AgentName) -> Agent:
|
80
83
|
response = await self._client.get(f"/agents/{name}")
|
81
84
|
self._raise_error(response)
|
82
|
-
|
85
|
+
response = AgentReadResponse.model_validate(response.json())
|
86
|
+
return Agent(**response.model_dump())
|
83
87
|
|
84
|
-
async def run_sync(self, *, agent: AgentName
|
88
|
+
async def run_sync(self, input: Input, *, agent: AgentName) -> Run:
|
85
89
|
response = await self._client.post(
|
86
90
|
"/runs",
|
87
91
|
content=RunCreateRequest(
|
88
92
|
agent_name=agent,
|
89
|
-
inputs=
|
93
|
+
inputs=self._unify_inputs(input),
|
90
94
|
mode=RunMode.SYNC,
|
91
95
|
session_id=self._session_id,
|
92
96
|
).model_dump_json(),
|
@@ -94,14 +98,14 @@ class Client:
|
|
94
98
|
self._raise_error(response)
|
95
99
|
response = RunCreateResponse.model_validate(response.json())
|
96
100
|
self._set_session(response)
|
97
|
-
return response
|
101
|
+
return Run(**response.model_dump())
|
98
102
|
|
99
|
-
async def run_async(self, *, agent: AgentName
|
103
|
+
async def run_async(self, input: Input, *, agent: AgentName) -> Run:
|
100
104
|
response = await self._client.post(
|
101
105
|
"/runs",
|
102
106
|
content=RunCreateRequest(
|
103
107
|
agent_name=agent,
|
104
|
-
inputs=
|
108
|
+
inputs=self._unify_inputs(input),
|
105
109
|
mode=RunMode.ASYNC,
|
106
110
|
session_id=self._session_id,
|
107
111
|
).model_dump_json(),
|
@@ -109,16 +113,16 @@ class Client:
|
|
109
113
|
self._raise_error(response)
|
110
114
|
response = RunCreateResponse.model_validate(response.json())
|
111
115
|
self._set_session(response)
|
112
|
-
return response
|
116
|
+
return Run(**response.model_dump())
|
113
117
|
|
114
|
-
async def run_stream(self, *, agent: AgentName
|
118
|
+
async def run_stream(self, input: Input, *, agent: AgentName) -> AsyncIterator[Event]:
|
115
119
|
async with aconnect_sse(
|
116
120
|
self._client,
|
117
121
|
"POST",
|
118
122
|
"/runs",
|
119
123
|
content=RunCreateRequest(
|
120
124
|
agent_name=agent,
|
121
|
-
inputs=
|
125
|
+
inputs=self._unify_inputs(input),
|
122
126
|
mode=RunMode.STREAM,
|
123
127
|
session_id=self._session_id,
|
124
128
|
).model_dump_json(),
|
@@ -136,25 +140,28 @@ class Client:
|
|
136
140
|
async def run_cancel(self, *, run_id: RunId) -> Run:
|
137
141
|
response = await self._client.post(f"/runs/{run_id}/cancel")
|
138
142
|
self._raise_error(response)
|
139
|
-
|
143
|
+
response = RunCancelResponse.model_validate(response.json())
|
144
|
+
return Run(**response.model_dump())
|
140
145
|
|
141
|
-
async def run_resume_sync(self, *, run_id: RunId
|
146
|
+
async def run_resume_sync(self, await_resume: AwaitResume, *, run_id: RunId) -> Run:
|
142
147
|
response = await self._client.post(
|
143
148
|
f"/runs/{run_id}",
|
144
149
|
content=RunResumeRequest(await_resume=await_resume, mode=RunMode.SYNC).model_dump_json(),
|
145
150
|
)
|
146
151
|
self._raise_error(response)
|
147
|
-
|
152
|
+
response = RunResumeResponse.model_validate(response.json())
|
153
|
+
return Run(**response.model_dump())
|
148
154
|
|
149
|
-
async def run_resume_async(self, *, run_id: RunId
|
155
|
+
async def run_resume_async(self, await_resume: AwaitResume, *, run_id: RunId) -> Run:
|
150
156
|
response = await self._client.post(
|
151
157
|
f"/runs/{run_id}",
|
152
158
|
content=RunResumeRequest(await_resume=await_resume, mode=RunMode.ASYNC).model_dump_json(),
|
153
159
|
)
|
154
160
|
self._raise_error(response)
|
155
|
-
|
161
|
+
response = RunResumeResponse.model_validate(response.json())
|
162
|
+
return Run(**response.model_dump())
|
156
163
|
|
157
|
-
async def run_resume_stream(self, *, run_id: RunId
|
164
|
+
async def run_resume_stream(self, await_resume: AwaitResume, *, run_id: RunId) -> AsyncIterator[Event]:
|
158
165
|
async with aconnect_sse(
|
159
166
|
self._client,
|
160
167
|
"POST",
|
@@ -183,3 +190,24 @@ class Client:
|
|
183
190
|
|
184
191
|
def _set_session(self, run: Run) -> None:
|
185
192
|
self._session_id = run.session_id
|
193
|
+
|
194
|
+
def _unify_inputs(self, input: Input) -> list[Message]:
|
195
|
+
if isinstance(input, list):
|
196
|
+
if len(input) == 0:
|
197
|
+
return []
|
198
|
+
if all(isinstance(item, Message) for item in input):
|
199
|
+
return input
|
200
|
+
elif all(isinstance(item, MessagePart) for item in input):
|
201
|
+
return [Message(parts=input)]
|
202
|
+
elif all(isinstance(item, str) for item in input):
|
203
|
+
return [Message(parts=[MessagePart(content=content) for content in input])]
|
204
|
+
else:
|
205
|
+
raise RuntimeError("List with mixed types is not supported")
|
206
|
+
else:
|
207
|
+
if isinstance(input, str):
|
208
|
+
input = MessagePart(content=input)
|
209
|
+
if isinstance(input, MessagePart):
|
210
|
+
input = Message(parts=[input])
|
211
|
+
if isinstance(input, Message):
|
212
|
+
input = [input]
|
213
|
+
return input
|
@@ -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)
|
@@ -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")
|
@@ -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,
|
@@ -0,0 +1,9 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
from acp_sdk.models import AwaitRequest, AwaitResume, Message
|
6
|
+
from acp_sdk.models.models import MessagePart
|
7
|
+
|
8
|
+
RunYield = Message | MessagePart | str | AwaitRequest | BaseModel | dict[str | Any] | None
|
9
|
+
RunYieldResume = AwaitResume | None
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import base64
|
2
2
|
import time
|
3
3
|
from collections.abc import AsyncGenerator, AsyncIterator, Generator
|
4
|
+
from datetime import timedelta
|
4
5
|
from threading import Thread
|
5
6
|
|
6
7
|
import pytest
|
@@ -10,8 +11,9 @@ from acp_sdk.server import Context, Server
|
|
10
11
|
from e2e.config import Config
|
11
12
|
|
12
13
|
|
13
|
-
@pytest.fixture(scope="module")
|
14
|
-
def server() -> Generator[None]:
|
14
|
+
@pytest.fixture(scope="module", params=[timedelta(minutes=1)])
|
15
|
+
def server(request: pytest.FixtureRequest) -> Generator[None]:
|
16
|
+
ttl = request.param
|
15
17
|
server = Server()
|
16
18
|
|
17
19
|
@server.agent()
|
@@ -74,7 +76,7 @@ def server() -> Generator[None]:
|
|
74
76
|
content_encoding="base64",
|
75
77
|
)
|
76
78
|
|
77
|
-
thread = Thread(target=server.run, kwargs={"port": Config.PORT}, daemon=True)
|
79
|
+
thread = Thread(target=server.run, kwargs={"run_ttl": ttl, "port": Config.PORT}, daemon=True)
|
78
80
|
thread.start()
|
79
81
|
|
80
82
|
time.sleep(1)
|
@@ -1,4 +1,6 @@
|
|
1
|
+
import asyncio
|
1
2
|
import base64
|
3
|
+
from datetime import timedelta
|
2
4
|
|
3
5
|
import pytest
|
4
6
|
from acp_sdk.client import Client
|
@@ -14,6 +16,7 @@ from acp_sdk.models import (
|
|
14
16
|
RunInProgressEvent,
|
15
17
|
RunStatus,
|
16
18
|
)
|
19
|
+
from acp_sdk.models.errors import ACPError
|
17
20
|
from acp_sdk.server import Server
|
18
21
|
|
19
22
|
inputs = [Message(parts=[MessagePart(content="Hello!")])]
|
@@ -22,27 +25,27 @@ await_resume = MessageAwaitResume(message=Message(parts=[]))
|
|
22
25
|
|
23
26
|
@pytest.mark.asyncio
|
24
27
|
async def test_run_sync(server: Server, client: Client) -> None:
|
25
|
-
run = await client.run_sync(agent="echo",
|
28
|
+
run = await client.run_sync(agent="echo", input=inputs)
|
26
29
|
assert run.status == RunStatus.COMPLETED
|
27
30
|
assert run.outputs == inputs
|
28
31
|
|
29
32
|
|
30
33
|
@pytest.mark.asyncio
|
31
34
|
async def test_run_async(server: Server, client: Client) -> None:
|
32
|
-
run = await client.run_async(agent="echo",
|
35
|
+
run = await client.run_async(agent="echo", input=inputs)
|
33
36
|
assert run.status == RunStatus.CREATED
|
34
37
|
|
35
38
|
|
36
39
|
@pytest.mark.asyncio
|
37
40
|
async def test_run_stream(server: Server, client: Client) -> None:
|
38
|
-
event_stream = [event async for event in client.run_stream(agent="echo",
|
41
|
+
event_stream = [event async for event in client.run_stream(agent="echo", input=inputs)]
|
39
42
|
assert isinstance(event_stream[0], RunCreatedEvent)
|
40
43
|
assert isinstance(event_stream[-1], RunCompletedEvent)
|
41
44
|
|
42
45
|
|
43
46
|
@pytest.mark.asyncio
|
44
47
|
async def test_run_status(server: Server, client: Client) -> None:
|
45
|
-
run = await client.run_async(agent="echo",
|
48
|
+
run = await client.run_async(agent="echo", input=inputs)
|
46
49
|
while run.status in (RunStatus.CREATED, RunStatus.IN_PROGRESS):
|
47
50
|
run = await client.run_status(run_id=run.run_id)
|
48
51
|
assert run.status == RunStatus.COMPLETED
|
@@ -50,7 +53,7 @@ async def test_run_status(server: Server, client: Client) -> None:
|
|
50
53
|
|
51
54
|
@pytest.mark.asyncio
|
52
55
|
async def test_failure(server: Server, client: Client) -> None:
|
53
|
-
run = await client.run_sync(agent="failer",
|
56
|
+
run = await client.run_sync(agent="failer", input=inputs)
|
54
57
|
assert run.status == RunStatus.FAILED
|
55
58
|
assert run.error is not None
|
56
59
|
assert run.error.code == ErrorCode.INVALID_INPUT
|
@@ -58,7 +61,7 @@ async def test_failure(server: Server, client: Client) -> None:
|
|
58
61
|
|
59
62
|
@pytest.mark.asyncio
|
60
63
|
async def test_run_cancel(server: Server, client: Client) -> None:
|
61
|
-
run = await client.run_sync(agent="awaiter",
|
64
|
+
run = await client.run_sync(agent="awaiter", input=inputs)
|
62
65
|
assert run.status == RunStatus.AWAITING
|
63
66
|
run = await client.run_cancel(run_id=run.run_id)
|
64
67
|
assert run.status == RunStatus.CANCELLING
|
@@ -66,7 +69,7 @@ async def test_run_cancel(server: Server, client: Client) -> None:
|
|
66
69
|
|
67
70
|
@pytest.mark.asyncio
|
68
71
|
async def test_run_resume_sync(server: Server, client: Client) -> None:
|
69
|
-
run = await client.run_sync(agent="awaiter",
|
72
|
+
run = await client.run_sync(agent="awaiter", input=inputs)
|
70
73
|
assert run.status == RunStatus.AWAITING
|
71
74
|
assert run.await_request is not None
|
72
75
|
|
@@ -76,7 +79,7 @@ async def test_run_resume_sync(server: Server, client: Client) -> None:
|
|
76
79
|
|
77
80
|
@pytest.mark.asyncio
|
78
81
|
async def test_run_resume_async(server: Server, client: Client) -> None:
|
79
|
-
run = await client.run_sync(agent="awaiter",
|
82
|
+
run = await client.run_sync(agent="awaiter", input=inputs)
|
80
83
|
assert run.status == RunStatus.AWAITING
|
81
84
|
assert run.await_request is not None
|
82
85
|
|
@@ -86,7 +89,7 @@ async def test_run_resume_async(server: Server, client: Client) -> None:
|
|
86
89
|
|
87
90
|
@pytest.mark.asyncio
|
88
91
|
async def test_run_resume_stream(server: Server, client: Client) -> None:
|
89
|
-
run = await client.run_sync(agent="awaiter",
|
92
|
+
run = await client.run_sync(agent="awaiter", input=inputs)
|
90
93
|
assert run.status == RunStatus.AWAITING
|
91
94
|
assert run.await_request is not None
|
92
95
|
|
@@ -98,15 +101,15 @@ async def test_run_resume_stream(server: Server, client: Client) -> None:
|
|
98
101
|
@pytest.mark.asyncio
|
99
102
|
async def test_run_session(server: Server, client: Client) -> None:
|
100
103
|
async with client.session() as session:
|
101
|
-
run = await session.run_sync(agent="echo",
|
104
|
+
run = await session.run_sync(agent="echo", input=inputs)
|
102
105
|
assert run.outputs == inputs
|
103
|
-
run = await session.run_sync(agent="echo",
|
106
|
+
run = await session.run_sync(agent="echo", input=inputs)
|
104
107
|
assert run.outputs == inputs + inputs + inputs
|
105
108
|
|
106
109
|
|
107
110
|
@pytest.mark.asyncio
|
108
111
|
async def test_mime_types(server: Server, client: Client) -> None:
|
109
|
-
run = await client.run_sync(agent="mime_types",
|
112
|
+
run = await client.run_sync(agent="mime_types", input=inputs)
|
110
113
|
assert run.status == RunStatus.COMPLETED
|
111
114
|
assert len(run.outputs) == 1
|
112
115
|
|
@@ -127,7 +130,7 @@ async def test_mime_types(server: Server, client: Client) -> None:
|
|
127
130
|
|
128
131
|
@pytest.mark.asyncio
|
129
132
|
async def test_base64_encoding(server: Server, client: Client) -> None:
|
130
|
-
run = await client.run_sync(agent="base64_encoding",
|
133
|
+
run = await client.run_sync(agent="base64_encoding", input=inputs)
|
131
134
|
assert run.status == RunStatus.COMPLETED
|
132
135
|
assert len(run.outputs) == 1
|
133
136
|
|
@@ -147,7 +150,7 @@ async def test_base64_encoding(server: Server, client: Client) -> None:
|
|
147
150
|
|
148
151
|
@pytest.mark.asyncio
|
149
152
|
async def test_artifacts(server: Server, client: Client) -> None:
|
150
|
-
run = await client.run_sync(agent="artifact_producer",
|
153
|
+
run = await client.run_sync(agent="artifact_producer", input=inputs)
|
151
154
|
assert run.status == RunStatus.COMPLETED
|
152
155
|
|
153
156
|
assert len(run.outputs) == 1
|
@@ -176,7 +179,7 @@ async def test_artifacts(server: Server, client: Client) -> None:
|
|
176
179
|
|
177
180
|
@pytest.mark.asyncio
|
178
181
|
async def test_artifact_streaming(server: Server, client: Client) -> None:
|
179
|
-
events = [event async for event in client.run_stream(agent="artifact_producer",
|
182
|
+
events = [event async for event in client.run_stream(agent="artifact_producer", input=inputs)]
|
180
183
|
|
181
184
|
assert isinstance(events[0], RunCreatedEvent)
|
182
185
|
assert isinstance(events[-1], RunCompletedEvent)
|
@@ -193,3 +196,32 @@ async def test_artifact_streaming(server: Server, client: Client) -> None:
|
|
193
196
|
assert "text/plain" in artifact_types
|
194
197
|
assert "application/json" in artifact_types
|
195
198
|
assert "image/png" in artifact_types
|
199
|
+
|
200
|
+
|
201
|
+
@pytest.mark.asyncio
|
202
|
+
@pytest.mark.parametrize("server", [timedelta(seconds=5)], indirect=True)
|
203
|
+
async def test_run_ttl(server: Server, client: Client) -> None:
|
204
|
+
run = await client.run_async(agent="echo", input=inputs)
|
205
|
+
run = await client.run_status(run_id=run.run_id)
|
206
|
+
await asyncio.sleep(6)
|
207
|
+
try:
|
208
|
+
run = await client.run_status(run_id=run.run_id)
|
209
|
+
raise AssertionError("Error expected")
|
210
|
+
except ACPError as e:
|
211
|
+
if e.error.code == ErrorCode.NOT_FOUND:
|
212
|
+
assert True
|
213
|
+
else:
|
214
|
+
raise AssertionError(f"Unexpected error code {e.error.code}")
|
215
|
+
|
216
|
+
|
217
|
+
@pytest.mark.asyncio
|
218
|
+
@pytest.mark.parametrize("server", [timedelta(seconds=5)], indirect=True)
|
219
|
+
async def test_session_ttl(server: Server, client: Client) -> None:
|
220
|
+
async with client.session() as session:
|
221
|
+
run = await session.run_sync(agent="echo", input=inputs)
|
222
|
+
await asyncio.sleep(3)
|
223
|
+
run = await session.run_sync(agent="echo", input=inputs)
|
224
|
+
assert len(run.outputs) == 3
|
225
|
+
await asyncio.sleep(3)
|
226
|
+
run = await session.run_sync(agent="echo", input=inputs)
|
227
|
+
assert len(run.outputs) == 7 # First run shall be forgotten
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import pytest
|
2
|
+
from acp_sdk.client import Client
|
3
|
+
from acp_sdk.models import Message, MessagePart, Run, RunCompletedEvent
|
4
|
+
from pytest_httpx import HTTPXMock
|
5
|
+
|
6
|
+
mock_run = Run(agent_name="mock", outputs=[Message(parts=[MessagePart(content="Hello!")])])
|
7
|
+
|
8
|
+
|
9
|
+
@pytest.mark.asyncio
|
10
|
+
async def test_run_sync(httpx_mock: HTTPXMock) -> None:
|
11
|
+
httpx_mock.add_response(content=mock_run.model_dump_json())
|
12
|
+
|
13
|
+
async with Client(base_url="http://localhost:8000") as client:
|
14
|
+
run = await client.run_sync("Howdy!", agent="mock")
|
15
|
+
assert run == mock_run
|
16
|
+
|
17
|
+
|
18
|
+
@pytest.mark.asyncio
|
19
|
+
async def test_run_async(httpx_mock: HTTPXMock) -> None:
|
20
|
+
httpx_mock.add_response(content=mock_run.model_dump_json())
|
21
|
+
|
22
|
+
async with Client(base_url="http://localhost:8000") as client:
|
23
|
+
run = await client.run_async("Howdy!", agent="mock")
|
24
|
+
assert run == mock_run
|
25
|
+
|
26
|
+
|
27
|
+
@pytest.mark.asyncio
|
28
|
+
async def test_run_stream(httpx_mock: HTTPXMock) -> None:
|
29
|
+
mock_event = RunCompletedEvent(run=mock_run)
|
30
|
+
httpx_mock.add_response(
|
31
|
+
headers={"content-type": "text/event-stream"}, content=f"data: {mock_event.model_dump_json()}\n\n"
|
32
|
+
)
|
33
|
+
|
34
|
+
async with Client(base_url="http://localhost:8000") as client:
|
35
|
+
async for event in client.run_stream("Howdy!", agent="mock"):
|
36
|
+
assert event == mock_event
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|