acp-sdk 0.1.0rc6__py3-none-any.whl → 0.1.0rc8__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/__init__.py +1 -0
- acp_sdk/client/client.py +49 -10
- acp_sdk/models/models.py +14 -4
- acp_sdk/models/schemas.py +4 -2
- acp_sdk/server/agent.py +6 -11
- acp_sdk/server/app.py +34 -19
- acp_sdk/server/bundle.py +29 -14
- acp_sdk/server/server.py +151 -8
- acp_sdk/server/session.py +21 -0
- acp_sdk/server/telemetry.py +7 -2
- acp_sdk/version.py +3 -0
- {acp_sdk-0.1.0rc6.dist-info → acp_sdk-0.1.0rc8.dist-info}/METADATA +12 -16
- acp_sdk-0.1.0rc8.dist-info/RECORD +24 -0
- acp_sdk-0.1.0rc6.dist-info/RECORD +0 -22
- {acp_sdk-0.1.0rc6.dist-info → acp_sdk-0.1.0rc8.dist-info}/WHEEL +0 -0
acp_sdk/__init__.py
CHANGED
acp_sdk/client/client.py
CHANGED
@@ -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
|
acp_sdk/models/models.py
CHANGED
@@ -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")
|
acp_sdk/models/schemas.py
CHANGED
@@ -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
|
acp_sdk/server/agent.py
CHANGED
@@ -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:
|
acp_sdk/server/app.py
CHANGED
@@ -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
|
acp_sdk/server/bundle.py
CHANGED
@@ -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)
|
acp_sdk/server/server.py
CHANGED
@@ -1,8 +1,13 @@
|
|
1
|
+
import asyncio
|
1
2
|
import inspect
|
2
|
-
|
3
|
+
import os
|
4
|
+
from collections.abc import AsyncGenerator, Awaitable, Coroutine, Generator
|
3
5
|
from typing import Any, Callable
|
4
6
|
|
5
|
-
|
7
|
+
import uvicorn
|
8
|
+
import uvicorn.config
|
9
|
+
|
10
|
+
from acp_sdk.models import Message, Metadata
|
6
11
|
from acp_sdk.server.agent import Agent
|
7
12
|
from acp_sdk.server.app import create_app
|
8
13
|
from acp_sdk.server.context import Context
|
@@ -13,9 +18,16 @@ from acp_sdk.server.types import RunYield, RunYieldResume
|
|
13
18
|
|
14
19
|
class Server:
|
15
20
|
def __init__(self) -> None:
|
16
|
-
self.
|
21
|
+
self._agents: list[Agent] = []
|
22
|
+
self._server: uvicorn.Server | None = None
|
17
23
|
|
18
|
-
def agent(
|
24
|
+
def agent(
|
25
|
+
self,
|
26
|
+
name: str | None = None,
|
27
|
+
description: str | None = None,
|
28
|
+
*,
|
29
|
+
metadata: Metadata | None = None,
|
30
|
+
) -> Callable:
|
19
31
|
"""Decorator to register an agent."""
|
20
32
|
|
21
33
|
def decorator(fn: Callable) -> Callable:
|
@@ -43,6 +55,10 @@ class Server:
|
|
43
55
|
def description(self) -> str:
|
44
56
|
return description or fn.__doc__ or ""
|
45
57
|
|
58
|
+
@property
|
59
|
+
def metadata(self) -> Metadata:
|
60
|
+
return metadata or Metadata()
|
61
|
+
|
46
62
|
async def run(self, input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
47
63
|
try:
|
48
64
|
gen: AsyncGenerator[RunYield, RunYieldResume] = (
|
@@ -66,6 +82,10 @@ class Server:
|
|
66
82
|
def description(self) -> str:
|
67
83
|
return description or fn.__doc__ or ""
|
68
84
|
|
85
|
+
@property
|
86
|
+
def metadata(self) -> Metadata:
|
87
|
+
return metadata or Metadata()
|
88
|
+
|
69
89
|
async def run(self, input: Message, context: Context) -> Coroutine[RunYield]:
|
70
90
|
return await (fn(input, context) if has_context_param else fn(input))
|
71
91
|
|
@@ -81,6 +101,10 @@ class Server:
|
|
81
101
|
def description(self) -> str:
|
82
102
|
return description or fn.__doc__ or ""
|
83
103
|
|
104
|
+
@property
|
105
|
+
def metadata(self) -> Metadata:
|
106
|
+
return metadata or Metadata()
|
107
|
+
|
84
108
|
def run(self, input: Message, context: Context) -> Generator[RunYield, RunYieldResume]:
|
85
109
|
yield from (fn(input, context) if has_context_param else fn(input))
|
86
110
|
|
@@ -96,6 +120,10 @@ class Server:
|
|
96
120
|
def description(self) -> str:
|
97
121
|
return description or fn.__doc__ or ""
|
98
122
|
|
123
|
+
@property
|
124
|
+
def metadata(self) -> Metadata:
|
125
|
+
return metadata or Metadata()
|
126
|
+
|
99
127
|
def run(self, input: Message, context: Context) -> RunYield:
|
100
128
|
return fn(input, context) if has_context_param else fn(input)
|
101
129
|
|
@@ -107,11 +135,67 @@ class Server:
|
|
107
135
|
return decorator
|
108
136
|
|
109
137
|
def register(self, *agents: Agent) -> None:
|
110
|
-
self.
|
138
|
+
self._agents.extend(agents)
|
111
139
|
|
112
|
-
def
|
113
|
-
self,
|
140
|
+
def run(
|
141
|
+
self,
|
142
|
+
configure_logger: bool = True,
|
143
|
+
configure_telemetry: bool = False,
|
144
|
+
host: str = "127.0.0.1",
|
145
|
+
port: int = 8000,
|
146
|
+
uds: str | None = None,
|
147
|
+
fd: int | None = None,
|
148
|
+
loop: uvicorn.config.LoopSetupType = "auto",
|
149
|
+
http: type[asyncio.Protocol] | uvicorn.config.HTTPProtocolType = "auto",
|
150
|
+
ws: type[asyncio.Protocol] | uvicorn.config.WSProtocolType = "auto",
|
151
|
+
ws_max_size: int = 16 * 1024 * 1024,
|
152
|
+
ws_max_queue: int = 32,
|
153
|
+
ws_ping_interval: float | None = 20.0,
|
154
|
+
ws_ping_timeout: float | None = 20.0,
|
155
|
+
ws_per_message_deflate: bool = True,
|
156
|
+
lifespan: uvicorn.config.LifespanType = "auto",
|
157
|
+
env_file: str | os.PathLike[str] | None = None,
|
158
|
+
log_config: dict[str, Any]
|
159
|
+
| str
|
160
|
+
| uvicorn.config.RawConfigParser
|
161
|
+
| uvicorn.config.IO[Any]
|
162
|
+
| None = uvicorn.config.LOGGING_CONFIG,
|
163
|
+
log_level: str | int | None = None,
|
164
|
+
access_log: bool = True,
|
165
|
+
use_colors: bool | None = None,
|
166
|
+
interface: uvicorn.config.InterfaceType = "auto",
|
167
|
+
reload: bool = False,
|
168
|
+
reload_dirs: list[str] | str | None = None,
|
169
|
+
reload_delay: float = 0.25,
|
170
|
+
reload_includes: list[str] | str | None = None,
|
171
|
+
reload_excludes: list[str] | str | None = None,
|
172
|
+
workers: int | None = None,
|
173
|
+
proxy_headers: bool = True,
|
174
|
+
server_header: bool = True,
|
175
|
+
date_header: bool = True,
|
176
|
+
forwarded_allow_ips: list[str] | str | None = None,
|
177
|
+
root_path: str = "",
|
178
|
+
limit_concurrency: int | None = None,
|
179
|
+
limit_max_requests: int | None = None,
|
180
|
+
backlog: int = 2048,
|
181
|
+
timeout_keep_alive: int = 5,
|
182
|
+
timeout_notify: int = 30,
|
183
|
+
timeout_graceful_shutdown: int | None = None,
|
184
|
+
callback_notify: Callable[..., Awaitable[None]] | None = None,
|
185
|
+
ssl_keyfile: str | os.PathLike[str] | None = None,
|
186
|
+
ssl_certfile: str | os.PathLike[str] | None = None,
|
187
|
+
ssl_keyfile_password: str | None = None,
|
188
|
+
ssl_version: int = uvicorn.config.SSL_PROTOCOL_VERSION,
|
189
|
+
ssl_cert_reqs: int = uvicorn.config.ssl.CERT_NONE,
|
190
|
+
ssl_ca_certs: str | None = None,
|
191
|
+
ssl_ciphers: str = "TLSv1",
|
192
|
+
headers: list[tuple[str, str]] | None = None,
|
193
|
+
factory: bool = False,
|
194
|
+
h11_max_incomplete_event_size: int | None = None,
|
114
195
|
) -> None:
|
196
|
+
if self._server:
|
197
|
+
raise RuntimeError("The server is already running")
|
198
|
+
|
115
199
|
import uvicorn
|
116
200
|
|
117
201
|
if configure_logger:
|
@@ -119,4 +203,63 @@ class Server:
|
|
119
203
|
if configure_telemetry:
|
120
204
|
configure_telemetry_func()
|
121
205
|
|
122
|
-
uvicorn.
|
206
|
+
config = uvicorn.Config(
|
207
|
+
create_app(*self._agents),
|
208
|
+
host,
|
209
|
+
port,
|
210
|
+
uds,
|
211
|
+
fd,
|
212
|
+
loop,
|
213
|
+
http,
|
214
|
+
ws,
|
215
|
+
ws_max_size,
|
216
|
+
ws_max_queue,
|
217
|
+
ws_ping_interval,
|
218
|
+
ws_ping_timeout,
|
219
|
+
ws_per_message_deflate,
|
220
|
+
lifespan,
|
221
|
+
env_file,
|
222
|
+
log_config,
|
223
|
+
log_level,
|
224
|
+
access_log,
|
225
|
+
use_colors,
|
226
|
+
interface,
|
227
|
+
reload,
|
228
|
+
reload_dirs,
|
229
|
+
reload_delay,
|
230
|
+
reload_includes,
|
231
|
+
reload_excludes,
|
232
|
+
workers,
|
233
|
+
proxy_headers,
|
234
|
+
server_header,
|
235
|
+
date_header,
|
236
|
+
forwarded_allow_ips,
|
237
|
+
root_path,
|
238
|
+
limit_concurrency,
|
239
|
+
limit_max_requests,
|
240
|
+
backlog,
|
241
|
+
timeout_keep_alive,
|
242
|
+
timeout_notify,
|
243
|
+
timeout_graceful_shutdown,
|
244
|
+
callback_notify,
|
245
|
+
ssl_keyfile,
|
246
|
+
ssl_certfile,
|
247
|
+
ssl_keyfile_password,
|
248
|
+
ssl_version,
|
249
|
+
ssl_cert_reqs,
|
250
|
+
ssl_ca_certs,
|
251
|
+
ssl_ciphers,
|
252
|
+
headers,
|
253
|
+
factory,
|
254
|
+
h11_max_incomplete_event_size,
|
255
|
+
)
|
256
|
+
self._server = uvicorn.Server(config)
|
257
|
+
self._server.run()
|
258
|
+
|
259
|
+
@property
|
260
|
+
def should_exit(self) -> bool:
|
261
|
+
return self._server.should_exit if self._server else False
|
262
|
+
|
263
|
+
@should_exit.setter
|
264
|
+
def should_exit(self, value: bool) -> None:
|
265
|
+
self._server.should_exit = value
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import uuid
|
2
|
+
from collections.abc import Iterator
|
3
|
+
|
4
|
+
from acp_sdk.models import Message, SessionId
|
5
|
+
from acp_sdk.models.models import RunStatus
|
6
|
+
from acp_sdk.server.bundle import RunBundle
|
7
|
+
|
8
|
+
|
9
|
+
class Session:
|
10
|
+
def __init__(self) -> None:
|
11
|
+
self.id: SessionId = uuid.uuid4()
|
12
|
+
self.bundles: list[RunBundle] = []
|
13
|
+
|
14
|
+
def append(self, bundle: RunBundle) -> None:
|
15
|
+
self.bundles.append(bundle)
|
16
|
+
|
17
|
+
def history(self) -> Iterator[Message]:
|
18
|
+
for bundle in self.bundles:
|
19
|
+
if bundle.run.status == RunStatus.COMPLETED:
|
20
|
+
yield from bundle.inputs
|
21
|
+
yield from bundle.run.outputs
|
acp_sdk/server/telemetry.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import logging
|
2
|
-
from importlib.metadata import version
|
3
2
|
|
4
3
|
from opentelemetry import metrics, trace
|
5
4
|
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
@@ -18,6 +17,8 @@ from opentelemetry.sdk.resources import (
|
|
18
17
|
from opentelemetry.sdk.trace import TracerProvider
|
19
18
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
20
19
|
|
20
|
+
from acp_sdk.version import __version__
|
21
|
+
|
21
22
|
root_logger = logging.getLogger()
|
22
23
|
|
23
24
|
|
@@ -28,7 +29,7 @@ def configure_telemetry() -> None:
|
|
28
29
|
attributes={
|
29
30
|
SERVICE_NAME: "acp-server",
|
30
31
|
SERVICE_NAMESPACE: "acp",
|
31
|
-
SERVICE_VERSION:
|
32
|
+
SERVICE_VERSION: __version__,
|
32
33
|
}
|
33
34
|
)
|
34
35
|
|
@@ -50,3 +51,7 @@ def configure_telemetry() -> None:
|
|
50
51
|
processor = BatchLogRecordProcessor(OTLPLogExporter())
|
51
52
|
logger_provider.add_log_record_processor(processor)
|
52
53
|
root_logger.addHandler(LoggingHandler(logger_provider=logger_provider))
|
54
|
+
|
55
|
+
|
56
|
+
def get_tracer() -> trace.Tracer:
|
57
|
+
return trace.get_tracer("acp-sdk", __version__)
|
acp_sdk/version.py
ADDED
@@ -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).
|
@@ -0,0 +1,24 @@
|
|
1
|
+
acp_sdk/__init__.py,sha256=tXdAUM9zcmdSKCAkVrOCrGcXcuVS-yuvQUoQwTe9pek,98
|
2
|
+
acp_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
acp_sdk/version.py,sha256=Niy83rgvigB4hL_rR-O4ySvI7dj6xnqkyOe_JTymi9s,73
|
4
|
+
acp_sdk/client/__init__.py,sha256=Bca1DORrswxzZsrR2aUFpATuNG2xNSmYvF1Z2WJaVbc,51
|
5
|
+
acp_sdk/client/client.py,sha256=phsFdVMdVmTcfmzUQUgqzzvq4zNvmEUgW2eL9cFJY2g,6140
|
6
|
+
acp_sdk/models/__init__.py,sha256=numSDBDT1QHx7n_Y3Deb5VOvKWcUBxbOEaMwQBSRHxc,151
|
7
|
+
acp_sdk/models/errors.py,sha256=rEyaMVvQuBi7fwWe_d0PGGySYsD3FZTluQ-SkC0yhAs,444
|
8
|
+
acp_sdk/models/models.py,sha256=LrOZIWwn_6SenSwzz2JeK7Q555vzqjBKFElCifxQgnk,4107
|
9
|
+
acp_sdk/models/schemas.py,sha256=LNs3oN1pQ4GD0ceV3-k6X6R39eu33nsAfnW6uEgOP0c,735
|
10
|
+
acp_sdk/server/__init__.py,sha256=CowMcwN_WSsnO_-ZoqWQKtNVfa21MW3X3trZ9haJjaA,329
|
11
|
+
acp_sdk/server/agent.py,sha256=wMwyB3Ouz361DX-RDPFD61nymoUOTvBIJt3_JU9KmOw,3297
|
12
|
+
acp_sdk/server/app.py,sha256=Ys5EN4MzmrwrpBGvycEP5dKEIYkDZmeBMMV1Aq58AU0,5897
|
13
|
+
acp_sdk/server/bundle.py,sha256=VUOqzBhwsADkoyPWqXSZ2KG59S3q4LMDfpFVAssnGSM,5386
|
14
|
+
acp_sdk/server/context.py,sha256=MgnLV6qcDIhc_0BjW7r4Jj1tHts4ZuwpdTGIBnz2Mgo,1036
|
15
|
+
acp_sdk/server/errors.py,sha256=fWlgVsQ5hs_AXwzc-wvy6QgoDWEMRUBlSrfJfhHHMyE,2085
|
16
|
+
acp_sdk/server/logging.py,sha256=Oc8yZigCsuDnHHPsarRzu0RX3NKaLEgpELM2yovGKDI,411
|
17
|
+
acp_sdk/server/server.py,sha256=6iCJZrMfIDtqEmxqFREpPbyAiuxVxbQD73QHiFJ6KpA,9307
|
18
|
+
acp_sdk/server/session.py,sha256=0cDr924HC5x2bBNbK9NSKVHAt5A_mi5dK8P4jP_ugq0,629
|
19
|
+
acp_sdk/server/telemetry.py,sha256=WIEHK8syOTG9SyWi3Y-cos7CsCF5-IHGiyL9bCaUN0E,1921
|
20
|
+
acp_sdk/server/types.py,sha256=2yJPkfUzjVIhHmc0SegGTMqDROe2uFgycb-7CATvYVw,161
|
21
|
+
acp_sdk/server/utils.py,sha256=EfrF9VCyVk3AM_ao-BIB9EzGbfTrh4V2Bz-VFr6f6Sg,351
|
22
|
+
acp_sdk-0.1.0rc8.dist-info/METADATA,sha256=hrIiPuTocgUWerNeGvolLPMY1DgqFqpTshMswZr7sQw,2041
|
23
|
+
acp_sdk-0.1.0rc8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
24
|
+
acp_sdk-0.1.0rc8.dist-info/RECORD,,
|
@@ -1,22 +0,0 @@
|
|
1
|
-
acp_sdk/__init__.py,sha256=XlcAbmvJfvJK5tmOFdxXP19aDwxxdG_jygsTCvXtjTk,43
|
2
|
-
acp_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
acp_sdk/client/__init__.py,sha256=Bca1DORrswxzZsrR2aUFpATuNG2xNSmYvF1Z2WJaVbc,51
|
4
|
-
acp_sdk/client/client.py,sha256=W6iXxH65UUODnvt3tnvgdqT_pP1meZUF2HwJpWCM0BM,5029
|
5
|
-
acp_sdk/models/__init__.py,sha256=numSDBDT1QHx7n_Y3Deb5VOvKWcUBxbOEaMwQBSRHxc,151
|
6
|
-
acp_sdk/models/errors.py,sha256=rEyaMVvQuBi7fwWe_d0PGGySYsD3FZTluQ-SkC0yhAs,444
|
7
|
-
acp_sdk/models/models.py,sha256=ACZPQp-wm43tdlx83hrX7QmdtBRWVXvU5bsotUOidHM,3908
|
8
|
-
acp_sdk/models/schemas.py,sha256=a0T29vfy93uVIce3ikbd3GSTdKBokMmfXWNVRJD3Eb8,662
|
9
|
-
acp_sdk/server/__init__.py,sha256=CowMcwN_WSsnO_-ZoqWQKtNVfa21MW3X3trZ9haJjaA,329
|
10
|
-
acp_sdk/server/agent.py,sha256=SZfJK634d4s8fRh0u1Tc56l58XCRHAh3hq5Hcabh6tI,3442
|
11
|
-
acp_sdk/server/app.py,sha256=7alacNo9y_f0-BjZI0dm_rzSweW-n5YiuY0hrVtEIR8,5420
|
12
|
-
acp_sdk/server/bundle.py,sha256=ZVkIOwWfYgi-VyGYSMKr9WrG_WzYbej-LD-YPUWQdeg,4982
|
13
|
-
acp_sdk/server/context.py,sha256=MgnLV6qcDIhc_0BjW7r4Jj1tHts4ZuwpdTGIBnz2Mgo,1036
|
14
|
-
acp_sdk/server/errors.py,sha256=fWlgVsQ5hs_AXwzc-wvy6QgoDWEMRUBlSrfJfhHHMyE,2085
|
15
|
-
acp_sdk/server/logging.py,sha256=Oc8yZigCsuDnHHPsarRzu0RX3NKaLEgpELM2yovGKDI,411
|
16
|
-
acp_sdk/server/server.py,sha256=H-oh-cRpo9NR7_Mdt7ECRXrGnk8mfAAb7pp94XLGoAs,4637
|
17
|
-
acp_sdk/server/telemetry.py,sha256=EwmtUrWMYid7XHiX76V1J6CigJPa2NrzEPOX0fBoY3o,1838
|
18
|
-
acp_sdk/server/types.py,sha256=2yJPkfUzjVIhHmc0SegGTMqDROe2uFgycb-7CATvYVw,161
|
19
|
-
acp_sdk/server/utils.py,sha256=EfrF9VCyVk3AM_ao-BIB9EzGbfTrh4V2Bz-VFr6f6Sg,351
|
20
|
-
acp_sdk-0.1.0rc6.dist-info/METADATA,sha256=t5zWJsU-C67HQNzCFyo561s7epz4KQvsFjlqUIkF9xw,2147
|
21
|
-
acp_sdk-0.1.0rc6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
22
|
-
acp_sdk-0.1.0rc6.dist-info/RECORD,,
|
File without changes
|