acp-sdk 1.0.0rc1__py3-none-any.whl → 1.0.0rc3__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
@@ -0,0 +1 @@
1
+ from acp_sdk.models import * # noqa: F403
@@ -1 +1 @@
1
- from acp_sdk.client.client import Client
1
+ from acp_sdk.client.client import Client as Client
acp_sdk/client/client.py CHANGED
@@ -1,21 +1,24 @@
1
+ from collections.abc import AsyncIterator
1
2
  from types import TracebackType
2
- from typing import AsyncIterator
3
+ from typing import Self
3
4
 
4
5
  import httpx
5
6
  from httpx_sse import EventSource, aconnect_sse
6
-
7
7
  from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
8
+ from pydantic import TypeAdapter
8
9
 
9
10
  from acp_sdk.models import (
11
+ ACPError,
10
12
  Agent,
11
13
  AgentName,
12
14
  AgentReadResponse,
13
15
  AgentsListResponse,
14
16
  AwaitResume,
17
+ Error,
15
18
  Message,
19
+ Run,
16
20
  RunCancelResponse,
17
21
  RunCreateRequest,
18
- Run,
19
22
  RunCreateResponse,
20
23
  RunEvent,
21
24
  RunId,
@@ -23,23 +26,20 @@ from acp_sdk.models import (
23
26
  RunResumeRequest,
24
27
  RunResumeResponse,
25
28
  )
26
- from pydantic import TypeAdapter
27
29
 
28
30
 
29
31
  class Client:
30
- def __init__(
31
- self, *, base_url: httpx.URL | str = "", client: httpx.AsyncClient | None = None
32
- ):
32
+ def __init__(self, *, base_url: httpx.URL | str = "", client: httpx.AsyncClient | None = None) -> None:
33
33
  self.base_url = base_url
34
34
 
35
35
  self._client = self._init_client(client)
36
36
 
37
- def _init_client(self, client: httpx.AsyncClient | None = None):
37
+ def _init_client(self, client: httpx.AsyncClient | None = None) -> httpx.AsyncClient:
38
38
  client = client or httpx.AsyncClient(base_url=self.base_url)
39
39
  HTTPXClientInstrumentor.instrument_client(client)
40
40
  return client
41
41
 
42
- async def __aenter__(self):
42
+ async def __aenter__(self) -> Self:
43
43
  await self._client.__aenter__()
44
44
  return self
45
45
 
@@ -48,56 +48,54 @@ class Client:
48
48
  exc_type: type[BaseException] | None = None,
49
49
  exc_value: BaseException | None = None,
50
50
  traceback: TracebackType | None = None,
51
- ):
51
+ ) -> None:
52
52
  await self._client.__aexit__(exc_type, exc_value, traceback)
53
53
 
54
54
  async def agents(self) -> AsyncIterator[Agent]:
55
55
  response = await self._client.get("/agents")
56
+ self._raise_error(response)
56
57
  for agent in AgentsListResponse.model_validate(response.json()).agents:
57
58
  yield agent
58
59
 
59
60
  async def agent(self, *, name: AgentName) -> Agent:
60
61
  response = await self._client.get(f"/agents/{name}")
62
+ self._raise_error(response)
61
63
  return AgentReadResponse.model_validate(response.json())
62
64
 
63
65
  async def run_sync(self, *, agent: AgentName, input: Message) -> Run:
64
66
  response = await self._client.post(
65
67
  "/runs",
66
- json=RunCreateRequest(
67
- agent_name=agent, input=input, mode=RunMode.SYNC
68
- ).model_dump(),
68
+ json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.SYNC).model_dump(),
69
69
  )
70
+ self._raise_error(response)
70
71
  return RunCreateResponse.model_validate(response.json())
71
72
 
72
73
  async def run_async(self, *, agent: AgentName, input: Message) -> Run:
73
74
  response = await self._client.post(
74
75
  "/runs",
75
- json=RunCreateRequest(
76
- agent_name=agent, input=input, mode=RunMode.ASYNC
77
- ).model_dump(),
76
+ json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.ASYNC).model_dump(),
78
77
  )
78
+ self._raise_error(response)
79
79
  return RunCreateResponse.model_validate(response.json())
80
80
 
81
- async def run_stream(
82
- self, *, agent: AgentName, input: Message
83
- ) -> AsyncIterator[RunEvent]:
81
+ async def run_stream(self, *, agent: AgentName, input: Message) -> AsyncIterator[RunEvent]:
84
82
  async with aconnect_sse(
85
83
  self._client,
86
84
  "POST",
87
85
  "/runs",
88
- json=RunCreateRequest(
89
- agent_name=agent, input=input, mode=RunMode.STREAM
90
- ).model_dump(),
86
+ json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.STREAM).model_dump(),
91
87
  ) as event_source:
92
88
  async for event in self._validate_stream(event_source):
93
89
  yield event
94
90
 
95
91
  async def run_status(self, *, run_id: RunId) -> Run:
96
92
  response = await self._client.get(f"/runs/{run_id}")
93
+ self._raise_error(response)
97
94
  return Run.model_validate(response.json())
98
95
 
99
96
  async def run_cancel(self, *, run_id: RunId) -> Run:
100
97
  response = await self._client.post(f"/runs/{run_id}/cancel")
98
+ self._raise_error(response)
101
99
  return RunCancelResponse.model_validate(response.json())
102
100
 
103
101
  async def run_resume_sync(self, *, run_id: RunId, await_: AwaitResume) -> Run:
@@ -105,6 +103,7 @@ class Client:
105
103
  f"/runs/{run_id}",
106
104
  json=RunResumeRequest(await_=await_, mode=RunMode.SYNC).model_dump(),
107
105
  )
106
+ self._raise_error(response)
108
107
  return RunResumeResponse.model_validate(response.json())
109
108
 
110
109
  async def run_resume_async(self, *, run_id: RunId, await_: AwaitResume) -> Run:
@@ -112,11 +111,10 @@ class Client:
112
111
  f"/runs/{run_id}",
113
112
  json=RunResumeRequest(await_=await_, mode=RunMode.ASYNC).model_dump(),
114
113
  )
114
+ self._raise_error(response)
115
115
  return RunResumeResponse.model_validate(response.json())
116
116
 
117
- async def run_resume_stream(
118
- self, *, run_id: RunId, await_: AwaitResume
119
- ) -> AsyncIterator[RunEvent]:
117
+ async def run_resume_stream(self, *, run_id: RunId, await_: AwaitResume) -> AsyncIterator[RunEvent]:
120
118
  async with aconnect_sse(
121
119
  self._client,
122
120
  "POST",
@@ -133,3 +131,9 @@ class Client:
133
131
  async for event in event_source.aiter_sse():
134
132
  event = TypeAdapter(RunEvent).validate_json(event.data)
135
133
  yield event
134
+
135
+ def _raise_error(self, response: httpx.Response) -> None:
136
+ try:
137
+ response.raise_for_status()
138
+ except httpx.HTTPError:
139
+ raise ACPError(Error.model_validate(response.json()))
@@ -0,0 +1,2 @@
1
+ from acp_sdk.models.errors import * # noqa: F403
2
+ from acp_sdk.models.models import * # noqa: F403
@@ -0,0 +1,23 @@
1
+ from enum import Enum
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class ErrorCode(str, Enum):
7
+ SERVER_ERROR = "server_error"
8
+ INVALID_INPUT = "invalid_input"
9
+ NOT_FOUND = "not_found"
10
+
11
+
12
+ class Error(BaseModel):
13
+ code: ErrorCode
14
+ message: str
15
+
16
+
17
+ class ACPError(Exception):
18
+ def __init__(self, error: Error) -> None:
19
+ super().__init__()
20
+ self.error = error
21
+
22
+ def __str__(self) -> str:
23
+ return str(self.error.message)
@@ -1,13 +1,15 @@
1
- from enum import Enum
2
- from typing import Annotated, Literal, Union
3
1
  import uuid
2
+ from collections.abc import Iterator
3
+ from enum import Enum
4
+ from typing import Any, Literal, Union
4
5
 
5
6
  from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel
6
7
 
8
+ from acp_sdk.models.errors import Error
9
+
7
10
 
8
- class ACPError(BaseModel):
9
- code: str
10
- message: str
11
+ class Metadata(BaseModel):
12
+ model_config = ConfigDict(extra="allow")
11
13
 
12
14
 
13
15
  class AnyModel(BaseModel):
@@ -40,24 +42,19 @@ MessagePart = Union[TextMessagePart, ImageMessagePart, ArtifactMessagePart]
40
42
  class Message(RootModel):
41
43
  root: list[MessagePart]
42
44
 
43
- def __init__(self, *items: MessagePart):
45
+ def __init__(self, *items: MessagePart) -> None:
44
46
  super().__init__(root=list(items))
45
47
 
46
- def __iter__(self):
48
+ def __iter__(self) -> Iterator[MessagePart]:
47
49
  return iter(self.root)
48
50
 
49
- def __getitem__(self, item):
50
- return self.root[item]
51
-
52
51
  def __add__(self, other: "Message") -> "Message":
53
52
  if not isinstance(other, Message):
54
53
  raise TypeError(f"Cannot concatenate Message with {type(other).__name__}")
55
54
  return Message(*(self.root + other.root))
56
55
 
57
- def __str__(self):
58
- return "".join(
59
- str(part) for part in self.root if isinstance(part, TextMessagePart)
60
- )
56
+ def __str__(self) -> str:
57
+ return "".join(str(part) for part in self.root if isinstance(part, TextMessagePart))
61
58
 
62
59
 
63
60
  AgentName = str
@@ -101,14 +98,14 @@ class Run(BaseModel):
101
98
  status: RunStatus = RunStatus.CREATED
102
99
  await_: Await | None = Field(None, alias="await")
103
100
  output: Message | None = None
104
- error: ACPError | None = None
101
+ error: Error | None = None
105
102
 
106
103
  model_config = ConfigDict(populate_by_name=True)
107
104
 
108
105
  def model_dump_json(
109
106
  self,
110
- **kwargs,
111
- ):
107
+ **kwargs: dict[str, Any],
108
+ ) -> str:
112
109
  return super().model_dump_json(
113
110
  by_alias=True,
114
111
  **kwargs,
@@ -128,8 +125,8 @@ class AwaitEvent(BaseModel):
128
125
 
129
126
  def model_dump_json(
130
127
  self,
131
- **kwargs,
132
- ):
128
+ **kwargs: dict[str, Any],
129
+ ) -> str:
133
130
  return super().model_dump_json(
134
131
  by_alias=True,
135
132
  **kwargs,
@@ -209,6 +206,7 @@ class RunCancelResponse(Run):
209
206
  class Agent(BaseModel):
210
207
  name: str
211
208
  description: str | None = None
209
+ metadata: Metadata = Metadata()
212
210
 
213
211
 
214
212
  class AgentsListResponse(BaseModel):
@@ -1,2 +1,3 @@
1
- from acp_sdk.server.server import create_app
2
- from acp_sdk.server.agent import Agent
1
+ from acp_sdk.server.agent import Agent as Agent
2
+ from acp_sdk.server.server import create_app as create_app
3
+ from acp_sdk.server.server import serve as serve
acp_sdk/server/agent.py CHANGED
@@ -1,13 +1,14 @@
1
1
  import abc
2
- from typing import AsyncGenerator
2
+ from collections.abc import AsyncGenerator
3
3
 
4
4
  from acp_sdk.models import (
5
5
  AgentName,
6
- Message,
7
6
  Await,
8
7
  AwaitResume,
8
+ Message,
9
9
  SessionId,
10
10
  )
11
+ from acp_sdk.models.models import Metadata
11
12
  from acp_sdk.server.context import Context
12
13
 
13
14
 
@@ -20,10 +21,12 @@ class Agent(abc.ABC):
20
21
  def description(self) -> str:
21
22
  return ""
22
23
 
24
+ @property
25
+ def metadata(self) -> Metadata:
26
+ return Metadata()
27
+
23
28
  @abc.abstractmethod
24
- def run(
25
- self, input: Message, *, context: Context
26
- ) -> AsyncGenerator[Message | Await, AwaitResume]:
29
+ def run(self, input: Message, *, context: Context) -> AsyncGenerator[Message | Await, AwaitResume]:
27
30
  pass
28
31
 
29
32
  async def session(self, session_id: SessionId | None) -> SessionId | None:
acp_sdk/server/bundle.py CHANGED
@@ -1,35 +1,36 @@
1
1
  import asyncio
2
2
  import logging
3
+ from collections.abc import AsyncGenerator
3
4
 
4
5
  from opentelemetry import trace
5
6
  from pydantic import ValidationError
6
7
 
7
- from acp_sdk.server.agent import Agent
8
8
  from acp_sdk.models import (
9
- ACPError,
10
9
  AnyModel,
11
10
  Await,
12
11
  AwaitEvent,
12
+ AwaitResume,
13
13
  CancelledEvent,
14
14
  CompletedEvent,
15
15
  CreatedEvent,
16
+ Error,
16
17
  FailedEvent,
17
18
  GenericEvent,
18
19
  InProgressEvent,
19
20
  Message,
20
21
  MessageEvent,
21
22
  Run,
22
- AwaitResume,
23
23
  RunEvent,
24
24
  RunStatus,
25
25
  )
26
+ from acp_sdk.models.errors import ErrorCode
27
+ from acp_sdk.server.agent import Agent
26
28
  from acp_sdk.server.context import Context
27
-
28
- logger = logging.getLogger("uvicorn.error")
29
+ from acp_sdk.server.logging import logger
29
30
 
30
31
 
31
32
  class RunBundle:
32
- def __init__(self, *, agent: Agent, run: Run, task: asyncio.Task | None = None):
33
+ def __init__(self, *, agent: Agent, run: Run, task: asyncio.Task | None = None) -> None:
33
34
  self.agent = agent
34
35
  self.run = run
35
36
  self.task = task
@@ -40,20 +41,19 @@ class RunBundle:
40
41
  self.await_queue: asyncio.Queue[AwaitResume] = asyncio.Queue(maxsize=1)
41
42
  self.await_or_terminate_event = asyncio.Event()
42
43
 
43
- async def stream(self):
44
- try:
45
- while True:
46
- event = await self.stream_queue.get()
47
- yield event
48
- self.stream_queue.task_done()
49
- except asyncio.QueueShutDown:
50
- pass
44
+ async def stream(self) -> AsyncGenerator[RunEvent]:
45
+ while True:
46
+ event = await self.stream_queue.get()
47
+ if event is None:
48
+ break
49
+ yield event
50
+ self.stream_queue.task_done()
51
51
 
52
- async def emit(self, event: RunEvent):
52
+ async def emit(self, event: RunEvent) -> None:
53
53
  await self.stream_queue.put(event)
54
54
 
55
55
  async def await_(self) -> AwaitResume:
56
- self.stream_queue.shutdown()
56
+ await self.stream_queue.put(None)
57
57
  self.await_queue.empty()
58
58
  self.await_or_terminate_event.set()
59
59
  self.await_or_terminate_event.clear()
@@ -61,14 +61,14 @@ class RunBundle:
61
61
  self.await_queue.task_done()
62
62
  return resume
63
63
 
64
- async def resume(self, resume: AwaitResume):
64
+ async def resume(self, resume: AwaitResume) -> None:
65
65
  self.stream_queue = asyncio.Queue()
66
66
  await self.await_queue.put(resume)
67
67
 
68
- async def join(self):
68
+ async def join(self) -> None:
69
69
  await self.await_or_terminate_event.wait()
70
70
 
71
- async def execute(self, input: Message):
71
+ async def execute(self, input: Message) -> None:
72
72
  with trace.get_tracer(__name__).start_as_current_span("execute"):
73
73
  run_logger = logging.LoggerAdapter(logger, {"run_id": self.run.run_id})
74
74
 
@@ -77,9 +77,7 @@ class RunBundle:
77
77
  self.run.session_id = await self.agent.session(self.run.session_id)
78
78
  run_logger.info("Session loaded")
79
79
 
80
- generator = self.agent.run(
81
- input=input, context=Context(session_id=self.run.session_id)
82
- )
80
+ generator = self.agent.run(input=input, context=Context(session_id=self.run.session_id))
83
81
  run_logger.info("Run started")
84
82
 
85
83
  self.run.status = RunStatus.IN_PROGRESS
@@ -124,10 +122,10 @@ class RunBundle:
124
122
  await self.emit(CancelledEvent(run=self.run))
125
123
  run_logger.info("Run cancelled")
126
124
  except Exception as e:
127
- self.run.error = ACPError(code="unspecified", message=str(e))
125
+ self.run.error = Error(code=ErrorCode.SERVER_ERROR, message=str(e))
128
126
  self.run.status = RunStatus.FAILED
129
127
  await self.emit(FailedEvent(run=self.run))
130
128
  run_logger.exception("Run failed")
131
129
  finally:
132
130
  self.await_or_terminate_event.set()
133
- self.stream_queue.shutdown()
131
+ await self.stream_queue.put(None)
acp_sdk/server/context.py CHANGED
@@ -2,5 +2,5 @@ from acp_sdk.models import SessionId
2
2
 
3
3
 
4
4
  class Context:
5
- def __init__(self, *, session_id: SessionId | None = None):
5
+ def __init__(self, *, session_id: SessionId | None = None) -> None:
6
6
  self.session_id = session_id
@@ -0,0 +1,54 @@
1
+ from fastapi import Request, status
2
+ from fastapi.exceptions import RequestValidationError
3
+ from fastapi.responses import JSONResponse
4
+ from starlette.exceptions import HTTPException as StarletteHTTPException
5
+
6
+ from acp_sdk.models import Error, ErrorCode
7
+ from acp_sdk.models.errors import ACPError
8
+ from acp_sdk.server.logging import logger
9
+
10
+
11
+ def error_code_to_status_code(error_code: ErrorCode) -> int:
12
+ match error_code:
13
+ case ErrorCode.NOT_FOUND:
14
+ return status.HTTP_404_NOT_FOUND
15
+ case ErrorCode.INVALID_INPUT:
16
+ return status.HTTP_422_UNPROCESSABLE_ENTITY
17
+ case _:
18
+ return status.HTTP_500_INTERNAL_SERVER_ERROR
19
+
20
+
21
+ def status_code_to_error_code(status_code: int) -> ErrorCode:
22
+ match status_code:
23
+ case status.HTTP_400_BAD_REQUEST:
24
+ return ErrorCode.INVALID_INPUT
25
+ case status.HTTP_404_NOT_FOUND:
26
+ return ErrorCode.NOT_FOUND
27
+ case status.HTTP_422_UNPROCESSABLE_ENTITY:
28
+ return ErrorCode.INVALID_INPUT
29
+ case _:
30
+ return ErrorCode.SERVER_ERROR
31
+
32
+
33
+ async def acp_error_handler(request: Request, exc: ACPError, *, status_code: int | None = None) -> JSONResponse:
34
+ error = exc.error
35
+ return JSONResponse(status_code=status_code or error_code_to_status_code(error.code), content=error.model_dump())
36
+
37
+
38
+ async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
39
+ return await acp_error_handler(
40
+ request,
41
+ ACPError(Error(code=status_code_to_error_code(exc.status_code), message=exc.detail)),
42
+ status_code=exc.status_code,
43
+ )
44
+
45
+
46
+ async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
47
+ return await acp_error_handler(request, ACPError(Error(code=ErrorCode.INVALID_INPUT, message=str(exc))))
48
+
49
+
50
+ async def catch_all_exception_handler(request: Request, exc: Exception) -> JSONResponse:
51
+ logger.error(exc)
52
+ return await acp_error_handler(
53
+ request, ACPError(Error(code=ErrorCode.SERVER_ERROR, message="An unexpected error occurred"))
54
+ )
@@ -0,0 +1,16 @@
1
+ import logging
2
+
3
+ from uvicorn.logging import DefaultFormatter
4
+
5
+ logger = logging.getLogger("acp")
6
+
7
+
8
+ def configure_logger() -> None:
9
+ """Utility that configures the root logger"""
10
+ root_logger = logging.getLogger()
11
+
12
+ handler = logging.StreamHandler()
13
+ handler.setFormatter(DefaultFormatter(fmt="%(levelprefix)s %(message)s"))
14
+
15
+ root_logger.addHandler(handler)
16
+ root_logger.setLevel(logging.INFO)
acp_sdk/server/server.py CHANGED
@@ -1,17 +1,17 @@
1
1
  import asyncio
2
-
3
- from acp_sdk.server.telemetry import configure_telemetry
4
- from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
2
+ from typing import Any
5
3
 
6
4
  from fastapi import FastAPI, HTTPException, status
7
5
  from fastapi.responses import JSONResponse, StreamingResponse
6
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
8
7
 
9
- from acp_sdk.server.agent import Agent
10
8
  from acp_sdk.models import (
11
- AgentName,
12
9
  Agent as AgentModel,
13
- AgentsListResponse,
10
+ )
11
+ from acp_sdk.models import (
12
+ AgentName,
14
13
  AgentReadResponse,
14
+ AgentsListResponse,
15
15
  Run,
16
16
  RunCancelResponse,
17
17
  RunCreateRequest,
@@ -23,47 +23,63 @@ from acp_sdk.models import (
23
23
  RunResumeResponse,
24
24
  RunStatus,
25
25
  )
26
+ from acp_sdk.models.errors import ACPError
27
+ from acp_sdk.server.agent import Agent
26
28
  from acp_sdk.server.bundle import RunBundle
29
+ from acp_sdk.server.errors import (
30
+ RequestValidationError,
31
+ StarletteHTTPException,
32
+ acp_error_handler,
33
+ catch_all_exception_handler,
34
+ http_exception_handler,
35
+ validation_exception_handler,
36
+ )
37
+ from acp_sdk.server.logging import configure_logger as configure_logger_func
38
+ from acp_sdk.server.telemetry import configure_telemetry as configure_telemetry_func
27
39
  from acp_sdk.server.utils import stream_sse
28
40
 
29
41
 
30
42
  def create_app(*agents: Agent) -> FastAPI:
31
43
  app = FastAPI(title="acp-agents")
32
44
 
33
- configure_telemetry()
34
45
  FastAPIInstrumentor.instrument_app(app)
35
46
 
36
47
  agents: dict[AgentName, Agent] = {agent.name: agent for agent in agents}
37
- runs: dict[RunId, RunBundle] = dict()
48
+ runs: dict[RunId, RunBundle] = {}
38
49
 
39
- def find_run_bundle(run_id: RunId):
40
- bundle = runs.get(run_id, None)
50
+ app.exception_handler(ACPError)(acp_error_handler)
51
+ app.exception_handler(StarletteHTTPException)(http_exception_handler)
52
+ app.exception_handler(RequestValidationError)(validation_exception_handler)
53
+ app.exception_handler(Exception)(catch_all_exception_handler)
54
+
55
+ def find_run_bundle(run_id: RunId) -> RunBundle:
56
+ bundle = runs.get(run_id)
41
57
  if not bundle:
42
58
  raise HTTPException(status_code=404, detail=f"Run {run_id} not found")
43
59
  return bundle
44
60
 
45
- def find_agent(agent_name: AgentName):
61
+ def find_agent(agent_name: AgentName) -> Agent:
46
62
  agent = agents.get(agent_name, None)
47
63
  if not agent:
48
64
  raise HTTPException(status_code=404, detail=f"Agent {agent_name} not found")
49
65
  return agent
50
66
 
51
67
  @app.get("/agents")
52
- async def list() -> AgentsListResponse:
68
+ async def list_agents() -> AgentsListResponse:
53
69
  return AgentsListResponse(
54
70
  agents=[
55
- AgentModel(name=agent.name, description=agent.description)
71
+ AgentModel(name=agent.name, description=agent.description, metadata=agent.metadata)
56
72
  for agent in agents.values()
57
73
  ]
58
74
  )
59
75
 
60
76
  @app.get("/agents/{name}")
61
- async def read(name: AgentName) -> AgentReadResponse:
77
+ async def read_agent(name: AgentName) -> AgentReadResponse:
62
78
  agent = find_agent(name)
63
- return AgentModel(name=agent.name, description=agent.description)
79
+ return AgentModel(name=agent.name, description=agent.description, metadata=agent.metadata)
64
80
 
65
81
  @app.post("/runs")
66
- async def create(request: RunCreateRequest) -> RunCreateResponse:
82
+ async def create_run(request: RunCreateRequest) -> RunCreateResponse:
67
83
  agent = find_agent(request.agent_name)
68
84
  bundle = RunBundle(
69
85
  agent=agent,
@@ -94,12 +110,12 @@ def create_app(*agents: Agent) -> FastAPI:
94
110
  raise NotImplementedError()
95
111
 
96
112
  @app.get("/runs/{run_id}")
97
- async def read(run_id: RunId) -> RunReadResponse:
113
+ async def read_run(run_id: RunId) -> RunReadResponse:
98
114
  bundle = find_run_bundle(run_id)
99
115
  return bundle.run
100
116
 
101
117
  @app.post("/runs/{run_id}")
102
- async def resume(run_id: RunId, request: RunResumeRequest) -> RunResumeResponse:
118
+ async def resume_run(run_id: RunId, request: RunResumeRequest) -> RunResumeResponse:
103
119
  bundle = find_run_bundle(run_id)
104
120
  bundle.stream_queue = asyncio.Queue() # TODO improve
105
121
  await bundle.await_queue.put(request.await_)
@@ -121,7 +137,7 @@ def create_app(*agents: Agent) -> FastAPI:
121
137
  raise NotImplementedError()
122
138
 
123
139
  @app.post("/runs/{run_id}/cancel")
124
- async def cancel(run_id: RunId) -> RunCancelResponse:
140
+ async def cancel_run(run_id: RunId) -> RunCancelResponse:
125
141
  bundle = find_run_bundle(run_id)
126
142
  if bundle.run.status.is_terminal:
127
143
  raise HTTPException(
@@ -130,8 +146,19 @@ def create_app(*agents: Agent) -> FastAPI:
130
146
  )
131
147
  bundle.task.cancel()
132
148
  bundle.run.status = RunStatus.CANCELLING
133
- return JSONResponse(
134
- status_code=status.HTTP_202_ACCEPTED, content=bundle.run.model_dump()
135
- )
149
+ return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=bundle.run.model_dump())
136
150
 
137
151
  return app
152
+
153
+
154
+ def serve(
155
+ *agents: Agent, configure_logger: bool = True, configure_telemetry: bool = False, **kwargs: dict[str, Any]
156
+ ) -> None:
157
+ import uvicorn
158
+
159
+ if configure_logger:
160
+ configure_logger_func()
161
+ if configure_telemetry:
162
+ configure_telemetry_func()
163
+
164
+ uvicorn.run(create_app(*agents), **kwargs)
@@ -1,45 +1,52 @@
1
1
  import logging
2
2
  from importlib.metadata import version
3
3
 
4
- from opentelemetry import trace
4
+ from opentelemetry import metrics, trace
5
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
6
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
7
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
8
+ from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
9
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
10
+ from opentelemetry.sdk.metrics import MeterProvider
11
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
5
12
  from opentelemetry.sdk.resources import (
6
- Resource,
7
13
  SERVICE_NAME,
8
14
  SERVICE_NAMESPACE,
9
15
  SERVICE_VERSION,
16
+ Resource,
10
17
  )
11
18
  from opentelemetry.sdk.trace import TracerProvider
12
- from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExportResult
13
- from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
14
-
15
- logger = logging.getLogger("uvicorn.error")
16
-
17
-
18
- class SilentOTLPSpanExporter(OTLPSpanExporter):
19
- def export(self, spans):
20
- try:
21
- return super().export(spans)
22
- except Exception as e:
23
- logger.warning(f"OpenTelemetry Exporter failed silently: {e}")
24
- return SpanExportResult.FAILURE
25
-
26
-
27
- def configure_telemetry():
28
- current_provider = trace.get_tracer_provider()
29
-
30
- # Detect default provider and override
31
- if isinstance(current_provider, trace.ProxyTracerProvider):
32
- provider = TracerProvider(
33
- resource=Resource(
34
- attributes={
35
- SERVICE_NAME: "acp-server",
36
- SERVICE_NAMESPACE: "acp",
37
- SERVICE_VERSION: version("acp-sdk"),
38
- }
39
- )
40
- )
41
-
42
- processor = BatchSpanProcessor(SilentOTLPSpanExporter())
43
- provider.add_span_processor(processor)
44
-
45
- trace.set_tracer_provider(provider)
19
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
20
+
21
+ root_logger = logging.getLogger()
22
+
23
+
24
+ def configure_telemetry() -> None:
25
+ """Utility that configures opentelemetry with OTLP exporter"""
26
+
27
+ resource = Resource(
28
+ attributes={
29
+ SERVICE_NAME: "acp-server",
30
+ SERVICE_NAMESPACE: "acp",
31
+ SERVICE_VERSION: version("acp-sdk"),
32
+ }
33
+ )
34
+
35
+ # Traces
36
+ provider = TracerProvider(resource=resource)
37
+ processor = BatchSpanProcessor(OTLPSpanExporter())
38
+ provider.add_span_processor(processor)
39
+ trace.set_tracer_provider(provider)
40
+
41
+ # Metrics
42
+ meter_provider = MeterProvider(
43
+ resource=resource,
44
+ metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter())],
45
+ )
46
+ metrics.set_meter_provider(meter_provider)
47
+
48
+ # Logs
49
+ logger_provider = LoggerProvider(resource=resource)
50
+ processor = BatchLogRecordProcessor(OTLPLogExporter())
51
+ logger_provider.add_log_record_processor(processor)
52
+ root_logger.addHandler(LoggingHandler(logger_provider=logger_provider))
acp_sdk/server/utils.py CHANGED
@@ -1,12 +1,14 @@
1
+ from collections.abc import AsyncGenerator
2
+
1
3
  from pydantic import BaseModel
2
4
 
3
5
  from acp_sdk.server.bundle import RunBundle
4
6
 
5
7
 
6
- def encode_sse(model: BaseModel):
8
+ def encode_sse(model: BaseModel) -> str:
7
9
  return f"data: {model.model_dump_json()}\n\n"
8
10
 
9
11
 
10
- async def stream_sse(bundle: RunBundle):
12
+ async def stream_sse(bundle: RunBundle) -> AsyncGenerator[str]:
11
13
  async for event in bundle.stream():
12
14
  yield encode_sse(event)
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acp-sdk
3
- Version: 1.0.0rc1
3
+ Version: 1.0.0rc3
4
4
  Summary: Agent Communication Protocol SDK
5
- Requires-Python: <4.0,>=3.13
5
+ Requires-Python: <4.0,>=3.11
6
6
  Requires-Dist: opentelemetry-api>=1.31.1
7
7
  Requires-Dist: pydantic>=2.11.1
8
8
  Provides-Extra: client
@@ -30,16 +30,14 @@ Install using pip:
30
30
  pip install acp-sdk
31
31
  ```
32
32
 
33
- ## Example
33
+ ## Examples
34
34
 
35
35
  The SDK can be used to implement both clients and servers.
36
36
 
37
37
  ### Server
38
38
 
39
- To run an example server use [FastAPI CLI](https://fastapi.tiangolo.com/fastapi-cli/) or other [deployment strategies](https://fastapi.tiangolo.com/deployment/):
40
-
41
39
  ```shell
42
- fastapi dev examples/servers/echo.py
40
+ python examples/servers/echo.py
43
41
  ```
44
42
 
45
43
  ### Client
@@ -0,0 +1,19 @@
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=klWKtCE_R8LIQEin0nqgiEyzGZbcvxntalk6qWqr7Js,100
6
+ acp_sdk/models/errors.py,sha256=rEyaMVvQuBi7fwWe_d0PGGySYsD3FZTluQ-SkC0yhAs,444
7
+ acp_sdk/models/models.py,sha256=HnDBIuANsiM-Kl7T1aElGgnOib14OxV28RyrxDQyiig,4433
8
+ acp_sdk/server/__init__.py,sha256=aVVbBzZdFdX3vnIgX0r4WfTHbAq1tVh9tt-oKJ7BxRg,156
9
+ acp_sdk/server/agent.py,sha256=hMYrbS6b-4nYBB2NV9Zw5RPbmD3oUMl5y-OcnjvNoo4,802
10
+ acp_sdk/server/bundle.py,sha256=kMLyHQZxM-hL-B79Qcxu4JdvA_M86Tj_kk1C2bSQ9Fw,4936
11
+ acp_sdk/server/context.py,sha256=oILWXHNTXq3mLX0RM7jiIKMC19dEiw7AYy6X0L6OUXg,163
12
+ acp_sdk/server/errors.py,sha256=fWlgVsQ5hs_AXwzc-wvy6QgoDWEMRUBlSrfJfhHHMyE,2085
13
+ acp_sdk/server/logging.py,sha256=Oc8yZigCsuDnHHPsarRzu0RX3NKaLEgpELM2yovGKDI,411
14
+ acp_sdk/server/server.py,sha256=yN_QzPBCNTlfkXsae6UpDliEn_PONliH4OglvuMD-Sc,5498
15
+ acp_sdk/server/telemetry.py,sha256=EwmtUrWMYid7XHiX76V1J6CigJPa2NrzEPOX0fBoY3o,1838
16
+ acp_sdk/server/utils.py,sha256=EfrF9VCyVk3AM_ao-BIB9EzGbfTrh4V2Bz-VFr6f6Sg,351
17
+ acp_sdk-1.0.0rc3.dist-info/METADATA,sha256=dGD1WNqlpUAE3rYQ-axzeWsmcS8RRxN0UFh_LLx_g9U,1183
18
+ acp_sdk-1.0.0rc3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
19
+ acp_sdk-1.0.0rc3.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- acp_sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- acp_sdk/models.py,sha256=0B_jdMxM7sYO9USTZzM1hvCS-qsO6lUKfqLTpGdkRQQ,4315
3
- acp_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- acp_sdk/client/__init__.py,sha256=h3uraY7H5vbtiz5i3N9kIpjF_bu7Nor6LJu_li-aiVc,41
5
- acp_sdk/client/client.py,sha256=EDcV6DhnDFJF58zAyzi4cgoNkb3znMSw7dZkiN7KKbY,4557
6
- acp_sdk/server/__init__.py,sha256=2bgh6BqT6I0w9GmN8GWdHa52HL64ZpbcICbxKZbInNY,84
7
- acp_sdk/server/agent.py,sha256=G3gmUDzb_msK0w4EuUe8hTbZRFCb8xy-bqpVFIAx9sU,687
8
- acp_sdk/server/bundle.py,sha256=cZmd39uYkykKOyQpVcXHtH_-N2y9PJX9jWEvO9bzlA8,4841
9
- acp_sdk/server/context.py,sha256=h8SY0-1DMHAMluOMni981EXXM2kAuBQcqrofK-qyi4s,155
10
- acp_sdk/server/server.py,sha256=dUZ-aDsNzSHnEeY3vOtbixrgr86JT2JUkTue6_9IVak,4443
11
- acp_sdk/server/telemetry.py,sha256=ZxiZUJm9PuZoc6rIl0MvaQkzIX3E4ENiwsxxqDZtfRo,1383
12
- acp_sdk/server/utils.py,sha256=VoqEZ1fZ7_NYSKmDIwBz2UwSL17NKTZ5k760ALgimE4,277
13
- acp_sdk-1.0.0rc1.dist-info/METADATA,sha256=0ddoScvUqT8HuRpVgBFNdtxQt8gxzyMU7ujyimZ5s1c,1350
14
- acp_sdk-1.0.0rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- acp_sdk-1.0.0rc1.dist-info/RECORD,,