acp-sdk 0.0.6__py3-none-any.whl → 1.0.0rc2__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/__init__.py +1 -0
- acp_sdk/client/client.py +123 -0
- acp_sdk/models.py +215 -0
- acp_sdk/server/__init__.py +2 -0
- acp_sdk/server/agent.py +30 -0
- acp_sdk/server/bundle.py +131 -0
- acp_sdk/server/context.py +6 -0
- acp_sdk/server/server.py +133 -0
- acp_sdk/server/telemetry.py +46 -0
- acp_sdk/server/utils.py +14 -0
- acp_sdk-1.0.0rc2.dist-info/METADATA +53 -0
- acp_sdk-1.0.0rc2.dist-info/RECORD +15 -0
- acp/__init__.py +0 -138
- acp/cli/__init__.py +0 -6
- acp/cli/claude.py +0 -139
- acp/cli/cli.py +0 -471
- acp/client/__main__.py +0 -79
- acp/client/session.py +0 -372
- acp/client/sse.py +0 -145
- acp/client/stdio.py +0 -153
- acp/server/__init__.py +0 -3
- acp/server/__main__.py +0 -50
- acp/server/highlevel/__init__.py +0 -9
- acp/server/highlevel/agents/__init__.py +0 -5
- acp/server/highlevel/agents/agent_manager.py +0 -110
- acp/server/highlevel/agents/base.py +0 -20
- acp/server/highlevel/agents/templates.py +0 -21
- acp/server/highlevel/context.py +0 -185
- acp/server/highlevel/exceptions.py +0 -25
- acp/server/highlevel/prompts/__init__.py +0 -4
- acp/server/highlevel/prompts/base.py +0 -167
- acp/server/highlevel/prompts/manager.py +0 -50
- acp/server/highlevel/prompts/prompt_manager.py +0 -33
- acp/server/highlevel/resources/__init__.py +0 -23
- acp/server/highlevel/resources/base.py +0 -48
- acp/server/highlevel/resources/resource_manager.py +0 -94
- acp/server/highlevel/resources/templates.py +0 -80
- acp/server/highlevel/resources/types.py +0 -185
- acp/server/highlevel/server.py +0 -705
- acp/server/highlevel/tools/__init__.py +0 -4
- acp/server/highlevel/tools/base.py +0 -83
- acp/server/highlevel/tools/tool_manager.py +0 -53
- acp/server/highlevel/utilities/__init__.py +0 -1
- acp/server/highlevel/utilities/func_metadata.py +0 -210
- acp/server/highlevel/utilities/logging.py +0 -43
- acp/server/highlevel/utilities/types.py +0 -54
- acp/server/lowlevel/__init__.py +0 -3
- acp/server/lowlevel/helper_types.py +0 -9
- acp/server/lowlevel/server.py +0 -643
- acp/server/models.py +0 -17
- acp/server/session.py +0 -315
- acp/server/sse.py +0 -175
- acp/server/stdio.py +0 -83
- acp/server/websocket.py +0 -61
- acp/shared/__init__.py +0 -0
- acp/shared/context.py +0 -14
- acp/shared/exceptions.py +0 -14
- acp/shared/memory.py +0 -87
- acp/shared/progress.py +0 -40
- acp/shared/session.py +0 -413
- acp/shared/version.py +0 -3
- acp/types.py +0 -1258
- acp_sdk-0.0.6.dist-info/METADATA +0 -46
- acp_sdk-0.0.6.dist-info/RECORD +0 -57
- acp_sdk-0.0.6.dist-info/entry_points.txt +0 -2
- acp_sdk-0.0.6.dist-info/licenses/LICENSE +0 -22
- {acp/client → acp_sdk}/__init__.py +0 -0
- {acp → acp_sdk}/py.typed +0 -0
- {acp_sdk-0.0.6.dist-info → acp_sdk-1.0.0rc2.dist-info}/WHEEL +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
from acp_sdk.client.client import Client as Client
|
acp_sdk/client/client.py
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
from collections.abc import AsyncIterator
|
2
|
+
from types import TracebackType
|
3
|
+
from typing import Self
|
4
|
+
|
5
|
+
import httpx
|
6
|
+
from httpx_sse import EventSource, aconnect_sse
|
7
|
+
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
8
|
+
from pydantic import TypeAdapter
|
9
|
+
|
10
|
+
from acp_sdk.models import (
|
11
|
+
Agent,
|
12
|
+
AgentName,
|
13
|
+
AgentReadResponse,
|
14
|
+
AgentsListResponse,
|
15
|
+
AwaitResume,
|
16
|
+
Message,
|
17
|
+
Run,
|
18
|
+
RunCancelResponse,
|
19
|
+
RunCreateRequest,
|
20
|
+
RunCreateResponse,
|
21
|
+
RunEvent,
|
22
|
+
RunId,
|
23
|
+
RunMode,
|
24
|
+
RunResumeRequest,
|
25
|
+
RunResumeResponse,
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
class Client:
|
30
|
+
def __init__(self, *, base_url: httpx.URL | str = "", client: httpx.AsyncClient | None = None) -> None:
|
31
|
+
self.base_url = base_url
|
32
|
+
|
33
|
+
self._client = self._init_client(client)
|
34
|
+
|
35
|
+
def _init_client(self, client: httpx.AsyncClient | None = None) -> Self:
|
36
|
+
client = client or httpx.AsyncClient(base_url=self.base_url)
|
37
|
+
HTTPXClientInstrumentor.instrument_client(client)
|
38
|
+
return client
|
39
|
+
|
40
|
+
async def __aenter__(self) -> Self:
|
41
|
+
await self._client.__aenter__()
|
42
|
+
return self
|
43
|
+
|
44
|
+
async def __aexit__(
|
45
|
+
self,
|
46
|
+
exc_type: type[BaseException] | None = None,
|
47
|
+
exc_value: BaseException | None = None,
|
48
|
+
traceback: TracebackType | None = None,
|
49
|
+
) -> None:
|
50
|
+
await self._client.__aexit__(exc_type, exc_value, traceback)
|
51
|
+
|
52
|
+
async def agents(self) -> AsyncIterator[Agent]:
|
53
|
+
response = await self._client.get("/agents")
|
54
|
+
for agent in AgentsListResponse.model_validate(response.json()).agents:
|
55
|
+
yield agent
|
56
|
+
|
57
|
+
async def agent(self, *, name: AgentName) -> Agent:
|
58
|
+
response = await self._client.get(f"/agents/{name}")
|
59
|
+
return AgentReadResponse.model_validate(response.json())
|
60
|
+
|
61
|
+
async def run_sync(self, *, agent: AgentName, input: Message) -> Run:
|
62
|
+
response = await self._client.post(
|
63
|
+
"/runs",
|
64
|
+
json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.SYNC).model_dump(),
|
65
|
+
)
|
66
|
+
return RunCreateResponse.model_validate(response.json())
|
67
|
+
|
68
|
+
async def run_async(self, *, agent: AgentName, input: Message) -> Run:
|
69
|
+
response = await self._client.post(
|
70
|
+
"/runs",
|
71
|
+
json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.ASYNC).model_dump(),
|
72
|
+
)
|
73
|
+
return RunCreateResponse.model_validate(response.json())
|
74
|
+
|
75
|
+
async def run_stream(self, *, agent: AgentName, input: Message) -> AsyncIterator[RunEvent]:
|
76
|
+
async with aconnect_sse(
|
77
|
+
self._client,
|
78
|
+
"POST",
|
79
|
+
"/runs",
|
80
|
+
json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.STREAM).model_dump(),
|
81
|
+
) as event_source:
|
82
|
+
async for event in self._validate_stream(event_source):
|
83
|
+
yield event
|
84
|
+
|
85
|
+
async def run_status(self, *, run_id: RunId) -> Run:
|
86
|
+
response = await self._client.get(f"/runs/{run_id}")
|
87
|
+
return Run.model_validate(response.json())
|
88
|
+
|
89
|
+
async def run_cancel(self, *, run_id: RunId) -> Run:
|
90
|
+
response = await self._client.post(f"/runs/{run_id}/cancel")
|
91
|
+
return RunCancelResponse.model_validate(response.json())
|
92
|
+
|
93
|
+
async def run_resume_sync(self, *, run_id: RunId, await_: AwaitResume) -> Run:
|
94
|
+
response = await self._client.post(
|
95
|
+
f"/runs/{run_id}",
|
96
|
+
json=RunResumeRequest(await_=await_, mode=RunMode.SYNC).model_dump(),
|
97
|
+
)
|
98
|
+
return RunResumeResponse.model_validate(response.json())
|
99
|
+
|
100
|
+
async def run_resume_async(self, *, run_id: RunId, await_: AwaitResume) -> Run:
|
101
|
+
response = await self._client.post(
|
102
|
+
f"/runs/{run_id}",
|
103
|
+
json=RunResumeRequest(await_=await_, mode=RunMode.ASYNC).model_dump(),
|
104
|
+
)
|
105
|
+
return RunResumeResponse.model_validate(response.json())
|
106
|
+
|
107
|
+
async def run_resume_stream(self, *, run_id: RunId, await_: AwaitResume) -> AsyncIterator[RunEvent]:
|
108
|
+
async with aconnect_sse(
|
109
|
+
self._client,
|
110
|
+
"POST",
|
111
|
+
f"/runs/{run_id}",
|
112
|
+
json=RunResumeRequest(await_=await_, mode=RunMode.STREAM).model_dump(),
|
113
|
+
) as event_source:
|
114
|
+
async for event in self._validate_stream(event_source):
|
115
|
+
yield event
|
116
|
+
|
117
|
+
async def _validate_stream(
|
118
|
+
self,
|
119
|
+
event_source: EventSource,
|
120
|
+
) -> AsyncIterator[RunEvent]:
|
121
|
+
async for event in event_source.aiter_sse():
|
122
|
+
event = TypeAdapter(RunEvent).validate_json(event.data)
|
123
|
+
yield event
|
acp_sdk/models.py
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
import uuid
|
2
|
+
from collections.abc import Iterator
|
3
|
+
from enum import Enum
|
4
|
+
from typing import Any, Literal, Union
|
5
|
+
|
6
|
+
from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel
|
7
|
+
|
8
|
+
|
9
|
+
class ACPError(BaseModel):
|
10
|
+
code: str
|
11
|
+
message: str
|
12
|
+
|
13
|
+
|
14
|
+
class AnyModel(BaseModel):
|
15
|
+
model_config = ConfigDict(extra="allow")
|
16
|
+
|
17
|
+
|
18
|
+
class MessagePartBase(BaseModel):
|
19
|
+
type: Literal["text", "image", "artifact"]
|
20
|
+
|
21
|
+
|
22
|
+
class TextMessagePart(MessagePartBase):
|
23
|
+
type: Literal["text"] = "text"
|
24
|
+
content: str
|
25
|
+
|
26
|
+
|
27
|
+
class ImageMessagePart(MessagePartBase):
|
28
|
+
type: Literal["image"] = "image"
|
29
|
+
content_url: AnyUrl
|
30
|
+
|
31
|
+
|
32
|
+
class ArtifactMessagePart(MessagePartBase):
|
33
|
+
type: Literal["artifact"] = "artifact"
|
34
|
+
name: str
|
35
|
+
content_url: AnyUrl
|
36
|
+
|
37
|
+
|
38
|
+
MessagePart = Union[TextMessagePart, ImageMessagePart, ArtifactMessagePart]
|
39
|
+
|
40
|
+
|
41
|
+
class Message(RootModel):
|
42
|
+
root: list[MessagePart]
|
43
|
+
|
44
|
+
def __init__(self, *items: MessagePart) -> None:
|
45
|
+
super().__init__(root=list(items))
|
46
|
+
|
47
|
+
def __iter__(self) -> Iterator[MessagePart]:
|
48
|
+
return iter(self.root)
|
49
|
+
|
50
|
+
def __add__(self, other: "Message") -> "Message":
|
51
|
+
if not isinstance(other, Message):
|
52
|
+
raise TypeError(f"Cannot concatenate Message with {type(other).__name__}")
|
53
|
+
return Message(*(self.root + other.root))
|
54
|
+
|
55
|
+
def __str__(self) -> str:
|
56
|
+
return "".join(str(part) for part in self.root if isinstance(part, TextMessagePart))
|
57
|
+
|
58
|
+
|
59
|
+
AgentName = str
|
60
|
+
SessionId = str
|
61
|
+
RunId = str
|
62
|
+
|
63
|
+
|
64
|
+
class RunMode(str, Enum):
|
65
|
+
SYNC = "sync"
|
66
|
+
ASYNC = "async"
|
67
|
+
STREAM = "stream"
|
68
|
+
|
69
|
+
|
70
|
+
class RunStatus(str, Enum):
|
71
|
+
CREATED = "created"
|
72
|
+
IN_PROGRESS = "in-progress"
|
73
|
+
AWAITING = "awaiting"
|
74
|
+
CANCELLING = "cancelling"
|
75
|
+
CANCELLED = "cancelled"
|
76
|
+
COMPLETED = "completed"
|
77
|
+
FAILED = "failed"
|
78
|
+
|
79
|
+
@property
|
80
|
+
def is_terminal(self) -> bool:
|
81
|
+
terminal_states = {RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED}
|
82
|
+
return self in terminal_states
|
83
|
+
|
84
|
+
|
85
|
+
class Await(BaseModel):
|
86
|
+
type: Literal["placeholder"] = "placeholder"
|
87
|
+
|
88
|
+
|
89
|
+
class AwaitResume(BaseModel):
|
90
|
+
pass
|
91
|
+
|
92
|
+
|
93
|
+
class Run(BaseModel):
|
94
|
+
run_id: RunId = str(uuid.uuid4())
|
95
|
+
agent_name: AgentName
|
96
|
+
session_id: SessionId | None = None
|
97
|
+
status: RunStatus = RunStatus.CREATED
|
98
|
+
await_: Await | None = Field(None, alias="await")
|
99
|
+
output: Message | None = None
|
100
|
+
error: ACPError | None = None
|
101
|
+
|
102
|
+
model_config = ConfigDict(populate_by_name=True)
|
103
|
+
|
104
|
+
def model_dump_json(
|
105
|
+
self,
|
106
|
+
**kwargs: dict[str, Any],
|
107
|
+
) -> str:
|
108
|
+
return super().model_dump_json(
|
109
|
+
by_alias=True,
|
110
|
+
**kwargs,
|
111
|
+
)
|
112
|
+
|
113
|
+
|
114
|
+
class MessageEvent(BaseModel):
|
115
|
+
type: Literal["message"] = "message"
|
116
|
+
message: Message
|
117
|
+
|
118
|
+
|
119
|
+
class AwaitEvent(BaseModel):
|
120
|
+
type: Literal["await"] = "await"
|
121
|
+
await_: Await | None = Field(alias="await")
|
122
|
+
|
123
|
+
model_config = ConfigDict(populate_by_name=True)
|
124
|
+
|
125
|
+
def model_dump_json(
|
126
|
+
self,
|
127
|
+
**kwargs: dict[str, Any],
|
128
|
+
) -> str:
|
129
|
+
return super().model_dump_json(
|
130
|
+
by_alias=True,
|
131
|
+
**kwargs,
|
132
|
+
)
|
133
|
+
|
134
|
+
|
135
|
+
class GenericEvent(BaseModel):
|
136
|
+
type: Literal["generic"] = "generic"
|
137
|
+
generic: AnyModel
|
138
|
+
|
139
|
+
|
140
|
+
class CreatedEvent(BaseModel):
|
141
|
+
type: Literal["created"] = "created"
|
142
|
+
run: Run
|
143
|
+
|
144
|
+
|
145
|
+
class InProgressEvent(BaseModel):
|
146
|
+
type: Literal["in-progress"] = "in-progress"
|
147
|
+
run: Run
|
148
|
+
|
149
|
+
|
150
|
+
class FailedEvent(BaseModel):
|
151
|
+
type: Literal["failed"] = "failed"
|
152
|
+
run: Run
|
153
|
+
|
154
|
+
|
155
|
+
class CancelledEvent(BaseModel):
|
156
|
+
type: Literal["cancelled"] = "cancelled"
|
157
|
+
run: Run
|
158
|
+
|
159
|
+
|
160
|
+
class CompletedEvent(BaseModel):
|
161
|
+
type: Literal["completed"] = "completed"
|
162
|
+
run: Run
|
163
|
+
|
164
|
+
|
165
|
+
RunEvent = Union[
|
166
|
+
CreatedEvent,
|
167
|
+
InProgressEvent,
|
168
|
+
MessageEvent,
|
169
|
+
AwaitEvent,
|
170
|
+
GenericEvent,
|
171
|
+
CancelledEvent,
|
172
|
+
FailedEvent,
|
173
|
+
CompletedEvent,
|
174
|
+
]
|
175
|
+
|
176
|
+
|
177
|
+
class RunCreateRequest(BaseModel):
|
178
|
+
agent_name: AgentName
|
179
|
+
session_id: SessionId | None = None
|
180
|
+
input: Message
|
181
|
+
mode: RunMode = RunMode.SYNC
|
182
|
+
|
183
|
+
|
184
|
+
class RunCreateResponse(Run):
|
185
|
+
pass
|
186
|
+
|
187
|
+
|
188
|
+
class RunResumeRequest(BaseModel):
|
189
|
+
await_: AwaitResume = Field(alias="await")
|
190
|
+
mode: RunMode
|
191
|
+
|
192
|
+
|
193
|
+
class RunResumeResponse(Run):
|
194
|
+
pass
|
195
|
+
|
196
|
+
|
197
|
+
class RunReadResponse(Run):
|
198
|
+
pass
|
199
|
+
|
200
|
+
|
201
|
+
class RunCancelResponse(Run):
|
202
|
+
pass
|
203
|
+
|
204
|
+
|
205
|
+
class Agent(BaseModel):
|
206
|
+
name: str
|
207
|
+
description: str | None = None
|
208
|
+
|
209
|
+
|
210
|
+
class AgentsListResponse(BaseModel):
|
211
|
+
agents: list[Agent]
|
212
|
+
|
213
|
+
|
214
|
+
class AgentReadResponse(Agent):
|
215
|
+
pass
|
acp_sdk/server/agent.py
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
import abc
|
2
|
+
from collections.abc import AsyncGenerator
|
3
|
+
|
4
|
+
from acp_sdk.models import (
|
5
|
+
AgentName,
|
6
|
+
Await,
|
7
|
+
AwaitResume,
|
8
|
+
Message,
|
9
|
+
SessionId,
|
10
|
+
)
|
11
|
+
from acp_sdk.server.context import Context
|
12
|
+
|
13
|
+
|
14
|
+
class Agent(abc.ABC):
|
15
|
+
@property
|
16
|
+
def name(self) -> AgentName:
|
17
|
+
return self.__class__.__name__
|
18
|
+
|
19
|
+
@property
|
20
|
+
def description(self) -> str:
|
21
|
+
return ""
|
22
|
+
|
23
|
+
@abc.abstractmethod
|
24
|
+
def run(self, input: Message, *, context: Context) -> AsyncGenerator[Message | Await, AwaitResume]:
|
25
|
+
pass
|
26
|
+
|
27
|
+
async def session(self, session_id: SessionId | None) -> SessionId | None:
|
28
|
+
if session_id:
|
29
|
+
raise NotImplementedError()
|
30
|
+
return None
|
acp_sdk/server/bundle.py
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
from collections.abc import AsyncGenerator
|
4
|
+
|
5
|
+
from opentelemetry import trace
|
6
|
+
from pydantic import ValidationError
|
7
|
+
|
8
|
+
from acp_sdk.models import (
|
9
|
+
ACPError,
|
10
|
+
AnyModel,
|
11
|
+
Await,
|
12
|
+
AwaitEvent,
|
13
|
+
AwaitResume,
|
14
|
+
CancelledEvent,
|
15
|
+
CompletedEvent,
|
16
|
+
CreatedEvent,
|
17
|
+
FailedEvent,
|
18
|
+
GenericEvent,
|
19
|
+
InProgressEvent,
|
20
|
+
Message,
|
21
|
+
MessageEvent,
|
22
|
+
Run,
|
23
|
+
RunEvent,
|
24
|
+
RunStatus,
|
25
|
+
)
|
26
|
+
from acp_sdk.server.agent import Agent
|
27
|
+
from acp_sdk.server.context import Context
|
28
|
+
|
29
|
+
logger = logging.getLogger("uvicorn.error")
|
30
|
+
|
31
|
+
|
32
|
+
class RunBundle:
|
33
|
+
def __init__(self, *, agent: Agent, run: Run, task: asyncio.Task | None = None) -> None:
|
34
|
+
self.agent = agent
|
35
|
+
self.run = run
|
36
|
+
self.task = task
|
37
|
+
|
38
|
+
self.stream_queue: asyncio.Queue[RunEvent] = asyncio.Queue()
|
39
|
+
self.composed_message = Message()
|
40
|
+
|
41
|
+
self.await_queue: asyncio.Queue[AwaitResume] = asyncio.Queue(maxsize=1)
|
42
|
+
self.await_or_terminate_event = asyncio.Event()
|
43
|
+
|
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
|
+
|
52
|
+
async def emit(self, event: RunEvent) -> None:
|
53
|
+
await self.stream_queue.put(event)
|
54
|
+
|
55
|
+
async def await_(self) -> AwaitResume:
|
56
|
+
await self.stream_queue.put(None)
|
57
|
+
self.await_queue.empty()
|
58
|
+
self.await_or_terminate_event.set()
|
59
|
+
self.await_or_terminate_event.clear()
|
60
|
+
resume = await self.await_queue.get()
|
61
|
+
self.await_queue.task_done()
|
62
|
+
return resume
|
63
|
+
|
64
|
+
async def resume(self, resume: AwaitResume) -> None:
|
65
|
+
self.stream_queue = asyncio.Queue()
|
66
|
+
await self.await_queue.put(resume)
|
67
|
+
|
68
|
+
async def join(self) -> None:
|
69
|
+
await self.await_or_terminate_event.wait()
|
70
|
+
|
71
|
+
async def execute(self, input: Message) -> None:
|
72
|
+
with trace.get_tracer(__name__).start_as_current_span("execute"):
|
73
|
+
run_logger = logging.LoggerAdapter(logger, {"run_id": self.run.run_id})
|
74
|
+
|
75
|
+
await self.emit(CreatedEvent(run=self.run))
|
76
|
+
try:
|
77
|
+
self.run.session_id = await self.agent.session(self.run.session_id)
|
78
|
+
run_logger.info("Session loaded")
|
79
|
+
|
80
|
+
generator = self.agent.run(input=input, context=Context(session_id=self.run.session_id))
|
81
|
+
run_logger.info("Run started")
|
82
|
+
|
83
|
+
self.run.status = RunStatus.IN_PROGRESS
|
84
|
+
await self.emit(InProgressEvent(run=self.run))
|
85
|
+
|
86
|
+
await_resume = None
|
87
|
+
while True:
|
88
|
+
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
|
94
|
+
self.run.status = RunStatus.AWAITING
|
95
|
+
await self.emit(
|
96
|
+
AwaitEvent.model_validate(
|
97
|
+
{
|
98
|
+
"run_id": self.run.run_id,
|
99
|
+
"type": "await",
|
100
|
+
"await": next,
|
101
|
+
}
|
102
|
+
)
|
103
|
+
)
|
104
|
+
run_logger.info("Run awaited")
|
105
|
+
await_resume = await self.await_()
|
106
|
+
self.run.status = RunStatus.IN_PROGRESS
|
107
|
+
await self.emit(InProgressEvent(run=self.run))
|
108
|
+
run_logger.info("Run resumed")
|
109
|
+
else:
|
110
|
+
try:
|
111
|
+
generic = AnyModel.model_validate(next)
|
112
|
+
await self.emit(GenericEvent(generic=generic))
|
113
|
+
except ValidationError:
|
114
|
+
raise TypeError("Invalid yield")
|
115
|
+
except StopAsyncIteration:
|
116
|
+
self.run.output = self.composed_message
|
117
|
+
self.run.status = RunStatus.COMPLETED
|
118
|
+
await self.emit(CompletedEvent(run=self.run))
|
119
|
+
run_logger.info("Run completed")
|
120
|
+
except asyncio.CancelledError:
|
121
|
+
self.run.status = RunStatus.CANCELLED
|
122
|
+
await self.emit(CancelledEvent(run=self.run))
|
123
|
+
run_logger.info("Run cancelled")
|
124
|
+
except Exception as e:
|
125
|
+
self.run.error = ACPError(code="unspecified", message=str(e))
|
126
|
+
self.run.status = RunStatus.FAILED
|
127
|
+
await self.emit(FailedEvent(run=self.run))
|
128
|
+
run_logger.exception("Run failed")
|
129
|
+
finally:
|
130
|
+
self.await_or_terminate_event.set()
|
131
|
+
await self.stream_queue.put(None)
|
acp_sdk/server/server.py
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
import asyncio
|
2
|
+
|
3
|
+
from fastapi import FastAPI, HTTPException, status
|
4
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
5
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
6
|
+
|
7
|
+
from acp_sdk.models import (
|
8
|
+
Agent as AgentModel,
|
9
|
+
)
|
10
|
+
from acp_sdk.models import (
|
11
|
+
AgentName,
|
12
|
+
AgentReadResponse,
|
13
|
+
AgentsListResponse,
|
14
|
+
Run,
|
15
|
+
RunCancelResponse,
|
16
|
+
RunCreateRequest,
|
17
|
+
RunCreateResponse,
|
18
|
+
RunId,
|
19
|
+
RunMode,
|
20
|
+
RunReadResponse,
|
21
|
+
RunResumeRequest,
|
22
|
+
RunResumeResponse,
|
23
|
+
RunStatus,
|
24
|
+
)
|
25
|
+
from acp_sdk.server.agent import Agent
|
26
|
+
from acp_sdk.server.bundle import RunBundle
|
27
|
+
from acp_sdk.server.telemetry import configure_telemetry
|
28
|
+
from acp_sdk.server.utils import stream_sse
|
29
|
+
|
30
|
+
|
31
|
+
def create_app(*agents: Agent) -> FastAPI:
|
32
|
+
app = FastAPI(title="acp-agents")
|
33
|
+
|
34
|
+
configure_telemetry()
|
35
|
+
FastAPIInstrumentor.instrument_app(app)
|
36
|
+
|
37
|
+
agents: dict[AgentName, Agent] = {agent.name: agent for agent in agents}
|
38
|
+
runs: dict[RunId, RunBundle] = {}
|
39
|
+
|
40
|
+
def find_run_bundle(run_id: RunId) -> RunBundle:
|
41
|
+
bundle = runs.get(run_id)
|
42
|
+
if not bundle:
|
43
|
+
raise HTTPException(status_code=404, detail=f"Run {run_id} not found")
|
44
|
+
return bundle
|
45
|
+
|
46
|
+
def find_agent(agent_name: AgentName) -> Agent:
|
47
|
+
agent = agents.get(agent_name, None)
|
48
|
+
if not agent:
|
49
|
+
raise HTTPException(status_code=404, detail=f"Agent {agent_name} not found")
|
50
|
+
return agent
|
51
|
+
|
52
|
+
@app.get("/agents")
|
53
|
+
async def list_agents() -> AgentsListResponse:
|
54
|
+
return AgentsListResponse(
|
55
|
+
agents=[AgentModel(name=agent.name, description=agent.description) for agent in agents.values()]
|
56
|
+
)
|
57
|
+
|
58
|
+
@app.get("/agents/{name}")
|
59
|
+
async def read_agent(name: AgentName) -> AgentReadResponse:
|
60
|
+
agent = find_agent(name)
|
61
|
+
return AgentModel(name=agent.name, description=agent.description)
|
62
|
+
|
63
|
+
@app.post("/runs")
|
64
|
+
async def create_run(request: RunCreateRequest) -> RunCreateResponse:
|
65
|
+
agent = find_agent(request.agent_name)
|
66
|
+
bundle = RunBundle(
|
67
|
+
agent=agent,
|
68
|
+
run=Run(
|
69
|
+
agent_name=agent.name,
|
70
|
+
session_id=request.session_id,
|
71
|
+
),
|
72
|
+
)
|
73
|
+
|
74
|
+
bundle.task = asyncio.create_task(bundle.execute(request.input))
|
75
|
+
runs[bundle.run.run_id] = bundle
|
76
|
+
|
77
|
+
match request.mode:
|
78
|
+
case RunMode.STREAM:
|
79
|
+
return StreamingResponse(
|
80
|
+
stream_sse(bundle),
|
81
|
+
media_type="text/event-stream",
|
82
|
+
)
|
83
|
+
case RunMode.SYNC:
|
84
|
+
await bundle.join()
|
85
|
+
return bundle.run
|
86
|
+
case RunMode.ASYNC:
|
87
|
+
return JSONResponse(
|
88
|
+
status_code=status.HTTP_202_ACCEPTED,
|
89
|
+
content=bundle.run.model_dump(),
|
90
|
+
)
|
91
|
+
case _:
|
92
|
+
raise NotImplementedError()
|
93
|
+
|
94
|
+
@app.get("/runs/{run_id}")
|
95
|
+
async def read_run(run_id: RunId) -> RunReadResponse:
|
96
|
+
bundle = find_run_bundle(run_id)
|
97
|
+
return bundle.run
|
98
|
+
|
99
|
+
@app.post("/runs/{run_id}")
|
100
|
+
async def resume_run(run_id: RunId, request: RunResumeRequest) -> RunResumeResponse:
|
101
|
+
bundle = find_run_bundle(run_id)
|
102
|
+
bundle.stream_queue = asyncio.Queue() # TODO improve
|
103
|
+
await bundle.await_queue.put(request.await_)
|
104
|
+
match request.mode:
|
105
|
+
case RunMode.STREAM:
|
106
|
+
return StreamingResponse(
|
107
|
+
stream_sse(bundle),
|
108
|
+
media_type="text/event-stream",
|
109
|
+
)
|
110
|
+
case RunMode.SYNC:
|
111
|
+
await bundle.join()
|
112
|
+
return bundle.run
|
113
|
+
case RunMode.ASYNC:
|
114
|
+
return JSONResponse(
|
115
|
+
status_code=status.HTTP_202_ACCEPTED,
|
116
|
+
content=bundle.run.model_dump(),
|
117
|
+
)
|
118
|
+
case _:
|
119
|
+
raise NotImplementedError()
|
120
|
+
|
121
|
+
@app.post("/runs/{run_id}/cancel")
|
122
|
+
async def cancel_run(run_id: RunId) -> RunCancelResponse:
|
123
|
+
bundle = find_run_bundle(run_id)
|
124
|
+
if bundle.run.status.is_terminal:
|
125
|
+
raise HTTPException(
|
126
|
+
status_code=403,
|
127
|
+
detail=f"Run with terminal status {bundle.run.status} can't be cancelled",
|
128
|
+
)
|
129
|
+
bundle.task.cancel()
|
130
|
+
bundle.run.status = RunStatus.CANCELLING
|
131
|
+
return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=bundle.run.model_dump())
|
132
|
+
|
133
|
+
return app
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import logging
|
2
|
+
from importlib.metadata import version
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from opentelemetry import trace
|
6
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
7
|
+
from opentelemetry.sdk.resources import (
|
8
|
+
SERVICE_NAME,
|
9
|
+
SERVICE_NAMESPACE,
|
10
|
+
SERVICE_VERSION,
|
11
|
+
Resource,
|
12
|
+
)
|
13
|
+
from opentelemetry.sdk.trace import TracerProvider
|
14
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExportResult
|
15
|
+
|
16
|
+
logger = logging.getLogger("uvicorn.error")
|
17
|
+
|
18
|
+
|
19
|
+
class SilentOTLPSpanExporter(OTLPSpanExporter):
|
20
|
+
def export(self, spans: Any) -> SpanExportResult:
|
21
|
+
try:
|
22
|
+
return super().export(spans)
|
23
|
+
except Exception as e:
|
24
|
+
logger.warning(f"OpenTelemetry Exporter failed silently: {e}")
|
25
|
+
return SpanExportResult.FAILURE
|
26
|
+
|
27
|
+
|
28
|
+
def configure_telemetry() -> None:
|
29
|
+
current_provider = trace.get_tracer_provider()
|
30
|
+
|
31
|
+
# Detect default provider and override
|
32
|
+
if isinstance(current_provider, trace.ProxyTracerProvider):
|
33
|
+
provider = TracerProvider(
|
34
|
+
resource=Resource(
|
35
|
+
attributes={
|
36
|
+
SERVICE_NAME: "acp-server",
|
37
|
+
SERVICE_NAMESPACE: "acp",
|
38
|
+
SERVICE_VERSION: version("acp-sdk"),
|
39
|
+
}
|
40
|
+
)
|
41
|
+
)
|
42
|
+
|
43
|
+
processor = BatchSpanProcessor(SilentOTLPSpanExporter())
|
44
|
+
provider.add_span_processor(processor)
|
45
|
+
|
46
|
+
trace.set_tracer_provider(provider)
|