acp-sdk 0.3.3__py3-none-any.whl → 0.5.0__py3-none-any.whl

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/client/client.py CHANGED
@@ -31,6 +31,9 @@ from acp_sdk.models import (
31
31
  RunResumeResponse,
32
32
  SessionId,
33
33
  )
34
+ from acp_sdk.models.models import MessagePart
35
+
36
+ Input = list[Message] | Message | list[MessagePart] | MessagePart | list[str] | str
34
37
 
35
38
 
36
39
  class Client:
@@ -79,14 +82,15 @@ class Client:
79
82
  async def agent(self, *, name: AgentName) -> Agent:
80
83
  response = await self._client.get(f"/agents/{name}")
81
84
  self._raise_error(response)
82
- return AgentReadResponse.model_validate(response.json())
85
+ response = AgentReadResponse.model_validate(response.json())
86
+ return Agent(**response.model_dump())
83
87
 
84
- async def run_sync(self, *, agent: AgentName, inputs: list[Message]) -> Run:
88
+ async def run_sync(self, input: Input, *, agent: AgentName) -> Run:
85
89
  response = await self._client.post(
86
90
  "/runs",
87
91
  content=RunCreateRequest(
88
92
  agent_name=agent,
89
- inputs=inputs,
93
+ inputs=self._unify_inputs(input),
90
94
  mode=RunMode.SYNC,
91
95
  session_id=self._session_id,
92
96
  ).model_dump_json(),
@@ -94,14 +98,14 @@ class Client:
94
98
  self._raise_error(response)
95
99
  response = RunCreateResponse.model_validate(response.json())
96
100
  self._set_session(response)
97
- return response
101
+ return Run(**response.model_dump())
98
102
 
99
- async def run_async(self, *, agent: AgentName, inputs: list[Message]) -> Run:
103
+ async def run_async(self, input: Input, *, agent: AgentName) -> Run:
100
104
  response = await self._client.post(
101
105
  "/runs",
102
106
  content=RunCreateRequest(
103
107
  agent_name=agent,
104
- inputs=inputs,
108
+ inputs=self._unify_inputs(input),
105
109
  mode=RunMode.ASYNC,
106
110
  session_id=self._session_id,
107
111
  ).model_dump_json(),
@@ -109,16 +113,16 @@ class Client:
109
113
  self._raise_error(response)
110
114
  response = RunCreateResponse.model_validate(response.json())
111
115
  self._set_session(response)
112
- return response
116
+ return Run(**response.model_dump())
113
117
 
114
- async def run_stream(self, *, agent: AgentName, inputs: list[Message]) -> AsyncIterator[Event]:
118
+ async def run_stream(self, input: Input, *, agent: AgentName) -> AsyncIterator[Event]:
115
119
  async with aconnect_sse(
116
120
  self._client,
117
121
  "POST",
118
122
  "/runs",
119
123
  content=RunCreateRequest(
120
124
  agent_name=agent,
121
- inputs=inputs,
125
+ inputs=self._unify_inputs(input),
122
126
  mode=RunMode.STREAM,
123
127
  session_id=self._session_id,
124
128
  ).model_dump_json(),
@@ -136,25 +140,28 @@ class Client:
136
140
  async def run_cancel(self, *, run_id: RunId) -> Run:
137
141
  response = await self._client.post(f"/runs/{run_id}/cancel")
138
142
  self._raise_error(response)
139
- return RunCancelResponse.model_validate(response.json())
143
+ response = RunCancelResponse.model_validate(response.json())
144
+ return Run(**response.model_dump())
140
145
 
141
- async def run_resume_sync(self, *, run_id: RunId, await_resume: AwaitResume) -> Run:
146
+ async def run_resume_sync(self, await_resume: AwaitResume, *, run_id: RunId) -> Run:
142
147
  response = await self._client.post(
143
148
  f"/runs/{run_id}",
144
149
  content=RunResumeRequest(await_resume=await_resume, mode=RunMode.SYNC).model_dump_json(),
145
150
  )
146
151
  self._raise_error(response)
147
- return RunResumeResponse.model_validate(response.json())
152
+ response = RunResumeResponse.model_validate(response.json())
153
+ return Run(**response.model_dump())
148
154
 
149
- async def run_resume_async(self, *, run_id: RunId, await_resume: AwaitResume) -> Run:
155
+ async def run_resume_async(self, await_resume: AwaitResume, *, run_id: RunId) -> Run:
150
156
  response = await self._client.post(
151
157
  f"/runs/{run_id}",
152
158
  content=RunResumeRequest(await_resume=await_resume, mode=RunMode.ASYNC).model_dump_json(),
153
159
  )
154
160
  self._raise_error(response)
155
- return RunResumeResponse.model_validate(response.json())
161
+ response = RunResumeResponse.model_validate(response.json())
162
+ return Run(**response.model_dump())
156
163
 
157
- async def run_resume_stream(self, *, run_id: RunId, await_resume: AwaitResume) -> AsyncIterator[Event]:
164
+ async def run_resume_stream(self, await_resume: AwaitResume, *, run_id: RunId) -> AsyncIterator[Event]:
158
165
  async with aconnect_sse(
159
166
  self._client,
160
167
  "POST",
@@ -183,3 +190,24 @@ class Client:
183
190
 
184
191
  def _set_session(self, run: Run) -> None:
185
192
  self._session_id = run.session_id
193
+
194
+ def _unify_inputs(self, input: Input) -> list[Message]:
195
+ if isinstance(input, list):
196
+ if len(input) == 0:
197
+ return []
198
+ if all(isinstance(item, Message) for item in input):
199
+ return input
200
+ elif all(isinstance(item, MessagePart) for item in input):
201
+ return [Message(parts=input)]
202
+ elif all(isinstance(item, str) for item in input):
203
+ return [Message(parts=[MessagePart(content=content) for content in input])]
204
+ else:
205
+ raise RuntimeError("List with mixed types is not supported")
206
+ else:
207
+ if isinstance(input, str):
208
+ input = MessagePart(content=input)
209
+ if isinstance(input, MessagePart):
210
+ input = Message(parts=[input])
211
+ if isinstance(input, Message):
212
+ input = [input]
213
+ return input
acp_sdk/server/app.py CHANGED
@@ -1,8 +1,10 @@
1
1
  from collections.abc import AsyncGenerator
2
2
  from concurrent.futures import ThreadPoolExecutor
3
3
  from contextlib import asynccontextmanager
4
+ from datetime import datetime, timedelta
4
5
  from enum import Enum
5
6
 
7
+ from cachetools import TTLCache
6
8
  from fastapi import FastAPI, HTTPException, status
7
9
  from fastapi.encoders import jsonable_encoder
8
10
  from fastapi.responses import JSONResponse, StreamingResponse
@@ -45,7 +47,7 @@ class Headers(str, Enum):
45
47
  RUN_ID = "Run-ID"
46
48
 
47
49
 
48
- def create_app(*agents: Agent) -> FastAPI:
50
+ def create_app(*agents: Agent, run_limit: int = 1000, run_ttl: timedelta = timedelta(hours=1)) -> FastAPI:
49
51
  executor: ThreadPoolExecutor
50
52
 
51
53
  @asynccontextmanager
@@ -60,8 +62,8 @@ def create_app(*agents: Agent) -> FastAPI:
60
62
  FastAPIInstrumentor.instrument_app(app)
61
63
 
62
64
  agents: dict[AgentName, Agent] = {agent.name: agent for agent in agents}
63
- runs: dict[RunId, RunBundle] = {}
64
- sessions: dict[SessionId, Session] = {}
65
+ runs: TTLCache[RunId, RunBundle] = TTLCache(maxsize=run_limit, ttl=run_ttl, timer=datetime.now)
66
+ sessions: TTLCache[SessionId, Session] = TTLCache(maxsize=run_limit, ttl=run_ttl, timer=datetime.now)
65
67
 
66
68
  app.exception_handler(ACPError)(acp_error_handler)
67
69
  app.exception_handler(StarletteHTTPException)(http_exception_handler)
acp_sdk/server/bundle.py CHANGED
@@ -3,7 +3,7 @@ import logging
3
3
  from collections.abc import AsyncGenerator
4
4
  from concurrent.futures import ThreadPoolExecutor
5
5
 
6
- from pydantic import ValidationError
6
+ from pydantic import BaseModel, ValidationError
7
7
 
8
8
  from acp_sdk.instrumentation import get_tracer
9
9
  from acp_sdk.models import (
@@ -88,6 +88,13 @@ class RunBundle:
88
88
  run_logger = logging.LoggerAdapter(logger, {"run_id": str(self.run.run_id)})
89
89
 
90
90
  in_message = False
91
+
92
+ async def flush_message() -> None:
93
+ nonlocal in_message
94
+ if in_message:
95
+ await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
96
+ in_message = False
97
+
91
98
  try:
92
99
  await self.emit(RunCreatedEvent(run=self.run))
93
100
 
@@ -103,7 +110,9 @@ class RunBundle:
103
110
  while True:
104
111
  next = await generator.asend(await_resume)
105
112
 
106
- if isinstance(next, MessagePart):
113
+ if isinstance(next, (MessagePart, str)):
114
+ if isinstance(next, str):
115
+ next = MessagePart(content=next)
107
116
  if not in_message:
108
117
  self.run.outputs.append(Message(parts=[]))
109
118
  in_message = True
@@ -111,9 +120,7 @@ class RunBundle:
111
120
  self.run.outputs[-1].parts.append(next)
112
121
  await self.emit(MessagePartEvent(part=next))
113
122
  elif isinstance(next, Message):
114
- if in_message:
115
- await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
116
- in_message = False
123
+ await flush_message()
117
124
  self.run.outputs.append(next)
118
125
  await self.emit(MessageCreatedEvent(message=next))
119
126
  for part in next.parts:
@@ -130,7 +137,9 @@ class RunBundle:
130
137
  elif isinstance(next, Error):
131
138
  raise ACPError(error=next)
132
139
  elif next is None:
133
- pass # Do nothing
140
+ await flush_message()
141
+ elif isinstance(next, BaseModel):
142
+ await self.emit(GenericEvent(generic=AnyModel(**next.model_dump())))
134
143
  else:
135
144
  try:
136
145
  generic = AnyModel.model_validate(next)
@@ -138,8 +147,7 @@ class RunBundle:
138
147
  except ValidationError:
139
148
  raise TypeError("Invalid yield")
140
149
  except StopAsyncIteration:
141
- if in_message:
142
- await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
150
+ await flush_message()
143
151
  self.run.status = RunStatus.COMPLETED
144
152
  await self.emit(RunCompletedEvent(run=self.run))
145
153
  run_logger.info("Run completed")
acp_sdk/server/errors.py CHANGED
@@ -33,7 +33,7 @@ def status_code_to_error_code(status_code: int) -> ErrorCode:
33
33
  async def acp_error_handler(request: Request, exc: ACPError, *, status_code: int | None = None) -> JSONResponse:
34
34
  error = exc.error
35
35
  return JSONResponse(
36
- status_code=status_code or error_code_to_status_code(error.code), content=error.model_dump_json()
36
+ status_code=status_code or error_code_to_status_code(error.code), content=error.model_dump(mode="json")
37
37
  )
38
38
 
39
39
 
acp_sdk/server/server.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import os
3
3
  from collections.abc import Awaitable
4
+ from datetime import timedelta
4
5
  from typing import Any, Callable
5
6
 
6
7
  import uvicorn
@@ -42,6 +43,8 @@ class Server:
42
43
  self,
43
44
  configure_logger: bool = True,
44
45
  configure_telemetry: bool = False,
46
+ run_limit: int = 1000,
47
+ run_ttl: timedelta = timedelta(hours=1),
45
48
  host: str = "127.0.0.1",
46
49
  port: int = 8000,
47
50
  uds: str | None = None,
@@ -105,7 +108,7 @@ class Server:
105
108
  configure_telemetry_func()
106
109
 
107
110
  config = uvicorn.Config(
108
- create_app(*self._agents),
111
+ create_app(*self._agents, run_limit=run_limit, run_ttl=run_ttl),
109
112
  host,
110
113
  port,
111
114
  uds,
acp_sdk/server/types.py CHANGED
@@ -1,6 +1,9 @@
1
1
  from typing import Any
2
2
 
3
+ from pydantic import BaseModel
4
+
3
5
  from acp_sdk.models import AwaitRequest, AwaitResume, Message
6
+ from acp_sdk.models.models import MessagePart
4
7
 
5
- RunYield = Message | AwaitRequest | dict[str | Any] | None
8
+ RunYield = Message | MessagePart | str | AwaitRequest | BaseModel | dict[str | Any] | None
6
9
  RunYieldResume = AwaitResume | None
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: acp-sdk
3
+ Version: 0.5.0
4
+ Summary: Agent Communication Protocol SDK
5
+ Author: IBM Corp.
6
+ Maintainer-email: Tomas Pilar <thomas7pilar@gmail.com>
7
+ License-Expression: Apache-2.0
8
+ Requires-Python: <4.0,>=3.11
9
+ Requires-Dist: cachetools>=5.5.2
10
+ Requires-Dist: fastapi[standard]>=0.115.8
11
+ Requires-Dist: httpx-sse>=0.4.0
12
+ Requires-Dist: httpx>=0.26.0
13
+ Requires-Dist: janus>=2.0.0
14
+ Requires-Dist: opentelemetry-api>=1.31.1
15
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.31.1
16
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.52b1
17
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.52b1
18
+ Requires-Dist: opentelemetry-sdk>=1.31.1
19
+ Requires-Dist: pydantic>=2.11.1
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Agent Communication Protocol SDK for Python
23
+
24
+ Agent Communication Protocol SDK for Python provides allows developers to serve and consume agents over the Agent Communication Protocol.
25
+
26
+ ## Prerequisites
27
+
28
+ ✅ Python >= 3.11
29
+
30
+ ## Installation
31
+
32
+ Install according to your Python package manager:
33
+
34
+ - `uv add acp-sdk`
35
+ - `pip install acp-sdk`
36
+ - `poetry add acp-sdk`
37
+ - ...
38
+
39
+ ## Quickstart
40
+
41
+ Register an agent and run the server:
42
+
43
+ ```py
44
+ server = Server()
45
+
46
+ @server.agent()
47
+ async def echo(inputs: list[Message]):
48
+ """Echoes everything"""
49
+ for message in inputs:
50
+ yield message
51
+
52
+ server.run(port=8000)
53
+ ```
54
+
55
+ From another process, connect to the server and run the agent:
56
+
57
+ ```py
58
+ async with Client(base_url="http://localhost:8000") as client:
59
+ run = await client.run_sync(agent="echo", inputs=[Message(parts=[MessagePart(content="Howdy!")])])
60
+ print(run)
61
+
62
+ ```
63
+
64
+
65
+ ➡️ Explore more in our [examples library](/examples/python).
@@ -3,23 +3,23 @@ acp_sdk/instrumentation.py,sha256=JqSyvILN3sGAfOZrmckQq4-M_4_5alyPn95DK0o5lfA,16
3
3
  acp_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  acp_sdk/version.py,sha256=Niy83rgvigB4hL_rR-O4ySvI7dj6xnqkyOe_JTymi9s,73
5
5
  acp_sdk/client/__init__.py,sha256=Bca1DORrswxzZsrR2aUFpATuNG2xNSmYvF1Z2WJaVbc,51
6
- acp_sdk/client/client.py,sha256=lEUs0Oc7MZbKkTF2E6e8Wn3dTW5cMVj6fD8TbuEOMDk,6584
6
+ acp_sdk/client/client.py,sha256=b8hiX7aymhKXOOgdUnhDXlP43Z_67btJg_gIPh4IaQ4,7893
7
7
  acp_sdk/models/__init__.py,sha256=numSDBDT1QHx7n_Y3Deb5VOvKWcUBxbOEaMwQBSRHxc,151
8
8
  acp_sdk/models/errors.py,sha256=rEyaMVvQuBi7fwWe_d0PGGySYsD3FZTluQ-SkC0yhAs,444
9
9
  acp_sdk/models/models.py,sha256=eOoOfftPKbTaLlqZlE4gE1ZwDkrQ_FdRiDx4wSJILP4,6539
10
10
  acp_sdk/models/schemas.py,sha256=Kj7drJSR8d-N3KHzu_qTnLdagrMtAyhid5swluuhHTw,645
11
11
  acp_sdk/server/__init__.py,sha256=mxBBBFaZuMEUENRMLwp1XZkuLeT9QghcFmNvjnqvAAU,377
12
12
  acp_sdk/server/agent.py,sha256=DhcPDPDL9jpDST40K_bInvDXfpF1cwxIhqXzu8z0blU,6203
13
- acp_sdk/server/app.py,sha256=3QJzGi05DKoIKyTeYYKKQhcYz4qorQKmVI94_guVkEY,6340
14
- acp_sdk/server/bundle.py,sha256=6LZnxsP1rawxic9CwDAQCsOV1v31qNI9meKXMty_yWg,6260
13
+ acp_sdk/server/app.py,sha256=YcW5kEwmRER9LKRsLt2vNqFso4_nnbWabdjiN-4_0-0,6601
14
+ acp_sdk/server/bundle.py,sha256=nLxQMUCSVzlmEgSUYRunnpD3jO2yE2w1XVvKW6NOTPE,6564
15
15
  acp_sdk/server/context.py,sha256=MgnLV6qcDIhc_0BjW7r4Jj1tHts4ZuwpdTGIBnz2Mgo,1036
16
- acp_sdk/server/errors.py,sha256=IGtpPpb2ChtYvCac8kf_P-RkcY71gBvX0yq97uZWa-w,2104
16
+ acp_sdk/server/errors.py,sha256=GSO8yYIqEeX8Y4Lz86ks35dMTHiQiXuOrLYYx0eXsbI,2110
17
17
  acp_sdk/server/logging.py,sha256=Oc8yZigCsuDnHHPsarRzu0RX3NKaLEgpELM2yovGKDI,411
18
- acp_sdk/server/server.py,sha256=-eT3fmnEsBUN44Spi2EP2eV0l4RAlKa8bzqxnhz16SM,5399
18
+ acp_sdk/server/server.py,sha256=6mIVyURgJvcZCPad1CV8TcgHC2mqyEFCGgGATGyP34Q,5548
19
19
  acp_sdk/server/session.py,sha256=0cDr924HC5x2bBNbK9NSKVHAt5A_mi5dK8P4jP_ugq0,629
20
20
  acp_sdk/server/telemetry.py,sha256=1BUxNg-xL_Vqgs27PDWNc3HikrQW2lidAtT_FKlp_Qk,1833
21
- acp_sdk/server/types.py,sha256=E0_9xWwgGzyvJjxtbeBBmSbIPhbbTSXLpHFL5dZDzxI,182
21
+ acp_sdk/server/types.py,sha256=teBNRWSks8XP1SCQKGEtbNWQahVD3RAOPnysTxcQPxI,292
22
22
  acp_sdk/server/utils.py,sha256=EfrF9VCyVk3AM_ao-BIB9EzGbfTrh4V2Bz-VFr6f6Sg,351
23
- acp_sdk-0.3.3.dist-info/METADATA,sha256=X4iVzszNG3pWp7eOLPHz6cxaGI4rmMN8MrtCx1ICTOI,3120
24
- acp_sdk-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- acp_sdk-0.3.3.dist-info/RECORD,,
23
+ acp_sdk-0.5.0.dist-info/METADATA,sha256=0Idz5Cn8GmBGtSnDUzzbLi3kmv-EGE6_vJVzbbP788k,1654
24
+ acp_sdk-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ acp_sdk-0.5.0.dist-info/RECORD,,
@@ -1,99 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: acp-sdk
3
- Version: 0.3.3
4
- Summary: Agent Communication Protocol SDK
5
- Author: IBM Corp.
6
- Maintainer-email: Tomas Pilar <thomas7pilar@gmail.com>
7
- License-Expression: Apache-2.0
8
- Requires-Python: <4.0,>=3.11
9
- Requires-Dist: fastapi[standard]>=0.115.8
10
- Requires-Dist: httpx-sse>=0.4.0
11
- Requires-Dist: httpx>=0.28.1
12
- Requires-Dist: janus>=2.0.0
13
- Requires-Dist: opentelemetry-api>=1.31.1
14
- Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.31.1
15
- Requires-Dist: opentelemetry-instrumentation-fastapi>=0.52b1
16
- Requires-Dist: opentelemetry-instrumentation-httpx>=0.52b1
17
- Requires-Dist: opentelemetry-sdk>=1.31.1
18
- Requires-Dist: pydantic>=2.11.1
19
- Description-Content-Type: text/markdown
20
-
21
- # Agent Communication Protocol SDK for Python
22
-
23
- Agent Communication Protocol SDK for Python provides allows developers to serve and consume agents over the Agent Communication Protocol.
24
-
25
- ## Prerequisites
26
-
27
- ✅ Python >= 3.11
28
-
29
- ## Installation
30
-
31
- Install with:
32
-
33
- ```shell
34
- pip install acp-sdk
35
- ```
36
-
37
- ## Overview
38
-
39
- ### Core
40
-
41
- The core of the SDK exposes [pydantic](https://docs.pydantic.dev/) data models corresponding to REST API requests, responses, resources, events and errors.
42
-
43
-
44
- ### Client
45
-
46
- The `client` submodule exposes [httpx](https://www.python-httpx.org/) based client with simple methods for communication over ACP.
47
-
48
- ```python
49
- async with Client(base_url="http://localhost:8000") as client:
50
- run = await client.run_sync(agent="echo", inputs=[Message(parts=[MessagePart(content="Howdy!")])])
51
- print(run)
52
-
53
- ```
54
-
55
- ### Server
56
-
57
- The `server` submodule exposes `Agent` class and `agent` decorator together with [fastapi](https://fastapi.tiangolo.com/) application factory, making it easy to expose agents over ACP. Additionaly, it exposes [uvicorn](https://www.uvicorn.org/) based server to serve agents with set up logging, [opentelemetry](https://opentelemetry.io/) and more.
58
-
59
- ```python
60
- server = Server()
61
-
62
- @server.agent()
63
- async def echo(inputs: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
64
- """Echoes everything"""
65
- for message in inputs:
66
- yield {"thought": "I should echo everything"}
67
- await asyncio.sleep(0.5)
68
- yield message
69
-
70
-
71
- server.run()
72
- ```
73
-
74
- ➡️ Explore more in our [examples library](/python/examples).
75
-
76
- ## Architecture
77
-
78
- The architecture of the SDK is outlined in the following segment. It focuses on central parts of the SDK without going into much detail.
79
-
80
- ### Models
81
-
82
- The core of the SDK contains pydantic models for requests, responses, resources, events and errors. Users of the SDK are meant to use these models directly or indirectly.
83
-
84
- ### Server
85
-
86
- The server module consists of 3 parts:
87
-
88
- 1. Agent interface
89
- 2. FastAPI application factory
90
- 3. Uvicorn based server
91
-
92
- Each part builds on top of the previous one. Not all parts need to be used, e.g. users are advised to bring their own ASGI server for production deployments.
93
-
94
- ### Client
95
-
96
- The client module consists of httpx based client with session support. The client is meant to be thin and mimic the REST API. Exception is session management which has been abstracted into a context manager.
97
-
98
-
99
-