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 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[RunEvent]:
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, CreatedEvent):
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[RunEvent]:
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[RunEvent]:
166
+ ) -> AsyncIterator[Event]:
167
167
  async for event in event_source.aiter_sse():
168
- event = TypeAdapter(RunEvent).validate_json(event.data)
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, RootModel
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 MessagePartBase(BaseModel):
20
- type: Literal["text", "image", "artifact"]
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
- class TextMessagePart(MessagePartBase):
24
- type: Literal["text"] = "text"
25
- content: str
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 ImageMessagePart(MessagePartBase):
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
- 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.root + other.root))
44
+ return Message(*(self.parts + other.parts))
55
45
 
56
46
  def __str__(self) -> str:
57
- return "".join(str(part) for part in self.root if isinstance(part, TextMessagePart))
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 Await(BaseModel):
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
- await_: Await | None = Field(None, alias="await")
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
- def model_dump_json(
111
- self,
112
- **kwargs: dict[str, Any],
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 MessageEvent(BaseModel):
121
- type: Literal["message"] = "message"
122
- message: Message
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["artifact"] = "artifact"
127
- artifact: Artifact
107
+ type: Literal["message.part"] = "message.part"
108
+ part: Artifact
128
109
 
129
110
 
130
- class AwaitEvent(BaseModel):
131
- type: Literal["await"] = "await"
132
- await_: Await | None = Field(alias="await")
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
- def model_dump_json(
137
- self,
138
- **kwargs: dict[str, Any],
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 CreatedEvent(BaseModel):
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 InProgressEvent(BaseModel):
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 FailedEvent(BaseModel):
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 CancelledEvent(BaseModel):
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 CompletedEvent(BaseModel):
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
- RunEvent = Union[
177
- CreatedEvent,
178
- InProgressEvent,
179
- MessageEvent,
151
+ Event = Union[
152
+ RunCreatedEvent,
153
+ RunInProgressEvent,
154
+ MessageCreatedEvent,
155
+ ArtifactEvent,
156
+ MessagePartEvent,
157
+ MessageCompletedEvent,
180
158
  AwaitEvent,
181
159
  GenericEvent,
182
- CancelledEvent,
183
- FailedEvent,
184
- CompletedEvent,
160
+ RunCancelledEvent,
161
+ RunFailedEvent,
162
+ RunCompletedEvent,
163
+ MessagePartEvent,
185
164
  ]
186
165
 
187
166
 
@@ -1,4 +1,5 @@
1
1
  from acp_sdk.server.agent import Agent as Agent
2
+ from acp_sdk.server.agent import agent as agent
2
3
  from acp_sdk.server.app import create_app as create_app
3
4
  from acp_sdk.server.context import Context as Context
4
5
  from acp_sdk.server.server import Server as Server
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
- FailedEvent,
14
+ Event,
20
15
  GenericEvent,
21
- InProgressEvent,
22
16
  Message,
23
- MessageEvent,
17
+ MessageCreatedEvent,
18
+ MessagePartEvent,
24
19
  Run,
25
- RunEvent,
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[RunEvent] = 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[RunEvent]:
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: RunEvent) -> None:
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.await_ = None
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.await_ = None
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(CreatedEvent(run=self.run))
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(InProgressEvent(run=self.run))
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
- if isinstance(next, Message):
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(MessageEvent(message=next))
105
- elif isinstance(next, Artifact):
106
- self.run.artifacts.append(next)
107
- await self.emit(ArtifactEvent(artifact=next))
108
- elif isinstance(next, Await):
109
- self.run.await_ = 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
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(InProgressEvent(run=self.run))
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(CompletedEvent(run=self.run))
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(CancelledEvent(run=self.run))
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(FailedEvent(run=self.run))
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 AsyncGenerator, Awaitable, Coroutine, Generator
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 Message, Metadata
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
- signature = inspect.signature(fn)
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 Await, AwaitResume, Message
3
+ from acp_sdk.models import AwaitRequest, AwaitResume, Message
4
4
 
5
- RunYield = Message | Await | dict[str | Any]
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=phsFdVMdVmTcfmzUQUgqzzvq4zNvmEUgW2eL9cFJY2g,6140
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=LrOZIWwn_6SenSwzz2JeK7Q555vzqjBKFElCifxQgnk,4107
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=CowMcwN_WSsnO_-ZoqWQKtNVfa21MW3X3trZ9haJjaA,329
11
- acp_sdk/server/agent.py,sha256=wMwyB3Ouz361DX-RDPFD61nymoUOTvBIJt3_JU9KmOw,3297
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=VUOqzBhwsADkoyPWqXSZ2KG59S3q4LMDfpFVAssnGSM,5386
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=6iCJZrMfIDtqEmxqFREpPbyAiuxVxbQD73QHiFJ6KpA,9307
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=2yJPkfUzjVIhHmc0SegGTMqDROe2uFgycb-7CATvYVw,161
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.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,,
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).