acp-sdk 0.1.0rc6__tar.gz → 0.1.0rc8__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.0rc6 → acp_sdk-0.1.0rc8}/PKG-INFO +12 -16
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/README.md +11 -15
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/examples/clients/advanced.py +2 -2
- acp_sdk-0.1.0rc8/examples/clients/session.py +18 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/examples/clients/simple.py +2 -2
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/examples/clients/stream.py +1 -1
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/examples/servers/awaiting.py +2 -2
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/examples/servers/echo.py +4 -4
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/pyproject.toml +1 -1
- acp_sdk-0.1.0rc8/pytest.ini +5 -0
- acp_sdk-0.1.0rc8/src/acp_sdk/__init__.py +2 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/client/client.py +49 -10
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/models/models.py +14 -4
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/models/schemas.py +4 -2
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/agent.py +6 -11
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/app.py +34 -19
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/bundle.py +29 -14
- acp_sdk-0.1.0rc8/src/acp_sdk/server/server.py +265 -0
- acp_sdk-0.1.0rc8/src/acp_sdk/server/session.py +21 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/telemetry.py +7 -2
- acp_sdk-0.1.0rc8/src/acp_sdk/version.py +3 -0
- acp_sdk-0.1.0rc8/tests/conftest.py +4 -0
- acp_sdk-0.1.0rc8/tests/e2e/__init__.py +0 -0
- acp_sdk-0.1.0rc8/tests/e2e/config.py +2 -0
- acp_sdk-0.1.0rc8/tests/e2e/fixtures/__init__.py +0 -0
- acp_sdk-0.1.0rc8/tests/e2e/fixtures/client.py +12 -0
- acp_sdk-0.1.0rc8/tests/e2e/fixtures/server.py +38 -0
- acp_sdk-0.1.0rc8/tests/e2e/test_suites/__init__.py +0 -0
- acp_sdk-0.1.0rc8/tests/e2e/test_suites/test_runs.py +89 -0
- acp_sdk-0.1.0rc6/examples/servers/multi-echo.py +0 -57
- acp_sdk-0.1.0rc6/src/acp_sdk/__init__.py +0 -1
- acp_sdk-0.1.0rc6/src/acp_sdk/server/server.py +0 -122
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/.gitignore +0 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/.python-version +0 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/client/__init__.py +0 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/models/__init__.py +0 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/models/errors.py +0 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/py.typed +0 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/__init__.py +0 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/context.py +0 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/errors.py +0 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/logging.py +0 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/types.py +0 -0
- {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/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.0rc8
|
4
4
|
Summary: Agent Communication Protocol SDK
|
5
5
|
Requires-Python: <4.0,>=3.11
|
6
6
|
Requires-Dist: opentelemetry-api>=1.31.1
|
@@ -47,7 +47,7 @@ The `client` submodule exposes [httpx]() based client with simple methods for co
|
|
47
47
|
|
48
48
|
```python
|
49
49
|
async with Client(base_url="http://localhost:8000") as client:
|
50
|
-
run = await client.run_sync(agent="echo",
|
50
|
+
run = await client.run_sync(agent="echo", inputs=[Message(TextMessagePart(content="Howdy!"))])
|
51
51
|
print(run.output)
|
52
52
|
```
|
53
53
|
|
@@ -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(inputs: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
63
|
+
"""Echoes everything"""
|
64
|
+
for message in inputs:
|
65
|
+
await asyncio.sleep(0.5)
|
66
|
+
yield {"thought": "I should echo everyting"}
|
67
|
+
await asyncio.sleep(0.5)
|
68
|
+
yield message
|
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).
|
@@ -28,7 +28,7 @@ The `client` submodule exposes [httpx]() based client with simple methods for co
|
|
28
28
|
|
29
29
|
```python
|
30
30
|
async with Client(base_url="http://localhost:8000") as client:
|
31
|
-
run = await client.run_sync(agent="echo",
|
31
|
+
run = await client.run_sync(agent="echo", inputs=[Message(TextMessagePart(content="Howdy!"))])
|
32
32
|
print(run.output)
|
33
33
|
```
|
34
34
|
|
@@ -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(inputs: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
44
|
+
"""Echoes everything"""
|
45
|
+
for message in inputs:
|
46
|
+
await asyncio.sleep(0.5)
|
47
|
+
yield {"thought": "I should echo everyting"}
|
48
|
+
await asyncio.sleep(0.5)
|
49
|
+
yield message
|
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).
|
@@ -13,8 +13,8 @@ async def example() -> None:
|
|
13
13
|
# Additional client configuration
|
14
14
|
)
|
15
15
|
) as client:
|
16
|
-
run = await client.run_sync(agent="echo",
|
17
|
-
print(run.
|
16
|
+
run = await client.run_sync(agent="echo", inputs=[Message(TextMessagePart(content="Howdy!"))])
|
17
|
+
print(run.outputs)
|
18
18
|
|
19
19
|
|
20
20
|
if __name__ == "__main__":
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import asyncio
|
2
|
+
|
3
|
+
from acp_sdk.client import Client
|
4
|
+
from acp_sdk.models import (
|
5
|
+
Message,
|
6
|
+
TextMessagePart,
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
async def example() -> None:
|
11
|
+
async with Client(base_url="http://localhost:8000") as client, client.session() as session:
|
12
|
+
run = await session.run_sync(agent="historian", inputs=[Message(TextMessagePart(content="Howdy!"))])
|
13
|
+
run = await session.run_sync(agent="historian", inputs=[Message(TextMessagePart(content="Howdy again!"))])
|
14
|
+
print(run.outputs)
|
15
|
+
|
16
|
+
|
17
|
+
if __name__ == "__main__":
|
18
|
+
asyncio.run(example())
|
@@ -9,8 +9,8 @@ from acp_sdk.models import (
|
|
9
9
|
|
10
10
|
async def example() -> None:
|
11
11
|
async with Client(base_url="http://localhost:8000") as client:
|
12
|
-
run = await client.run_sync(agent="echo",
|
13
|
-
print(run.
|
12
|
+
run = await client.run_sync(agent="echo", inputs=[Message(TextMessagePart(content="Howdy!"))])
|
13
|
+
print(run.outputs)
|
14
14
|
|
15
15
|
|
16
16
|
if __name__ == "__main__":
|
@@ -6,7 +6,7 @@ from acp_sdk.models import Message, TextMessagePart
|
|
6
6
|
|
7
7
|
async def example() -> None:
|
8
8
|
async with Client(base_url="http://localhost:8000") as client:
|
9
|
-
async for event in client.run_stream(agent="echo",
|
9
|
+
async for event in client.run_stream(agent="echo", inputs=[Message(TextMessagePart(content="Howdy!"))]):
|
10
10
|
print(event)
|
11
11
|
|
12
12
|
|
@@ -13,11 +13,11 @@ server = Server()
|
|
13
13
|
|
14
14
|
|
15
15
|
@server.agent()
|
16
|
-
async def awaiting(
|
16
|
+
async def awaiting(inputs: list[Message], context: Context) -> AsyncGenerator[Message | Await | Any, AwaitResume]:
|
17
17
|
"""Greets and awaits for more data"""
|
18
18
|
yield Message(TextMessagePart(content="Hello!"))
|
19
19
|
data = yield Await()
|
20
20
|
yield Message(TextMessagePart(content=f"Thanks for {data}"))
|
21
21
|
|
22
22
|
|
23
|
-
server()
|
23
|
+
server.run()
|
@@ -10,13 +10,13 @@ server = Server()
|
|
10
10
|
|
11
11
|
|
12
12
|
@server.agent()
|
13
|
-
async def echo(
|
13
|
+
async def echo(inputs: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
14
14
|
"""Echoes everything"""
|
15
|
-
for
|
15
|
+
for message in inputs:
|
16
16
|
await asyncio.sleep(0.5)
|
17
17
|
yield {"thought": "I should echo everyting"}
|
18
18
|
await asyncio.sleep(0.5)
|
19
|
-
yield
|
19
|
+
yield message
|
20
20
|
|
21
21
|
|
22
|
-
server()
|
22
|
+
server.run()
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
import uuid
|
2
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
3
|
+
from contextlib import asynccontextmanager
|
2
4
|
from types import TracebackType
|
3
5
|
from typing import Self
|
4
6
|
|
@@ -14,6 +16,7 @@ from acp_sdk.models import (
|
|
14
16
|
AgentReadResponse,
|
15
17
|
AgentsListResponse,
|
16
18
|
AwaitResume,
|
19
|
+
CreatedEvent,
|
17
20
|
Error,
|
18
21
|
Message,
|
19
22
|
Run,
|
@@ -25,12 +28,20 @@ from acp_sdk.models import (
|
|
25
28
|
RunMode,
|
26
29
|
RunResumeRequest,
|
27
30
|
RunResumeResponse,
|
31
|
+
SessionId,
|
28
32
|
)
|
29
33
|
|
30
34
|
|
31
35
|
class Client:
|
32
|
-
def __init__(
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
*,
|
39
|
+
base_url: httpx.URL | str = "",
|
40
|
+
session_id: SessionId | None = None,
|
41
|
+
client: httpx.AsyncClient | None = None,
|
42
|
+
) -> None:
|
33
43
|
self.base_url = base_url
|
44
|
+
self.session_id = session_id
|
34
45
|
|
35
46
|
self._client = self._init_client(client)
|
36
47
|
|
@@ -51,6 +62,10 @@ class Client:
|
|
51
62
|
) -> None:
|
52
63
|
await self._client.__aexit__(exc_type, exc_value, traceback)
|
53
64
|
|
65
|
+
@asynccontextmanager
|
66
|
+
async def session(self, session_id: SessionId | None = None) -> AsyncGenerator[Self]:
|
67
|
+
yield Client(client=self._client, session_id=session_id or uuid.uuid4())
|
68
|
+
|
54
69
|
async def agents(self) -> AsyncIterator[Agent]:
|
55
70
|
response = await self._client.get("/agents")
|
56
71
|
self._raise_error(response)
|
@@ -62,30 +77,51 @@ class Client:
|
|
62
77
|
self._raise_error(response)
|
63
78
|
return AgentReadResponse.model_validate(response.json())
|
64
79
|
|
65
|
-
async def run_sync(self, *, agent: AgentName,
|
80
|
+
async def run_sync(self, *, agent: AgentName, inputs: list[Message]) -> Run:
|
66
81
|
response = await self._client.post(
|
67
82
|
"/runs",
|
68
|
-
|
83
|
+
content=RunCreateRequest(
|
84
|
+
agent_name=agent,
|
85
|
+
inputs=inputs,
|
86
|
+
mode=RunMode.SYNC,
|
87
|
+
session_id=self.session_id,
|
88
|
+
).model_dump_json(),
|
69
89
|
)
|
70
90
|
self._raise_error(response)
|
71
|
-
|
91
|
+
response = RunCreateResponse.model_validate(response.json())
|
92
|
+
self._set_session(response)
|
93
|
+
return response
|
72
94
|
|
73
|
-
async def run_async(self, *, agent: AgentName,
|
95
|
+
async def run_async(self, *, agent: AgentName, inputs: list[Message]) -> Run:
|
74
96
|
response = await self._client.post(
|
75
97
|
"/runs",
|
76
|
-
|
98
|
+
content=RunCreateRequest(
|
99
|
+
agent_name=agent,
|
100
|
+
inputs=inputs,
|
101
|
+
mode=RunMode.ASYNC,
|
102
|
+
session_id=self.session_id,
|
103
|
+
).model_dump_json(),
|
77
104
|
)
|
78
105
|
self._raise_error(response)
|
79
|
-
|
106
|
+
response = RunCreateResponse.model_validate(response.json())
|
107
|
+
self._set_session(response)
|
108
|
+
return response
|
80
109
|
|
81
|
-
async def run_stream(self, *, agent: AgentName,
|
110
|
+
async def run_stream(self, *, agent: AgentName, inputs: list[Message]) -> AsyncIterator[RunEvent]:
|
82
111
|
async with aconnect_sse(
|
83
112
|
self._client,
|
84
113
|
"POST",
|
85
114
|
"/runs",
|
86
|
-
|
115
|
+
content=RunCreateRequest(
|
116
|
+
agent_name=agent,
|
117
|
+
inputs=inputs,
|
118
|
+
mode=RunMode.STREAM,
|
119
|
+
session_id=self.session_id,
|
120
|
+
).model_dump_json(),
|
87
121
|
) as event_source:
|
88
122
|
async for event in self._validate_stream(event_source):
|
123
|
+
if isinstance(event, CreatedEvent):
|
124
|
+
self._set_session(event.run)
|
89
125
|
yield event
|
90
126
|
|
91
127
|
async def run_status(self, *, run_id: RunId) -> Run:
|
@@ -137,3 +173,6 @@ class Client:
|
|
137
173
|
response.raise_for_status()
|
138
174
|
except httpx.HTTPError:
|
139
175
|
raise ACPError(Error.model_validate(response.json()))
|
176
|
+
|
177
|
+
def _set_session(self, run: Run) -> None:
|
178
|
+
self.session_id = run.session_id
|
@@ -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):
|
@@ -91,13 +91,18 @@ class AwaitResume(BaseModel):
|
|
91
91
|
pass
|
92
92
|
|
93
93
|
|
94
|
+
class Artifact(BaseModel):
|
95
|
+
pass
|
96
|
+
|
97
|
+
|
94
98
|
class Run(BaseModel):
|
95
|
-
run_id: RunId =
|
99
|
+
run_id: RunId = Field(default_factory=uuid.uuid4)
|
96
100
|
agent_name: AgentName
|
97
101
|
session_id: SessionId | None = None
|
98
102
|
status: RunStatus = RunStatus.CREATED
|
99
103
|
await_: Await | None = Field(None, alias="await")
|
100
|
-
|
104
|
+
outputs: list[Message] = []
|
105
|
+
artifacts: list[Artifact] = []
|
101
106
|
error: Error | None = None
|
102
107
|
|
103
108
|
model_config = ConfigDict(populate_by_name=True)
|
@@ -117,6 +122,11 @@ class MessageEvent(BaseModel):
|
|
117
122
|
message: Message
|
118
123
|
|
119
124
|
|
125
|
+
class ArtifactEvent(BaseModel):
|
126
|
+
type: Literal["artifact"] = "artifact"
|
127
|
+
artifact: Artifact
|
128
|
+
|
129
|
+
|
120
130
|
class AwaitEvent(BaseModel):
|
121
131
|
type: Literal["await"] = "await"
|
122
132
|
await_: Await | None = Field(alias="await")
|
@@ -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
|
|
@@ -14,7 +14,7 @@ class AgentReadResponse(Agent):
|
|
14
14
|
class RunCreateRequest(BaseModel):
|
15
15
|
agent_name: AgentName
|
16
16
|
session_id: SessionId | None = None
|
17
|
-
|
17
|
+
inputs: list[Message]
|
18
18
|
mode: RunMode = RunMode.SYNC
|
19
19
|
|
20
20
|
|
@@ -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
|
@@ -31,19 +31,14 @@ class Agent(abc.ABC):
|
|
31
31
|
|
32
32
|
@abc.abstractmethod
|
33
33
|
def run(
|
34
|
-
self,
|
34
|
+
self, inputs: list[Message], context: Context
|
35
35
|
) -> (
|
36
36
|
AsyncGenerator[RunYield, RunYieldResume] | Generator[RunYield, RunYieldResume] | Coroutine[RunYield] | RunYield
|
37
37
|
):
|
38
38
|
pass
|
39
39
|
|
40
|
-
async def session(self, session_id: SessionId | None) -> SessionId | None:
|
41
|
-
if session_id:
|
42
|
-
raise NotImplementedError()
|
43
|
-
return None
|
44
|
-
|
45
40
|
async def execute(
|
46
|
-
self,
|
41
|
+
self, inputs: list[Message], session_id: SessionId | None, executor: ThreadPoolExecutor
|
47
42
|
) -> AsyncGenerator[RunYield, RunYieldResume]:
|
48
43
|
yield_queue: janus.Queue[RunYield] = janus.Queue()
|
49
44
|
yield_resume_queue: janus.Queue[RunYieldResume] = janus.Queue()
|
@@ -53,13 +48,13 @@ class Agent(abc.ABC):
|
|
53
48
|
)
|
54
49
|
|
55
50
|
if inspect.isasyncgenfunction(self.run):
|
56
|
-
run = asyncio.create_task(self._run_async_gen(
|
51
|
+
run = asyncio.create_task(self._run_async_gen(inputs, context))
|
57
52
|
elif inspect.iscoroutinefunction(self.run):
|
58
|
-
run = asyncio.create_task(self._run_coro(
|
53
|
+
run = asyncio.create_task(self._run_coro(inputs, context))
|
59
54
|
elif inspect.isgeneratorfunction(self.run):
|
60
|
-
run = asyncio.get_running_loop().run_in_executor(executor, self._run_gen,
|
55
|
+
run = asyncio.get_running_loop().run_in_executor(executor, self._run_gen, inputs, context)
|
61
56
|
else:
|
62
|
-
run = asyncio.get_running_loop().run_in_executor(executor, self._run_func,
|
57
|
+
run = asyncio.get_running_loop().run_in_executor(executor, self._run_func, inputs, context)
|
63
58
|
|
64
59
|
try:
|
65
60
|
while True:
|
@@ -1,9 +1,10 @@
|
|
1
|
-
import asyncio
|
2
1
|
from collections.abc import AsyncGenerator
|
3
2
|
from concurrent.futures import ThreadPoolExecutor
|
4
3
|
from contextlib import asynccontextmanager
|
4
|
+
from enum import Enum
|
5
5
|
|
6
6
|
from fastapi import FastAPI, HTTPException, status
|
7
|
+
from fastapi.encoders import jsonable_encoder
|
7
8
|
from fastapi.responses import JSONResponse, StreamingResponse
|
8
9
|
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
9
10
|
|
@@ -23,7 +24,7 @@ from acp_sdk.models import (
|
|
23
24
|
RunReadResponse,
|
24
25
|
RunResumeRequest,
|
25
26
|
RunResumeResponse,
|
26
|
-
|
27
|
+
SessionId,
|
27
28
|
)
|
28
29
|
from acp_sdk.models.errors import ACPError
|
29
30
|
from acp_sdk.server.agent import Agent
|
@@ -36,16 +37,21 @@ from acp_sdk.server.errors import (
|
|
36
37
|
http_exception_handler,
|
37
38
|
validation_exception_handler,
|
38
39
|
)
|
40
|
+
from acp_sdk.server.session import Session
|
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
|
|
45
51
|
@asynccontextmanager
|
46
52
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
47
53
|
nonlocal executor
|
48
|
-
with ThreadPoolExecutor(
|
54
|
+
with ThreadPoolExecutor() as exec:
|
49
55
|
executor = exec
|
50
56
|
yield
|
51
57
|
|
@@ -55,6 +61,7 @@ def create_app(*agents: Agent) -> FastAPI:
|
|
55
61
|
|
56
62
|
agents: dict[AgentName, Agent] = {agent.name: agent for agent in agents}
|
57
63
|
runs: dict[RunId, RunBundle] = {}
|
64
|
+
sessions: dict[SessionId, Session] = {}
|
58
65
|
|
59
66
|
app.exception_handler(ACPError)(acp_error_handler)
|
60
67
|
app.exception_handler(StarletteHTTPException)(http_exception_handler)
|
@@ -90,31 +97,41 @@ def create_app(*agents: Agent) -> FastAPI:
|
|
90
97
|
@app.post("/runs")
|
91
98
|
async def create_run(request: RunCreateRequest) -> RunCreateResponse:
|
92
99
|
agent = find_agent(request.agent_name)
|
100
|
+
|
101
|
+
session = sessions.get(request.session_id, Session()) if request.session_id else Session()
|
102
|
+
nonlocal executor
|
93
103
|
bundle = RunBundle(
|
94
104
|
agent=agent,
|
95
|
-
run=Run(
|
96
|
-
|
97
|
-
|
98
|
-
|
105
|
+
run=Run(agent_name=agent.name, session_id=session.id),
|
106
|
+
inputs=request.inputs,
|
107
|
+
history=list(session.history()),
|
108
|
+
executor=executor,
|
99
109
|
)
|
110
|
+
session.append(bundle)
|
100
111
|
|
101
|
-
nonlocal executor
|
102
|
-
bundle.task = asyncio.create_task(bundle.execute(request.input, executor=executor))
|
103
112
|
runs[bundle.run.run_id] = bundle
|
113
|
+
sessions[session.id] = session
|
114
|
+
|
115
|
+
headers = {Headers.RUN_ID: str(bundle.run.run_id)}
|
104
116
|
|
105
117
|
match request.mode:
|
106
118
|
case RunMode.STREAM:
|
107
119
|
return StreamingResponse(
|
108
120
|
stream_sse(bundle),
|
121
|
+
headers=headers,
|
109
122
|
media_type="text/event-stream",
|
110
123
|
)
|
111
124
|
case RunMode.SYNC:
|
112
125
|
await bundle.join()
|
113
|
-
return
|
126
|
+
return JSONResponse(
|
127
|
+
headers=headers,
|
128
|
+
content=jsonable_encoder(bundle.run),
|
129
|
+
)
|
114
130
|
case RunMode.ASYNC:
|
115
131
|
return JSONResponse(
|
116
132
|
status_code=status.HTTP_202_ACCEPTED,
|
117
|
-
|
133
|
+
headers=headers,
|
134
|
+
content=jsonable_encoder(bundle.run),
|
118
135
|
)
|
119
136
|
case _:
|
120
137
|
raise NotImplementedError()
|
@@ -127,8 +144,7 @@ def create_app(*agents: Agent) -> FastAPI:
|
|
127
144
|
@app.post("/runs/{run_id}")
|
128
145
|
async def resume_run(run_id: RunId, request: RunResumeRequest) -> RunResumeResponse:
|
129
146
|
bundle = find_run_bundle(run_id)
|
130
|
-
bundle.
|
131
|
-
await bundle.await_queue.put(request.await_)
|
147
|
+
await bundle.resume(request.await_)
|
132
148
|
match request.mode:
|
133
149
|
case RunMode.STREAM:
|
134
150
|
return StreamingResponse(
|
@@ -141,7 +157,7 @@ def create_app(*agents: Agent) -> FastAPI:
|
|
141
157
|
case RunMode.ASYNC:
|
142
158
|
return JSONResponse(
|
143
159
|
status_code=status.HTTP_202_ACCEPTED,
|
144
|
-
content=bundle.run
|
160
|
+
content=jsonable_encoder(bundle.run),
|
145
161
|
)
|
146
162
|
case _:
|
147
163
|
raise NotImplementedError()
|
@@ -151,11 +167,10 @@ def create_app(*agents: Agent) -> FastAPI:
|
|
151
167
|
bundle = find_run_bundle(run_id)
|
152
168
|
if bundle.run.status.is_terminal:
|
153
169
|
raise HTTPException(
|
154
|
-
status_code=
|
155
|
-
detail=f"Run
|
170
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
171
|
+
detail=f"Run in terminal status {bundle.run.status} can't be cancelled",
|
156
172
|
)
|
157
|
-
bundle.
|
158
|
-
|
159
|
-
return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=bundle.run.model_dump())
|
173
|
+
await bundle.cancel()
|
174
|
+
return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder(bundle.run))
|
160
175
|
|
161
176
|
return app
|
@@ -3,11 +3,12 @@ import logging
|
|
3
3
|
from collections.abc import AsyncGenerator
|
4
4
|
from concurrent.futures import ThreadPoolExecutor
|
5
5
|
|
6
|
-
from opentelemetry import trace
|
7
6
|
from pydantic import ValidationError
|
8
7
|
|
9
8
|
from acp_sdk.models import (
|
10
9
|
AnyModel,
|
10
|
+
Artifact,
|
11
|
+
ArtifactEvent,
|
11
12
|
Await,
|
12
13
|
AwaitEvent,
|
13
14
|
AwaitResume,
|
@@ -27,20 +28,25 @@ from acp_sdk.models import (
|
|
27
28
|
from acp_sdk.models.errors import ErrorCode
|
28
29
|
from acp_sdk.server.agent import Agent
|
29
30
|
from acp_sdk.server.logging import logger
|
31
|
+
from acp_sdk.server.telemetry import get_tracer
|
30
32
|
|
31
33
|
|
32
34
|
class RunBundle:
|
33
|
-
def __init__(
|
35
|
+
def __init__(
|
36
|
+
self, *, agent: Agent, run: Run, inputs: list[Message], history: list[Message], executor: ThreadPoolExecutor
|
37
|
+
) -> None:
|
34
38
|
self.agent = agent
|
35
39
|
self.run = run
|
36
|
-
self.
|
40
|
+
self.inputs = inputs
|
41
|
+
self.history = history
|
37
42
|
|
38
43
|
self.stream_queue: asyncio.Queue[RunEvent] = asyncio.Queue()
|
39
|
-
self.composed_message = Message()
|
40
44
|
|
41
45
|
self.await_queue: asyncio.Queue[AwaitResume] = asyncio.Queue(maxsize=1)
|
42
46
|
self.await_or_terminate_event = asyncio.Event()
|
43
47
|
|
48
|
+
self.task = asyncio.create_task(self._execute(inputs, executor=executor))
|
49
|
+
|
44
50
|
async def stream(self) -> AsyncGenerator[RunEvent]:
|
45
51
|
while True:
|
46
52
|
event = await self.stream_queue.get()
|
@@ -64,20 +70,27 @@ class RunBundle:
|
|
64
70
|
async def resume(self, resume: AwaitResume) -> None:
|
65
71
|
self.stream_queue = asyncio.Queue()
|
66
72
|
await self.await_queue.put(resume)
|
73
|
+
self.run.status = RunStatus.IN_PROGRESS
|
74
|
+
self.run.await_ = None
|
75
|
+
|
76
|
+
async def cancel(self) -> None:
|
77
|
+
self.task.cancel()
|
78
|
+
self.run.status = RunStatus.CANCELLING
|
79
|
+
self.run.await_ = None
|
67
80
|
|
68
81
|
async def join(self) -> None:
|
69
82
|
await self.await_or_terminate_event.wait()
|
70
83
|
|
71
|
-
async def
|
72
|
-
with
|
73
|
-
run_logger = logging.LoggerAdapter(logger, {"run_id": self.run.run_id})
|
84
|
+
async def _execute(self, inputs: list[Message], *, executor: ThreadPoolExecutor) -> None:
|
85
|
+
with get_tracer().start_as_current_span("run"):
|
86
|
+
run_logger = logging.LoggerAdapter(logger, {"run_id": str(self.run.run_id)})
|
74
87
|
|
75
|
-
await self.emit(CreatedEvent(run=self.run))
|
76
88
|
try:
|
77
|
-
|
78
|
-
run_logger.info("Session loaded")
|
89
|
+
await self.emit(CreatedEvent(run=self.run))
|
79
90
|
|
80
|
-
generator = self.agent.execute(
|
91
|
+
generator = self.agent.execute(
|
92
|
+
inputs=self.history + inputs, session_id=self.run.session_id, executor=executor
|
93
|
+
)
|
81
94
|
run_logger.info("Run started")
|
82
95
|
|
83
96
|
self.run.status = RunStatus.IN_PROGRESS
|
@@ -87,8 +100,11 @@ class RunBundle:
|
|
87
100
|
while True:
|
88
101
|
next = await generator.asend(await_resume)
|
89
102
|
if isinstance(next, Message):
|
90
|
-
self.
|
103
|
+
self.run.outputs.append(next)
|
91
104
|
await self.emit(MessageEvent(message=next))
|
105
|
+
elif isinstance(next, Artifact):
|
106
|
+
self.run.artifacts.append(next)
|
107
|
+
await self.emit(ArtifactEvent(artifact=next))
|
92
108
|
elif isinstance(next, Await):
|
93
109
|
self.run.await_ = next
|
94
110
|
self.run.status = RunStatus.AWAITING
|
@@ -103,7 +119,6 @@ class RunBundle:
|
|
103
119
|
)
|
104
120
|
run_logger.info("Run awaited")
|
105
121
|
await_resume = await self.await_()
|
106
|
-
self.run.status = RunStatus.IN_PROGRESS
|
107
122
|
await self.emit(InProgressEvent(run=self.run))
|
108
123
|
run_logger.info("Run resumed")
|
109
124
|
else:
|
@@ -113,7 +128,6 @@ class RunBundle:
|
|
113
128
|
except ValidationError:
|
114
129
|
raise TypeError("Invalid yield")
|
115
130
|
except StopAsyncIteration:
|
116
|
-
self.run.output = self.composed_message
|
117
131
|
self.run.status = RunStatus.COMPLETED
|
118
132
|
await self.emit(CompletedEvent(run=self.run))
|
119
133
|
run_logger.info("Run completed")
|
@@ -126,6 +140,7 @@ class RunBundle:
|
|
126
140
|
self.run.status = RunStatus.FAILED
|
127
141
|
await self.emit(FailedEvent(run=self.run))
|
128
142
|
run_logger.exception("Run failed")
|
143
|
+
raise
|
129
144
|
finally:
|
130
145
|
self.await_or_terminate_event.set()
|
131
146
|
await self.stream_queue.put(None)
|