acp-sdk 0.1.0rc5__tar.gz → 0.1.0rc7__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.
Files changed (33) hide show
  1. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/PKG-INFO +11 -15
  2. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/README.md +10 -14
  3. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/servers/awaiting.py +1 -1
  4. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/servers/echo.py +1 -1
  5. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/servers/multi-echo.py +5 -7
  6. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/pyproject.toml +1 -1
  7. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/models/models.py +3 -3
  8. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/models/schemas.py +3 -1
  9. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/app.py +17 -4
  10. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/bundle.py +1 -1
  11. acp_sdk-0.1.0rc7/src/acp_sdk/server/server.py +243 -0
  12. acp_sdk-0.1.0rc7/tests/__init__.py +0 -0
  13. acp_sdk-0.1.0rc7/tests/test_e2e.py +113 -0
  14. acp_sdk-0.1.0rc5/src/acp_sdk/server/server.py +0 -120
  15. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/.gitignore +0 -0
  16. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/.python-version +0 -0
  17. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/clients/advanced.py +0 -0
  18. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/clients/simple.py +0 -0
  19. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/clients/stream.py +0 -0
  20. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/__init__.py +0 -0
  21. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/client/__init__.py +0 -0
  22. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/client/client.py +0 -0
  23. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/models/__init__.py +0 -0
  24. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/models/errors.py +0 -0
  25. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/py.typed +0 -0
  26. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/__init__.py +0 -0
  27. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/agent.py +0 -0
  28. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/context.py +0 -0
  29. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/errors.py +0 -0
  30. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/logging.py +0 -0
  31. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/telemetry.py +0 -0
  32. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/types.py +0 -0
  33. {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acp-sdk
3
- Version: 0.1.0rc5
3
+ Version: 0.1.0rc7
4
4
  Summary: Agent Communication Protocol SDK
5
5
  Requires-Python: <4.0,>=3.11
6
6
  Requires-Dist: opentelemetry-api>=1.31.1
@@ -56,23 +56,19 @@ async with Client(base_url="http://localhost:8000") as client:
56
56
  The `server` submodule exposes [fastapi] application factory that makes it easy to expose any agent over ACP.
57
57
 
58
58
  ```python
59
- class EchoAgent(Agent):
60
- @property
61
- def name(self) -> str:
62
- return "echo"
59
+ server = Server()
63
60
 
64
- @property
65
- def description(self) -> str:
66
- return "Echoes everything"
61
+ @server.agent()
62
+ async def echo(input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
63
+ """Echoes everything"""
64
+ for part in input:
65
+ await asyncio.sleep(0.5)
66
+ yield {"thought": "I should echo everyting"}
67
+ await asyncio.sleep(0.5)
68
+ yield Message(part)
67
69
 
68
- async def run(self, input: Message, *, context: Context) -> AsyncGenerator[Message | Await, AwaitResume]:
69
- for part in input:
70
- await asyncio.sleep(0.5)
71
- yield {"thought": "I should echo everyting"}
72
- yield Message(part)
73
70
 
74
-
75
- serve(EchoAgent())
71
+ server.run()
76
72
  ```
77
73
 
78
74
  ➡️ Explore more in our [examples library](/python/examples).
@@ -37,23 +37,19 @@ async with Client(base_url="http://localhost:8000") as client:
37
37
  The `server` submodule exposes [fastapi] application factory that makes it easy to expose any agent over ACP.
38
38
 
39
39
  ```python
40
- class EchoAgent(Agent):
41
- @property
42
- def name(self) -> str:
43
- return "echo"
40
+ server = Server()
44
41
 
45
- @property
46
- def description(self) -> str:
47
- return "Echoes everything"
42
+ @server.agent()
43
+ async def echo(input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
44
+ """Echoes everything"""
45
+ for part in input:
46
+ await asyncio.sleep(0.5)
47
+ yield {"thought": "I should echo everyting"}
48
+ await asyncio.sleep(0.5)
49
+ yield Message(part)
48
50
 
49
- async def run(self, input: Message, *, context: Context) -> AsyncGenerator[Message | Await, AwaitResume]:
50
- for part in input:
51
- await asyncio.sleep(0.5)
52
- yield {"thought": "I should echo everyting"}
53
- yield Message(part)
54
51
 
55
-
56
- serve(EchoAgent())
52
+ server.run()
57
53
  ```
58
54
 
59
55
  ➡️ Explore more in our [examples library](/python/examples).
@@ -20,4 +20,4 @@ async def awaiting(input: Message, context: Context) -> AsyncGenerator[Message |
20
20
  yield Message(TextMessagePart(content=f"Thanks for {data}"))
21
21
 
22
22
 
23
- server.run()
23
+ server()
@@ -19,4 +19,4 @@ async def echo(input: Message, context: Context) -> AsyncGenerator[RunYield, Run
19
19
  yield Message(part)
20
20
 
21
21
 
22
- server.run()
22
+ server()
@@ -5,13 +5,11 @@ from acp_sdk.models import (
5
5
  )
6
6
  from acp_sdk.server import Agent, Context, RunYield, RunYieldResume, Server
7
7
 
8
- # This example showcases several ways to create echo agent using decoratos.
9
-
10
8
  server = Server()
11
9
 
12
10
 
13
11
  @server.agent()
14
- async def async_gen_echo(input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
12
+ async def async_gen_echo(input: Message) -> AsyncGenerator[RunYield, RunYieldResume]:
15
13
  """Echoes everything"""
16
14
  yield {"thought": "I should echo everyting"}
17
15
  yield input
@@ -25,10 +23,10 @@ async def async_echo(input: Message, context: Context) -> RunYield:
25
23
 
26
24
 
27
25
  @server.agent()
28
- def gen_echo(input: Message, context: Context) -> Generator[RunYield, RunYieldResume]:
26
+ def gen_echo(input: Message) -> Generator[RunYield, RunYieldResume]:
29
27
  """Echoes everything"""
30
28
  yield {"thought": "I should echo everyting"}
31
- return input
29
+ yield input
32
30
 
33
31
 
34
32
  @server.agent()
@@ -41,7 +39,7 @@ def sync_echo(input: Message, context: Context) -> RunYield:
41
39
  class EchoAgent(Agent):
42
40
  @property
43
41
  def name(self) -> str:
44
- return "class_echo"
42
+ return "instance_echo"
45
43
 
46
44
  @property
47
45
  def description(self) -> str:
@@ -56,4 +54,4 @@ class EchoAgent(Agent):
56
54
  server.register(EchoAgent())
57
55
 
58
56
 
59
- server.run()
57
+ server()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acp-sdk"
3
- version = "0.1.0rc5"
3
+ version = "0.1.0rc7"
4
4
  description = "Agent Communication Protocol SDK"
5
5
  readme = "README.md"
6
6
  authors = []
@@ -58,8 +58,8 @@ class Message(RootModel):
58
58
 
59
59
 
60
60
  AgentName = str
61
- SessionId = str
62
- RunId = str
61
+ SessionId = uuid.UUID
62
+ RunId = uuid.UUID
63
63
 
64
64
 
65
65
  class RunMode(str, Enum):
@@ -92,7 +92,7 @@ class AwaitResume(BaseModel):
92
92
 
93
93
 
94
94
  class Run(BaseModel):
95
- run_id: RunId = str(uuid.uuid4())
95
+ run_id: RunId = Field(default_factory=uuid.uuid4)
96
96
  agent_name: AgentName
97
97
  session_id: SessionId | None = None
98
98
  status: RunStatus = RunStatus.CREATED
@@ -1,4 +1,4 @@
1
- from pydantic import BaseModel, Field
1
+ from pydantic import BaseModel, ConfigDict, Field
2
2
 
3
3
  from acp_sdk.models.models import Agent, AgentName, AwaitResume, Message, Run, RunMode, SessionId
4
4
 
@@ -26,6 +26,8 @@ class RunResumeRequest(BaseModel):
26
26
  await_: AwaitResume = Field(alias="await")
27
27
  mode: RunMode
28
28
 
29
+ model_config = ConfigDict(populate_by_name=True)
30
+
29
31
 
30
32
  class RunResumeResponse(Run):
31
33
  pass
@@ -2,8 +2,10 @@ import asyncio
2
2
  from collections.abc import AsyncGenerator
3
3
  from concurrent.futures import ThreadPoolExecutor
4
4
  from contextlib import asynccontextmanager
5
+ from enum import Enum
5
6
 
6
7
  from fastapi import FastAPI, HTTPException, status
8
+ from fastapi.encoders import jsonable_encoder
7
9
  from fastapi.responses import JSONResponse, StreamingResponse
8
10
  from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
9
11
 
@@ -39,6 +41,10 @@ from acp_sdk.server.errors import (
39
41
  from acp_sdk.server.utils import stream_sse
40
42
 
41
43
 
44
+ class Headers(str, Enum):
45
+ RUN_ID = "Run-ID"
46
+
47
+
42
48
  def create_app(*agents: Agent) -> FastAPI:
43
49
  executor: ThreadPoolExecutor
44
50
 
@@ -102,19 +108,26 @@ def create_app(*agents: Agent) -> FastAPI:
102
108
  bundle.task = asyncio.create_task(bundle.execute(request.input, executor=executor))
103
109
  runs[bundle.run.run_id] = bundle
104
110
 
111
+ headers = {Headers.RUN_ID: str(bundle.run.run_id)}
112
+
105
113
  match request.mode:
106
114
  case RunMode.STREAM:
107
115
  return StreamingResponse(
108
116
  stream_sse(bundle),
117
+ headers=headers,
109
118
  media_type="text/event-stream",
110
119
  )
111
120
  case RunMode.SYNC:
112
121
  await bundle.join()
113
- return bundle.run
122
+ return JSONResponse(
123
+ headers=headers,
124
+ content=jsonable_encoder(bundle.run),
125
+ )
114
126
  case RunMode.ASYNC:
115
127
  return JSONResponse(
116
128
  status_code=status.HTTP_202_ACCEPTED,
117
- content=bundle.run.model_dump(),
129
+ headers=headers,
130
+ content=jsonable_encoder(bundle.run),
118
131
  )
119
132
  case _:
120
133
  raise NotImplementedError()
@@ -141,7 +154,7 @@ def create_app(*agents: Agent) -> FastAPI:
141
154
  case RunMode.ASYNC:
142
155
  return JSONResponse(
143
156
  status_code=status.HTTP_202_ACCEPTED,
144
- content=bundle.run.model_dump(),
157
+ content=jsonable_encoder(bundle.run),
145
158
  )
146
159
  case _:
147
160
  raise NotImplementedError()
@@ -156,6 +169,6 @@ def create_app(*agents: Agent) -> FastAPI:
156
169
  )
157
170
  bundle.task.cancel()
158
171
  bundle.run.status = RunStatus.CANCELLING
159
- return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=bundle.run.model_dump())
172
+ return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder(bundle.run))
160
173
 
161
174
  return app
@@ -70,7 +70,7 @@ class RunBundle:
70
70
 
71
71
  async def execute(self, input: Message, *, executor: ThreadPoolExecutor) -> None:
72
72
  with trace.get_tracer(__name__).start_as_current_span("execute"):
73
- run_logger = logging.LoggerAdapter(logger, {"run_id": self.run.run_id})
73
+ run_logger = logging.LoggerAdapter(logger, {"run_id": str(self.run.run_id)})
74
74
 
75
75
  await self.emit(CreatedEvent(run=self.run))
76
76
  try:
@@ -0,0 +1,243 @@
1
+ import asyncio
2
+ import inspect
3
+ import os
4
+ from collections.abc import AsyncGenerator, Awaitable, Coroutine, Generator
5
+ from typing import Any, Callable
6
+
7
+ import uvicorn
8
+ import uvicorn.config
9
+
10
+ from acp_sdk.models import Message
11
+ from acp_sdk.server.agent import Agent
12
+ from acp_sdk.server.app import create_app
13
+ from acp_sdk.server.context import Context
14
+ from acp_sdk.server.logging import configure_logger as configure_logger_func
15
+ from acp_sdk.server.telemetry import configure_telemetry as configure_telemetry_func
16
+ from acp_sdk.server.types import RunYield, RunYieldResume
17
+
18
+
19
+ class Server:
20
+ def __init__(self) -> None:
21
+ self._agents: list[Agent] = []
22
+ self._server: uvicorn.Server | None = None
23
+
24
+ def agent(self, name: str | None = None, description: str | None = None) -> Callable:
25
+ """Decorator to register an agent."""
26
+
27
+ def decorator(fn: Callable) -> Callable:
28
+ signature = inspect.signature(fn)
29
+ parameters = list(signature.parameters.values())
30
+
31
+ if len(parameters) == 0:
32
+ raise TypeError("The agent function must have at least 'input' argument")
33
+ if len(parameters) > 2:
34
+ raise TypeError("The agent function must have only 'input' and 'context' arguments")
35
+ if len(parameters) == 2 and parameters[1].name != "context":
36
+ raise TypeError("The second argument of the agent function must be 'context'")
37
+
38
+ has_context_param = len(parameters) == 2
39
+
40
+ agent: Agent
41
+ if inspect.isasyncgenfunction(fn):
42
+
43
+ class DecoratedAgent(Agent):
44
+ @property
45
+ def name(self) -> str:
46
+ return name or fn.__name__
47
+
48
+ @property
49
+ def description(self) -> str:
50
+ return description or fn.__doc__ or ""
51
+
52
+ async def run(self, input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
53
+ try:
54
+ gen: AsyncGenerator[RunYield, RunYieldResume] = (
55
+ fn(input, context) if has_context_param else fn(input)
56
+ )
57
+ value = None
58
+ while True:
59
+ value = yield await gen.asend(value)
60
+ except StopAsyncIteration:
61
+ pass
62
+
63
+ agent = DecoratedAgent()
64
+ elif inspect.iscoroutinefunction(fn):
65
+
66
+ class DecoratedAgent(Agent):
67
+ @property
68
+ def name(self) -> str:
69
+ return name or fn.__name__
70
+
71
+ @property
72
+ def description(self) -> str:
73
+ return description or fn.__doc__ or ""
74
+
75
+ async def run(self, input: Message, context: Context) -> Coroutine[RunYield]:
76
+ return await (fn(input, context) if has_context_param else fn(input))
77
+
78
+ agent = DecoratedAgent()
79
+ elif inspect.isgeneratorfunction(fn):
80
+
81
+ class DecoratedAgent(Agent):
82
+ @property
83
+ def name(self) -> str:
84
+ return name or fn.__name__
85
+
86
+ @property
87
+ def description(self) -> str:
88
+ return description or fn.__doc__ or ""
89
+
90
+ def run(self, input: Message, context: Context) -> Generator[RunYield, RunYieldResume]:
91
+ yield from (fn(input, context) if has_context_param else fn(input))
92
+
93
+ agent = DecoratedAgent()
94
+ else:
95
+
96
+ class DecoratedAgent(Agent):
97
+ @property
98
+ def name(self) -> str:
99
+ return name or fn.__name__
100
+
101
+ @property
102
+ def description(self) -> str:
103
+ return description or fn.__doc__ or ""
104
+
105
+ def run(self, input: Message, context: Context) -> RunYield:
106
+ return fn(input, context) if has_context_param else fn(input)
107
+
108
+ agent = DecoratedAgent()
109
+
110
+ self.register(agent)
111
+ return fn
112
+
113
+ return decorator
114
+
115
+ def register(self, *agents: Agent) -> None:
116
+ self._agents.extend(agents)
117
+
118
+ def run(
119
+ self,
120
+ configure_logger: bool = True,
121
+ configure_telemetry: bool = False,
122
+ host: str = "127.0.0.1",
123
+ port: int = 8000,
124
+ uds: str | None = None,
125
+ fd: int | None = None,
126
+ loop: uvicorn.config.LoopSetupType = "auto",
127
+ http: type[asyncio.Protocol] | uvicorn.config.HTTPProtocolType = "auto",
128
+ ws: type[asyncio.Protocol] | uvicorn.config.WSProtocolType = "auto",
129
+ ws_max_size: int = 16 * 1024 * 1024,
130
+ ws_max_queue: int = 32,
131
+ ws_ping_interval: float | None = 20.0,
132
+ ws_ping_timeout: float | None = 20.0,
133
+ ws_per_message_deflate: bool = True,
134
+ lifespan: uvicorn.config.LifespanType = "auto",
135
+ env_file: str | os.PathLike[str] | None = None,
136
+ log_config: dict[str, Any]
137
+ | str
138
+ | uvicorn.config.RawConfigParser
139
+ | uvicorn.config.IO[Any]
140
+ | None = uvicorn.config.LOGGING_CONFIG,
141
+ log_level: str | int | None = None,
142
+ access_log: bool = True,
143
+ use_colors: bool | None = None,
144
+ interface: uvicorn.config.InterfaceType = "auto",
145
+ reload: bool = False,
146
+ reload_dirs: list[str] | str | None = None,
147
+ reload_delay: float = 0.25,
148
+ reload_includes: list[str] | str | None = None,
149
+ reload_excludes: list[str] | str | None = None,
150
+ workers: int | None = None,
151
+ proxy_headers: bool = True,
152
+ server_header: bool = True,
153
+ date_header: bool = True,
154
+ forwarded_allow_ips: list[str] | str | None = None,
155
+ root_path: str = "",
156
+ limit_concurrency: int | None = None,
157
+ limit_max_requests: int | None = None,
158
+ backlog: int = 2048,
159
+ timeout_keep_alive: int = 5,
160
+ timeout_notify: int = 30,
161
+ timeout_graceful_shutdown: int | None = None,
162
+ callback_notify: Callable[..., Awaitable[None]] | None = None,
163
+ ssl_keyfile: str | os.PathLike[str] | None = None,
164
+ ssl_certfile: str | os.PathLike[str] | None = None,
165
+ ssl_keyfile_password: str | None = None,
166
+ ssl_version: int = uvicorn.config.SSL_PROTOCOL_VERSION,
167
+ ssl_cert_reqs: int = uvicorn.config.ssl.CERT_NONE,
168
+ ssl_ca_certs: str | None = None,
169
+ ssl_ciphers: str = "TLSv1",
170
+ headers: list[tuple[str, str]] | None = None,
171
+ factory: bool = False,
172
+ h11_max_incomplete_event_size: int | None = None,
173
+ ) -> None:
174
+ if self._server:
175
+ raise RuntimeError("The server is already running")
176
+
177
+ import uvicorn
178
+
179
+ if configure_logger:
180
+ configure_logger_func()
181
+ if configure_telemetry:
182
+ configure_telemetry_func()
183
+
184
+ config = uvicorn.Config(
185
+ create_app(*self._agents),
186
+ host,
187
+ port,
188
+ uds,
189
+ fd,
190
+ loop,
191
+ http,
192
+ ws,
193
+ ws_max_size,
194
+ ws_max_queue,
195
+ ws_ping_interval,
196
+ ws_ping_timeout,
197
+ ws_per_message_deflate,
198
+ lifespan,
199
+ env_file,
200
+ log_config,
201
+ log_level,
202
+ access_log,
203
+ use_colors,
204
+ interface,
205
+ reload,
206
+ reload_dirs,
207
+ reload_delay,
208
+ reload_includes,
209
+ reload_excludes,
210
+ workers,
211
+ proxy_headers,
212
+ server_header,
213
+ date_header,
214
+ forwarded_allow_ips,
215
+ root_path,
216
+ limit_concurrency,
217
+ limit_max_requests,
218
+ backlog,
219
+ timeout_keep_alive,
220
+ timeout_notify,
221
+ timeout_graceful_shutdown,
222
+ callback_notify,
223
+ ssl_keyfile,
224
+ ssl_certfile,
225
+ ssl_keyfile_password,
226
+ ssl_version,
227
+ ssl_cert_reqs,
228
+ ssl_ca_certs,
229
+ ssl_ciphers,
230
+ headers,
231
+ factory,
232
+ h11_max_incomplete_event_size,
233
+ )
234
+ self._server = uvicorn.Server(config)
235
+ self._server.run()
236
+
237
+ @property
238
+ def should_exit(self) -> bool:
239
+ return self._server.should_exit if self._server else False
240
+
241
+ @should_exit.setter
242
+ def should_exit(self, value: bool) -> None:
243
+ self._server.should_exit = value
File without changes
@@ -0,0 +1,113 @@
1
+ import time
2
+ from collections.abc import AsyncGenerator, AsyncIterator, Generator
3
+ from threading import Thread
4
+
5
+ import pytest
6
+ import pytest_asyncio
7
+ from acp_sdk.client import Client
8
+ from acp_sdk.models import Await, AwaitResume, CompletedEvent, CreatedEvent, Message, RunStatus, TextMessagePart
9
+ from acp_sdk.models.models import InProgressEvent
10
+ from acp_sdk.server import Context, Server
11
+
12
+ PORT = 8000
13
+
14
+
15
+ @pytest.fixture
16
+ def server() -> Generator[None]:
17
+ server = Server()
18
+
19
+ @server.agent()
20
+ async def echo(input: Message, context: Context) -> AsyncIterator[Message]:
21
+ yield input
22
+
23
+ @server.agent()
24
+ async def awaiter(input: Message, context: Context) -> AsyncGenerator[Message | Await, AwaitResume]:
25
+ yield Await()
26
+ yield Message(TextMessagePart(content="empty"))
27
+
28
+ thread = Thread(target=server.run, kwargs={"port": PORT}, daemon=True)
29
+ thread.start()
30
+
31
+ time.sleep(1)
32
+
33
+ yield
34
+
35
+ server.should_exit = True
36
+ thread.join(timeout=2)
37
+
38
+
39
+ @pytest_asyncio.fixture
40
+ async def client() -> AsyncIterator[Client]:
41
+ async with Client(base_url=f"http://localhost:{PORT}") as client:
42
+ yield client
43
+
44
+
45
+ @pytest.mark.asyncio
46
+ async def test_run_sync(server: Server, client: Client) -> None:
47
+ input = Message(TextMessagePart(content="Hello!"))
48
+ run = await client.run_sync(agent="echo", input=input)
49
+ assert run.status == RunStatus.COMPLETED
50
+ assert run.output == input
51
+
52
+
53
+ @pytest.mark.asyncio
54
+ async def test_run_async(server: Server, client: Client) -> None:
55
+ input = Message(TextMessagePart(content="Hello!"))
56
+ run = await client.run_async(agent="echo", input=input)
57
+ assert run.status == RunStatus.CREATED
58
+ assert run.output is None
59
+
60
+
61
+ @pytest.mark.asyncio
62
+ async def test_run_stream(server: Server, client: Client) -> None:
63
+ input = Message(TextMessagePart(content="Hello!"))
64
+ event_stream = [event async for event in client.run_stream(agent="echo", input=input)]
65
+ assert isinstance(event_stream[0], CreatedEvent)
66
+ assert isinstance(event_stream[-1], CompletedEvent)
67
+
68
+
69
+ @pytest.mark.asyncio
70
+ async def test_run_status(server: Server, client: Client) -> None:
71
+ input = Message(TextMessagePart(content="Hello!"))
72
+ run = await client.run_async(agent="echo", input=input)
73
+ while run.status in (RunStatus.CREATED, RunStatus.IN_PROGRESS):
74
+ run = await client.run_status(run_id=run.run_id)
75
+ assert run.status == RunStatus.COMPLETED
76
+
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_run_resume_sync(server: Server, client: Client) -> None:
80
+ input = Message(TextMessagePart(content="Hello!"))
81
+
82
+ run = await client.run_sync(agent="awaiter", input=input)
83
+ assert run.status == RunStatus.AWAITING
84
+ assert run.await_ is not None
85
+
86
+ run = await client.run_resume_sync(run_id=run.run_id, await_=AwaitResume())
87
+ assert run.status == RunStatus.COMPLETED
88
+ assert run.output is not None
89
+
90
+
91
+ @pytest.mark.asyncio
92
+ async def test_run_resume_async(server: Server, client: Client) -> None:
93
+ input = Message(TextMessagePart(content="Hello!"))
94
+
95
+ run = await client.run_sync(agent="awaiter", input=input)
96
+ assert run.status == RunStatus.AWAITING
97
+ assert run.await_ is not None
98
+
99
+ run = await client.run_resume_async(run_id=run.run_id, await_=AwaitResume())
100
+ assert run.status == RunStatus.AWAITING
101
+
102
+
103
+ @pytest.mark.asyncio
104
+ async def test_run_resume_stream(server: Server, client: Client) -> None:
105
+ input = Message(TextMessagePart(content="Hello!"))
106
+
107
+ run = await client.run_sync(agent="awaiter", input=input)
108
+ assert run.status == RunStatus.AWAITING
109
+ assert run.await_ is not None
110
+
111
+ event_stream = [event async for event in client.run_resume_stream(run_id=run.run_id, await_=AwaitResume())]
112
+ assert isinstance(event_stream[0], InProgressEvent)
113
+ assert isinstance(event_stream[-1], CompletedEvent)
@@ -1,120 +0,0 @@
1
- import inspect
2
- from collections.abc import AsyncGenerator, Coroutine, Generator
3
- from typing import Any, Callable
4
-
5
- from acp_sdk.models import Message
6
- from acp_sdk.server.agent import Agent
7
- from acp_sdk.server.app import create_app
8
- from acp_sdk.server.context import Context
9
- from acp_sdk.server.logging import configure_logger as configure_logger_func
10
- from acp_sdk.server.telemetry import configure_telemetry as configure_telemetry_func
11
- from acp_sdk.server.types import RunYield, RunYieldResume
12
-
13
-
14
- class Server:
15
- def __init__(self) -> None:
16
- self.agents: list[Agent] = []
17
-
18
- def agent(self, name: str | None = None, description: str | None = None) -> Callable:
19
- """Decorator to register an agent."""
20
-
21
- def decorator(fn: Callable) -> Callable:
22
- # check agent's function signature
23
- signature = inspect.signature(fn)
24
- parameters = list(signature.parameters.values())
25
-
26
- # validate agent's function
27
- if inspect.isasyncgenfunction(fn):
28
- if len(parameters) != 2:
29
- raise TypeError(
30
- "The agent generator function must have one 'input' argument and one 'context' argument"
31
- )
32
- else:
33
- if len(parameters) != 2:
34
- raise TypeError("The agent function must have one 'input' argument and one 'context' argument")
35
-
36
- agent: Agent
37
- if inspect.isasyncgenfunction(fn):
38
-
39
- class DecoratedAgent(Agent):
40
- @property
41
- def name(self) -> str:
42
- return name or fn.__name__
43
-
44
- @property
45
- def description(self) -> str:
46
- return description or fn.__doc__ or ""
47
-
48
- async def run(self, input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
49
- try:
50
- gen: AsyncGenerator[RunYield, RunYieldResume] = fn(input, context)
51
- value = None
52
- while True:
53
- value = yield await gen.asend(value)
54
- except StopAsyncIteration:
55
- pass
56
-
57
- agent = DecoratedAgent()
58
- elif inspect.iscoroutinefunction(fn):
59
-
60
- class DecoratedAgent(Agent):
61
- @property
62
- def name(self) -> str:
63
- return name or fn.__name__
64
-
65
- @property
66
- def description(self) -> str:
67
- return description or fn.__doc__ or ""
68
-
69
- async def run(self, input: Message, context: Context) -> Coroutine[RunYield]:
70
- return await fn(input, context)
71
-
72
- agent = DecoratedAgent()
73
- elif inspect.isgeneratorfunction(fn):
74
-
75
- class DecoratedAgent(Agent):
76
- @property
77
- def name(self) -> str:
78
- return name or fn.__name__
79
-
80
- @property
81
- def description(self) -> str:
82
- return description or fn.__doc__ or ""
83
-
84
- def run(self, input: Message, context: Context) -> Generator[RunYield, RunYieldResume]:
85
- yield from fn(input, context)
86
-
87
- agent = DecoratedAgent()
88
- else:
89
-
90
- class DecoratedAgent(Agent):
91
- @property
92
- def name(self) -> str:
93
- return name or fn.__name__
94
-
95
- @property
96
- def description(self) -> str:
97
- return description or fn.__doc__ or ""
98
-
99
- def run(self, input: Message, context: Context) -> RunYield:
100
- return fn(input, context)
101
-
102
- agent = DecoratedAgent()
103
-
104
- self.register(agent)
105
- return fn
106
-
107
- return decorator
108
-
109
- def register(self, *agents: Agent) -> None:
110
- self.agents.extend(agents)
111
-
112
- def run(self, configure_logger: bool = True, configure_telemetry: bool = False, **kwargs: dict[str, Any]) -> None:
113
- import uvicorn
114
-
115
- if configure_logger:
116
- configure_logger_func()
117
- if configure_telemetry:
118
- configure_telemetry_func()
119
-
120
- uvicorn.run(create_app(*self.agents), **kwargs)
File without changes
File without changes