acp-sdk 0.1.0rc8__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/client/client.py +7 -7
- acp_sdk/models/models.py +58 -79
- acp_sdk/server/__init__.py +1 -0
- acp_sdk/server/agent.py +78 -0
- acp_sdk/server/bundle.py +42 -28
- acp_sdk/server/server.py +4 -103
- acp_sdk/server/types.py +2 -2
- acp_sdk-0.2.0.dist-info/METADATA +113 -0
- {acp_sdk-0.1.0rc8.dist-info → acp_sdk-0.2.0.dist-info}/RECORD +10 -10
- acp_sdk-0.1.0rc8.dist-info/METADATA +0 -74
- {acp_sdk-0.1.0rc8.dist-info → acp_sdk-0.2.0.dist-info}/WHEEL +0 -0
acp_sdk/client/client.py
CHANGED
@@ -16,14 +16,14 @@ from acp_sdk.models import (
|
|
16
16
|
AgentReadResponse,
|
17
17
|
AgentsListResponse,
|
18
18
|
AwaitResume,
|
19
|
-
CreatedEvent,
|
20
19
|
Error,
|
20
|
+
Event,
|
21
21
|
Message,
|
22
22
|
Run,
|
23
23
|
RunCancelResponse,
|
24
|
+
RunCreatedEvent,
|
24
25
|
RunCreateRequest,
|
25
26
|
RunCreateResponse,
|
26
|
-
RunEvent,
|
27
27
|
RunId,
|
28
28
|
RunMode,
|
29
29
|
RunResumeRequest,
|
@@ -107,7 +107,7 @@ class Client:
|
|
107
107
|
self._set_session(response)
|
108
108
|
return response
|
109
109
|
|
110
|
-
async def run_stream(self, *, agent: AgentName, inputs: list[Message]) -> AsyncIterator[
|
110
|
+
async def run_stream(self, *, agent: AgentName, inputs: list[Message]) -> AsyncIterator[Event]:
|
111
111
|
async with aconnect_sse(
|
112
112
|
self._client,
|
113
113
|
"POST",
|
@@ -120,7 +120,7 @@ class Client:
|
|
120
120
|
).model_dump_json(),
|
121
121
|
) as event_source:
|
122
122
|
async for event in self._validate_stream(event_source):
|
123
|
-
if isinstance(event,
|
123
|
+
if isinstance(event, RunCreatedEvent):
|
124
124
|
self._set_session(event.run)
|
125
125
|
yield event
|
126
126
|
|
@@ -150,7 +150,7 @@ class Client:
|
|
150
150
|
self._raise_error(response)
|
151
151
|
return RunResumeResponse.model_validate(response.json())
|
152
152
|
|
153
|
-
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]:
|
154
154
|
async with aconnect_sse(
|
155
155
|
self._client,
|
156
156
|
"POST",
|
@@ -163,9 +163,9 @@ class Client:
|
|
163
163
|
async def _validate_stream(
|
164
164
|
self,
|
165
165
|
event_source: EventSource,
|
166
|
-
) -> AsyncIterator[
|
166
|
+
) -> AsyncIterator[Event]:
|
167
167
|
async for event in event_source.aiter_sse():
|
168
|
-
event = TypeAdapter(
|
168
|
+
event = TypeAdapter(Event).validate_json(event.data)
|
169
169
|
yield event
|
170
170
|
|
171
171
|
def _raise_error(self, response: httpx.Response) -> None:
|
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
|
-
|
41
36
|
|
42
|
-
class Message(RootModel):
|
43
|
-
root: list[MessagePart]
|
44
37
|
|
45
|
-
|
46
|
-
|
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
|
|
@@ -91,56 +83,39 @@ class AwaitResume(BaseModel):
|
|
91
83
|
pass
|
92
84
|
|
93
85
|
|
94
|
-
class Artifact(BaseModel):
|
95
|
-
pass
|
96
|
-
|
97
|
-
|
98
86
|
class Run(BaseModel):
|
99
87
|
run_id: RunId = Field(default_factory=uuid.uuid4)
|
100
88
|
agent_name: AgentName
|
101
89
|
session_id: SessionId | None = None
|
102
90
|
status: RunStatus = RunStatus.CREATED
|
103
|
-
|
91
|
+
await_request: AwaitRequest | None = None
|
104
92
|
outputs: list[Message] = []
|
105
|
-
artifacts: list[Artifact] = []
|
106
93
|
error: Error | None = None
|
107
94
|
|
108
|
-
model_config = ConfigDict(populate_by_name=True)
|
109
95
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
) -> str:
|
114
|
-
return super().model_dump_json(
|
115
|
-
by_alias=True,
|
116
|
-
**kwargs,
|
117
|
-
)
|
96
|
+
class MessageCreatedEvent(BaseModel):
|
97
|
+
type: Literal["message.created"] = "message.created"
|
98
|
+
message: Message
|
118
99
|
|
119
100
|
|
120
|
-
class
|
121
|
-
type: Literal["message"] = "message"
|
122
|
-
|
101
|
+
class MessagePartEvent(BaseModel):
|
102
|
+
type: Literal["message.part"] = "message.part"
|
103
|
+
part: MessagePart
|
123
104
|
|
124
105
|
|
125
106
|
class ArtifactEvent(BaseModel):
|
126
|
-
type: Literal["
|
127
|
-
|
107
|
+
type: Literal["message.part"] = "message.part"
|
108
|
+
part: Artifact
|
128
109
|
|
129
110
|
|
130
|
-
class
|
131
|
-
type: Literal["
|
132
|
-
|
111
|
+
class MessageCompletedEvent(BaseModel):
|
112
|
+
type: Literal["message.completed"] = "message.completed"
|
113
|
+
message: Message
|
133
114
|
|
134
|
-
model_config = ConfigDict(populate_by_name=True)
|
135
115
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
) -> str:
|
140
|
-
return super().model_dump_json(
|
141
|
-
by_alias=True,
|
142
|
-
**kwargs,
|
143
|
-
)
|
116
|
+
class AwaitEvent(BaseModel):
|
117
|
+
type: Literal["await"] = "await"
|
118
|
+
await_request: AwaitRequest | None = None
|
144
119
|
|
145
120
|
|
146
121
|
class GenericEvent(BaseModel):
|
@@ -148,40 +123,44 @@ class GenericEvent(BaseModel):
|
|
148
123
|
generic: AnyModel
|
149
124
|
|
150
125
|
|
151
|
-
class
|
152
|
-
type: Literal["created"] = "created"
|
126
|
+
class RunCreatedEvent(BaseModel):
|
127
|
+
type: Literal["run.created"] = "run.created"
|
153
128
|
run: Run
|
154
129
|
|
155
130
|
|
156
|
-
class
|
157
|
-
type: Literal["in-progress"] = "in-progress"
|
131
|
+
class RunInProgressEvent(BaseModel):
|
132
|
+
type: Literal["run.in-progress"] = "run.in-progress"
|
158
133
|
run: Run
|
159
134
|
|
160
135
|
|
161
|
-
class
|
162
|
-
type: Literal["failed"] = "failed"
|
136
|
+
class RunFailedEvent(BaseModel):
|
137
|
+
type: Literal["run.failed"] = "run.failed"
|
163
138
|
run: Run
|
164
139
|
|
165
140
|
|
166
|
-
class
|
167
|
-
type: Literal["cancelled"] = "cancelled"
|
141
|
+
class RunCancelledEvent(BaseModel):
|
142
|
+
type: Literal["run.cancelled"] = "run.cancelled"
|
168
143
|
run: Run
|
169
144
|
|
170
145
|
|
171
|
-
class
|
172
|
-
type: Literal["completed"] = "completed"
|
146
|
+
class RunCompletedEvent(BaseModel):
|
147
|
+
type: Literal["run.completed"] = "run.completed"
|
173
148
|
run: Run
|
174
149
|
|
175
150
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
151
|
+
Event = Union[
|
152
|
+
RunCreatedEvent,
|
153
|
+
RunInProgressEvent,
|
154
|
+
MessageCreatedEvent,
|
155
|
+
ArtifactEvent,
|
156
|
+
MessagePartEvent,
|
157
|
+
MessageCompletedEvent,
|
180
158
|
AwaitEvent,
|
181
159
|
GenericEvent,
|
182
|
-
|
183
|
-
|
184
|
-
|
160
|
+
RunCancelledEvent,
|
161
|
+
RunFailedEvent,
|
162
|
+
RunCompletedEvent,
|
163
|
+
MessagePartEvent,
|
185
164
|
]
|
186
165
|
|
187
166
|
|
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
|
|
@@ -98,3 +99,80 @@ class Agent(abc.ABC):
|
|
98
99
|
context.yield_sync(self.run(input, context))
|
99
100
|
finally:
|
100
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/bundle.py
CHANGED
@@ -7,25 +7,25 @@ from pydantic import ValidationError
|
|
7
7
|
|
8
8
|
from acp_sdk.models import (
|
9
9
|
AnyModel,
|
10
|
-
Artifact,
|
11
|
-
ArtifactEvent,
|
12
|
-
Await,
|
13
10
|
AwaitEvent,
|
11
|
+
AwaitRequest,
|
14
12
|
AwaitResume,
|
15
|
-
CancelledEvent,
|
16
|
-
CompletedEvent,
|
17
|
-
CreatedEvent,
|
18
13
|
Error,
|
19
|
-
|
14
|
+
Event,
|
20
15
|
GenericEvent,
|
21
|
-
InProgressEvent,
|
22
16
|
Message,
|
23
|
-
|
17
|
+
MessageCreatedEvent,
|
18
|
+
MessagePartEvent,
|
24
19
|
Run,
|
25
|
-
|
20
|
+
RunCancelledEvent,
|
21
|
+
RunCompletedEvent,
|
22
|
+
RunCreatedEvent,
|
23
|
+
RunFailedEvent,
|
24
|
+
RunInProgressEvent,
|
26
25
|
RunStatus,
|
27
26
|
)
|
28
27
|
from acp_sdk.models.errors import ErrorCode
|
28
|
+
from acp_sdk.models.models import MessageCompletedEvent, MessagePart
|
29
29
|
from acp_sdk.server.agent import Agent
|
30
30
|
from acp_sdk.server.logging import logger
|
31
31
|
from acp_sdk.server.telemetry import get_tracer
|
@@ -40,14 +40,14 @@ class RunBundle:
|
|
40
40
|
self.inputs = inputs
|
41
41
|
self.history = history
|
42
42
|
|
43
|
-
self.stream_queue: asyncio.Queue[
|
43
|
+
self.stream_queue: asyncio.Queue[Event] = asyncio.Queue()
|
44
44
|
|
45
45
|
self.await_queue: asyncio.Queue[AwaitResume] = asyncio.Queue(maxsize=1)
|
46
46
|
self.await_or_terminate_event = asyncio.Event()
|
47
47
|
|
48
48
|
self.task = asyncio.create_task(self._execute(inputs, executor=executor))
|
49
49
|
|
50
|
-
async def stream(self) -> AsyncGenerator[
|
50
|
+
async def stream(self) -> AsyncGenerator[Event]:
|
51
51
|
while True:
|
52
52
|
event = await self.stream_queue.get()
|
53
53
|
if event is None:
|
@@ -55,7 +55,7 @@ class RunBundle:
|
|
55
55
|
yield event
|
56
56
|
self.stream_queue.task_done()
|
57
57
|
|
58
|
-
async def emit(self, event:
|
58
|
+
async def emit(self, event: Event) -> None:
|
59
59
|
await self.stream_queue.put(event)
|
60
60
|
|
61
61
|
async def await_(self) -> AwaitResume:
|
@@ -71,12 +71,12 @@ class RunBundle:
|
|
71
71
|
self.stream_queue = asyncio.Queue()
|
72
72
|
await self.await_queue.put(resume)
|
73
73
|
self.run.status = RunStatus.IN_PROGRESS
|
74
|
-
self.run.
|
74
|
+
self.run.await_request = None
|
75
75
|
|
76
76
|
async def cancel(self) -> None:
|
77
77
|
self.task.cancel()
|
78
78
|
self.run.status = RunStatus.CANCELLING
|
79
|
-
self.run.
|
79
|
+
self.run.await_request = None
|
80
80
|
|
81
81
|
async def join(self) -> None:
|
82
82
|
await self.await_or_terminate_event.wait()
|
@@ -85,8 +85,9 @@ class RunBundle:
|
|
85
85
|
with get_tracer().start_as_current_span("run"):
|
86
86
|
run_logger = logging.LoggerAdapter(logger, {"run_id": str(self.run.run_id)})
|
87
87
|
|
88
|
+
in_message = False
|
88
89
|
try:
|
89
|
-
await self.emit(
|
90
|
+
await self.emit(RunCreatedEvent(run=self.run))
|
90
91
|
|
91
92
|
generator = self.agent.execute(
|
92
93
|
inputs=self.history + inputs, session_id=self.run.session_id, executor=executor
|
@@ -94,19 +95,30 @@ class RunBundle:
|
|
94
95
|
run_logger.info("Run started")
|
95
96
|
|
96
97
|
self.run.status = RunStatus.IN_PROGRESS
|
97
|
-
await self.emit(
|
98
|
+
await self.emit(RunInProgressEvent(run=self.run))
|
98
99
|
|
99
100
|
await_resume = None
|
100
101
|
while True:
|
101
102
|
next = await generator.asend(await_resume)
|
102
|
-
|
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
|
103
115
|
self.run.outputs.append(next)
|
104
|
-
await self.emit(
|
105
|
-
|
106
|
-
|
107
|
-
await self.emit(
|
108
|
-
elif isinstance(next,
|
109
|
-
self.run.
|
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
|
110
122
|
self.run.status = RunStatus.AWAITING
|
111
123
|
await self.emit(
|
112
124
|
AwaitEvent.model_validate(
|
@@ -119,7 +131,7 @@ class RunBundle:
|
|
119
131
|
)
|
120
132
|
run_logger.info("Run awaited")
|
121
133
|
await_resume = await self.await_()
|
122
|
-
await self.emit(
|
134
|
+
await self.emit(RunInProgressEvent(run=self.run))
|
123
135
|
run_logger.info("Run resumed")
|
124
136
|
else:
|
125
137
|
try:
|
@@ -128,17 +140,19 @@ class RunBundle:
|
|
128
140
|
except ValidationError:
|
129
141
|
raise TypeError("Invalid yield")
|
130
142
|
except StopAsyncIteration:
|
143
|
+
if in_message:
|
144
|
+
await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
|
131
145
|
self.run.status = RunStatus.COMPLETED
|
132
|
-
await self.emit(
|
146
|
+
await self.emit(RunCompletedEvent(run=self.run))
|
133
147
|
run_logger.info("Run completed")
|
134
148
|
except asyncio.CancelledError:
|
135
149
|
self.run.status = RunStatus.CANCELLED
|
136
|
-
await self.emit(
|
150
|
+
await self.emit(RunCancelledEvent(run=self.run))
|
137
151
|
run_logger.info("Run cancelled")
|
138
152
|
except Exception as e:
|
139
153
|
self.run.error = Error(code=ErrorCode.SERVER_ERROR, message=str(e))
|
140
154
|
self.run.status = RunStatus.FAILED
|
141
|
-
await self.emit(
|
155
|
+
await self.emit(RunFailedEvent(run=self.run))
|
142
156
|
run_logger.exception("Run failed")
|
143
157
|
raise
|
144
158
|
finally:
|
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:
|
@@ -31,104 +29,7 @@ class Server:
|
|
31
29
|
"""Decorator to register an agent."""
|
32
30
|
|
33
31
|
def decorator(fn: Callable) -> Callable:
|
34
|
-
|
35
|
-
parameters = list(signature.parameters.values())
|
36
|
-
|
37
|
-
if len(parameters) == 0:
|
38
|
-
raise TypeError("The agent function must have at least 'input' argument")
|
39
|
-
if len(parameters) > 2:
|
40
|
-
raise TypeError("The agent function must have only 'input' and 'context' arguments")
|
41
|
-
if len(parameters) == 2 and parameters[1].name != "context":
|
42
|
-
raise TypeError("The second argument of the agent function must be 'context'")
|
43
|
-
|
44
|
-
has_context_param = len(parameters) == 2
|
45
|
-
|
46
|
-
agent: Agent
|
47
|
-
if inspect.isasyncgenfunction(fn):
|
48
|
-
|
49
|
-
class DecoratedAgent(Agent):
|
50
|
-
@property
|
51
|
-
def name(self) -> str:
|
52
|
-
return name or fn.__name__
|
53
|
-
|
54
|
-
@property
|
55
|
-
def description(self) -> str:
|
56
|
-
return description or fn.__doc__ or ""
|
57
|
-
|
58
|
-
@property
|
59
|
-
def metadata(self) -> Metadata:
|
60
|
-
return metadata or Metadata()
|
61
|
-
|
62
|
-
async def run(self, input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
|
63
|
-
try:
|
64
|
-
gen: AsyncGenerator[RunYield, RunYieldResume] = (
|
65
|
-
fn(input, context) if has_context_param else fn(input)
|
66
|
-
)
|
67
|
-
value = None
|
68
|
-
while True:
|
69
|
-
value = yield await gen.asend(value)
|
70
|
-
except StopAsyncIteration:
|
71
|
-
pass
|
72
|
-
|
73
|
-
agent = DecoratedAgent()
|
74
|
-
elif inspect.iscoroutinefunction(fn):
|
75
|
-
|
76
|
-
class DecoratedAgent(Agent):
|
77
|
-
@property
|
78
|
-
def name(self) -> str:
|
79
|
-
return name or fn.__name__
|
80
|
-
|
81
|
-
@property
|
82
|
-
def description(self) -> str:
|
83
|
-
return description or fn.__doc__ or ""
|
84
|
-
|
85
|
-
@property
|
86
|
-
def metadata(self) -> Metadata:
|
87
|
-
return metadata or Metadata()
|
88
|
-
|
89
|
-
async def run(self, input: Message, context: Context) -> Coroutine[RunYield]:
|
90
|
-
return await (fn(input, context) if has_context_param else fn(input))
|
91
|
-
|
92
|
-
agent = DecoratedAgent()
|
93
|
-
elif inspect.isgeneratorfunction(fn):
|
94
|
-
|
95
|
-
class DecoratedAgent(Agent):
|
96
|
-
@property
|
97
|
-
def name(self) -> str:
|
98
|
-
return name or fn.__name__
|
99
|
-
|
100
|
-
@property
|
101
|
-
def description(self) -> str:
|
102
|
-
return description or fn.__doc__ or ""
|
103
|
-
|
104
|
-
@property
|
105
|
-
def metadata(self) -> Metadata:
|
106
|
-
return metadata or Metadata()
|
107
|
-
|
108
|
-
def run(self, input: Message, context: Context) -> Generator[RunYield, RunYieldResume]:
|
109
|
-
yield from (fn(input, context) if has_context_param else fn(input))
|
110
|
-
|
111
|
-
agent = DecoratedAgent()
|
112
|
-
else:
|
113
|
-
|
114
|
-
class DecoratedAgent(Agent):
|
115
|
-
@property
|
116
|
-
def name(self) -> str:
|
117
|
-
return name or fn.__name__
|
118
|
-
|
119
|
-
@property
|
120
|
-
def description(self) -> str:
|
121
|
-
return description or fn.__doc__ or ""
|
122
|
-
|
123
|
-
@property
|
124
|
-
def metadata(self) -> Metadata:
|
125
|
-
return metadata or Metadata()
|
126
|
-
|
127
|
-
def run(self, input: Message, context: Context) -> RunYield:
|
128
|
-
return fn(input, context) if has_context_param else fn(input)
|
129
|
-
|
130
|
-
agent = DecoratedAgent()
|
131
|
-
|
32
|
+
agent = agent_decorator(name=name, description=description, metadata=metadata)(fn)
|
132
33
|
self.register(agent)
|
133
34
|
return fn
|
134
35
|
|
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
|
@@ -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
|
+
|
@@ -2,23 +2,23 @@ acp_sdk/__init__.py,sha256=tXdAUM9zcmdSKCAkVrOCrGcXcuVS-yuvQUoQwTe9pek,98
|
|
2
2
|
acp_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
acp_sdk/version.py,sha256=Niy83rgvigB4hL_rR-O4ySvI7dj6xnqkyOe_JTymi9s,73
|
4
4
|
acp_sdk/client/__init__.py,sha256=Bca1DORrswxzZsrR2aUFpATuNG2xNSmYvF1Z2WJaVbc,51
|
5
|
-
acp_sdk/client/client.py,sha256=
|
5
|
+
acp_sdk/client/client.py,sha256=yXEtwdCxBCXwNz7xeeD7gPT9J7SYG2rb8QAjhuYhTlY,6131
|
6
6
|
acp_sdk/models/__init__.py,sha256=numSDBDT1QHx7n_Y3Deb5VOvKWcUBxbOEaMwQBSRHxc,151
|
7
7
|
acp_sdk/models/errors.py,sha256=rEyaMVvQuBi7fwWe_d0PGGySYsD3FZTluQ-SkC0yhAs,444
|
8
|
-
acp_sdk/models/models.py,sha256=
|
8
|
+
acp_sdk/models/models.py,sha256=NCYYKKnWU1OSHQ0r1lSP_y4Z5cY3Agx-jmwfEwF3dLw,4001
|
9
9
|
acp_sdk/models/schemas.py,sha256=LNs3oN1pQ4GD0ceV3-k6X6R39eu33nsAfnW6uEgOP0c,735
|
10
|
-
acp_sdk/server/__init__.py,sha256=
|
11
|
-
acp_sdk/server/agent.py,sha256=
|
10
|
+
acp_sdk/server/__init__.py,sha256=mxBBBFaZuMEUENRMLwp1XZkuLeT9QghcFmNvjnqvAAU,377
|
11
|
+
acp_sdk/server/agent.py,sha256=fGky5MIuknw-Gy-THqhWLt9I9-gUyNIar8qEAvZb3uQ,6195
|
12
12
|
acp_sdk/server/app.py,sha256=Ys5EN4MzmrwrpBGvycEP5dKEIYkDZmeBMMV1Aq58AU0,5897
|
13
|
-
acp_sdk/server/bundle.py,sha256=
|
13
|
+
acp_sdk/server/bundle.py,sha256=BOOGuzEPS4bp6dhflcsQkyawllbsWoS3l6JqwKqI9n0,6312
|
14
14
|
acp_sdk/server/context.py,sha256=MgnLV6qcDIhc_0BjW7r4Jj1tHts4ZuwpdTGIBnz2Mgo,1036
|
15
15
|
acp_sdk/server/errors.py,sha256=fWlgVsQ5hs_AXwzc-wvy6QgoDWEMRUBlSrfJfhHHMyE,2085
|
16
16
|
acp_sdk/server/logging.py,sha256=Oc8yZigCsuDnHHPsarRzu0RX3NKaLEgpELM2yovGKDI,411
|
17
|
-
acp_sdk/server/server.py,sha256
|
17
|
+
acp_sdk/server/server.py,sha256=-eT3fmnEsBUN44Spi2EP2eV0l4RAlKa8bzqxnhz16SM,5399
|
18
18
|
acp_sdk/server/session.py,sha256=0cDr924HC5x2bBNbK9NSKVHAt5A_mi5dK8P4jP_ugq0,629
|
19
19
|
acp_sdk/server/telemetry.py,sha256=WIEHK8syOTG9SyWi3Y-cos7CsCF5-IHGiyL9bCaUN0E,1921
|
20
|
-
acp_sdk/server/types.py,sha256=
|
20
|
+
acp_sdk/server/types.py,sha256=1bqMCjwZM3JzvJ1h4aBHWzjbldMQ45HqcezBD6hUoYo,175
|
21
21
|
acp_sdk/server/utils.py,sha256=EfrF9VCyVk3AM_ao-BIB9EzGbfTrh4V2Bz-VFr6f6Sg,351
|
22
|
-
acp_sdk-0.
|
23
|
-
acp_sdk-0.
|
24
|
-
acp_sdk-0.
|
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.0rc8
|
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", inputs=[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(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
|
69
|
-
|
70
|
-
|
71
|
-
server.run()
|
72
|
-
```
|
73
|
-
|
74
|
-
➡️ Explore more in our [examples library](/python/examples).
|
File without changes
|