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 CHANGED
@@ -1 +1,2 @@
1
1
  from acp_sdk.models import * # noqa: F403
2
+ from acp_sdk.version import __version__ as __version__
acp_sdk/client/client.py CHANGED
@@ -1,4 +1,6 @@
1
- from collections.abc import AsyncIterator
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__(self, *, base_url: httpx.URL | str = "", client: httpx.AsyncClient | None = None) -> None:
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, input: Message) -> Run:
80
+ async def run_sync(self, *, agent: AgentName, inputs: list[Message]) -> Run:
66
81
  response = await self._client.post(
67
82
  "/runs",
68
- json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.SYNC).model_dump(),
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
- return RunCreateResponse.model_validate(response.json())
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, input: Message) -> Run:
95
+ async def run_async(self, *, agent: AgentName, inputs: list[Message]) -> Run:
74
96
  response = await self._client.post(
75
97
  "/runs",
76
- json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.ASYNC).model_dump(),
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
- return RunCreateResponse.model_validate(response.json())
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, input: Message) -> AsyncIterator[RunEvent]:
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
- json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.STREAM).model_dump(),
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[RunEvent]:
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[RunEvent]:
166
+ ) -> AsyncIterator[Event]:
131
167
  async for event in event_source.aiter_sse():
132
- event = TypeAdapter(RunEvent).validate_json(event.data)
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, 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
36
 
41
37
 
42
- class Message(RootModel):
43
- root: list[MessagePart]
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.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
 
@@ -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
- await_: Await | None = Field(None, alias="await")
100
- output: Message | None = None
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
- def model_dump_json(
106
- self,
107
- **kwargs: dict[str, Any],
108
- ) -> str:
109
- return super().model_dump_json(
110
- by_alias=True,
111
- **kwargs,
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
- class MessageEvent(BaseModel):
116
- type: Literal["message"] = "message"
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
- await_: Await | None = Field(alias="await")
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 CreatedEvent(BaseModel):
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 InProgressEvent(BaseModel):
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 FailedEvent(BaseModel):
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 CancelledEvent(BaseModel):
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 CompletedEvent(BaseModel):
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
- RunEvent = Union[
167
- CreatedEvent,
168
- InProgressEvent,
169
- MessageEvent,
151
+ Event = Union[
152
+ RunCreatedEvent,
153
+ RunInProgressEvent,
154
+ MessageCreatedEvent,
155
+ ArtifactEvent,
156
+ MessagePartEvent,
157
+ MessageCompletedEvent,
170
158
  AwaitEvent,
171
159
  GenericEvent,
172
- CancelledEvent,
173
- FailedEvent,
174
- CompletedEvent,
160
+ RunCancelledEvent,
161
+ RunFailedEvent,
162
+ RunCompletedEvent,
163
+ MessagePartEvent,
175
164
  ]
176
165
 
177
166
 
acp_sdk/models/schemas.py CHANGED
@@ -14,7 +14,7 @@ class AgentReadResponse(Agent):
14
14
  class RunCreateRequest(BaseModel):
15
15
  agent_name: AgentName
16
16
  session_id: SessionId | None = None
17
- input: Message
17
+ inputs: list[Message]
18
18
  mode: RunMode = RunMode.SYNC
19
19
 
20
20
 
@@ -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
 
@@ -31,19 +32,14 @@ class Agent(abc.ABC):
31
32
 
32
33
  @abc.abstractmethod
33
34
  def run(
34
- self, input: Message, context: Context
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, input: Message, session_id: SessionId | None, executor: ThreadPoolExecutor
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(input, context))
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(input, context))
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, input, context)
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, input, context)
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
- RunStatus,
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(max_workers=5) as exec:
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
- agent_name=agent.name,
103
- session_id=request.session_id,
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.stream_queue = asyncio.Queue() # TODO improve
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=403,
168
- detail=f"Run with terminal status {bundle.run.status} can't be cancelled",
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.task.cancel()
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
- FailedEvent,
14
+ Event,
19
15
  GenericEvent,
20
- InProgressEvent,
21
16
  Message,
22
- MessageEvent,
17
+ MessageCreatedEvent,
18
+ MessagePartEvent,
23
19
  Run,
24
- RunEvent,
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__(self, *, agent: Agent, run: Run, task: asyncio.Task | None = None) -> None:
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.task = task
40
+ self.inputs = inputs
41
+ self.history = history
37
42
 
38
- self.stream_queue: asyncio.Queue[RunEvent] = 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
- async def stream(self) -> AsyncGenerator[RunEvent]:
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: RunEvent) -> None:
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 execute(self, input: Message, *, executor: ThreadPoolExecutor) -> None:
72
- with trace.get_tracer(__name__).start_as_current_span("execute"):
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
- await self.emit(CreatedEvent(run=self.run))
88
+ in_message = False
76
89
  try:
77
- self.run.session_id = await self.agent.session(self.run.session_id)
78
- run_logger.info("Session loaded")
90
+ await self.emit(RunCreatedEvent(run=self.run))
79
91
 
80
- generator = self.agent.execute(input=input, session_id=self.run.session_id, executor=executor)
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(InProgressEvent(run=self.run))
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
- if isinstance(next, Message):
90
- self.composed_message += next
91
- await self.emit(MessageEvent(message=next))
92
- elif isinstance(next, Await):
93
- self.run.await_ = next
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.status = RunStatus.IN_PROGRESS
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
- self.run.output = self.composed_message
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(CompletedEvent(run=self.run))
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(CancelledEvent(run=self.run))
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(FailedEvent(run=self.run))
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 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
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(self, name: str | None = None, description: str | None = None) -> Callable:
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
- signature = inspect.signature(fn)
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
@@ -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: version("acp-sdk"),
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 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
acp_sdk/version.py ADDED
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("acp-sdk")
@@ -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,,