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 +5 -8
- acp_sdk/models/models.py +8 -2
- acp_sdk/server/app.py +16 -4
- acp_sdk/server/server.py +54 -1
- acp_sdk/server/session.py +2 -2
- acp_sdk/server/utils.py +36 -1
- {acp_sdk-0.7.1.dist-info → acp_sdk-0.7.3.dist-info}/METADATA +1 -1
- {acp_sdk-0.7.1.dist-info → acp_sdk-0.7.3.dist-info}/RECORD +9 -9
- {acp_sdk-0.7.1.dist-info → acp_sdk-0.7.3.dist-info}/WHEEL +0 -0
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
|
-
|
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(
|
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(
|
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(
|
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
|
-
|
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
|
-
|
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.")
|
@@ -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=
|
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=
|
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=
|
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=
|
21
|
-
acp_sdk/server/session.py,sha256=
|
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=
|
25
|
-
acp_sdk-0.7.
|
26
|
-
acp_sdk-0.7.
|
27
|
-
acp_sdk-0.7.
|
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,,
|
File without changes
|