acp-sdk 0.0.6__py3-none-any.whl → 1.0.0rc1__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 +135 -0
- acp_sdk/models.py +219 -0
- acp_sdk/server/__init__.py +2 -0
- acp_sdk/server/agent.py +32 -0
- acp_sdk/server/bundle.py +133 -0
- acp_sdk/server/context.py +6 -0
- acp_sdk/server/server.py +137 -0
- acp_sdk/server/telemetry.py +45 -0
- acp_sdk/server/utils.py +12 -0
- acp_sdk-1.0.0rc1.dist-info/METADATA +53 -0
- acp_sdk-1.0.0rc1.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.0rc1.dist-info}/WHEEL +0 -0
acp/server/__main__.py
DELETED
@@ -1,50 +0,0 @@
|
|
1
|
-
import importlib.metadata
|
2
|
-
import logging
|
3
|
-
import sys
|
4
|
-
|
5
|
-
import anyio
|
6
|
-
|
7
|
-
from acp.server.models import InitializationOptions
|
8
|
-
from acp.server.session import ServerSession
|
9
|
-
from acp.server.stdio import stdio_server
|
10
|
-
from acp.types import ServerCapabilities
|
11
|
-
|
12
|
-
if not sys.warnoptions:
|
13
|
-
import warnings
|
14
|
-
|
15
|
-
warnings.simplefilter("ignore")
|
16
|
-
|
17
|
-
logging.basicConfig(level=logging.INFO)
|
18
|
-
logger = logging.getLogger("server")
|
19
|
-
|
20
|
-
|
21
|
-
async def receive_loop(session: ServerSession):
|
22
|
-
logger.info("Starting receive loop")
|
23
|
-
async for message in session.incoming_messages:
|
24
|
-
if isinstance(message, Exception):
|
25
|
-
logger.error("Error: %s", message)
|
26
|
-
continue
|
27
|
-
|
28
|
-
logger.info("Received message from client: %s", message)
|
29
|
-
|
30
|
-
|
31
|
-
async def main():
|
32
|
-
version = importlib.metadata.version("mcp")
|
33
|
-
async with stdio_server() as (read_stream, write_stream):
|
34
|
-
async with (
|
35
|
-
ServerSession(
|
36
|
-
read_stream,
|
37
|
-
write_stream,
|
38
|
-
InitializationOptions(
|
39
|
-
server_name="mcp",
|
40
|
-
server_version=version,
|
41
|
-
capabilities=ServerCapabilities(),
|
42
|
-
),
|
43
|
-
) as session,
|
44
|
-
write_stream,
|
45
|
-
):
|
46
|
-
await receive_loop(session)
|
47
|
-
|
48
|
-
|
49
|
-
if __name__ == "__main__":
|
50
|
-
anyio.run(main, backend="trio")
|
acp/server/highlevel/__init__.py
DELETED
@@ -1,110 +0,0 @@
|
|
1
|
-
from typing import TYPE_CHECKING, Any
|
2
|
-
|
3
|
-
from acp.server.highlevel.agents.base import Agent
|
4
|
-
from acp.server.highlevel.agents.templates import AgentTemplate
|
5
|
-
from acp.server.highlevel.exceptions import AgentError
|
6
|
-
from acp.server.highlevel.utilities.logging import get_logger
|
7
|
-
|
8
|
-
if TYPE_CHECKING:
|
9
|
-
from acp.server.highlevel.server import Context
|
10
|
-
|
11
|
-
logger = get_logger(__name__)
|
12
|
-
|
13
|
-
|
14
|
-
class AgentManager:
|
15
|
-
"""Manages FastMCP agents."""
|
16
|
-
|
17
|
-
def __init__(self, warn_on_duplicate_agents: bool = True):
|
18
|
-
self._agents: dict[str, Agent] = {}
|
19
|
-
self._templates: dict[str, AgentTemplate] = {}
|
20
|
-
self.warn_on_duplicate_agents = warn_on_duplicate_agents
|
21
|
-
|
22
|
-
def get_template(self, name: str) -> AgentTemplate | None:
|
23
|
-
"""Get agent template by name."""
|
24
|
-
return self._templates.get(name)
|
25
|
-
|
26
|
-
def list_templates(self) -> list[AgentTemplate]:
|
27
|
-
"""List all registered agent templates."""
|
28
|
-
return list(self._templates.values())
|
29
|
-
|
30
|
-
def add_template(
|
31
|
-
self,
|
32
|
-
template: AgentTemplate,
|
33
|
-
) -> AgentTemplate:
|
34
|
-
"""Add a template to the server."""
|
35
|
-
existing = self._templates.get(template.name)
|
36
|
-
if existing:
|
37
|
-
if self.warn_on_duplicate_agents:
|
38
|
-
logger.warning(f"Agent template already exists: {template.name}")
|
39
|
-
return existing
|
40
|
-
self._templates[template.name] = template
|
41
|
-
return template
|
42
|
-
|
43
|
-
def get_agent(self, name: str) -> Agent | None:
|
44
|
-
"""Get agent by name."""
|
45
|
-
return self._agents.get(name)
|
46
|
-
|
47
|
-
def list_agents(self) -> list[Agent]:
|
48
|
-
"""List all registered agents."""
|
49
|
-
return list(self._agents.values())
|
50
|
-
|
51
|
-
def add_agent(
|
52
|
-
self,
|
53
|
-
agent: Agent,
|
54
|
-
) -> Agent:
|
55
|
-
"""Add an agent to the server."""
|
56
|
-
existing = self._agents.get(agent.name)
|
57
|
-
if existing:
|
58
|
-
if self.warn_on_duplicate_agents:
|
59
|
-
logger.warning(f"Agent already exists: {agent.name}")
|
60
|
-
return existing
|
61
|
-
self._agents[agent.name] = agent
|
62
|
-
return agent
|
63
|
-
|
64
|
-
async def create_agent(
|
65
|
-
self, name: str, config: dict[str, Any], context: "Context"
|
66
|
-
) -> Agent:
|
67
|
-
"""Call an agent by name with arguments."""
|
68
|
-
template = self.get_template(name)
|
69
|
-
if not template:
|
70
|
-
raise AgentError(f"Unknown agent template: {name}")
|
71
|
-
|
72
|
-
agent = await template.create_fn(template.model_validate(config), context)
|
73
|
-
existing = self._agents.get(agent.name)
|
74
|
-
if existing:
|
75
|
-
if self.warn_on_duplicate_agents:
|
76
|
-
logger.warning(f"Agent already exists: {agent.name}")
|
77
|
-
return existing
|
78
|
-
self._agents[agent.name] = agent
|
79
|
-
return agent
|
80
|
-
|
81
|
-
async def destroy_agent(self, name: str, context: "Context") -> None:
|
82
|
-
"""Call an agent by name with arguments."""
|
83
|
-
agent = self.get_agent(name)
|
84
|
-
if not agent:
|
85
|
-
raise AgentError(f"Unknown agent: {name}")
|
86
|
-
|
87
|
-
if not agent.destroy_fn:
|
88
|
-
raise AgentError(f"Agent cannot be destroyed: {name}")
|
89
|
-
|
90
|
-
try:
|
91
|
-
await agent.destroy_fn(context)
|
92
|
-
except Exception as e:
|
93
|
-
logger.warning(f"Error destroying agent {name}: {e}")
|
94
|
-
finally:
|
95
|
-
del self._agents[name]
|
96
|
-
return
|
97
|
-
|
98
|
-
async def run_agent(
|
99
|
-
self, name: str, input: dict[str, Any], *, context: "Context"
|
100
|
-
) -> Any:
|
101
|
-
"""Run an agent by name with input."""
|
102
|
-
agent = self.get_agent(name)
|
103
|
-
if not agent:
|
104
|
-
raise AgentError(f"Unknown agent: {name}")
|
105
|
-
|
106
|
-
try:
|
107
|
-
output = await agent.run_fn(agent.input.model_validate(input), ctx=context) # type: ignore
|
108
|
-
return output.model_dump()
|
109
|
-
except Exception as e:
|
110
|
-
raise AgentError(f"Error running agent {name}: {e}") from e
|
@@ -1,20 +0,0 @@
|
|
1
|
-
from typing import Awaitable, Callable, Type
|
2
|
-
|
3
|
-
from pydantic import BaseModel, ConfigDict, Field
|
4
|
-
|
5
|
-
from acp.server.highlevel.context import Context
|
6
|
-
|
7
|
-
|
8
|
-
class Agent(BaseModel):
|
9
|
-
"""Internal agent info."""
|
10
|
-
|
11
|
-
name: str = Field(description="Name of the agent")
|
12
|
-
description: str | None = Field(description="Description of what the agent does")
|
13
|
-
|
14
|
-
input: Type[BaseModel] = Field(description="Model for input")
|
15
|
-
output: Type[BaseModel] = Field(description="Model for output")
|
16
|
-
|
17
|
-
run_fn: Callable[[BaseModel, "Context"], Awaitable[BaseModel]] = Field(exclude=True)
|
18
|
-
destroy_fn: Callable[["Context"], Awaitable[None]] | None = Field(exclude=True)
|
19
|
-
|
20
|
-
model_config = ConfigDict(extra="allow")
|
@@ -1,21 +0,0 @@
|
|
1
|
-
from typing import Awaitable, Callable, Type
|
2
|
-
|
3
|
-
from pydantic import BaseModel, ConfigDict, Field
|
4
|
-
|
5
|
-
from acp.server.highlevel.agents.base import Agent
|
6
|
-
from acp.server.highlevel.context import Context
|
7
|
-
|
8
|
-
|
9
|
-
class AgentTemplate(BaseModel):
|
10
|
-
"""A template for creating agents."""
|
11
|
-
|
12
|
-
name: str = Field(description="Name of the agent")
|
13
|
-
description: str | None = Field(description="Description of what the agent does")
|
14
|
-
|
15
|
-
config: Type[BaseModel] = Field(description="Model for config")
|
16
|
-
input: Type[BaseModel] = Field(description="Model for run input")
|
17
|
-
output: Type[BaseModel] = Field(description="Model for run output")
|
18
|
-
|
19
|
-
create_fn: Callable[[BaseModel, "Context"], Awaitable[Agent]] = Field(exclude=True)
|
20
|
-
|
21
|
-
model_config = ConfigDict(extra="allow")
|
acp/server/highlevel/context.py
DELETED
@@ -1,185 +0,0 @@
|
|
1
|
-
from typing import TYPE_CHECKING, Any, Literal
|
2
|
-
|
3
|
-
from pydantic import BaseModel
|
4
|
-
from pydantic.networks import AnyUrl
|
5
|
-
|
6
|
-
from acp.server.lowlevel.helper_types import ReadResourceContents
|
7
|
-
from acp.shared.context import RequestContext
|
8
|
-
|
9
|
-
if TYPE_CHECKING:
|
10
|
-
from acp.server.highlevel.server import Server
|
11
|
-
|
12
|
-
|
13
|
-
class Context(BaseModel):
|
14
|
-
"""Context object providing access to MCP capabilities.
|
15
|
-
|
16
|
-
This provides a cleaner interface to MCP's RequestContext functionality.
|
17
|
-
It gets injected into tool and resource functions that request it via type hints.
|
18
|
-
|
19
|
-
To use context in a tool function, add a parameter with the Context type annotation:
|
20
|
-
|
21
|
-
```python
|
22
|
-
@server.tool()
|
23
|
-
def my_tool(x: int, ctx: Context) -> str:
|
24
|
-
# Log messages to the client
|
25
|
-
ctx.info(f"Processing {x}")
|
26
|
-
ctx.debug("Debug info")
|
27
|
-
ctx.warning("Warning message")
|
28
|
-
ctx.error("Error message")
|
29
|
-
|
30
|
-
# Report progress
|
31
|
-
ctx.report_progress(50, 100)
|
32
|
-
|
33
|
-
# Access resources
|
34
|
-
data = ctx.read_resource("resource://data")
|
35
|
-
|
36
|
-
# Get request info
|
37
|
-
request_id = ctx.request_id
|
38
|
-
client_id = ctx.client_id
|
39
|
-
|
40
|
-
return str(x)
|
41
|
-
```
|
42
|
-
|
43
|
-
The context parameter name can be anything as long as it's annotated with Context.
|
44
|
-
The context is optional - tools that don't need it can omit the parameter.
|
45
|
-
"""
|
46
|
-
|
47
|
-
_request_context: RequestContext | None
|
48
|
-
_fastmcp: "Server | None"
|
49
|
-
|
50
|
-
def __init__(
|
51
|
-
self,
|
52
|
-
*,
|
53
|
-
request_context: RequestContext | None = None,
|
54
|
-
fastmcp: "Server | None" = None,
|
55
|
-
**kwargs: Any,
|
56
|
-
):
|
57
|
-
super().__init__(**kwargs)
|
58
|
-
self._request_context = request_context
|
59
|
-
self._fastmcp = fastmcp
|
60
|
-
|
61
|
-
@property
|
62
|
-
def fastmcp(self) -> "Server":
|
63
|
-
"""Access to the FastMCP server."""
|
64
|
-
if self._fastmcp is None:
|
65
|
-
raise ValueError("Context is not available outside of a request")
|
66
|
-
return self._fastmcp
|
67
|
-
|
68
|
-
@property
|
69
|
-
def request_context(self) -> RequestContext:
|
70
|
-
"""Access to the underlying request context."""
|
71
|
-
if self._request_context is None:
|
72
|
-
raise ValueError("Context is not available outside of a request")
|
73
|
-
return self._request_context
|
74
|
-
|
75
|
-
async def report_progress(
|
76
|
-
self, progress: float, total: float | None = None
|
77
|
-
) -> None:
|
78
|
-
"""Report progress for the current operation.
|
79
|
-
|
80
|
-
Args:
|
81
|
-
progress: Current progress value e.g. 24
|
82
|
-
total: Optional total value e.g. 100
|
83
|
-
"""
|
84
|
-
|
85
|
-
progress_token = (
|
86
|
-
self.request_context.meta.progressToken
|
87
|
-
if self.request_context.meta
|
88
|
-
else None
|
89
|
-
)
|
90
|
-
|
91
|
-
if progress_token is None:
|
92
|
-
return
|
93
|
-
|
94
|
-
await self.request_context.session.send_progress_notification(
|
95
|
-
progress_token=progress_token, progress=progress, total=total
|
96
|
-
)
|
97
|
-
|
98
|
-
async def report_agent_run_progress(self, delta: BaseModel) -> None:
|
99
|
-
"""Report progress for the agent run operation.
|
100
|
-
|
101
|
-
Args:
|
102
|
-
delta: partial run output
|
103
|
-
"""
|
104
|
-
|
105
|
-
progress_token = (
|
106
|
-
self.request_context.meta.progressToken
|
107
|
-
if self.request_context.meta
|
108
|
-
else None
|
109
|
-
)
|
110
|
-
|
111
|
-
if not progress_token:
|
112
|
-
return
|
113
|
-
|
114
|
-
await self.request_context.session.send_agent_run_progress(
|
115
|
-
progress_token=progress_token, delta=delta.model_dump()
|
116
|
-
)
|
117
|
-
|
118
|
-
async def read_resource(self, uri: str | AnyUrl) -> ReadResourceContents:
|
119
|
-
"""Read a resource by URI.
|
120
|
-
|
121
|
-
Args:
|
122
|
-
uri: Resource URI to read
|
123
|
-
|
124
|
-
Returns:
|
125
|
-
The resource content as either text or bytes
|
126
|
-
"""
|
127
|
-
assert self._fastmcp is not None, (
|
128
|
-
"Context is not available outside of a request"
|
129
|
-
)
|
130
|
-
return await self._fastmcp.read_resource(uri)
|
131
|
-
|
132
|
-
async def log(
|
133
|
-
self,
|
134
|
-
level: Literal["debug", "info", "warning", "error"],
|
135
|
-
message: str,
|
136
|
-
*,
|
137
|
-
logger_name: str | None = None,
|
138
|
-
) -> None:
|
139
|
-
"""Send a log message to the client.
|
140
|
-
|
141
|
-
Args:
|
142
|
-
level: Log level (debug, info, warning, error)
|
143
|
-
message: Log message
|
144
|
-
logger_name: Optional logger name
|
145
|
-
**extra: Additional structured data to include
|
146
|
-
"""
|
147
|
-
await self.request_context.session.send_log_message(
|
148
|
-
level=level, data=message, logger=logger_name
|
149
|
-
)
|
150
|
-
|
151
|
-
@property
|
152
|
-
def client_id(self) -> str | None:
|
153
|
-
"""Get the client ID if available."""
|
154
|
-
return (
|
155
|
-
getattr(self.request_context.meta, "client_id", None)
|
156
|
-
if self.request_context.meta
|
157
|
-
else None
|
158
|
-
)
|
159
|
-
|
160
|
-
@property
|
161
|
-
def request_id(self) -> str:
|
162
|
-
"""Get the unique ID for this request."""
|
163
|
-
return str(self.request_context.request_id)
|
164
|
-
|
165
|
-
@property
|
166
|
-
def session(self):
|
167
|
-
"""Access to the underlying session for advanced usage."""
|
168
|
-
return self.request_context.session
|
169
|
-
|
170
|
-
# Convenience methods for common log levels
|
171
|
-
async def debug(self, message: str, **extra: Any) -> None:
|
172
|
-
"""Send a debug log message."""
|
173
|
-
await self.log("debug", message, **extra)
|
174
|
-
|
175
|
-
async def info(self, message: str, **extra: Any) -> None:
|
176
|
-
"""Send an info log message."""
|
177
|
-
await self.log("info", message, **extra)
|
178
|
-
|
179
|
-
async def warning(self, message: str, **extra: Any) -> None:
|
180
|
-
"""Send a warning log message."""
|
181
|
-
await self.log("warning", message, **extra)
|
182
|
-
|
183
|
-
async def error(self, message: str, **extra: Any) -> None:
|
184
|
-
"""Send an error log message."""
|
185
|
-
await self.log("error", message, **extra)
|
@@ -1,25 +0,0 @@
|
|
1
|
-
"""Custom exceptions for FastMCP."""
|
2
|
-
|
3
|
-
|
4
|
-
class FastMCPError(Exception):
|
5
|
-
"""Base error for FastMCP."""
|
6
|
-
|
7
|
-
|
8
|
-
class ValidationError(FastMCPError):
|
9
|
-
"""Error in validating parameters or return values."""
|
10
|
-
|
11
|
-
|
12
|
-
class ResourceError(FastMCPError):
|
13
|
-
"""Error in resource operations."""
|
14
|
-
|
15
|
-
|
16
|
-
class ToolError(FastMCPError):
|
17
|
-
"""Error in tool operations."""
|
18
|
-
|
19
|
-
|
20
|
-
class AgentError(FastMCPError):
|
21
|
-
"""Error in agent operations."""
|
22
|
-
|
23
|
-
|
24
|
-
class InvalidSignature(Exception):
|
25
|
-
"""Invalid signature for use with FastMCP."""
|
@@ -1,167 +0,0 @@
|
|
1
|
-
"""Base classes for FastMCP prompts."""
|
2
|
-
|
3
|
-
import inspect
|
4
|
-
import json
|
5
|
-
from collections.abc import Callable
|
6
|
-
from typing import Any, Awaitable, Literal, Sequence
|
7
|
-
|
8
|
-
import pydantic_core
|
9
|
-
from pydantic import BaseModel, Field, TypeAdapter, validate_call
|
10
|
-
|
11
|
-
from acp.types import EmbeddedResource, ImageContent, TextContent
|
12
|
-
|
13
|
-
CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
|
14
|
-
|
15
|
-
|
16
|
-
class Message(BaseModel):
|
17
|
-
"""Base class for all prompt messages."""
|
18
|
-
|
19
|
-
role: Literal["user", "assistant"]
|
20
|
-
content: CONTENT_TYPES
|
21
|
-
|
22
|
-
def __init__(self, content: str | CONTENT_TYPES, **kwargs):
|
23
|
-
if isinstance(content, str):
|
24
|
-
content = TextContent(type="text", text=content)
|
25
|
-
super().__init__(content=content, **kwargs)
|
26
|
-
|
27
|
-
|
28
|
-
class UserMessage(Message):
|
29
|
-
"""A message from the user."""
|
30
|
-
|
31
|
-
role: Literal["user", "assistant"] = "user"
|
32
|
-
|
33
|
-
def __init__(self, content: str | CONTENT_TYPES, **kwargs):
|
34
|
-
super().__init__(content=content, **kwargs)
|
35
|
-
|
36
|
-
|
37
|
-
class AssistantMessage(Message):
|
38
|
-
"""A message from the assistant."""
|
39
|
-
|
40
|
-
role: Literal["user", "assistant"] = "assistant"
|
41
|
-
|
42
|
-
def __init__(self, content: str | CONTENT_TYPES, **kwargs):
|
43
|
-
super().__init__(content=content, **kwargs)
|
44
|
-
|
45
|
-
|
46
|
-
message_validator = TypeAdapter(UserMessage | AssistantMessage)
|
47
|
-
|
48
|
-
SyncPromptResult = (
|
49
|
-
str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
|
50
|
-
)
|
51
|
-
PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
|
52
|
-
|
53
|
-
|
54
|
-
class PromptArgument(BaseModel):
|
55
|
-
"""An argument that can be passed to a prompt."""
|
56
|
-
|
57
|
-
name: str = Field(description="Name of the argument")
|
58
|
-
description: str | None = Field(
|
59
|
-
None, description="Description of what the argument does"
|
60
|
-
)
|
61
|
-
required: bool = Field(
|
62
|
-
default=False, description="Whether the argument is required"
|
63
|
-
)
|
64
|
-
|
65
|
-
|
66
|
-
class Prompt(BaseModel):
|
67
|
-
"""A prompt template that can be rendered with parameters."""
|
68
|
-
|
69
|
-
name: str = Field(description="Name of the prompt")
|
70
|
-
description: str | None = Field(
|
71
|
-
None, description="Description of what the prompt does"
|
72
|
-
)
|
73
|
-
arguments: list[PromptArgument] | None = Field(
|
74
|
-
None, description="Arguments that can be passed to the prompt"
|
75
|
-
)
|
76
|
-
fn: Callable = Field(exclude=True)
|
77
|
-
|
78
|
-
@classmethod
|
79
|
-
def from_function(
|
80
|
-
cls,
|
81
|
-
fn: Callable[..., PromptResult],
|
82
|
-
name: str | None = None,
|
83
|
-
description: str | None = None,
|
84
|
-
) -> "Prompt":
|
85
|
-
"""Create a Prompt from a function.
|
86
|
-
|
87
|
-
The function can return:
|
88
|
-
- A string (converted to a message)
|
89
|
-
- A Message object
|
90
|
-
- A dict (converted to a message)
|
91
|
-
- A sequence of any of the above
|
92
|
-
"""
|
93
|
-
func_name = name or fn.__name__
|
94
|
-
|
95
|
-
if func_name == "<lambda>":
|
96
|
-
raise ValueError("You must provide a name for lambda functions")
|
97
|
-
|
98
|
-
# Get schema from TypeAdapter - will fail if function isn't properly typed
|
99
|
-
parameters = TypeAdapter(fn).json_schema()
|
100
|
-
|
101
|
-
# Convert parameters to PromptArguments
|
102
|
-
arguments = []
|
103
|
-
if "properties" in parameters:
|
104
|
-
for param_name, param in parameters["properties"].items():
|
105
|
-
required = param_name in parameters.get("required", [])
|
106
|
-
arguments.append(
|
107
|
-
PromptArgument(
|
108
|
-
name=param_name,
|
109
|
-
description=param.get("description"),
|
110
|
-
required=required,
|
111
|
-
)
|
112
|
-
)
|
113
|
-
|
114
|
-
# ensure the arguments are properly cast
|
115
|
-
fn = validate_call(fn)
|
116
|
-
|
117
|
-
return cls(
|
118
|
-
name=func_name,
|
119
|
-
description=description or fn.__doc__ or "",
|
120
|
-
arguments=arguments,
|
121
|
-
fn=fn,
|
122
|
-
)
|
123
|
-
|
124
|
-
async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:
|
125
|
-
"""Render the prompt with arguments."""
|
126
|
-
# Validate required arguments
|
127
|
-
if self.arguments:
|
128
|
-
required = {arg.name for arg in self.arguments if arg.required}
|
129
|
-
provided = set(arguments or {})
|
130
|
-
missing = required - provided
|
131
|
-
if missing:
|
132
|
-
raise ValueError(f"Missing required arguments: {missing}")
|
133
|
-
|
134
|
-
try:
|
135
|
-
# Call function and check if result is a coroutine
|
136
|
-
result = self.fn(**(arguments or {}))
|
137
|
-
if inspect.iscoroutine(result):
|
138
|
-
result = await result
|
139
|
-
|
140
|
-
# Validate messages
|
141
|
-
if not isinstance(result, (list, tuple)):
|
142
|
-
result = [result]
|
143
|
-
|
144
|
-
# Convert result to messages
|
145
|
-
messages = []
|
146
|
-
for msg in result:
|
147
|
-
try:
|
148
|
-
if isinstance(msg, Message):
|
149
|
-
messages.append(msg)
|
150
|
-
elif isinstance(msg, dict):
|
151
|
-
msg = message_validator.validate_python(msg)
|
152
|
-
messages.append(msg)
|
153
|
-
elif isinstance(msg, str):
|
154
|
-
messages.append(
|
155
|
-
UserMessage(content=TextContent(type="text", text=msg))
|
156
|
-
)
|
157
|
-
else:
|
158
|
-
msg = json.dumps(pydantic_core.to_jsonable_python(msg))
|
159
|
-
messages.append(Message(role="user", content=msg))
|
160
|
-
except Exception:
|
161
|
-
raise ValueError(
|
162
|
-
f"Could not convert prompt result to message: {msg}"
|
163
|
-
)
|
164
|
-
|
165
|
-
return messages
|
166
|
-
except Exception as e:
|
167
|
-
raise ValueError(f"Error rendering prompt {self.name}: {e}")
|
@@ -1,50 +0,0 @@
|
|
1
|
-
"""Prompt management functionality."""
|
2
|
-
|
3
|
-
from typing import Any
|
4
|
-
|
5
|
-
from acp.server.highlevel.prompts.base import Message, Prompt
|
6
|
-
from acp.server.highlevel.utilities.logging import get_logger
|
7
|
-
|
8
|
-
logger = get_logger(__name__)
|
9
|
-
|
10
|
-
|
11
|
-
class PromptManager:
|
12
|
-
"""Manages FastMCP prompts."""
|
13
|
-
|
14
|
-
def __init__(self, warn_on_duplicate_prompts: bool = True):
|
15
|
-
self._prompts: dict[str, Prompt] = {}
|
16
|
-
self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
|
17
|
-
|
18
|
-
def get_prompt(self, name: str) -> Prompt | None:
|
19
|
-
"""Get prompt by name."""
|
20
|
-
return self._prompts.get(name)
|
21
|
-
|
22
|
-
def list_prompts(self) -> list[Prompt]:
|
23
|
-
"""List all registered prompts."""
|
24
|
-
return list(self._prompts.values())
|
25
|
-
|
26
|
-
def add_prompt(
|
27
|
-
self,
|
28
|
-
prompt: Prompt,
|
29
|
-
) -> Prompt:
|
30
|
-
"""Add a prompt to the manager."""
|
31
|
-
|
32
|
-
# Check for duplicates
|
33
|
-
existing = self._prompts.get(prompt.name)
|
34
|
-
if existing:
|
35
|
-
if self.warn_on_duplicate_prompts:
|
36
|
-
logger.warning(f"Prompt already exists: {prompt.name}")
|
37
|
-
return existing
|
38
|
-
|
39
|
-
self._prompts[prompt.name] = prompt
|
40
|
-
return prompt
|
41
|
-
|
42
|
-
async def render_prompt(
|
43
|
-
self, name: str, arguments: dict[str, Any] | None = None
|
44
|
-
) -> list[Message]:
|
45
|
-
"""Render a prompt by name with arguments."""
|
46
|
-
prompt = self.get_prompt(name)
|
47
|
-
if not prompt:
|
48
|
-
raise ValueError(f"Unknown prompt: {name}")
|
49
|
-
|
50
|
-
return await prompt.render(arguments)
|
@@ -1,33 +0,0 @@
|
|
1
|
-
"""Prompt management functionality."""
|
2
|
-
|
3
|
-
from acp.server.highlevel.prompts.base import Prompt
|
4
|
-
from acp.server.highlevel.utilities.logging import get_logger
|
5
|
-
|
6
|
-
logger = get_logger(__name__)
|
7
|
-
|
8
|
-
|
9
|
-
class PromptManager:
|
10
|
-
"""Manages FastMCP prompts."""
|
11
|
-
|
12
|
-
def __init__(self, warn_on_duplicate_prompts: bool = True):
|
13
|
-
self._prompts: dict[str, Prompt] = {}
|
14
|
-
self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
|
15
|
-
|
16
|
-
def add_prompt(self, prompt: Prompt) -> Prompt:
|
17
|
-
"""Add a prompt to the manager."""
|
18
|
-
logger.debug(f"Adding prompt: {prompt.name}")
|
19
|
-
existing = self._prompts.get(prompt.name)
|
20
|
-
if existing:
|
21
|
-
if self.warn_on_duplicate_prompts:
|
22
|
-
logger.warning(f"Prompt already exists: {prompt.name}")
|
23
|
-
return existing
|
24
|
-
self._prompts[prompt.name] = prompt
|
25
|
-
return prompt
|
26
|
-
|
27
|
-
def get_prompt(self, name: str) -> Prompt | None:
|
28
|
-
"""Get prompt by name."""
|
29
|
-
return self._prompts.get(name)
|
30
|
-
|
31
|
-
def list_prompts(self) -> list[Prompt]:
|
32
|
-
"""List all registered prompts."""
|
33
|
-
return list(self._prompts.values())
|