acp-sdk 0.7.1__py3-none-any.whl → 0.7.2__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
@@ -120,6 +120,11 @@ class Client:
120
120
  response = AgentReadResponse.model_validate(response.json())
121
121
  return Agent(**response.model_dump())
122
122
 
123
+ async def ping(self) -> bool:
124
+ response = await self._client.get("/healthcheck")
125
+ self._raise_error(response)
126
+ return response.json() == "OK"
127
+
123
128
  async def run_sync(self, input: Input, *, agent: AgentName) -> Run:
124
129
  response = await self._client.post(
125
130
  "/runs",
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,6 +104,10 @@ 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)
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/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.2
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=WJNKGCml3A3zrOdSOIC8cS5s-ySfWLDWSuvAr9y25x8,8504
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=tMWbtJgEEqIYe6gtJiVRCzt0OTTATsLLZt51SfBIqLc,6806
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
20
+ acp_sdk/server/server.py,sha256=3b_nhbDR_VUz9lneJow4Jpw-6cF2xf2k94grYmfbm1E,8144
21
21
  acp_sdk/server/session.py,sha256=fCRPHc2sQrz4G0n25I1S4LB7mI1wo0yYQJ1V9WRWH3g,627
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.2.dist-info/METADATA,sha256=-g7nf-ZOtPPNVh46fhjUjYljp6Lodnu4qSwD_vSJqbI,1651
26
+ acp_sdk-0.7.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ acp_sdk-0.7.2.dist-info/RECORD,,