acp-sdk 0.6.0__tar.gz → 0.7.1__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.6.0 → acp_sdk-0.7.1}/PKG-INFO +4 -4
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/README.md +3 -3
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/docs/client.md +5 -5
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/docs/server.md +4 -4
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/pyproject.toml +1 -1
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/client/client.py +5 -28
- acp_sdk-0.7.1/src/acp_sdk/client/types.py +3 -0
- acp_sdk-0.7.1/src/acp_sdk/client/utils.py +24 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/models/models.py +1 -1
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/models/schemas.py +1 -1
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/agent.py +14 -14
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/app.py +1 -1
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/bundle.py +10 -10
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/session.py +2 -2
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/tests/e2e/fixtures/server.py +8 -8
- acp_sdk-0.7.1/tests/e2e/test_suites/test_discovery.py +18 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/tests/e2e/test_suites/test_runs.py +35 -35
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/tests/unit/client/test_client.py +1 -1
- acp_sdk-0.7.1/tests/unit/client/test_utils.py +30 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/.gitignore +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/.python-version +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/docs/_sidebar.md +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/docs/index.html +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/docs/models.md +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/pytest.ini +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/__init__.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/client/__init__.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/instrumentation.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/models/__init__.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/models/errors.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/py.typed +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/__init__.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/context.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/errors.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/logging.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/server.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/telemetry.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/types.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/server/utils.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/src/acp_sdk/version.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/tests/conftest.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/tests/e2e/__init__.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/tests/e2e/config.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/tests/e2e/fixtures/__init__.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/tests/e2e/fixtures/client.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/tests/e2e/test_suites/__init__.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/tests/unit/models/__init__.py +0 -0
- {acp_sdk-0.6.0 → acp_sdk-0.7.1}/tests/unit/models/test_models.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: acp-sdk
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.7.1
|
4
4
|
Summary: Agent Communication Protocol SDK
|
5
5
|
Author: IBM Corp.
|
6
6
|
Maintainer-email: Tomas Pilar <thomas7pilar@gmail.com>
|
@@ -44,9 +44,9 @@ Register an agent and run the server:
|
|
44
44
|
server = Server()
|
45
45
|
|
46
46
|
@server.agent()
|
47
|
-
async def echo(
|
47
|
+
async def echo(input: list[Message]):
|
48
48
|
"""Echoes everything"""
|
49
|
-
for message in
|
49
|
+
for message in input:
|
50
50
|
yield message
|
51
51
|
|
52
52
|
server.run(port=8000)
|
@@ -56,7 +56,7 @@ From another process, connect to the server and run the agent:
|
|
56
56
|
|
57
57
|
```py
|
58
58
|
async with Client(base_url="http://localhost:8000") as client:
|
59
|
-
run = await client.run_sync(agent="echo",
|
59
|
+
run = await client.run_sync(agent="echo", input=[Message(parts=[MessagePart(content="Howdy!")])])
|
60
60
|
print(run)
|
61
61
|
|
62
62
|
```
|
@@ -23,9 +23,9 @@ Register an agent and run the server:
|
|
23
23
|
server = Server()
|
24
24
|
|
25
25
|
@server.agent()
|
26
|
-
async def echo(
|
26
|
+
async def echo(input: list[Message]):
|
27
27
|
"""Echoes everything"""
|
28
|
-
for message in
|
28
|
+
for message in input:
|
29
29
|
yield message
|
30
30
|
|
31
31
|
server.run(port=8000)
|
@@ -35,7 +35,7 @@ From another process, connect to the server and run the agent:
|
|
35
35
|
|
36
36
|
```py
|
37
37
|
async with Client(base_url="http://localhost:8000") as client:
|
38
|
-
run = await client.run_sync(agent="echo",
|
38
|
+
run = await client.run_sync(agent="echo", input=[Message(parts=[MessagePart(content="Howdy!")])])
|
39
39
|
print(run)
|
40
40
|
|
41
41
|
```
|
@@ -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(agent_name="agent",
|
74
|
+
run = await client.run_async(agent_name="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(agent_name="agent",
|
79
|
-
print(run.
|
78
|
+
run = await client.run_sync(agent_name="agent", input=[message])
|
79
|
+
print(run.output)
|
80
80
|
|
81
81
|
# Stream - as sync but also receives events
|
82
|
-
async for event in client.run_stream(agent_name="agent",
|
82
|
+
async for event in client.run_stream(agent_name="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(agent_name=agent.name,
|
98
|
+
await session.run_sync(agent_name=agent.name, input=[Message(parts=[MessagePart(content="Hello!")])])
|
99
99
|
```
|
@@ -47,9 +47,9 @@ server = Server()
|
|
47
47
|
|
48
48
|
|
49
49
|
@server.agent()
|
50
|
-
async def echo(
|
50
|
+
async def echo(input: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
51
51
|
"""Echoes everything"""
|
52
|
-
for message in
|
52
|
+
for message in input:
|
53
53
|
await asyncio.sleep(0.5)
|
54
54
|
yield {"thought": "I should echo everything"}
|
55
55
|
await asyncio.sleep(0.5)
|
@@ -81,9 +81,9 @@ from acp_sdk.server import RunYield, RunYieldResume, agent, create_app
|
|
81
81
|
|
82
82
|
|
83
83
|
@agent()
|
84
|
-
async def echo(
|
84
|
+
async def echo(input: list[Message]) -> AsyncGenerator[RunYield, RunYieldResume]:
|
85
85
|
"""Echoes everything"""
|
86
|
-
for message in
|
86
|
+
for message in input:
|
87
87
|
yield message
|
88
88
|
|
89
89
|
|
@@ -11,6 +11,8 @@ from httpx_sse import EventSource, aconnect_sse
|
|
11
11
|
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
12
12
|
from pydantic import TypeAdapter
|
13
13
|
|
14
|
+
from acp_sdk.client.types import Input
|
15
|
+
from acp_sdk.client.utils import input_to_messages
|
14
16
|
from acp_sdk.instrumentation import get_tracer
|
15
17
|
from acp_sdk.models import (
|
16
18
|
ACPError,
|
@@ -21,7 +23,6 @@ from acp_sdk.models import (
|
|
21
23
|
AwaitResume,
|
22
24
|
Error,
|
23
25
|
Event,
|
24
|
-
Message,
|
25
26
|
Run,
|
26
27
|
RunCancelResponse,
|
27
28
|
RunCreatedEvent,
|
@@ -33,9 +34,6 @@ from acp_sdk.models import (
|
|
33
34
|
RunResumeResponse,
|
34
35
|
SessionId,
|
35
36
|
)
|
36
|
-
from acp_sdk.models.models import MessagePart
|
37
|
-
|
38
|
-
Input = list[Message] | Message | list[MessagePart] | MessagePart | list[str] | str
|
39
37
|
|
40
38
|
|
41
39
|
class Client:
|
@@ -127,7 +125,7 @@ class Client:
|
|
127
125
|
"/runs",
|
128
126
|
content=RunCreateRequest(
|
129
127
|
agent_name=agent,
|
130
|
-
|
128
|
+
input=input_to_messages(input),
|
131
129
|
mode=RunMode.SYNC,
|
132
130
|
session_id=self._session_id,
|
133
131
|
).model_dump_json(),
|
@@ -142,7 +140,7 @@ class Client:
|
|
142
140
|
"/runs",
|
143
141
|
content=RunCreateRequest(
|
144
142
|
agent_name=agent,
|
145
|
-
|
143
|
+
input=input_to_messages(input),
|
146
144
|
mode=RunMode.ASYNC,
|
147
145
|
session_id=self._session_id,
|
148
146
|
).model_dump_json(),
|
@@ -159,7 +157,7 @@ class Client:
|
|
159
157
|
"/runs",
|
160
158
|
content=RunCreateRequest(
|
161
159
|
agent_name=agent,
|
162
|
-
|
160
|
+
input=input_to_messages(input),
|
163
161
|
mode=RunMode.STREAM,
|
164
162
|
session_id=self._session_id,
|
165
163
|
).model_dump_json(),
|
@@ -227,24 +225,3 @@ class Client:
|
|
227
225
|
|
228
226
|
def _set_session(self, run: Run) -> None:
|
229
227
|
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
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from acp_sdk.client.types import Input
|
2
|
+
from acp_sdk.models.models import Message, MessagePart
|
3
|
+
|
4
|
+
|
5
|
+
def input_to_messages(input: Input) -> list[Message]:
|
6
|
+
if isinstance(input, list):
|
7
|
+
if len(input) == 0:
|
8
|
+
return []
|
9
|
+
if all(isinstance(item, Message) for item in input):
|
10
|
+
return input
|
11
|
+
elif all(isinstance(item, MessagePart) for item in input):
|
12
|
+
return [Message(parts=input)]
|
13
|
+
elif all(isinstance(item, str) for item in input):
|
14
|
+
return [Message(parts=[MessagePart(content=content) for content in input])]
|
15
|
+
else:
|
16
|
+
raise TypeError("List with mixed types is not supported")
|
17
|
+
else:
|
18
|
+
if isinstance(input, str):
|
19
|
+
input = MessagePart(content=input)
|
20
|
+
if isinstance(input, MessagePart):
|
21
|
+
input = Message(parts=[input])
|
22
|
+
if isinstance(input, Message):
|
23
|
+
input = [input]
|
24
|
+
return input
|
@@ -32,14 +32,14 @@ class Agent(abc.ABC):
|
|
32
32
|
|
33
33
|
@abc.abstractmethod
|
34
34
|
def run(
|
35
|
-
self,
|
35
|
+
self, input: list[Message], context: Context
|
36
36
|
) -> (
|
37
37
|
AsyncGenerator[RunYield, RunYieldResume] | Generator[RunYield, RunYieldResume] | Coroutine[RunYield] | RunYield
|
38
38
|
):
|
39
39
|
pass
|
40
40
|
|
41
41
|
async def execute(
|
42
|
-
self,
|
42
|
+
self, input: list[Message], session_id: SessionId | None, executor: ThreadPoolExecutor
|
43
43
|
) -> AsyncGenerator[RunYield, RunYieldResume]:
|
44
44
|
yield_queue: janus.Queue[RunYield] = janus.Queue()
|
45
45
|
yield_resume_queue: janus.Queue[RunYieldResume] = janus.Queue()
|
@@ -49,13 +49,13 @@ class Agent(abc.ABC):
|
|
49
49
|
)
|
50
50
|
|
51
51
|
if inspect.isasyncgenfunction(self.run):
|
52
|
-
run = asyncio.create_task(self._run_async_gen(
|
52
|
+
run = asyncio.create_task(self._run_async_gen(input, context))
|
53
53
|
elif inspect.iscoroutinefunction(self.run):
|
54
|
-
run = asyncio.create_task(self._run_coro(
|
54
|
+
run = asyncio.create_task(self._run_coro(input, context))
|
55
55
|
elif inspect.isgeneratorfunction(self.run):
|
56
|
-
run = asyncio.get_running_loop().run_in_executor(executor, self._run_gen,
|
56
|
+
run = asyncio.get_running_loop().run_in_executor(executor, self._run_gen, input, context)
|
57
57
|
else:
|
58
|
-
run = asyncio.get_running_loop().run_in_executor(executor, self._run_func,
|
58
|
+
run = asyncio.get_running_loop().run_in_executor(executor, self._run_func, input, context)
|
59
59
|
|
60
60
|
try:
|
61
61
|
while True:
|
@@ -66,7 +66,7 @@ class Agent(abc.ABC):
|
|
66
66
|
finally:
|
67
67
|
await run # Raise exceptions
|
68
68
|
|
69
|
-
async def _run_async_gen(self, input: Message, context: Context) -> None:
|
69
|
+
async def _run_async_gen(self, input: list[Message], context: Context) -> None:
|
70
70
|
try:
|
71
71
|
gen: AsyncGenerator[RunYield, RunYieldResume] = self.run(input, context)
|
72
72
|
value = None
|
@@ -77,13 +77,13 @@ class Agent(abc.ABC):
|
|
77
77
|
finally:
|
78
78
|
context.shutdown()
|
79
79
|
|
80
|
-
async def _run_coro(self, input: Message, context: Context) -> None:
|
80
|
+
async def _run_coro(self, input: list[Message], context: Context) -> None:
|
81
81
|
try:
|
82
82
|
await context.yield_async(await self.run(input, context))
|
83
83
|
finally:
|
84
84
|
context.shutdown()
|
85
85
|
|
86
|
-
def _run_gen(self, input: Message, context: Context) -> None:
|
86
|
+
def _run_gen(self, input: list[Message], context: Context) -> None:
|
87
87
|
try:
|
88
88
|
gen: Generator[RunYield, RunYieldResume] = self.run(input, context)
|
89
89
|
value = None
|
@@ -94,7 +94,7 @@ class Agent(abc.ABC):
|
|
94
94
|
finally:
|
95
95
|
context.shutdown()
|
96
96
|
|
97
|
-
def _run_func(self, input: Message, context: Context) -> None:
|
97
|
+
def _run_func(self, input: list[Message], context: Context) -> None:
|
98
98
|
try:
|
99
99
|
context.yield_sync(self.run(input, context))
|
100
100
|
finally:
|
@@ -139,7 +139,7 @@ def agent(
|
|
139
139
|
if inspect.isasyncgenfunction(fn):
|
140
140
|
|
141
141
|
class AsyncGenDecoratorAgent(DecoratorAgentBase):
|
142
|
-
async def run(self, input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
142
|
+
async def run(self, input: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
143
143
|
try:
|
144
144
|
gen: AsyncGenerator[RunYield, RunYieldResume] = (
|
145
145
|
fn(input, context) if has_context_param else fn(input)
|
@@ -154,21 +154,21 @@ def agent(
|
|
154
154
|
elif inspect.iscoroutinefunction(fn):
|
155
155
|
|
156
156
|
class CoroDecoratorAgent(DecoratorAgentBase):
|
157
|
-
async def run(self, input: Message, context: Context) -> Coroutine[RunYield]:
|
157
|
+
async def run(self, input: list[Message], context: Context) -> Coroutine[RunYield]:
|
158
158
|
return await (fn(input, context) if has_context_param else fn(input))
|
159
159
|
|
160
160
|
agent = CoroDecoratorAgent()
|
161
161
|
elif inspect.isgeneratorfunction(fn):
|
162
162
|
|
163
163
|
class GenDecoratorAgent(DecoratorAgentBase):
|
164
|
-
def run(self, input: Message, context: Context) -> Generator[RunYield, RunYieldResume]:
|
164
|
+
def run(self, input: list[Message], context: Context) -> Generator[RunYield, RunYieldResume]:
|
165
165
|
yield from (fn(input, context) if has_context_param else fn(input))
|
166
166
|
|
167
167
|
agent = GenDecoratorAgent()
|
168
168
|
else:
|
169
169
|
|
170
170
|
class FuncDecoratorAgent(DecoratorAgentBase):
|
171
|
-
def run(self, input: Message, context: Context) -> RunYield:
|
171
|
+
def run(self, input: list[Message], context: Context) -> RunYield:
|
172
172
|
return fn(input, context) if has_context_param else fn(input)
|
173
173
|
|
174
174
|
agent = FuncDecoratorAgent()
|
@@ -105,7 +105,7 @@ def create_app(*agents: Agent, run_limit: int = 1000, run_ttl: timedelta = timed
|
|
105
105
|
bundle = RunBundle(
|
106
106
|
agent=agent,
|
107
107
|
run=Run(agent_name=agent.name, session_id=session.id),
|
108
|
-
|
108
|
+
input=request.input,
|
109
109
|
history=list(session.history()),
|
110
110
|
executor=executor,
|
111
111
|
)
|
@@ -35,11 +35,11 @@ from acp_sdk.server.logging import logger
|
|
35
35
|
|
36
36
|
class RunBundle:
|
37
37
|
def __init__(
|
38
|
-
self, *, agent: Agent, run: Run,
|
38
|
+
self, *, agent: Agent, run: Run, input: list[Message], history: list[Message], executor: ThreadPoolExecutor
|
39
39
|
) -> None:
|
40
40
|
self.agent = agent
|
41
41
|
self.run = run
|
42
|
-
self.
|
42
|
+
self.input = input
|
43
43
|
self.history = history
|
44
44
|
|
45
45
|
self.stream_queue: asyncio.Queue[Event] = asyncio.Queue()
|
@@ -47,7 +47,7 @@ class RunBundle:
|
|
47
47
|
self.await_queue: asyncio.Queue[AwaitResume] = asyncio.Queue(maxsize=1)
|
48
48
|
self.await_or_terminate_event = asyncio.Event()
|
49
49
|
|
50
|
-
self.task = asyncio.create_task(self._execute(
|
50
|
+
self.task = asyncio.create_task(self._execute(input, executor=executor))
|
51
51
|
|
52
52
|
async def stream(self) -> AsyncGenerator[Event]:
|
53
53
|
while True:
|
@@ -83,7 +83,7 @@ class RunBundle:
|
|
83
83
|
async def join(self) -> None:
|
84
84
|
await self.await_or_terminate_event.wait()
|
85
85
|
|
86
|
-
async def _execute(self,
|
86
|
+
async def _execute(self, input: list[Message], *, executor: ThreadPoolExecutor) -> None:
|
87
87
|
with get_tracer().start_as_current_span("run"):
|
88
88
|
run_logger = logging.LoggerAdapter(logger, {"run_id": str(self.run.run_id)})
|
89
89
|
|
@@ -92,14 +92,14 @@ class RunBundle:
|
|
92
92
|
async def flush_message() -> None:
|
93
93
|
nonlocal in_message
|
94
94
|
if in_message:
|
95
|
-
await self.emit(MessageCompletedEvent(message=self.run.
|
95
|
+
await self.emit(MessageCompletedEvent(message=self.run.output[-1]))
|
96
96
|
in_message = False
|
97
97
|
|
98
98
|
try:
|
99
99
|
await self.emit(RunCreatedEvent(run=self.run))
|
100
100
|
|
101
101
|
generator = self.agent.execute(
|
102
|
-
|
102
|
+
input=self.history + input, session_id=self.run.session_id, executor=executor
|
103
103
|
)
|
104
104
|
run_logger.info("Run started")
|
105
105
|
|
@@ -114,14 +114,14 @@ class RunBundle:
|
|
114
114
|
if isinstance(next, str):
|
115
115
|
next = MessagePart(content=next)
|
116
116
|
if not in_message:
|
117
|
-
self.run.
|
117
|
+
self.run.output.append(Message(parts=[]))
|
118
118
|
in_message = True
|
119
|
-
await self.emit(MessageCreatedEvent(message=self.run.
|
120
|
-
self.run.
|
119
|
+
await self.emit(MessageCreatedEvent(message=self.run.output[-1]))
|
120
|
+
self.run.output[-1].parts.append(next)
|
121
121
|
await self.emit(MessagePartEvent(part=next))
|
122
122
|
elif isinstance(next, Message):
|
123
123
|
await flush_message()
|
124
|
-
self.run.
|
124
|
+
self.run.output.append(next)
|
125
125
|
await self.emit(MessageCreatedEvent(message=next))
|
126
126
|
for part in next.parts:
|
127
127
|
await self.emit(MessagePartEvent(part=part))
|
@@ -17,36 +17,36 @@ def server(request: pytest.FixtureRequest) -> Generator[None]:
|
|
17
17
|
server = Server()
|
18
18
|
|
19
19
|
@server.agent()
|
20
|
-
async def echo(
|
21
|
-
for message in
|
20
|
+
async def echo(input: list[Message], context: Context) -> AsyncIterator[Message]:
|
21
|
+
for message in input:
|
22
22
|
yield message
|
23
23
|
|
24
24
|
@server.agent()
|
25
25
|
async def awaiter(
|
26
|
-
|
26
|
+
input: list[Message], context: Context
|
27
27
|
) -> AsyncGenerator[Message | MessageAwaitRequest, AwaitResume]:
|
28
28
|
yield MessageAwaitRequest(message=Message(parts=[]))
|
29
29
|
yield MessagePart(content="empty", content_type="text/plain")
|
30
30
|
|
31
31
|
@server.agent()
|
32
|
-
async def failer(
|
32
|
+
async def failer(input: list[Message], context: Context) -> AsyncIterator[Message]:
|
33
33
|
yield Error(code=ErrorCode.INVALID_INPUT, message="Wrong question buddy!")
|
34
34
|
|
35
35
|
@server.agent()
|
36
|
-
async def sessioner(
|
36
|
+
async def sessioner(input: list[Message], context: Context) -> AsyncIterator[Message]:
|
37
37
|
assert context.session_id is not None
|
38
38
|
|
39
39
|
yield MessagePart(content=str(context.session_id), content_type="text/plain")
|
40
40
|
|
41
41
|
@server.agent()
|
42
|
-
async def mime_types(
|
42
|
+
async def mime_types(input: list[Message], context: Context) -> AsyncIterator[Message]:
|
43
43
|
yield MessagePart(content="<h1>HTML Content</h1>", content_type="text/html")
|
44
44
|
yield MessagePart(content='{"key": "value"}', content_type="application/json")
|
45
45
|
yield MessagePart(content="console.log('Hello');", content_type="application/javascript")
|
46
46
|
yield MessagePart(content="body { color: red; }", content_type="text/css")
|
47
47
|
|
48
48
|
@server.agent()
|
49
|
-
async def base64_encoding(
|
49
|
+
async def base64_encoding(input: list[Message], context: Context) -> AsyncIterator[Message]:
|
50
50
|
yield Message(
|
51
51
|
parts=[
|
52
52
|
MessagePart(
|
@@ -61,7 +61,7 @@ def server(request: pytest.FixtureRequest) -> Generator[None]:
|
|
61
61
|
)
|
62
62
|
|
63
63
|
@server.agent()
|
64
|
-
async def artifact_producer(
|
64
|
+
async def artifact_producer(input: list[Message], context: Context) -> AsyncGenerator[Message | Artifact, None]:
|
65
65
|
yield MessagePart(content="Processing with artifacts", content_type="text/plain")
|
66
66
|
yield Artifact(name="text-result.txt", content_type="text/plain", content="This is a text artifact result")
|
67
67
|
yield Artifact(
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import pytest
|
2
|
+
from acp_sdk.client import Client
|
3
|
+
from acp_sdk.models import Agent
|
4
|
+
from acp_sdk.server import Server
|
5
|
+
|
6
|
+
|
7
|
+
@pytest.mark.asyncio
|
8
|
+
async def test_agents_list(server: Server, client: Client) -> None:
|
9
|
+
async for agent in client.agents():
|
10
|
+
assert isinstance(agent, Agent)
|
11
|
+
|
12
|
+
|
13
|
+
@pytest.mark.asyncio
|
14
|
+
async def test_agents_details(server: Server, client: Client) -> None:
|
15
|
+
agent_name = "echo"
|
16
|
+
agent = await client.agent(name=agent_name)
|
17
|
+
assert isinstance(agent, Agent)
|
18
|
+
assert agent.name == agent_name
|
@@ -19,33 +19,33 @@ from acp_sdk.models import (
|
|
19
19
|
from acp_sdk.models.errors import ACPError
|
20
20
|
from acp_sdk.server import Server
|
21
21
|
|
22
|
-
|
22
|
+
input = [Message(parts=[MessagePart(content="Hello!")])]
|
23
23
|
await_resume = MessageAwaitResume(message=Message(parts=[]))
|
24
24
|
|
25
25
|
|
26
26
|
@pytest.mark.asyncio
|
27
27
|
async def test_run_sync(server: Server, client: Client) -> None:
|
28
|
-
run = await client.run_sync(agent="echo", input=
|
28
|
+
run = await client.run_sync(agent="echo", input=input)
|
29
29
|
assert run.status == RunStatus.COMPLETED
|
30
|
-
assert run.
|
30
|
+
assert run.output == input
|
31
31
|
|
32
32
|
|
33
33
|
@pytest.mark.asyncio
|
34
34
|
async def test_run_async(server: Server, client: Client) -> None:
|
35
|
-
run = await client.run_async(agent="echo", input=
|
35
|
+
run = await client.run_async(agent="echo", input=input)
|
36
36
|
assert run.status == RunStatus.CREATED
|
37
37
|
|
38
38
|
|
39
39
|
@pytest.mark.asyncio
|
40
40
|
async def test_run_stream(server: Server, client: Client) -> None:
|
41
|
-
event_stream = [event async for event in client.run_stream(agent="echo", input=
|
41
|
+
event_stream = [event async for event in client.run_stream(agent="echo", input=input)]
|
42
42
|
assert isinstance(event_stream[0], RunCreatedEvent)
|
43
43
|
assert isinstance(event_stream[-1], RunCompletedEvent)
|
44
44
|
|
45
45
|
|
46
46
|
@pytest.mark.asyncio
|
47
47
|
async def test_run_status(server: Server, client: Client) -> None:
|
48
|
-
run = await client.run_async(agent="echo", input=
|
48
|
+
run = await client.run_async(agent="echo", input=input)
|
49
49
|
while run.status in (RunStatus.CREATED, RunStatus.IN_PROGRESS):
|
50
50
|
run = await client.run_status(run_id=run.run_id)
|
51
51
|
assert run.status == RunStatus.COMPLETED
|
@@ -53,7 +53,7 @@ async def test_run_status(server: Server, client: Client) -> None:
|
|
53
53
|
|
54
54
|
@pytest.mark.asyncio
|
55
55
|
async def test_failure(server: Server, client: Client) -> None:
|
56
|
-
run = await client.run_sync(agent="failer", input=
|
56
|
+
run = await client.run_sync(agent="failer", input=input)
|
57
57
|
assert run.status == RunStatus.FAILED
|
58
58
|
assert run.error is not None
|
59
59
|
assert run.error.code == ErrorCode.INVALID_INPUT
|
@@ -61,7 +61,7 @@ async def test_failure(server: Server, client: Client) -> None:
|
|
61
61
|
|
62
62
|
@pytest.mark.asyncio
|
63
63
|
async def test_run_cancel(server: Server, client: Client) -> None:
|
64
|
-
run = await client.run_sync(agent="awaiter", input=
|
64
|
+
run = await client.run_sync(agent="awaiter", input=input)
|
65
65
|
assert run.status == RunStatus.AWAITING
|
66
66
|
run = await client.run_cancel(run_id=run.run_id)
|
67
67
|
assert run.status == RunStatus.CANCELLING
|
@@ -69,7 +69,7 @@ async def test_run_cancel(server: Server, client: Client) -> None:
|
|
69
69
|
|
70
70
|
@pytest.mark.asyncio
|
71
71
|
async def test_run_resume_sync(server: Server, client: Client) -> None:
|
72
|
-
run = await client.run_sync(agent="awaiter", input=
|
72
|
+
run = await client.run_sync(agent="awaiter", input=input)
|
73
73
|
assert run.status == RunStatus.AWAITING
|
74
74
|
assert run.await_request is not None
|
75
75
|
|
@@ -79,7 +79,7 @@ async def test_run_resume_sync(server: Server, client: Client) -> None:
|
|
79
79
|
|
80
80
|
@pytest.mark.asyncio
|
81
81
|
async def test_run_resume_async(server: Server, client: Client) -> None:
|
82
|
-
run = await client.run_sync(agent="awaiter", input=
|
82
|
+
run = await client.run_sync(agent="awaiter", input=input)
|
83
83
|
assert run.status == RunStatus.AWAITING
|
84
84
|
assert run.await_request is not None
|
85
85
|
|
@@ -89,7 +89,7 @@ async def test_run_resume_async(server: Server, client: Client) -> None:
|
|
89
89
|
|
90
90
|
@pytest.mark.asyncio
|
91
91
|
async def test_run_resume_stream(server: Server, client: Client) -> None:
|
92
|
-
run = await client.run_sync(agent="awaiter", input=
|
92
|
+
run = await client.run_sync(agent="awaiter", input=input)
|
93
93
|
assert run.status == RunStatus.AWAITING
|
94
94
|
assert run.await_request is not None
|
95
95
|
|
@@ -101,19 +101,19 @@ async def test_run_resume_stream(server: Server, client: Client) -> None:
|
|
101
101
|
@pytest.mark.asyncio
|
102
102
|
async def test_run_session(server: Server, client: Client) -> None:
|
103
103
|
async with client.session() as session:
|
104
|
-
run = await session.run_sync(agent="echo", input=
|
105
|
-
assert run.
|
106
|
-
run = await session.run_sync(agent="echo", input=
|
107
|
-
assert run.
|
104
|
+
run = await session.run_sync(agent="echo", input=input)
|
105
|
+
assert run.output == input
|
106
|
+
run = await session.run_sync(agent="echo", input=input)
|
107
|
+
assert run.output == input + input + input
|
108
108
|
|
109
109
|
|
110
110
|
@pytest.mark.asyncio
|
111
111
|
async def test_mime_types(server: Server, client: Client) -> None:
|
112
|
-
run = await client.run_sync(agent="mime_types", input=
|
112
|
+
run = await client.run_sync(agent="mime_types", input=input)
|
113
113
|
assert run.status == RunStatus.COMPLETED
|
114
|
-
assert len(run.
|
114
|
+
assert len(run.output) == 1
|
115
115
|
|
116
|
-
message_parts = run.
|
116
|
+
message_parts = run.output[0].parts
|
117
117
|
content_types = [part.content_type for part in message_parts]
|
118
118
|
|
119
119
|
assert "text/html" in content_types
|
@@ -130,11 +130,11 @@ async def test_mime_types(server: Server, client: Client) -> None:
|
|
130
130
|
|
131
131
|
@pytest.mark.asyncio
|
132
132
|
async def test_base64_encoding(server: Server, client: Client) -> None:
|
133
|
-
run = await client.run_sync(agent="base64_encoding", input=
|
133
|
+
run = await client.run_sync(agent="base64_encoding", input=input)
|
134
134
|
assert run.status == RunStatus.COMPLETED
|
135
|
-
assert len(run.
|
135
|
+
assert len(run.output) == 1
|
136
136
|
|
137
|
-
message_parts = run.
|
137
|
+
message_parts = run.output[0].parts
|
138
138
|
assert len(message_parts) == 2
|
139
139
|
|
140
140
|
base64_part = next((part for part in message_parts if part.content_encoding == "base64"), None)
|
@@ -150,17 +150,17 @@ async def test_base64_encoding(server: Server, client: Client) -> None:
|
|
150
150
|
|
151
151
|
@pytest.mark.asyncio
|
152
152
|
async def test_artifacts(server: Server, client: Client) -> None:
|
153
|
-
run = await client.run_sync(agent="artifact_producer", input=
|
153
|
+
run = await client.run_sync(agent="artifact_producer", input=input)
|
154
154
|
assert run.status == RunStatus.COMPLETED
|
155
155
|
|
156
|
-
assert len(run.
|
157
|
-
assert run.
|
156
|
+
assert len(run.output) == 1
|
157
|
+
assert run.output[0].parts[0].content == "Processing with artifacts"
|
158
158
|
|
159
|
-
assert len(run.
|
159
|
+
assert len(run.output[0].parts) == 4
|
160
160
|
|
161
|
-
text_artifact = next((a for a in run.
|
162
|
-
json_artifact = next((a for a in run.
|
163
|
-
image_artifact = next((a for a in run.
|
161
|
+
text_artifact = next((a for a in run.output[0].parts if a.name == "text-result.txt"), None)
|
162
|
+
json_artifact = next((a for a in run.output[0].parts if a.name == "data.json"), None)
|
163
|
+
image_artifact = next((a for a in run.output[0].parts if a.name == "image.png"), None)
|
164
164
|
|
165
165
|
assert text_artifact is not None
|
166
166
|
assert text_artifact.content_type == "text/plain"
|
@@ -179,7 +179,7 @@ async def test_artifacts(server: Server, client: Client) -> None:
|
|
179
179
|
|
180
180
|
@pytest.mark.asyncio
|
181
181
|
async def test_artifact_streaming(server: Server, client: Client) -> None:
|
182
|
-
events = [event async for event in client.run_stream(agent="artifact_producer", input=
|
182
|
+
events = [event async for event in client.run_stream(agent="artifact_producer", input=input)]
|
183
183
|
|
184
184
|
assert isinstance(events[0], RunCreatedEvent)
|
185
185
|
assert isinstance(events[-1], RunCompletedEvent)
|
@@ -201,7 +201,7 @@ async def test_artifact_streaming(server: Server, client: Client) -> None:
|
|
201
201
|
@pytest.mark.asyncio
|
202
202
|
@pytest.mark.parametrize("server", [timedelta(seconds=5)], indirect=True)
|
203
203
|
async def test_run_ttl(server: Server, client: Client) -> None:
|
204
|
-
run = await client.run_async(agent="echo", input=
|
204
|
+
run = await client.run_async(agent="echo", input=input)
|
205
205
|
run = await client.run_status(run_id=run.run_id)
|
206
206
|
await asyncio.sleep(6)
|
207
207
|
try:
|
@@ -218,10 +218,10 @@ async def test_run_ttl(server: Server, client: Client) -> None:
|
|
218
218
|
@pytest.mark.parametrize("server", [timedelta(seconds=5)], indirect=True)
|
219
219
|
async def test_session_ttl(server: Server, client: Client) -> None:
|
220
220
|
async with client.session() as session:
|
221
|
-
run = await session.run_sync(agent="echo", input=
|
221
|
+
run = await session.run_sync(agent="echo", input=input)
|
222
222
|
await asyncio.sleep(3)
|
223
|
-
run = await session.run_sync(agent="echo", input=
|
224
|
-
assert len(run.
|
223
|
+
run = await session.run_sync(agent="echo", input=input)
|
224
|
+
assert len(run.output) == 3
|
225
225
|
await asyncio.sleep(3)
|
226
|
-
run = await session.run_sync(agent="echo", input=
|
227
|
-
assert len(run.
|
226
|
+
run = await session.run_sync(agent="echo", input=input)
|
227
|
+
assert len(run.output) == 7 # First run shall be forgotten
|
@@ -3,7 +3,7 @@ from acp_sdk.client import Client
|
|
3
3
|
from acp_sdk.models import Message, MessagePart, Run, RunCompletedEvent
|
4
4
|
from pytest_httpx import HTTPXMock
|
5
5
|
|
6
|
-
mock_run = Run(agent_name="mock",
|
6
|
+
mock_run = Run(agent_name="mock", output=[Message(parts=[MessagePart(content="Hello!")])])
|
7
7
|
|
8
8
|
|
9
9
|
@pytest.mark.asyncio
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import pytest
|
2
|
+
from acp_sdk.client.types import Input
|
3
|
+
from acp_sdk.client.utils import input_to_messages
|
4
|
+
from acp_sdk.models import Message, MessagePart
|
5
|
+
|
6
|
+
|
7
|
+
@pytest.mark.parametrize(
|
8
|
+
"input,messages",
|
9
|
+
[
|
10
|
+
([], []),
|
11
|
+
("Hello", [Message(parts=[MessagePart(content="Hello")])]),
|
12
|
+
(["Hello"], [Message(parts=[MessagePart(content="Hello")])]),
|
13
|
+
(MessagePart(content="Hello"), [Message(parts=[MessagePart(content="Hello")])]),
|
14
|
+
([MessagePart(content="Hello")], [Message(parts=[MessagePart(content="Hello")])]),
|
15
|
+
(Message(parts=[MessagePart(content="Hello")]), [Message(parts=[MessagePart(content="Hello")])]),
|
16
|
+
([Message(parts=[MessagePart(content="Hello")])], [Message(parts=[MessagePart(content="Hello")])]),
|
17
|
+
],
|
18
|
+
)
|
19
|
+
def test_input_to_messages(input: Input, messages: list[Message]) -> None:
|
20
|
+
result = input_to_messages(input)
|
21
|
+
assert result == messages
|
22
|
+
|
23
|
+
|
24
|
+
@pytest.mark.parametrize(
|
25
|
+
"input",
|
26
|
+
[["foo", Message(parts=[])], ["foo", MessagePart(content="foo")], [Message(parts=[]), MessagePart(content="foo")]],
|
27
|
+
)
|
28
|
+
def test_input_to_messages_mixed_input(input: Input) -> None:
|
29
|
+
with pytest.raises(TypeError):
|
30
|
+
input_to_messages(["foo", Message(parts=[])])
|
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
|