acp-sdk 0.7.1__tar.gz → 0.7.3__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.7.1 → acp_sdk-0.7.3}/PKG-INFO +1 -1
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/docs/client.md +4 -4
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/pyproject.toml +1 -1
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/client/client.py +5 -8
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/models/models.py +8 -2
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/server/app.py +16 -4
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/server/server.py +54 -1
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/server/session.py +2 -2
- acp_sdk-0.7.3/src/acp_sdk/server/utils.py +49 -0
- acp_sdk-0.7.3/tests/unit/client/test_client.py +159 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/unit/models/test_models.py +19 -0
- acp_sdk-0.7.1/src/acp_sdk/server/utils.py +0 -14
- acp_sdk-0.7.1/tests/unit/client/test_client.py +0 -36
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/.gitignore +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/.python-version +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/README.md +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/docs/_sidebar.md +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/docs/index.html +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/docs/models.md +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/docs/server.md +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/pytest.ini +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/client/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/client/types.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/client/utils.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/instrumentation.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/models/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/models/errors.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/models/schemas.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/py.typed +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/server/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/server/agent.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/server/bundle.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/server/context.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/server/errors.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/server/logging.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/server/telemetry.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/server/types.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/src/acp_sdk/version.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/conftest.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/e2e/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/e2e/config.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/e2e/fixtures/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/e2e/fixtures/client.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/e2e/fixtures/server.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/e2e/test_suites/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/e2e/test_suites/test_discovery.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/e2e/test_suites/test_runs.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/unit/client/test_utils.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.3}/tests/unit/models/__init__.py +0 -0
@@ -71,15 +71,15 @@ async with Client(base_url="http://localhost:8000") as client:
|
|
71
71
|
message = Message(parts=[MessagePart(content="Hello")])
|
72
72
|
|
73
73
|
# Async
|
74
|
-
run = await client.run_async(
|
74
|
+
run = await client.run_async(agent="agent", input=[message])
|
75
75
|
print(run.status)
|
76
76
|
|
77
77
|
# Sync - waits for completion, failure, cancellation or await
|
78
|
-
run = await client.run_sync(
|
78
|
+
run = await client.run_sync(agent="agent", input=[message])
|
79
79
|
print(run.output)
|
80
80
|
|
81
81
|
# Stream - as sync but also receives events
|
82
|
-
async for event in client.run_stream(
|
82
|
+
async for event in client.run_stream(agent="agent", input=[message])
|
83
83
|
print(event)
|
84
84
|
```
|
85
85
|
|
@@ -95,5 +95,5 @@ async with Client(base_url="http://localhost:8000" as client:
|
|
95
95
|
|
96
96
|
async with client.session() as session:
|
97
97
|
for agent in agents:
|
98
|
-
await session.run_sync(
|
98
|
+
await session.run_sync(agent=agent.name, input=[Message(parts=[MessagePart(content="Hello!")])])
|
99
99
|
```
|
@@ -25,7 +25,6 @@ from acp_sdk.models import (
|
|
25
25
|
Event,
|
26
26
|
Run,
|
27
27
|
RunCancelResponse,
|
28
|
-
RunCreatedEvent,
|
29
28
|
RunCreateRequest,
|
30
29
|
RunCreateResponse,
|
31
30
|
RunId,
|
@@ -120,6 +119,11 @@ class Client:
|
|
120
119
|
response = AgentReadResponse.model_validate(response.json())
|
121
120
|
return Agent(**response.model_dump())
|
122
121
|
|
122
|
+
async def ping(self) -> bool:
|
123
|
+
response = await self._client.get("/healthcheck")
|
124
|
+
self._raise_error(response)
|
125
|
+
return response.json() == "OK"
|
126
|
+
|
123
127
|
async def run_sync(self, input: Input, *, agent: AgentName) -> Run:
|
124
128
|
response = await self._client.post(
|
125
129
|
"/runs",
|
@@ -132,7 +136,6 @@ class Client:
|
|
132
136
|
)
|
133
137
|
self._raise_error(response)
|
134
138
|
response = RunCreateResponse.model_validate(response.json())
|
135
|
-
self._set_session(response)
|
136
139
|
return Run(**response.model_dump())
|
137
140
|
|
138
141
|
async def run_async(self, input: Input, *, agent: AgentName) -> Run:
|
@@ -147,7 +150,6 @@ class Client:
|
|
147
150
|
)
|
148
151
|
self._raise_error(response)
|
149
152
|
response = RunCreateResponse.model_validate(response.json())
|
150
|
-
self._set_session(response)
|
151
153
|
return Run(**response.model_dump())
|
152
154
|
|
153
155
|
async def run_stream(self, input: Input, *, agent: AgentName) -> AsyncIterator[Event]:
|
@@ -163,8 +165,6 @@ class Client:
|
|
163
165
|
).model_dump_json(),
|
164
166
|
) as event_source:
|
165
167
|
async for event in self._validate_stream(event_source):
|
166
|
-
if isinstance(event, RunCreatedEvent):
|
167
|
-
self._set_session(event.run)
|
168
168
|
yield event
|
169
169
|
|
170
170
|
async def run_status(self, *, run_id: RunId) -> Run:
|
@@ -222,6 +222,3 @@ class Client:
|
|
222
222
|
response.raise_for_status()
|
223
223
|
except httpx.HTTPError:
|
224
224
|
raise ACPError(Error.model_validate(response.json()))
|
225
|
-
|
226
|
-
def _set_session(self, run: Run) -> None:
|
227
|
-
self._session_id = run.session_id
|
@@ -47,6 +47,11 @@ class Dependency(BaseModel):
|
|
47
47
|
name: str
|
48
48
|
|
49
49
|
|
50
|
+
class Capability(BaseModel):
|
51
|
+
name: str
|
52
|
+
description: str
|
53
|
+
|
54
|
+
|
50
55
|
class Metadata(BaseModel):
|
51
56
|
annotations: AnyModel | None = None
|
52
57
|
documentation: str | None = None
|
@@ -54,7 +59,8 @@ class Metadata(BaseModel):
|
|
54
59
|
programming_language: str | None = None
|
55
60
|
natural_languages: list[str] | None = None
|
56
61
|
framework: str | None = None
|
57
|
-
|
62
|
+
capabilities: list[Capability] | None = None
|
63
|
+
domains: list[str] | None = None
|
58
64
|
tags: list[str] | None = None
|
59
65
|
created_at: datetime | None = None
|
60
66
|
updated_at: datetime | None = None
|
@@ -93,7 +99,7 @@ class Message(BaseModel):
|
|
93
99
|
def __add__(self, other: "Message") -> "Message":
|
94
100
|
if not isinstance(other, Message):
|
95
101
|
raise TypeError(f"Cannot concatenate Message with {type(other).__name__}")
|
96
|
-
return Message(
|
102
|
+
return Message(parts=self.parts + other.parts)
|
97
103
|
|
98
104
|
def __str__(self) -> str:
|
99
105
|
return "".join(
|
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta
|
|
5
5
|
from enum import Enum
|
6
6
|
|
7
7
|
from cachetools import TTLCache
|
8
|
-
from fastapi import FastAPI, HTTPException, status
|
8
|
+
from fastapi import Depends, FastAPI, HTTPException, status
|
9
9
|
from fastapi.encoders import jsonable_encoder
|
10
10
|
from fastapi.responses import JSONResponse, StreamingResponse
|
11
11
|
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
@@ -47,7 +47,12 @@ class Headers(str, Enum):
|
|
47
47
|
RUN_ID = "Run-ID"
|
48
48
|
|
49
49
|
|
50
|
-
def create_app(
|
50
|
+
def create_app(
|
51
|
+
*agents: Agent,
|
52
|
+
run_limit: int = 1000,
|
53
|
+
run_ttl: timedelta = timedelta(hours=1),
|
54
|
+
dependencies: list[Depends] | None = None,
|
55
|
+
) -> FastAPI:
|
51
56
|
executor: ThreadPoolExecutor
|
52
57
|
|
53
58
|
@asynccontextmanager
|
@@ -57,7 +62,10 @@ def create_app(*agents: Agent, run_limit: int = 1000, run_ttl: timedelta = timed
|
|
57
62
|
executor = exec
|
58
63
|
yield
|
59
64
|
|
60
|
-
app = FastAPI(
|
65
|
+
app = FastAPI(
|
66
|
+
lifespan=lifespan,
|
67
|
+
dependencies=dependencies,
|
68
|
+
)
|
61
69
|
|
62
70
|
FastAPIInstrumentor.instrument_app(app)
|
63
71
|
|
@@ -96,11 +104,15 @@ def create_app(*agents: Agent, run_limit: int = 1000, run_ttl: timedelta = timed
|
|
96
104
|
agent = find_agent(name)
|
97
105
|
return AgentModel(name=agent.name, description=agent.description, metadata=agent.metadata)
|
98
106
|
|
107
|
+
@app.get("/healthcheck")
|
108
|
+
async def healthcheck() -> str:
|
109
|
+
return "OK"
|
110
|
+
|
99
111
|
@app.post("/runs")
|
100
112
|
async def create_run(request: RunCreateRequest) -> RunCreateResponse:
|
101
113
|
agent = find_agent(request.agent_name)
|
102
114
|
|
103
|
-
session = sessions.get(request.session_id, Session()) if request.session_id else Session()
|
115
|
+
session = sessions.get(request.session_id, Session(id=request.session_id)) if request.session_id else Session()
|
104
116
|
nonlocal executor
|
105
117
|
bundle = RunBundle(
|
106
118
|
agent=agent,
|
@@ -4,6 +4,7 @@ from collections.abc import Awaitable
|
|
4
4
|
from datetime import timedelta
|
5
5
|
from typing import Any, Callable
|
6
6
|
|
7
|
+
import requests
|
7
8
|
import uvicorn
|
8
9
|
import uvicorn.config
|
9
10
|
|
@@ -12,7 +13,9 @@ from acp_sdk.server.agent import Agent
|
|
12
13
|
from acp_sdk.server.agent import agent as agent_decorator
|
13
14
|
from acp_sdk.server.app import create_app
|
14
15
|
from acp_sdk.server.logging import configure_logger as configure_logger_func
|
16
|
+
from acp_sdk.server.logging import logger
|
15
17
|
from acp_sdk.server.telemetry import configure_telemetry as configure_telemetry_func
|
18
|
+
from acp_sdk.server.utils import async_request_with_retry
|
16
19
|
|
17
20
|
|
18
21
|
class Server:
|
@@ -43,6 +46,7 @@ class Server:
|
|
43
46
|
self,
|
44
47
|
configure_logger: bool = True,
|
45
48
|
configure_telemetry: bool = False,
|
49
|
+
self_registration: bool = True,
|
46
50
|
run_limit: int = 1000,
|
47
51
|
run_ttl: timedelta = timedelta(hours=1),
|
48
52
|
host: str = "127.0.0.1",
|
@@ -158,7 +162,14 @@ class Server:
|
|
158
162
|
h11_max_incomplete_event_size,
|
159
163
|
)
|
160
164
|
self._server = uvicorn.Server(config)
|
161
|
-
|
165
|
+
|
166
|
+
asyncio.run(self._serve(self_registration=self_registration))
|
167
|
+
|
168
|
+
async def _serve(self, self_registration: bool = True) -> None:
|
169
|
+
registration_task = asyncio.create_task(self._register_agent()) if self_registration else None
|
170
|
+
await self._server.serve()
|
171
|
+
if registration_task:
|
172
|
+
registration_task.cancel()
|
162
173
|
|
163
174
|
@property
|
164
175
|
def should_exit(self) -> bool:
|
@@ -167,3 +178,45 @@ class Server:
|
|
167
178
|
@should_exit.setter
|
168
179
|
def should_exit(self, value: bool) -> None:
|
169
180
|
self._server.should_exit = value
|
181
|
+
|
182
|
+
async def _register_agent(self) -> None:
|
183
|
+
"""If not in PRODUCTION mode, register agent to the beeai platform and provide missing env variables"""
|
184
|
+
if os.getenv("PRODUCTION_MODE", False):
|
185
|
+
logger.debug("Agent is not automatically registered in the production mode.")
|
186
|
+
return
|
187
|
+
|
188
|
+
url = os.getenv("PLATFORM_URL", "http://127.0.0.1:8333")
|
189
|
+
for agent in self._agents:
|
190
|
+
request_data = {
|
191
|
+
"location": f"http://{self._server.config.host}:{self._server.config.port}",
|
192
|
+
"id": agent.name,
|
193
|
+
}
|
194
|
+
try:
|
195
|
+
await async_request_with_retry(
|
196
|
+
lambda client, data=request_data: client.post(
|
197
|
+
f"{url}/api/v1/provider/register/unmanaged", json=data
|
198
|
+
)
|
199
|
+
)
|
200
|
+
logger.info("Agent registered to the beeai server.")
|
201
|
+
|
202
|
+
# check missing env keyes
|
203
|
+
envs_request = await async_request_with_retry(lambda client: client.get(f"{url}/api/v1/env"))
|
204
|
+
envs = envs_request.get("env")
|
205
|
+
|
206
|
+
# register all available envs
|
207
|
+
missing_keyes = []
|
208
|
+
for env in agent.metadata.model_dump().get("env", []):
|
209
|
+
server_env = envs.get(env.get("name"))
|
210
|
+
if server_env:
|
211
|
+
logger.debug(f"Env variable {env['name']} = '{server_env}' added dynamically")
|
212
|
+
os.environ[env["name"]] = server_env
|
213
|
+
elif env.get("required"):
|
214
|
+
missing_keyes.append(env)
|
215
|
+
if len(missing_keyes):
|
216
|
+
logger.error(f"Can not run agent, missing required env variables: {missing_keyes}")
|
217
|
+
raise Exception("Missing env variables")
|
218
|
+
|
219
|
+
except requests.exceptions.ConnectionError as e:
|
220
|
+
logger.warning(f"Can not reach server, check if running on {url} : {e}")
|
221
|
+
except (requests.exceptions.HTTPError, Exception) as e:
|
222
|
+
logger.warning(f"Agent can not be registered to beeai server: {e}")
|
@@ -7,8 +7,8 @@ from acp_sdk.server.bundle import RunBundle
|
|
7
7
|
|
8
8
|
|
9
9
|
class Session:
|
10
|
-
def __init__(self) -> None:
|
11
|
-
self.id: SessionId = uuid.uuid4()
|
10
|
+
def __init__(self, id: SessionId | None = None) -> None:
|
11
|
+
self.id: SessionId = id or uuid.uuid4()
|
12
12
|
self.bundles: list[RunBundle] = []
|
13
13
|
|
14
14
|
def append(self, bundle: RunBundle) -> None:
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import asyncio
|
2
|
+
from collections.abc import AsyncGenerator, Coroutine
|
3
|
+
from typing import Any, Callable
|
4
|
+
|
5
|
+
import httpx
|
6
|
+
import requests
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
from acp_sdk.server.bundle import RunBundle
|
10
|
+
from acp_sdk.server.logging import logger
|
11
|
+
|
12
|
+
|
13
|
+
def encode_sse(model: BaseModel) -> str:
|
14
|
+
return f"data: {model.model_dump_json()}\n\n"
|
15
|
+
|
16
|
+
|
17
|
+
async def stream_sse(bundle: RunBundle) -> AsyncGenerator[str]:
|
18
|
+
async for event in bundle.stream():
|
19
|
+
yield encode_sse(event)
|
20
|
+
|
21
|
+
|
22
|
+
async def async_request_with_retry(
|
23
|
+
request_func: Callable[[httpx.AsyncClient], Coroutine[Any, Any, httpx.Response]],
|
24
|
+
max_retries: int = 5,
|
25
|
+
backoff_factor: float = 1,
|
26
|
+
) -> dict[str, Any]:
|
27
|
+
async with httpx.AsyncClient() as client:
|
28
|
+
retries = 0
|
29
|
+
while retries < max_retries:
|
30
|
+
try:
|
31
|
+
response = await request_func(client)
|
32
|
+
response.raise_for_status()
|
33
|
+
return response.json()
|
34
|
+
except httpx.HTTPStatusError as e:
|
35
|
+
if e.response.status_code in [429, 500, 502, 503, 504, 509]:
|
36
|
+
retries += 1
|
37
|
+
backoff = backoff_factor * (2 ** (retries - 1))
|
38
|
+
logger.warning(f"Request retry (try {retries}/{max_retries}), waiting {backoff} seconds...")
|
39
|
+
await asyncio.sleep(backoff)
|
40
|
+
else:
|
41
|
+
logger.debug("A non-retryable error was encountered.")
|
42
|
+
raise
|
43
|
+
except httpx.RequestError:
|
44
|
+
retries += 1
|
45
|
+
backoff = backoff_factor * (2 ** (retries - 1))
|
46
|
+
logger.warning(f"Request retry (try {retries}/{max_retries}), waiting {backoff} seconds...")
|
47
|
+
await asyncio.sleep(backoff)
|
48
|
+
|
49
|
+
raise requests.exceptions.ConnectionError(f"Request failed after {max_retries} retries.")
|
@@ -0,0 +1,159 @@
|
|
1
|
+
import json
|
2
|
+
import uuid
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
from acp_sdk.client import Client
|
6
|
+
from acp_sdk.models import Agent, AgentsListResponse, Message, MessagePart, Run, RunCompletedEvent
|
7
|
+
from acp_sdk.models.models import MessageAwaitResume
|
8
|
+
from pytest_httpx import HTTPXMock
|
9
|
+
|
10
|
+
mock_agent = Agent(name="mock")
|
11
|
+
mock_agents = [mock_agent]
|
12
|
+
mock_run = Run(
|
13
|
+
agent_name=mock_agent.name, session_id=uuid.uuid4(), output=[Message(parts=[MessagePart(content="Hello!")])]
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
@pytest.mark.asyncio
|
18
|
+
async def test_agents(httpx_mock: HTTPXMock) -> None:
|
19
|
+
httpx_mock.add_response(
|
20
|
+
url="http://test/agents", method="GET", content=AgentsListResponse(agents=mock_agents).model_dump_json()
|
21
|
+
)
|
22
|
+
|
23
|
+
async with Client(base_url="http://test") as client:
|
24
|
+
agents = [agent async for agent in client.agents()]
|
25
|
+
assert agents == mock_agents
|
26
|
+
|
27
|
+
|
28
|
+
@pytest.mark.asyncio
|
29
|
+
async def test_agent(httpx_mock: HTTPXMock) -> None:
|
30
|
+
httpx_mock.add_response(
|
31
|
+
url=f"http://test/agents/{mock_agent.name}", method="GET", content=mock_agent.model_dump_json()
|
32
|
+
)
|
33
|
+
|
34
|
+
async with Client(base_url="http://test") as client:
|
35
|
+
agent = await client.agent(name=mock_agent.name)
|
36
|
+
assert agent == mock_agent
|
37
|
+
|
38
|
+
|
39
|
+
@pytest.mark.asyncio
|
40
|
+
async def test_run_sync(httpx_mock: HTTPXMock) -> None:
|
41
|
+
httpx_mock.add_response(url="http://test/runs", method="POST", content=mock_run.model_dump_json())
|
42
|
+
|
43
|
+
async with Client(base_url="http://test") as client:
|
44
|
+
run = await client.run_sync("Howdy!", agent=mock_run.agent_name)
|
45
|
+
assert run == mock_run
|
46
|
+
|
47
|
+
|
48
|
+
@pytest.mark.asyncio
|
49
|
+
async def test_run_async(httpx_mock: HTTPXMock) -> None:
|
50
|
+
httpx_mock.add_response(url="http://test/runs", method="POST", content=mock_run.model_dump_json())
|
51
|
+
|
52
|
+
async with Client(base_url="http://test") as client:
|
53
|
+
run = await client.run_async("Howdy!", agent=mock_run.agent_name)
|
54
|
+
assert run == mock_run
|
55
|
+
|
56
|
+
|
57
|
+
@pytest.mark.asyncio
|
58
|
+
async def test_run_stream(httpx_mock: HTTPXMock) -> None:
|
59
|
+
mock_event = RunCompletedEvent(run=mock_run)
|
60
|
+
httpx_mock.add_response(
|
61
|
+
url="http://test/runs",
|
62
|
+
method="POST",
|
63
|
+
headers={"content-type": "text/event-stream"},
|
64
|
+
content=f"data: {mock_event.model_dump_json()}\n\n",
|
65
|
+
)
|
66
|
+
|
67
|
+
async with Client(base_url="http://test") as client:
|
68
|
+
async for event in client.run_stream("Howdy!", agent=mock_run.agent_name):
|
69
|
+
assert event == mock_event
|
70
|
+
|
71
|
+
|
72
|
+
@pytest.mark.asyncio
|
73
|
+
async def test_run_status(httpx_mock: HTTPXMock) -> None:
|
74
|
+
httpx_mock.add_response(url=f"http://test/runs/{mock_run.run_id}", method="GET", content=mock_run.model_dump_json())
|
75
|
+
|
76
|
+
async with Client(base_url="http://test") as client:
|
77
|
+
run = await client.run_status(run_id=mock_run.run_id)
|
78
|
+
assert run == mock_run
|
79
|
+
|
80
|
+
|
81
|
+
@pytest.mark.asyncio
|
82
|
+
async def test_run_cancel(httpx_mock: HTTPXMock) -> None:
|
83
|
+
httpx_mock.add_response(
|
84
|
+
url=f"http://test/runs/{mock_run.run_id}/cancel", method="POST", content=mock_run.model_dump_json()
|
85
|
+
)
|
86
|
+
|
87
|
+
async with Client(base_url="http://test") as client:
|
88
|
+
run = await client.run_cancel(run_id=mock_run.run_id)
|
89
|
+
assert run == mock_run
|
90
|
+
|
91
|
+
|
92
|
+
@pytest.mark.asyncio
|
93
|
+
async def test_run_resume_sync(httpx_mock: HTTPXMock) -> None:
|
94
|
+
httpx_mock.add_response(
|
95
|
+
url=f"http://test/runs/{mock_run.run_id}", method="POST", content=mock_run.model_dump_json()
|
96
|
+
)
|
97
|
+
|
98
|
+
async with Client(base_url="http://test") as client:
|
99
|
+
run = await client.run_resume_sync(MessageAwaitResume(message=Message(parts=[])), run_id=mock_run.run_id)
|
100
|
+
assert run == mock_run
|
101
|
+
|
102
|
+
|
103
|
+
@pytest.mark.asyncio
|
104
|
+
async def test_run_resume_async(httpx_mock: HTTPXMock) -> None:
|
105
|
+
httpx_mock.add_response(
|
106
|
+
url=f"http://test/runs/{mock_run.run_id}", method="POST", content=mock_run.model_dump_json()
|
107
|
+
)
|
108
|
+
|
109
|
+
async with Client(base_url="http://test") as client:
|
110
|
+
run = await client.run_resume_async(MessageAwaitResume(message=Message(parts=[])), run_id=mock_run.run_id)
|
111
|
+
assert run == mock_run
|
112
|
+
|
113
|
+
|
114
|
+
@pytest.mark.asyncio
|
115
|
+
async def test_run_resume_stream(httpx_mock: HTTPXMock) -> None:
|
116
|
+
mock_event = RunCompletedEvent(run=mock_run)
|
117
|
+
httpx_mock.add_response(
|
118
|
+
url=f"http://test/runs/{mock_run.run_id}",
|
119
|
+
method="POST",
|
120
|
+
headers={"content-type": "text/event-stream"},
|
121
|
+
content=f"data: {mock_event.model_dump_json()}\n\n",
|
122
|
+
)
|
123
|
+
|
124
|
+
async with Client(base_url="http://test") as client:
|
125
|
+
async for event in client.run_resume_stream(
|
126
|
+
MessageAwaitResume(message=Message(parts=[])), run_id=mock_run.run_id
|
127
|
+
):
|
128
|
+
assert event == mock_event
|
129
|
+
|
130
|
+
|
131
|
+
@pytest.mark.asyncio
|
132
|
+
async def test_session(httpx_mock: HTTPXMock) -> None:
|
133
|
+
httpx_mock.add_response(url="http://test/runs", method="POST", content=mock_run.model_dump_json(), is_reusable=True)
|
134
|
+
|
135
|
+
async with Client(base_url="http://test") as client, client.session(mock_run.session_id) as session:
|
136
|
+
assert session._session_id == mock_run.session_id
|
137
|
+
await session.run_sync("Howdy!", agent=mock_run.agent_name)
|
138
|
+
await client.run_sync("Howdy!", agent=mock_run.agent_name)
|
139
|
+
|
140
|
+
requests = httpx_mock.get_requests()
|
141
|
+
body = json.loads(requests[0].content)
|
142
|
+
assert body["session_id"] == str(mock_run.session_id)
|
143
|
+
|
144
|
+
body = json.loads(requests[1].content)
|
145
|
+
assert body["session_id"] is None
|
146
|
+
|
147
|
+
|
148
|
+
@pytest.mark.asyncio
|
149
|
+
async def test_no_session(httpx_mock: HTTPXMock) -> None:
|
150
|
+
httpx_mock.add_response(url="http://test/runs", method="POST", content=mock_run.model_dump_json(), is_reusable=True)
|
151
|
+
|
152
|
+
async with Client(base_url="http://test") as client:
|
153
|
+
await client.run_sync("Howdy!", agent=mock_run.agent_name)
|
154
|
+
await client.run_sync("Howdy!", agent=mock_run.agent_name)
|
155
|
+
|
156
|
+
requests = httpx_mock.get_requests()
|
157
|
+
|
158
|
+
body = json.loads(requests[1].content)
|
159
|
+
assert body["session_id"] is None
|
@@ -2,6 +2,25 @@ import pytest
|
|
2
2
|
from acp_sdk.models.models import Message, MessagePart
|
3
3
|
|
4
4
|
|
5
|
+
@pytest.mark.parametrize(
|
6
|
+
"first,second,result",
|
7
|
+
[
|
8
|
+
(
|
9
|
+
Message(parts=[MessagePart(content_type="text/plain", content="Foo")]),
|
10
|
+
Message(parts=[MessagePart(content_type="text/plain", content="Bar")]),
|
11
|
+
Message(
|
12
|
+
parts=[
|
13
|
+
MessagePart(content_type="text/plain", content="Foo"),
|
14
|
+
MessagePart(content_type="text/plain", content="Bar"),
|
15
|
+
]
|
16
|
+
),
|
17
|
+
)
|
18
|
+
],
|
19
|
+
)
|
20
|
+
def test_message_add(first: Message, second: Message, result: Message) -> None:
|
21
|
+
assert first + second == result
|
22
|
+
|
23
|
+
|
5
24
|
@pytest.mark.parametrize(
|
6
25
|
"uncompressed,compressed",
|
7
26
|
[
|
@@ -1,14 +0,0 @@
|
|
1
|
-
from collections.abc import AsyncGenerator
|
2
|
-
|
3
|
-
from pydantic import BaseModel
|
4
|
-
|
5
|
-
from acp_sdk.server.bundle import RunBundle
|
6
|
-
|
7
|
-
|
8
|
-
def encode_sse(model: BaseModel) -> str:
|
9
|
-
return f"data: {model.model_dump_json()}\n\n"
|
10
|
-
|
11
|
-
|
12
|
-
async def stream_sse(bundle: RunBundle) -> AsyncGenerator[str]:
|
13
|
-
async for event in bundle.stream():
|
14
|
-
yield encode_sse(event)
|
@@ -1,36 +0,0 @@
|
|
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", output=[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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|