acp-sdk 0.7.1__py3-none-any.whl → 0.7.3__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
@@ -25,7 +25,6 @@ from acp_sdk.models import (
25
25
  Event,
26
26
  Run,
27
27
  RunCancelResponse,
28
- RunCreatedEvent,
29
28
  RunCreateRequest,
30
29
  RunCreateResponse,
31
30
  RunId,
@@ -120,6 +119,11 @@ class Client:
120
119
  response = AgentReadResponse.model_validate(response.json())
121
120
  return Agent(**response.model_dump())
122
121
 
122
+ async def ping(self) -> bool:
123
+ response = await self._client.get("/healthcheck")
124
+ self._raise_error(response)
125
+ return response.json() == "OK"
126
+
123
127
  async def run_sync(self, input: Input, *, agent: AgentName) -> Run:
124
128
  response = await self._client.post(
125
129
  "/runs",
@@ -132,7 +136,6 @@ class Client:
132
136
  )
133
137
  self._raise_error(response)
134
138
  response = RunCreateResponse.model_validate(response.json())
135
- self._set_session(response)
136
139
  return Run(**response.model_dump())
137
140
 
138
141
  async def run_async(self, input: Input, *, agent: AgentName) -> Run:
@@ -147,7 +150,6 @@ class Client:
147
150
  )
148
151
  self._raise_error(response)
149
152
  response = RunCreateResponse.model_validate(response.json())
150
- self._set_session(response)
151
153
  return Run(**response.model_dump())
152
154
 
153
155
  async def run_stream(self, input: Input, *, agent: AgentName) -> AsyncIterator[Event]:
@@ -163,8 +165,6 @@ class Client:
163
165
  ).model_dump_json(),
164
166
  ) as event_source:
165
167
  async for event in self._validate_stream(event_source):
166
- if isinstance(event, RunCreatedEvent):
167
- self._set_session(event.run)
168
168
  yield event
169
169
 
170
170
  async def run_status(self, *, run_id: RunId) -> Run:
@@ -222,6 +222,3 @@ class Client:
222
222
  response.raise_for_status()
223
223
  except httpx.HTTPError:
224
224
  raise ACPError(Error.model_validate(response.json()))
225
-
226
- def _set_session(self, run: Run) -> None:
227
- self._session_id = run.session_id
acp_sdk/models/models.py CHANGED
@@ -47,6 +47,11 @@ class Dependency(BaseModel):
47
47
  name: str
48
48
 
49
49
 
50
+ class Capability(BaseModel):
51
+ name: str
52
+ description: str
53
+
54
+
50
55
  class Metadata(BaseModel):
51
56
  annotations: AnyModel | None = None
52
57
  documentation: str | None = None
@@ -54,7 +59,8 @@ class Metadata(BaseModel):
54
59
  programming_language: str | None = None
55
60
  natural_languages: list[str] | None = None
56
61
  framework: str | None = None
57
- use_cases: list[str] | None = None
62
+ capabilities: list[Capability] | None = None
63
+ domains: list[str] | None = None
58
64
  tags: list[str] | None = None
59
65
  created_at: datetime | None = None
60
66
  updated_at: datetime | None = None
@@ -93,7 +99,7 @@ class Message(BaseModel):
93
99
  def __add__(self, other: "Message") -> "Message":
94
100
  if not isinstance(other, Message):
95
101
  raise TypeError(f"Cannot concatenate Message with {type(other).__name__}")
96
- return Message(*(self.parts + other.parts))
102
+ return Message(parts=self.parts + other.parts)
97
103
 
98
104
  def __str__(self) -> str:
99
105
  return "".join(
acp_sdk/server/app.py CHANGED
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta
5
5
  from enum import Enum
6
6
 
7
7
  from cachetools import TTLCache
8
- from fastapi import FastAPI, HTTPException, status
8
+ from fastapi import Depends, FastAPI, HTTPException, status
9
9
  from fastapi.encoders import jsonable_encoder
10
10
  from fastapi.responses import JSONResponse, StreamingResponse
11
11
  from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
@@ -47,7 +47,12 @@ class Headers(str, Enum):
47
47
  RUN_ID = "Run-ID"
48
48
 
49
49
 
50
- def create_app(*agents: Agent, run_limit: int = 1000, run_ttl: timedelta = timedelta(hours=1)) -> FastAPI:
50
+ def create_app(
51
+ *agents: Agent,
52
+ run_limit: int = 1000,
53
+ run_ttl: timedelta = timedelta(hours=1),
54
+ dependencies: list[Depends] | None = None,
55
+ ) -> FastAPI:
51
56
  executor: ThreadPoolExecutor
52
57
 
53
58
  @asynccontextmanager
@@ -57,7 +62,10 @@ def create_app(*agents: Agent, run_limit: int = 1000, run_ttl: timedelta = timed
57
62
  executor = exec
58
63
  yield
59
64
 
60
- app = FastAPI(lifespan=lifespan)
65
+ app = FastAPI(
66
+ lifespan=lifespan,
67
+ dependencies=dependencies,
68
+ )
61
69
 
62
70
  FastAPIInstrumentor.instrument_app(app)
63
71
 
@@ -96,11 +104,15 @@ def create_app(*agents: Agent, run_limit: int = 1000, run_ttl: timedelta = timed
96
104
  agent = find_agent(name)
97
105
  return AgentModel(name=agent.name, description=agent.description, metadata=agent.metadata)
98
106
 
107
+ @app.get("/healthcheck")
108
+ async def healthcheck() -> str:
109
+ return "OK"
110
+
99
111
  @app.post("/runs")
100
112
  async def create_run(request: RunCreateRequest) -> RunCreateResponse:
101
113
  agent = find_agent(request.agent_name)
102
114
 
103
- session = sessions.get(request.session_id, Session()) if request.session_id else Session()
115
+ session = sessions.get(request.session_id, Session(id=request.session_id)) if request.session_id else Session()
104
116
  nonlocal executor
105
117
  bundle = RunBundle(
106
118
  agent=agent,
acp_sdk/server/server.py CHANGED
@@ -4,6 +4,7 @@ from collections.abc import Awaitable
4
4
  from datetime import timedelta
5
5
  from typing import Any, Callable
6
6
 
7
+ import requests
7
8
  import uvicorn
8
9
  import uvicorn.config
9
10
 
@@ -12,7 +13,9 @@ from acp_sdk.server.agent import Agent
12
13
  from acp_sdk.server.agent import agent as agent_decorator
13
14
  from acp_sdk.server.app import create_app
14
15
  from acp_sdk.server.logging import configure_logger as configure_logger_func
16
+ from acp_sdk.server.logging import logger
15
17
  from acp_sdk.server.telemetry import configure_telemetry as configure_telemetry_func
18
+ from acp_sdk.server.utils import async_request_with_retry
16
19
 
17
20
 
18
21
  class Server:
@@ -43,6 +46,7 @@ class Server:
43
46
  self,
44
47
  configure_logger: bool = True,
45
48
  configure_telemetry: bool = False,
49
+ self_registration: bool = True,
46
50
  run_limit: int = 1000,
47
51
  run_ttl: timedelta = timedelta(hours=1),
48
52
  host: str = "127.0.0.1",
@@ -158,7 +162,14 @@ class Server:
158
162
  h11_max_incomplete_event_size,
159
163
  )
160
164
  self._server = uvicorn.Server(config)
161
- self._server.run()
165
+
166
+ asyncio.run(self._serve(self_registration=self_registration))
167
+
168
+ async def _serve(self, self_registration: bool = True) -> None:
169
+ registration_task = asyncio.create_task(self._register_agent()) if self_registration else None
170
+ await self._server.serve()
171
+ if registration_task:
172
+ registration_task.cancel()
162
173
 
163
174
  @property
164
175
  def should_exit(self) -> bool:
@@ -167,3 +178,45 @@ class Server:
167
178
  @should_exit.setter
168
179
  def should_exit(self, value: bool) -> None:
169
180
  self._server.should_exit = value
181
+
182
+ async def _register_agent(self) -> None:
183
+ """If not in PRODUCTION mode, register agent to the beeai platform and provide missing env variables"""
184
+ if os.getenv("PRODUCTION_MODE", False):
185
+ logger.debug("Agent is not automatically registered in the production mode.")
186
+ return
187
+
188
+ url = os.getenv("PLATFORM_URL", "http://127.0.0.1:8333")
189
+ for agent in self._agents:
190
+ request_data = {
191
+ "location": f"http://{self._server.config.host}:{self._server.config.port}",
192
+ "id": agent.name,
193
+ }
194
+ try:
195
+ await async_request_with_retry(
196
+ lambda client, data=request_data: client.post(
197
+ f"{url}/api/v1/provider/register/unmanaged", json=data
198
+ )
199
+ )
200
+ logger.info("Agent registered to the beeai server.")
201
+
202
+ # check missing env keyes
203
+ envs_request = await async_request_with_retry(lambda client: client.get(f"{url}/api/v1/env"))
204
+ envs = envs_request.get("env")
205
+
206
+ # register all available envs
207
+ missing_keyes = []
208
+ for env in agent.metadata.model_dump().get("env", []):
209
+ server_env = envs.get(env.get("name"))
210
+ if server_env:
211
+ logger.debug(f"Env variable {env['name']} = '{server_env}' added dynamically")
212
+ os.environ[env["name"]] = server_env
213
+ elif env.get("required"):
214
+ missing_keyes.append(env)
215
+ if len(missing_keyes):
216
+ logger.error(f"Can not run agent, missing required env variables: {missing_keyes}")
217
+ raise Exception("Missing env variables")
218
+
219
+ except requests.exceptions.ConnectionError as e:
220
+ logger.warning(f"Can not reach server, check if running on {url} : {e}")
221
+ except (requests.exceptions.HTTPError, Exception) as e:
222
+ logger.warning(f"Agent can not be registered to beeai server: {e}")
acp_sdk/server/session.py CHANGED
@@ -7,8 +7,8 @@ from acp_sdk.server.bundle import RunBundle
7
7
 
8
8
 
9
9
  class Session:
10
- def __init__(self) -> None:
11
- self.id: SessionId = uuid.uuid4()
10
+ def __init__(self, id: SessionId | None = None) -> None:
11
+ self.id: SessionId = id or uuid.uuid4()
12
12
  self.bundles: list[RunBundle] = []
13
13
 
14
14
  def append(self, bundle: RunBundle) -> None:
acp_sdk/server/utils.py CHANGED
@@ -1,8 +1,13 @@
1
- from collections.abc import AsyncGenerator
1
+ import asyncio
2
+ from collections.abc import AsyncGenerator, Coroutine
3
+ from typing import Any, Callable
2
4
 
5
+ import httpx
6
+ import requests
3
7
  from pydantic import BaseModel
4
8
 
5
9
  from acp_sdk.server.bundle import RunBundle
10
+ from acp_sdk.server.logging import logger
6
11
 
7
12
 
8
13
  def encode_sse(model: BaseModel) -> str:
@@ -12,3 +17,33 @@ def encode_sse(model: BaseModel) -> str:
12
17
  async def stream_sse(bundle: RunBundle) -> AsyncGenerator[str]:
13
18
  async for event in bundle.stream():
14
19
  yield encode_sse(event)
20
+
21
+
22
+ async def async_request_with_retry(
23
+ request_func: Callable[[httpx.AsyncClient], Coroutine[Any, Any, httpx.Response]],
24
+ max_retries: int = 5,
25
+ backoff_factor: float = 1,
26
+ ) -> dict[str, Any]:
27
+ async with httpx.AsyncClient() as client:
28
+ retries = 0
29
+ while retries < max_retries:
30
+ try:
31
+ response = await request_func(client)
32
+ response.raise_for_status()
33
+ return response.json()
34
+ except httpx.HTTPStatusError as e:
35
+ if e.response.status_code in [429, 500, 502, 503, 504, 509]:
36
+ retries += 1
37
+ backoff = backoff_factor * (2 ** (retries - 1))
38
+ logger.warning(f"Request retry (try {retries}/{max_retries}), waiting {backoff} seconds...")
39
+ await asyncio.sleep(backoff)
40
+ else:
41
+ logger.debug("A non-retryable error was encountered.")
42
+ raise
43
+ except httpx.RequestError:
44
+ retries += 1
45
+ backoff = backoff_factor * (2 ** (retries - 1))
46
+ logger.warning(f"Request retry (try {retries}/{max_retries}), waiting {backoff} seconds...")
47
+ await asyncio.sleep(backoff)
48
+
49
+ raise requests.exceptions.ConnectionError(f"Request failed after {max_retries} retries.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acp-sdk
3
- Version: 0.7.1
3
+ Version: 0.7.3
4
4
  Summary: Agent Communication Protocol SDK
5
5
  Author: IBM Corp.
6
6
  Maintainer-email: Tomas Pilar <thomas7pilar@gmail.com>
@@ -3,25 +3,25 @@ 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=uIHlnB5TR9qe2yqWgQoSQBmdXZhMmsmmygxuhtnL6ew,8336
6
+ acp_sdk/client/client.py,sha256=O5i7FW5L_u3OCtJD2_muqSMwVhymlTtxNdk2elVkcO8,8218
7
7
  acp_sdk/client/types.py,sha256=_H6zYt-2OHOOYRtssRnbDIiwmgsl2-KIXc9lb-mJLFA,133
8
8
  acp_sdk/client/utils.py,sha256=2jhJyrPJmVFRoDJh0q_JMqOMlC3IxCh-6HXed-PIZS8,924
9
9
  acp_sdk/models/__init__.py,sha256=numSDBDT1QHx7n_Y3Deb5VOvKWcUBxbOEaMwQBSRHxc,151
10
10
  acp_sdk/models/errors.py,sha256=rEyaMVvQuBi7fwWe_d0PGGySYsD3FZTluQ-SkC0yhAs,444
11
- acp_sdk/models/models.py,sha256=rQpc5eFSyDK_ALBXSjg6dTRNA6B0UFY3ssECY_1oIDc,6538
11
+ acp_sdk/models/models.py,sha256=aA_ylU-Z0ZAlqPB8j-JfqJ2q516pgvTIyZ0wbfY8-UU,6654
12
12
  acp_sdk/models/schemas.py,sha256=OQ2bEfwFFZ1D9mhpgzfPBCcnPNvbl4qRfCWChH2wCro,644
13
13
  acp_sdk/server/__init__.py,sha256=mxBBBFaZuMEUENRMLwp1XZkuLeT9QghcFmNvjnqvAAU,377
14
14
  acp_sdk/server/agent.py,sha256=wvwpi83osmW7zQWxVnzyVMXIvzOswAfhKWHscVIldhA,6245
15
- acp_sdk/server/app.py,sha256=IocQBnXn1K6MP7aUvGSTD3TfVCKy9DGRZA3liYlfdYk,6599
15
+ acp_sdk/server/app.py,sha256=QEkyM68oc9oxVl0ex5E4IMvyAToSiSIaBcUvwELWG4c,6827
16
16
  acp_sdk/server/bundle.py,sha256=P3DfmzQOewsK8jr8yG8_RsGSrXQI4PsZCQbspVxKlus,6552
17
17
  acp_sdk/server/context.py,sha256=MgnLV6qcDIhc_0BjW7r4Jj1tHts4ZuwpdTGIBnz2Mgo,1036
18
18
  acp_sdk/server/errors.py,sha256=GSO8yYIqEeX8Y4Lz86ks35dMTHiQiXuOrLYYx0eXsbI,2110
19
19
  acp_sdk/server/logging.py,sha256=Oc8yZigCsuDnHHPsarRzu0RX3NKaLEgpELM2yovGKDI,411
20
- acp_sdk/server/server.py,sha256=6mIVyURgJvcZCPad1CV8TcgHC2mqyEFCGgGATGyP34Q,5548
21
- acp_sdk/server/session.py,sha256=fCRPHc2sQrz4G0n25I1S4LB7mI1wo0yYQJ1V9WRWH3g,627
20
+ acp_sdk/server/server.py,sha256=3b_nhbDR_VUz9lneJow4Jpw-6cF2xf2k94grYmfbm1E,8144
21
+ acp_sdk/server/session.py,sha256=ekz1o6Sy1tQZlpaoS_VgbvFuUQh2qpiHG71mvBdvhgc,662
22
22
  acp_sdk/server/telemetry.py,sha256=1BUxNg-xL_Vqgs27PDWNc3HikrQW2lidAtT_FKlp_Qk,1833
23
23
  acp_sdk/server/types.py,sha256=teBNRWSks8XP1SCQKGEtbNWQahVD3RAOPnysTxcQPxI,292
24
- acp_sdk/server/utils.py,sha256=EfrF9VCyVk3AM_ao-BIB9EzGbfTrh4V2Bz-VFr6f6Sg,351
25
- acp_sdk-0.7.1.dist-info/METADATA,sha256=akqHFFa6cRGdqOTalPhM_lO9f9LGyaMRotTocAjRw9E,1651
26
- acp_sdk-0.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
- acp_sdk-0.7.1.dist-info/RECORD,,
24
+ acp_sdk/server/utils.py,sha256=y-DDWv_QI25OJJYP5cni2FzfolKXtH3S2SYOm6OL_gc,1835
25
+ acp_sdk-0.7.3.dist-info/METADATA,sha256=v9-DPCCuw6NbWTuFyUDtI9UoTqxkkt1C4iS0wS1ATkw,1651
26
+ acp_sdk-0.7.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ acp_sdk-0.7.3.dist-info/RECORD,,