acp-sdk 0.1.0rc7__py3-none-any.whl → 0.2.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/__init__.py +1 -0
- acp_sdk/client/client.py +53 -14
- acp_sdk/models/models.py +61 -72
- acp_sdk/models/schemas.py +1 -1
- acp_sdk/server/__init__.py +1 -0
- acp_sdk/server/agent.py +84 -11
- acp_sdk/server/app.py +17 -15
- acp_sdk/server/bundle.py +62 -33
- acp_sdk/server/server.py +11 -88
- acp_sdk/server/session.py +21 -0
- acp_sdk/server/telemetry.py +7 -2
- acp_sdk/server/types.py +2 -2
- acp_sdk/version.py +3 -0
- acp_sdk-0.2.0.dist-info/METADATA +113 -0
- acp_sdk-0.2.0.dist-info/RECORD +24 -0
- acp_sdk-0.1.0rc7.dist-info/METADATA +0 -74
- acp_sdk-0.1.0rc7.dist-info/RECORD +0 -22
- {acp_sdk-0.1.0rc7.dist-info → acp_sdk-0.2.0.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
|
|
@@ -15,22 +17,31 @@ from acp_sdk.models import (
|
|
15
17
|
AgentsListResponse,
|
16
18
|
AwaitResume,
|
17
19
|
Error,
|
20
|
+
Event,
|
18
21
|
Message,
|
19
22
|
Run,
|
20
23
|
RunCancelResponse,
|
24
|
+
RunCreatedEvent,
|
21
25
|
RunCreateRequest,
|
22
26
|
RunCreateResponse,
|
23
|
-
RunEvent,
|
24
27
|
RunId,
|
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[Event]:
|
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, RunCreatedEvent):
|
124
|
+
self._set_session(event.run)
|
89
125
|
yield event
|
90
126
|
|
91
127
|
async def run_status(self, *, run_id: RunId) -> Run:
|
@@ -114,7 +150,7 @@ class Client:
|
|
114
150
|
self._raise_error(response)
|
115
151
|
return RunResumeResponse.model_validate(response.json())
|
116
152
|
|
117
|
-
async def run_resume_stream(self, *, run_id: RunId, await_: AwaitResume) -> AsyncIterator[
|
153
|
+
async def run_resume_stream(self, *, run_id: RunId, await_: AwaitResume) -> AsyncIterator[Event]:
|
118
154
|
async with aconnect_sse(
|
119
155
|
self._client,
|
120
156
|
"POST",
|
@@ -127,9 +163,9 @@ class Client:
|
|
127
163
|
async def _validate_stream(
|
128
164
|
self,
|
129
165
|
event_source: EventSource,
|
130
|
-
) -> AsyncIterator[
|
166
|
+
) -> AsyncIterator[Event]:
|
131
167
|
async for event in event_source.aiter_sse():
|
132
|
-
event = TypeAdapter(
|
168
|
+
event = TypeAdapter(Event).validate_json(event.data)
|
133
169
|
yield event
|
134
170
|
|
135
171
|
def _raise_error(self, response: httpx.Response) -> None:
|
@@ -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
@@ -1,9 +1,8 @@
|
|
1
1
|
import uuid
|
2
|
-
from collections.abc import Iterator
|
3
2
|
from enum import Enum
|
4
|
-
from typing import Any, Literal, Union
|
3
|
+
from typing import Any, Literal, Optional, Union
|
5
4
|
|
6
|
-
from pydantic import AnyUrl, BaseModel, ConfigDict, Field
|
5
|
+
from pydantic import AnyUrl, BaseModel, ConfigDict, Field
|
7
6
|
|
8
7
|
from acp_sdk.models.errors import Error
|
9
8
|
|
@@ -16,45 +15,38 @@ class AnyModel(BaseModel):
|
|
16
15
|
model_config = ConfigDict(extra="allow")
|
17
16
|
|
18
17
|
|
19
|
-
class
|
20
|
-
|
18
|
+
class MessagePart(BaseModel):
|
19
|
+
name: Optional[str] = None
|
20
|
+
content_type: str
|
21
|
+
content: Optional[str] = None
|
22
|
+
content_encoding: Optional[Literal["plain", "base64"]] = "plain"
|
23
|
+
content_url: Optional[AnyUrl] = None
|
21
24
|
|
25
|
+
model_config = ConfigDict(extra="forbid")
|
22
26
|
|
23
|
-
|
24
|
-
|
25
|
-
|
27
|
+
def model_post_init(self, __context: Any) -> None:
|
28
|
+
if self.content is None and self.content_url is None:
|
29
|
+
raise ValueError("Either content or content_url must be provided")
|
30
|
+
if self.content is not None and self.content_url is not None:
|
31
|
+
raise ValueError("Only one of content or content_url can be provided")
|
26
32
|
|
27
33
|
|
28
|
-
class
|
29
|
-
type: Literal["image"] = "image"
|
30
|
-
content_url: AnyUrl
|
31
|
-
|
32
|
-
|
33
|
-
class ArtifactMessagePart(MessagePartBase):
|
34
|
-
type: Literal["artifact"] = "artifact"
|
34
|
+
class Artifact(MessagePart):
|
35
35
|
name: str
|
36
|
-
content_url: AnyUrl
|
37
|
-
|
38
|
-
|
39
|
-
MessagePart = Union[TextMessagePart, ImageMessagePart, ArtifactMessagePart]
|
40
36
|
|
41
37
|
|
42
|
-
class Message(
|
43
|
-
|
44
|
-
|
45
|
-
def __init__(self, *items: MessagePart) -> None:
|
46
|
-
super().__init__(root=list(items))
|
47
|
-
|
48
|
-
def __iter__(self) -> Iterator[MessagePart]:
|
49
|
-
return iter(self.root)
|
38
|
+
class Message(BaseModel):
|
39
|
+
parts: list[MessagePart]
|
50
40
|
|
51
41
|
def __add__(self, other: "Message") -> "Message":
|
52
42
|
if not isinstance(other, Message):
|
53
43
|
raise TypeError(f"Cannot concatenate Message with {type(other).__name__}")
|
54
|
-
return Message(*(self.
|
44
|
+
return Message(*(self.parts + other.parts))
|
55
45
|
|
56
46
|
def __str__(self) -> str:
|
57
|
-
return "".join(
|
47
|
+
return "".join(
|
48
|
+
part.content for part in self.parts if part.content is not None and part.content_type == "text/plain"
|
49
|
+
)
|
58
50
|
|
59
51
|
|
60
52
|
AgentName = str
|
@@ -83,7 +75,7 @@ class RunStatus(str, Enum):
|
|
83
75
|
return self in terminal_states
|
84
76
|
|
85
77
|
|
86
|
-
class
|
78
|
+
class AwaitRequest(BaseModel):
|
87
79
|
type: Literal["placeholder"] = "placeholder"
|
88
80
|
|
89
81
|
|
@@ -96,41 +88,34 @@ class Run(BaseModel):
|
|
96
88
|
agent_name: AgentName
|
97
89
|
session_id: SessionId | None = None
|
98
90
|
status: RunStatus = RunStatus.CREATED
|
99
|
-
|
100
|
-
|
91
|
+
await_request: AwaitRequest | None = None
|
92
|
+
outputs: list[Message] = []
|
101
93
|
error: Error | None = None
|
102
94
|
|
103
|
-
model_config = ConfigDict(populate_by_name=True)
|
104
95
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
96
|
+
class MessageCreatedEvent(BaseModel):
|
97
|
+
type: Literal["message.created"] = "message.created"
|
98
|
+
message: Message
|
99
|
+
|
100
|
+
|
101
|
+
class MessagePartEvent(BaseModel):
|
102
|
+
type: Literal["message.part"] = "message.part"
|
103
|
+
part: MessagePart
|
104
|
+
|
113
105
|
|
106
|
+
class ArtifactEvent(BaseModel):
|
107
|
+
type: Literal["message.part"] = "message.part"
|
108
|
+
part: Artifact
|
114
109
|
|
115
|
-
|
116
|
-
|
110
|
+
|
111
|
+
class MessageCompletedEvent(BaseModel):
|
112
|
+
type: Literal["message.completed"] = "message.completed"
|
117
113
|
message: Message
|
118
114
|
|
119
115
|
|
120
116
|
class AwaitEvent(BaseModel):
|
121
117
|
type: Literal["await"] = "await"
|
122
|
-
|
123
|
-
|
124
|
-
model_config = ConfigDict(populate_by_name=True)
|
125
|
-
|
126
|
-
def model_dump_json(
|
127
|
-
self,
|
128
|
-
**kwargs: dict[str, Any],
|
129
|
-
) -> str:
|
130
|
-
return super().model_dump_json(
|
131
|
-
by_alias=True,
|
132
|
-
**kwargs,
|
133
|
-
)
|
118
|
+
await_request: AwaitRequest | None = None
|
134
119
|
|
135
120
|
|
136
121
|
class GenericEvent(BaseModel):
|
@@ -138,40 +123,44 @@ class GenericEvent(BaseModel):
|
|
138
123
|
generic: AnyModel
|
139
124
|
|
140
125
|
|
141
|
-
class
|
142
|
-
type: Literal["created"] = "created"
|
126
|
+
class RunCreatedEvent(BaseModel):
|
127
|
+
type: Literal["run.created"] = "run.created"
|
143
128
|
run: Run
|
144
129
|
|
145
130
|
|
146
|
-
class
|
147
|
-
type: Literal["in-progress"] = "in-progress"
|
131
|
+
class RunInProgressEvent(BaseModel):
|
132
|
+
type: Literal["run.in-progress"] = "run.in-progress"
|
148
133
|
run: Run
|
149
134
|
|
150
135
|
|
151
|
-
class
|
152
|
-
type: Literal["failed"] = "failed"
|
136
|
+
class RunFailedEvent(BaseModel):
|
137
|
+
type: Literal["run.failed"] = "run.failed"
|
153
138
|
run: Run
|
154
139
|
|
155
140
|
|
156
|
-
class
|
157
|
-
type: Literal["cancelled"] = "cancelled"
|
141
|
+
class RunCancelledEvent(BaseModel):
|
142
|
+
type: Literal["run.cancelled"] = "run.cancelled"
|
158
143
|
run: Run
|
159
144
|
|
160
145
|
|
161
|
-
class
|
162
|
-
type: Literal["completed"] = "completed"
|
146
|
+
class RunCompletedEvent(BaseModel):
|
147
|
+
type: Literal["run.completed"] = "run.completed"
|
163
148
|
run: Run
|
164
149
|
|
165
150
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
151
|
+
Event = Union[
|
152
|
+
RunCreatedEvent,
|
153
|
+
RunInProgressEvent,
|
154
|
+
MessageCreatedEvent,
|
155
|
+
ArtifactEvent,
|
156
|
+
MessagePartEvent,
|
157
|
+
MessageCompletedEvent,
|
170
158
|
AwaitEvent,
|
171
159
|
GenericEvent,
|
172
|
-
|
173
|
-
|
174
|
-
|
160
|
+
RunCancelledEvent,
|
161
|
+
RunFailedEvent,
|
162
|
+
RunCompletedEvent,
|
163
|
+
MessagePartEvent,
|
175
164
|
]
|
176
165
|
|
177
166
|
|
acp_sdk/models/schemas.py
CHANGED
acp_sdk/server/__init__.py
CHANGED
acp_sdk/server/agent.py
CHANGED
@@ -3,6 +3,7 @@ import asyncio
|
|
3
3
|
import inspect
|
4
4
|
from collections.abc import AsyncGenerator, Coroutine, Generator
|
5
5
|
from concurrent.futures import ThreadPoolExecutor
|
6
|
+
from typing import Callable
|
6
7
|
|
7
8
|
import janus
|
8
9
|
|
@@ -31,19 +32,14 @@ class Agent(abc.ABC):
|
|
31
32
|
|
32
33
|
@abc.abstractmethod
|
33
34
|
def run(
|
34
|
-
self,
|
35
|
+
self, inputs: list[Message], context: Context
|
35
36
|
) -> (
|
36
37
|
AsyncGenerator[RunYield, RunYieldResume] | Generator[RunYield, RunYieldResume] | Coroutine[RunYield] | RunYield
|
37
38
|
):
|
38
39
|
pass
|
39
40
|
|
40
|
-
async def session(self, session_id: SessionId | None) -> SessionId | None:
|
41
|
-
if session_id:
|
42
|
-
raise NotImplementedError()
|
43
|
-
return None
|
44
|
-
|
45
41
|
async def execute(
|
46
|
-
self,
|
42
|
+
self, inputs: list[Message], session_id: SessionId | None, executor: ThreadPoolExecutor
|
47
43
|
) -> AsyncGenerator[RunYield, RunYieldResume]:
|
48
44
|
yield_queue: janus.Queue[RunYield] = janus.Queue()
|
49
45
|
yield_resume_queue: janus.Queue[RunYieldResume] = janus.Queue()
|
@@ -53,13 +49,13 @@ class Agent(abc.ABC):
|
|
53
49
|
)
|
54
50
|
|
55
51
|
if inspect.isasyncgenfunction(self.run):
|
56
|
-
run = asyncio.create_task(self._run_async_gen(
|
52
|
+
run = asyncio.create_task(self._run_async_gen(inputs, context))
|
57
53
|
elif inspect.iscoroutinefunction(self.run):
|
58
|
-
run = asyncio.create_task(self._run_coro(
|
54
|
+
run = asyncio.create_task(self._run_coro(inputs, context))
|
59
55
|
elif inspect.isgeneratorfunction(self.run):
|
60
|
-
run = asyncio.get_running_loop().run_in_executor(executor, self._run_gen,
|
56
|
+
run = asyncio.get_running_loop().run_in_executor(executor, self._run_gen, inputs, context)
|
61
57
|
else:
|
62
|
-
run = asyncio.get_running_loop().run_in_executor(executor, self._run_func,
|
58
|
+
run = asyncio.get_running_loop().run_in_executor(executor, self._run_func, inputs, context)
|
63
59
|
|
64
60
|
try:
|
65
61
|
while True:
|
@@ -103,3 +99,80 @@ class Agent(abc.ABC):
|
|
103
99
|
context.yield_sync(self.run(input, context))
|
104
100
|
finally:
|
105
101
|
context.shutdown()
|
102
|
+
|
103
|
+
|
104
|
+
def agent(
|
105
|
+
name: str | None = None,
|
106
|
+
description: str | None = None,
|
107
|
+
*,
|
108
|
+
metadata: Metadata | None = None,
|
109
|
+
) -> Callable[[Callable], Agent]:
|
110
|
+
"""Decorator to create an agent."""
|
111
|
+
|
112
|
+
def decorator(fn: Callable) -> Agent:
|
113
|
+
signature = inspect.signature(fn)
|
114
|
+
parameters = list(signature.parameters.values())
|
115
|
+
|
116
|
+
if len(parameters) == 0:
|
117
|
+
raise TypeError("The agent function must have at least 'input' argument")
|
118
|
+
if len(parameters) > 2:
|
119
|
+
raise TypeError("The agent function must have only 'input' and 'context' arguments")
|
120
|
+
if len(parameters) == 2 and parameters[1].name != "context":
|
121
|
+
raise TypeError("The second argument of the agent function must be 'context'")
|
122
|
+
|
123
|
+
has_context_param = len(parameters) == 2
|
124
|
+
|
125
|
+
class DecoratorAgentBase(Agent):
|
126
|
+
@property
|
127
|
+
def name(self) -> str:
|
128
|
+
return name or fn.__name__
|
129
|
+
|
130
|
+
@property
|
131
|
+
def description(self) -> str:
|
132
|
+
return description or fn.__doc__ or ""
|
133
|
+
|
134
|
+
@property
|
135
|
+
def metadata(self) -> Metadata:
|
136
|
+
return metadata or Metadata()
|
137
|
+
|
138
|
+
agent: Agent
|
139
|
+
if inspect.isasyncgenfunction(fn):
|
140
|
+
|
141
|
+
class AsyncGenDecoratorAgent(DecoratorAgentBase):
|
142
|
+
async def run(self, input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
143
|
+
try:
|
144
|
+
gen: AsyncGenerator[RunYield, RunYieldResume] = (
|
145
|
+
fn(input, context) if has_context_param else fn(input)
|
146
|
+
)
|
147
|
+
value = None
|
148
|
+
while True:
|
149
|
+
value = yield await gen.asend(value)
|
150
|
+
except StopAsyncIteration:
|
151
|
+
pass
|
152
|
+
|
153
|
+
agent = AsyncGenDecoratorAgent()
|
154
|
+
elif inspect.iscoroutinefunction(fn):
|
155
|
+
|
156
|
+
class CoroDecoratorAgent(DecoratorAgentBase):
|
157
|
+
async def run(self, input: Message, context: Context) -> Coroutine[RunYield]:
|
158
|
+
return await (fn(input, context) if has_context_param else fn(input))
|
159
|
+
|
160
|
+
agent = CoroDecoratorAgent()
|
161
|
+
elif inspect.isgeneratorfunction(fn):
|
162
|
+
|
163
|
+
class GenDecoratorAgent(DecoratorAgentBase):
|
164
|
+
def run(self, input: Message, context: Context) -> Generator[RunYield, RunYieldResume]:
|
165
|
+
yield from (fn(input, context) if has_context_param else fn(input))
|
166
|
+
|
167
|
+
agent = GenDecoratorAgent()
|
168
|
+
else:
|
169
|
+
|
170
|
+
class FuncDecoratorAgent(DecoratorAgentBase):
|
171
|
+
def run(self, input: Message, context: Context) -> RunYield:
|
172
|
+
return fn(input, context) if has_context_param else fn(input)
|
173
|
+
|
174
|
+
agent = FuncDecoratorAgent()
|
175
|
+
|
176
|
+
return agent
|
177
|
+
|
178
|
+
return decorator
|
acp_sdk/server/app.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import asyncio
|
2
1
|
from collections.abc import AsyncGenerator
|
3
2
|
from concurrent.futures import ThreadPoolExecutor
|
4
3
|
from contextlib import asynccontextmanager
|
@@ -25,7 +24,7 @@ from acp_sdk.models import (
|
|
25
24
|
RunReadResponse,
|
26
25
|
RunResumeRequest,
|
27
26
|
RunResumeResponse,
|
28
|
-
|
27
|
+
SessionId,
|
29
28
|
)
|
30
29
|
from acp_sdk.models.errors import ACPError
|
31
30
|
from acp_sdk.server.agent import Agent
|
@@ -38,6 +37,7 @@ from acp_sdk.server.errors import (
|
|
38
37
|
http_exception_handler,
|
39
38
|
validation_exception_handler,
|
40
39
|
)
|
40
|
+
from acp_sdk.server.session import Session
|
41
41
|
from acp_sdk.server.utils import stream_sse
|
42
42
|
|
43
43
|
|
@@ -51,7 +51,7 @@ def create_app(*agents: Agent) -> FastAPI:
|
|
51
51
|
@asynccontextmanager
|
52
52
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
53
53
|
nonlocal executor
|
54
|
-
with ThreadPoolExecutor(
|
54
|
+
with ThreadPoolExecutor() as exec:
|
55
55
|
executor = exec
|
56
56
|
yield
|
57
57
|
|
@@ -61,6 +61,7 @@ def create_app(*agents: Agent) -> FastAPI:
|
|
61
61
|
|
62
62
|
agents: dict[AgentName, Agent] = {agent.name: agent for agent in agents}
|
63
63
|
runs: dict[RunId, RunBundle] = {}
|
64
|
+
sessions: dict[SessionId, Session] = {}
|
64
65
|
|
65
66
|
app.exception_handler(ACPError)(acp_error_handler)
|
66
67
|
app.exception_handler(StarletteHTTPException)(http_exception_handler)
|
@@ -96,17 +97,20 @@ def create_app(*agents: Agent) -> FastAPI:
|
|
96
97
|
@app.post("/runs")
|
97
98
|
async def create_run(request: RunCreateRequest) -> RunCreateResponse:
|
98
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
|
99
103
|
bundle = RunBundle(
|
100
104
|
agent=agent,
|
101
|
-
run=Run(
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
+
run=Run(agent_name=agent.name, session_id=session.id),
|
106
|
+
inputs=request.inputs,
|
107
|
+
history=list(session.history()),
|
108
|
+
executor=executor,
|
105
109
|
)
|
110
|
+
session.append(bundle)
|
106
111
|
|
107
|
-
nonlocal executor
|
108
|
-
bundle.task = asyncio.create_task(bundle.execute(request.input, executor=executor))
|
109
112
|
runs[bundle.run.run_id] = bundle
|
113
|
+
sessions[session.id] = session
|
110
114
|
|
111
115
|
headers = {Headers.RUN_ID: str(bundle.run.run_id)}
|
112
116
|
|
@@ -140,8 +144,7 @@ def create_app(*agents: Agent) -> FastAPI:
|
|
140
144
|
@app.post("/runs/{run_id}")
|
141
145
|
async def resume_run(run_id: RunId, request: RunResumeRequest) -> RunResumeResponse:
|
142
146
|
bundle = find_run_bundle(run_id)
|
143
|
-
bundle.
|
144
|
-
await bundle.await_queue.put(request.await_)
|
147
|
+
await bundle.resume(request.await_)
|
145
148
|
match request.mode:
|
146
149
|
case RunMode.STREAM:
|
147
150
|
return StreamingResponse(
|
@@ -164,11 +167,10 @@ def create_app(*agents: Agent) -> FastAPI:
|
|
164
167
|
bundle = find_run_bundle(run_id)
|
165
168
|
if bundle.run.status.is_terminal:
|
166
169
|
raise HTTPException(
|
167
|
-
status_code=
|
168
|
-
detail=f"Run
|
170
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
171
|
+
detail=f"Run in terminal status {bundle.run.status} can't be cancelled",
|
169
172
|
)
|
170
|
-
bundle.
|
171
|
-
bundle.run.status = RunStatus.CANCELLING
|
173
|
+
await bundle.cancel()
|
172
174
|
return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder(bundle.run))
|
173
175
|
|
174
176
|
return app
|
acp_sdk/server/bundle.py
CHANGED
@@ -3,45 +3,51 @@ 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,
|
11
|
-
Await,
|
12
10
|
AwaitEvent,
|
11
|
+
AwaitRequest,
|
13
12
|
AwaitResume,
|
14
|
-
CancelledEvent,
|
15
|
-
CompletedEvent,
|
16
|
-
CreatedEvent,
|
17
13
|
Error,
|
18
|
-
|
14
|
+
Event,
|
19
15
|
GenericEvent,
|
20
|
-
InProgressEvent,
|
21
16
|
Message,
|
22
|
-
|
17
|
+
MessageCreatedEvent,
|
18
|
+
MessagePartEvent,
|
23
19
|
Run,
|
24
|
-
|
20
|
+
RunCancelledEvent,
|
21
|
+
RunCompletedEvent,
|
22
|
+
RunCreatedEvent,
|
23
|
+
RunFailedEvent,
|
24
|
+
RunInProgressEvent,
|
25
25
|
RunStatus,
|
26
26
|
)
|
27
27
|
from acp_sdk.models.errors import ErrorCode
|
28
|
+
from acp_sdk.models.models import MessageCompletedEvent, MessagePart
|
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
|
-
self.stream_queue: asyncio.Queue[
|
39
|
-
self.composed_message = Message()
|
43
|
+
self.stream_queue: asyncio.Queue[Event] = asyncio.Queue()
|
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
|
|
44
|
-
|
48
|
+
self.task = asyncio.create_task(self._execute(inputs, executor=executor))
|
49
|
+
|
50
|
+
async def stream(self) -> AsyncGenerator[Event]:
|
45
51
|
while True:
|
46
52
|
event = await self.stream_queue.get()
|
47
53
|
if event is None:
|
@@ -49,7 +55,7 @@ class RunBundle:
|
|
49
55
|
yield event
|
50
56
|
self.stream_queue.task_done()
|
51
57
|
|
52
|
-
async def emit(self, event:
|
58
|
+
async def emit(self, event: Event) -> None:
|
53
59
|
await self.stream_queue.put(event)
|
54
60
|
|
55
61
|
async def await_(self) -> AwaitResume:
|
@@ -64,33 +70,55 @@ 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_request = None
|
75
|
+
|
76
|
+
async def cancel(self) -> None:
|
77
|
+
self.task.cancel()
|
78
|
+
self.run.status = RunStatus.CANCELLING
|
79
|
+
self.run.await_request = 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
|
84
|
+
async def _execute(self, inputs: list[Message], *, executor: ThreadPoolExecutor) -> None:
|
85
|
+
with get_tracer().start_as_current_span("run"):
|
73
86
|
run_logger = logging.LoggerAdapter(logger, {"run_id": str(self.run.run_id)})
|
74
87
|
|
75
|
-
|
88
|
+
in_message = False
|
76
89
|
try:
|
77
|
-
|
78
|
-
run_logger.info("Session loaded")
|
90
|
+
await self.emit(RunCreatedEvent(run=self.run))
|
79
91
|
|
80
|
-
generator = self.agent.execute(
|
92
|
+
generator = self.agent.execute(
|
93
|
+
inputs=self.history + inputs, session_id=self.run.session_id, executor=executor
|
94
|
+
)
|
81
95
|
run_logger.info("Run started")
|
82
96
|
|
83
97
|
self.run.status = RunStatus.IN_PROGRESS
|
84
|
-
await self.emit(
|
98
|
+
await self.emit(RunInProgressEvent(run=self.run))
|
85
99
|
|
86
100
|
await_resume = None
|
87
101
|
while True:
|
88
102
|
next = await generator.asend(await_resume)
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
103
|
+
|
104
|
+
if isinstance(next, MessagePart):
|
105
|
+
if not in_message:
|
106
|
+
self.run.outputs.append(Message(parts=[]))
|
107
|
+
in_message = True
|
108
|
+
await self.emit(MessageCreatedEvent(message=self.run.outputs[-1]))
|
109
|
+
self.run.outputs[-1].parts.append(next)
|
110
|
+
await self.emit(MessagePartEvent(part=next))
|
111
|
+
elif isinstance(next, Message):
|
112
|
+
if in_message:
|
113
|
+
await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
|
114
|
+
in_message = False
|
115
|
+
self.run.outputs.append(next)
|
116
|
+
await self.emit(MessageCreatedEvent(message=next))
|
117
|
+
for part in next.parts:
|
118
|
+
await self.emit(MessagePartEvent(part=part))
|
119
|
+
await self.emit(MessageCompletedEvent(message=next))
|
120
|
+
elif isinstance(next, AwaitRequest):
|
121
|
+
self.run.await_request = next
|
94
122
|
self.run.status = RunStatus.AWAITING
|
95
123
|
await self.emit(
|
96
124
|
AwaitEvent.model_validate(
|
@@ -103,8 +131,7 @@ class RunBundle:
|
|
103
131
|
)
|
104
132
|
run_logger.info("Run awaited")
|
105
133
|
await_resume = await self.await_()
|
106
|
-
self.run
|
107
|
-
await self.emit(InProgressEvent(run=self.run))
|
134
|
+
await self.emit(RunInProgressEvent(run=self.run))
|
108
135
|
run_logger.info("Run resumed")
|
109
136
|
else:
|
110
137
|
try:
|
@@ -113,19 +140,21 @@ class RunBundle:
|
|
113
140
|
except ValidationError:
|
114
141
|
raise TypeError("Invalid yield")
|
115
142
|
except StopAsyncIteration:
|
116
|
-
|
143
|
+
if in_message:
|
144
|
+
await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
|
117
145
|
self.run.status = RunStatus.COMPLETED
|
118
|
-
await self.emit(
|
146
|
+
await self.emit(RunCompletedEvent(run=self.run))
|
119
147
|
run_logger.info("Run completed")
|
120
148
|
except asyncio.CancelledError:
|
121
149
|
self.run.status = RunStatus.CANCELLED
|
122
|
-
await self.emit(
|
150
|
+
await self.emit(RunCancelledEvent(run=self.run))
|
123
151
|
run_logger.info("Run cancelled")
|
124
152
|
except Exception as e:
|
125
153
|
self.run.error = Error(code=ErrorCode.SERVER_ERROR, message=str(e))
|
126
154
|
self.run.status = RunStatus.FAILED
|
127
|
-
await self.emit(
|
155
|
+
await self.emit(RunFailedEvent(run=self.run))
|
128
156
|
run_logger.exception("Run failed")
|
157
|
+
raise
|
129
158
|
finally:
|
130
159
|
self.await_or_terminate_event.set()
|
131
160
|
await self.stream_queue.put(None)
|
acp_sdk/server/server.py
CHANGED
@@ -1,19 +1,17 @@
|
|
1
1
|
import asyncio
|
2
|
-
import inspect
|
3
2
|
import os
|
4
|
-
from collections.abc import
|
3
|
+
from collections.abc import Awaitable
|
5
4
|
from typing import Any, Callable
|
6
5
|
|
7
6
|
import uvicorn
|
8
7
|
import uvicorn.config
|
9
8
|
|
10
|
-
from acp_sdk.models import
|
9
|
+
from acp_sdk.models import Metadata
|
11
10
|
from acp_sdk.server.agent import Agent
|
11
|
+
from acp_sdk.server.agent import agent as agent_decorator
|
12
12
|
from acp_sdk.server.app import create_app
|
13
|
-
from acp_sdk.server.context import Context
|
14
13
|
from acp_sdk.server.logging import configure_logger as configure_logger_func
|
15
14
|
from acp_sdk.server.telemetry import configure_telemetry as configure_telemetry_func
|
16
|
-
from acp_sdk.server.types import RunYield, RunYieldResume
|
17
15
|
|
18
16
|
|
19
17
|
class Server:
|
@@ -21,92 +19,17 @@ class Server:
|
|
21
19
|
self._agents: list[Agent] = []
|
22
20
|
self._server: uvicorn.Server | None = None
|
23
21
|
|
24
|
-
def agent(
|
22
|
+
def agent(
|
23
|
+
self,
|
24
|
+
name: str | None = None,
|
25
|
+
description: str | None = None,
|
26
|
+
*,
|
27
|
+
metadata: Metadata | None = None,
|
28
|
+
) -> Callable:
|
25
29
|
"""Decorator to register an agent."""
|
26
30
|
|
27
31
|
def decorator(fn: Callable) -> Callable:
|
28
|
-
|
29
|
-
parameters = list(signature.parameters.values())
|
30
|
-
|
31
|
-
if len(parameters) == 0:
|
32
|
-
raise TypeError("The agent function must have at least 'input' argument")
|
33
|
-
if len(parameters) > 2:
|
34
|
-
raise TypeError("The agent function must have only 'input' and 'context' arguments")
|
35
|
-
if len(parameters) == 2 and parameters[1].name != "context":
|
36
|
-
raise TypeError("The second argument of the agent function must be 'context'")
|
37
|
-
|
38
|
-
has_context_param = len(parameters) == 2
|
39
|
-
|
40
|
-
agent: Agent
|
41
|
-
if inspect.isasyncgenfunction(fn):
|
42
|
-
|
43
|
-
class DecoratedAgent(Agent):
|
44
|
-
@property
|
45
|
-
def name(self) -> str:
|
46
|
-
return name or fn.__name__
|
47
|
-
|
48
|
-
@property
|
49
|
-
def description(self) -> str:
|
50
|
-
return description or fn.__doc__ or ""
|
51
|
-
|
52
|
-
async def run(self, input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
53
|
-
try:
|
54
|
-
gen: AsyncGenerator[RunYield, RunYieldResume] = (
|
55
|
-
fn(input, context) if has_context_param else fn(input)
|
56
|
-
)
|
57
|
-
value = None
|
58
|
-
while True:
|
59
|
-
value = yield await gen.asend(value)
|
60
|
-
except StopAsyncIteration:
|
61
|
-
pass
|
62
|
-
|
63
|
-
agent = DecoratedAgent()
|
64
|
-
elif inspect.iscoroutinefunction(fn):
|
65
|
-
|
66
|
-
class DecoratedAgent(Agent):
|
67
|
-
@property
|
68
|
-
def name(self) -> str:
|
69
|
-
return name or fn.__name__
|
70
|
-
|
71
|
-
@property
|
72
|
-
def description(self) -> str:
|
73
|
-
return description or fn.__doc__ or ""
|
74
|
-
|
75
|
-
async def run(self, input: Message, context: Context) -> Coroutine[RunYield]:
|
76
|
-
return await (fn(input, context) if has_context_param else fn(input))
|
77
|
-
|
78
|
-
agent = DecoratedAgent()
|
79
|
-
elif inspect.isgeneratorfunction(fn):
|
80
|
-
|
81
|
-
class DecoratedAgent(Agent):
|
82
|
-
@property
|
83
|
-
def name(self) -> str:
|
84
|
-
return name or fn.__name__
|
85
|
-
|
86
|
-
@property
|
87
|
-
def description(self) -> str:
|
88
|
-
return description or fn.__doc__ or ""
|
89
|
-
|
90
|
-
def run(self, input: Message, context: Context) -> Generator[RunYield, RunYieldResume]:
|
91
|
-
yield from (fn(input, context) if has_context_param else fn(input))
|
92
|
-
|
93
|
-
agent = DecoratedAgent()
|
94
|
-
else:
|
95
|
-
|
96
|
-
class DecoratedAgent(Agent):
|
97
|
-
@property
|
98
|
-
def name(self) -> str:
|
99
|
-
return name or fn.__name__
|
100
|
-
|
101
|
-
@property
|
102
|
-
def description(self) -> str:
|
103
|
-
return description or fn.__doc__ or ""
|
104
|
-
|
105
|
-
def run(self, input: Message, context: Context) -> RunYield:
|
106
|
-
return fn(input, context) if has_context_param else fn(input)
|
107
|
-
|
108
|
-
agent = DecoratedAgent()
|
109
|
-
|
32
|
+
agent = agent_decorator(name=name, description=description, metadata=metadata)(fn)
|
110
33
|
self.register(agent)
|
111
34
|
return fn
|
112
35
|
|
@@ -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/server/types.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from typing import Any
|
2
2
|
|
3
|
-
from acp_sdk.models import
|
3
|
+
from acp_sdk.models import AwaitRequest, AwaitResume, Message
|
4
4
|
|
5
|
-
RunYield = Message |
|
5
|
+
RunYield = Message | AwaitRequest | dict[str | Any]
|
6
6
|
RunYieldResume = AwaitResume | None
|
acp_sdk/version.py
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: acp-sdk
|
3
|
+
Version: 0.2.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: opentelemetry-api>=1.31.1
|
10
|
+
Requires-Dist: pydantic>=2.11.1
|
11
|
+
Provides-Extra: client
|
12
|
+
Requires-Dist: httpx-sse>=0.4.0; extra == 'client'
|
13
|
+
Requires-Dist: httpx>=0.28.1; extra == 'client'
|
14
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.52b1; extra == 'client'
|
15
|
+
Provides-Extra: server
|
16
|
+
Requires-Dist: fastapi[standard]>=0.115.8; extra == 'server'
|
17
|
+
Requires-Dist: janus>=2.0.0; extra == 'server'
|
18
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.31.1; extra == 'server'
|
19
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.52b1; extra == 'server'
|
20
|
+
Requires-Dist: opentelemetry-sdk>=1.31.1; extra == 'server'
|
21
|
+
Description-Content-Type: text/markdown
|
22
|
+
|
23
|
+
# Agent Communication Protocol SDK for Python
|
24
|
+
|
25
|
+
Agent Communication Protocol SDK for Python provides allows developers to serve and consume agents over the Agent Communication Protocol.
|
26
|
+
|
27
|
+
## Prerequisites
|
28
|
+
|
29
|
+
✅ Python >= 3.11
|
30
|
+
|
31
|
+
## Installation
|
32
|
+
|
33
|
+
Install to use client:
|
34
|
+
|
35
|
+
```shell
|
36
|
+
pip install acp-sdk[client]
|
37
|
+
```
|
38
|
+
|
39
|
+
Install to use server:
|
40
|
+
|
41
|
+
```shell
|
42
|
+
pip install acp-sdk[server]
|
43
|
+
```
|
44
|
+
|
45
|
+
Install to use models only:
|
46
|
+
|
47
|
+
```shell
|
48
|
+
pip install acp-sdk
|
49
|
+
```
|
50
|
+
|
51
|
+
## Overview
|
52
|
+
|
53
|
+
### Core
|
54
|
+
|
55
|
+
The core of the SDK exposes [pydantic](https://docs.pydantic.dev/) data models corresponding to REST API requests, responses, resources, events and errors.
|
56
|
+
|
57
|
+
|
58
|
+
### Client
|
59
|
+
|
60
|
+
The `client` submodule exposes [httpx](https://www.python-httpx.org/) based client with simple methods for communication over ACP.
|
61
|
+
|
62
|
+
```python
|
63
|
+
async with Client(base_url="http://localhost:8000") as client:
|
64
|
+
run = await client.run_sync(agent="echo", inputs=[Message(parts=[MessagePart(content="Howdy!")])])
|
65
|
+
print(run)
|
66
|
+
|
67
|
+
```
|
68
|
+
|
69
|
+
### Server
|
70
|
+
|
71
|
+
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.
|
72
|
+
|
73
|
+
```python
|
74
|
+
server = Server()
|
75
|
+
|
76
|
+
@server.agent()
|
77
|
+
async def echo(inputs: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
78
|
+
"""Echoes everything"""
|
79
|
+
for message in inputs:
|
80
|
+
yield {"thought": "I should echo everyting"}
|
81
|
+
await asyncio.sleep(0.5)
|
82
|
+
yield message
|
83
|
+
|
84
|
+
|
85
|
+
server.run()
|
86
|
+
```
|
87
|
+
|
88
|
+
➡️ Explore more in our [examples library](/python/examples).
|
89
|
+
|
90
|
+
## Architecture
|
91
|
+
|
92
|
+
The architecture of the SDK is outlined in the following segment. It focuses on central parts of the SDK without going into much detail.
|
93
|
+
|
94
|
+
### Models
|
95
|
+
|
96
|
+
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.
|
97
|
+
|
98
|
+
### Server
|
99
|
+
|
100
|
+
The server module consists of 3 parts:
|
101
|
+
|
102
|
+
1. Agent interface
|
103
|
+
2. FastAPI application factory
|
104
|
+
3. Uvicorn based server
|
105
|
+
|
106
|
+
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.
|
107
|
+
|
108
|
+
### Client
|
109
|
+
|
110
|
+
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.
|
111
|
+
|
112
|
+
|
113
|
+
|
@@ -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=yXEtwdCxBCXwNz7xeeD7gPT9J7SYG2rb8QAjhuYhTlY,6131
|
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=NCYYKKnWU1OSHQ0r1lSP_y4Z5cY3Agx-jmwfEwF3dLw,4001
|
9
|
+
acp_sdk/models/schemas.py,sha256=LNs3oN1pQ4GD0ceV3-k6X6R39eu33nsAfnW6uEgOP0c,735
|
10
|
+
acp_sdk/server/__init__.py,sha256=mxBBBFaZuMEUENRMLwp1XZkuLeT9QghcFmNvjnqvAAU,377
|
11
|
+
acp_sdk/server/agent.py,sha256=fGky5MIuknw-Gy-THqhWLt9I9-gUyNIar8qEAvZb3uQ,6195
|
12
|
+
acp_sdk/server/app.py,sha256=Ys5EN4MzmrwrpBGvycEP5dKEIYkDZmeBMMV1Aq58AU0,5897
|
13
|
+
acp_sdk/server/bundle.py,sha256=BOOGuzEPS4bp6dhflcsQkyawllbsWoS3l6JqwKqI9n0,6312
|
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=-eT3fmnEsBUN44Spi2EP2eV0l4RAlKa8bzqxnhz16SM,5399
|
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=1bqMCjwZM3JzvJ1h4aBHWzjbldMQ45HqcezBD6hUoYo,175
|
21
|
+
acp_sdk/server/utils.py,sha256=EfrF9VCyVk3AM_ao-BIB9EzGbfTrh4V2Bz-VFr6f6Sg,351
|
22
|
+
acp_sdk-0.2.0.dist-info/METADATA,sha256=B9sMxk3P4tBEoVCkEeKD_95EuohKvErdLR3VTU0JiKQ,3463
|
23
|
+
acp_sdk-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
24
|
+
acp_sdk-0.2.0.dist-info/RECORD,,
|
@@ -1,74 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: acp-sdk
|
3
|
-
Version: 0.1.0rc7
|
4
|
-
Summary: Agent Communication Protocol SDK
|
5
|
-
Requires-Python: <4.0,>=3.11
|
6
|
-
Requires-Dist: opentelemetry-api>=1.31.1
|
7
|
-
Requires-Dist: pydantic>=2.11.1
|
8
|
-
Provides-Extra: client
|
9
|
-
Requires-Dist: httpx-sse>=0.4.0; extra == 'client'
|
10
|
-
Requires-Dist: httpx>=0.28.1; extra == 'client'
|
11
|
-
Requires-Dist: opentelemetry-instrumentation-httpx>=0.52b1; extra == 'client'
|
12
|
-
Provides-Extra: server
|
13
|
-
Requires-Dist: fastapi[standard]>=0.115.8; extra == 'server'
|
14
|
-
Requires-Dist: janus>=2.0.0; extra == 'server'
|
15
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.31.1; extra == 'server'
|
16
|
-
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.52b1; extra == 'server'
|
17
|
-
Requires-Dist: opentelemetry-sdk>=1.31.1; extra == 'server'
|
18
|
-
Description-Content-Type: text/markdown
|
19
|
-
|
20
|
-
# Agent Communication Protocol SDK for Python
|
21
|
-
|
22
|
-
Agent Communication Protocol SDK for Python provides allows developers to serve and consume agents over the Agent Communication Protocol.
|
23
|
-
|
24
|
-
## Prerequisites
|
25
|
-
|
26
|
-
✅ Python >= 3.11
|
27
|
-
|
28
|
-
## Installation
|
29
|
-
|
30
|
-
Install to use client:
|
31
|
-
|
32
|
-
```shell
|
33
|
-
pip install acp-sdk[client]
|
34
|
-
```
|
35
|
-
|
36
|
-
Install to use server:
|
37
|
-
|
38
|
-
```shell
|
39
|
-
pip install acp-sdk[server]
|
40
|
-
```
|
41
|
-
|
42
|
-
## Overview
|
43
|
-
|
44
|
-
### Client
|
45
|
-
|
46
|
-
The `client` submodule exposes [httpx]() 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", input=Message(TextMessagePart(content="Howdy!")))
|
51
|
-
print(run.output)
|
52
|
-
```
|
53
|
-
|
54
|
-
### Server
|
55
|
-
|
56
|
-
The `server` submodule exposes [fastapi] application factory that makes it easy to expose any agent over ACP.
|
57
|
-
|
58
|
-
```python
|
59
|
-
server = Server()
|
60
|
-
|
61
|
-
@server.agent()
|
62
|
-
async def echo(input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
63
|
-
"""Echoes everything"""
|
64
|
-
for part in input:
|
65
|
-
await asyncio.sleep(0.5)
|
66
|
-
yield {"thought": "I should echo everyting"}
|
67
|
-
await asyncio.sleep(0.5)
|
68
|
-
yield Message(part)
|
69
|
-
|
70
|
-
|
71
|
-
server.run()
|
72
|
-
```
|
73
|
-
|
74
|
-
➡️ Explore more in our [examples library](/python/examples).
|
@@ -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=JhjI_M9GO-1lMrpZJo0IChDVrzUfnDyH5R5hFcqkXEE,3936
|
8
|
-
acp_sdk/models/schemas.py,sha256=TM_0aRsBj_9G__zzWPYDx4wAXsGs2RUH57IOTi4--Ek,728
|
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=rf8I2qcaaXoM-36Za8vs3Re7gygcTSMs4MXbMUBJENU,5803
|
12
|
-
acp_sdk/server/bundle.py,sha256=LZzinifpbKxEBivoxOSdxUmzEcu3XNAkCiIJc2Ivveg,4987
|
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=B1XGptw2kTgaB52klI4aNqDAlmyT2rNpMtKDSCxyVc4,8665
|
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.0rc7.dist-info/METADATA,sha256=IoDvdN2USySSWbm7I05iIuuRz-7vCzaBtsnilmgW5IQ,2033
|
21
|
-
acp_sdk-0.1.0rc7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
22
|
-
acp_sdk-0.1.0rc7.dist-info/RECORD,,
|
File without changes
|