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.
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/PKG-INFO +11 -15
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/README.md +10 -14
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/servers/awaiting.py +1 -1
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/servers/echo.py +1 -1
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/servers/multi-echo.py +5 -7
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/pyproject.toml +1 -1
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/models/models.py +3 -3
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/models/schemas.py +3 -1
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/app.py +17 -4
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/bundle.py +1 -1
- acp_sdk-0.1.0rc7/src/acp_sdk/server/server.py +243 -0
- acp_sdk-0.1.0rc7/tests/__init__.py +0 -0
- acp_sdk-0.1.0rc7/tests/test_e2e.py +113 -0
- acp_sdk-0.1.0rc5/src/acp_sdk/server/server.py +0 -120
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/.gitignore +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/.python-version +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/clients/advanced.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/clients/simple.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/examples/clients/stream.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/__init__.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/client/__init__.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/client/client.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/models/__init__.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/models/errors.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/py.typed +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/__init__.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/agent.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/context.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/errors.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/logging.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/telemetry.py +0 -0
- {acp_sdk-0.1.0rc5 → acp_sdk-0.1.0rc7}/src/acp_sdk/server/types.py +0 -0
- {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.
|
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
|
-
|
60
|
-
@property
|
61
|
-
def name(self) -> str:
|
62
|
-
return "echo"
|
59
|
+
server = Server()
|
63
60
|
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
41
|
-
@property
|
42
|
-
def name(self) -> str:
|
43
|
-
return "echo"
|
40
|
+
server = Server()
|
44
41
|
|
45
|
-
|
46
|
-
|
47
|
-
|
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).
|
@@ -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
|
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
|
26
|
+
def gen_echo(input: Message) -> Generator[RunYield, RunYieldResume]:
|
29
27
|
"""Echoes everything"""
|
30
28
|
yield {"thought": "I should echo everyting"}
|
31
|
-
|
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 "
|
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
|
57
|
+
server()
|
@@ -58,8 +58,8 @@ class Message(RootModel):
|
|
58
58
|
|
59
59
|
|
60
60
|
AgentName = str
|
61
|
-
SessionId =
|
62
|
-
RunId =
|
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 =
|
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
|
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
|
-
|
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
|
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
|
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
|
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
|