acp-sdk 0.7.1__tar.gz → 0.7.2__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.2}/PKG-INFO +1 -1
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/docs/client.md +4 -4
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/pyproject.toml +1 -1
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/client/client.py +5 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/models/models.py +8 -2
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/server/app.py +15 -3
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/server/server.py +54 -1
- acp_sdk-0.7.2/src/acp_sdk/server/utils.py +49 -0
- acp_sdk-0.7.2/tests/unit/client/test_client.py +145 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/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.2}/.gitignore +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/.python-version +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/README.md +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/docs/_sidebar.md +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/docs/index.html +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/docs/models.md +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/docs/server.md +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/pytest.ini +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/client/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/client/types.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/client/utils.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/instrumentation.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/models/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/models/errors.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/models/schemas.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/py.typed +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/server/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/server/agent.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/server/bundle.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/server/context.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/server/errors.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/server/logging.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/server/session.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/server/telemetry.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/server/types.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/src/acp_sdk/version.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/tests/conftest.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/tests/e2e/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/tests/e2e/config.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/tests/e2e/fixtures/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/tests/e2e/fixtures/client.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/tests/e2e/fixtures/server.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/tests/e2e/test_suites/__init__.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/tests/e2e/test_suites/test_discovery.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/tests/e2e/test_suites/test_runs.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/tests/unit/client/test_utils.py +0 -0
- {acp_sdk-0.7.1 → acp_sdk-0.7.2}/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
|
```
|
@@ -120,6 +120,11 @@ class Client:
|
|
120
120
|
response = AgentReadResponse.model_validate(response.json())
|
121
121
|
return Agent(**response.model_dump())
|
122
122
|
|
123
|
+
async def ping(self) -> bool:
|
124
|
+
response = await self._client.get("/healthcheck")
|
125
|
+
self._raise_error(response)
|
126
|
+
return response.json() == "OK"
|
127
|
+
|
123
128
|
async def run_sync(self, input: Input, *, agent: AgentName) -> Run:
|
124
129
|
response = await self._client.post(
|
125
130
|
"/runs",
|
@@ -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,6 +104,10 @@ 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)
|
@@ -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}")
|
@@ -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,145 @@
|
|
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
|
@@ -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
|
File without changes
|