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.
Files changed (69) hide show
  1. acp_sdk/client/__init__.py +1 -0
  2. acp_sdk/client/client.py +135 -0
  3. acp_sdk/models.py +219 -0
  4. acp_sdk/server/__init__.py +2 -0
  5. acp_sdk/server/agent.py +32 -0
  6. acp_sdk/server/bundle.py +133 -0
  7. acp_sdk/server/context.py +6 -0
  8. acp_sdk/server/server.py +137 -0
  9. acp_sdk/server/telemetry.py +45 -0
  10. acp_sdk/server/utils.py +12 -0
  11. acp_sdk-1.0.0rc1.dist-info/METADATA +53 -0
  12. acp_sdk-1.0.0rc1.dist-info/RECORD +15 -0
  13. acp/__init__.py +0 -138
  14. acp/cli/__init__.py +0 -6
  15. acp/cli/claude.py +0 -139
  16. acp/cli/cli.py +0 -471
  17. acp/client/__main__.py +0 -79
  18. acp/client/session.py +0 -372
  19. acp/client/sse.py +0 -145
  20. acp/client/stdio.py +0 -153
  21. acp/server/__init__.py +0 -3
  22. acp/server/__main__.py +0 -50
  23. acp/server/highlevel/__init__.py +0 -9
  24. acp/server/highlevel/agents/__init__.py +0 -5
  25. acp/server/highlevel/agents/agent_manager.py +0 -110
  26. acp/server/highlevel/agents/base.py +0 -20
  27. acp/server/highlevel/agents/templates.py +0 -21
  28. acp/server/highlevel/context.py +0 -185
  29. acp/server/highlevel/exceptions.py +0 -25
  30. acp/server/highlevel/prompts/__init__.py +0 -4
  31. acp/server/highlevel/prompts/base.py +0 -167
  32. acp/server/highlevel/prompts/manager.py +0 -50
  33. acp/server/highlevel/prompts/prompt_manager.py +0 -33
  34. acp/server/highlevel/resources/__init__.py +0 -23
  35. acp/server/highlevel/resources/base.py +0 -48
  36. acp/server/highlevel/resources/resource_manager.py +0 -94
  37. acp/server/highlevel/resources/templates.py +0 -80
  38. acp/server/highlevel/resources/types.py +0 -185
  39. acp/server/highlevel/server.py +0 -705
  40. acp/server/highlevel/tools/__init__.py +0 -4
  41. acp/server/highlevel/tools/base.py +0 -83
  42. acp/server/highlevel/tools/tool_manager.py +0 -53
  43. acp/server/highlevel/utilities/__init__.py +0 -1
  44. acp/server/highlevel/utilities/func_metadata.py +0 -210
  45. acp/server/highlevel/utilities/logging.py +0 -43
  46. acp/server/highlevel/utilities/types.py +0 -54
  47. acp/server/lowlevel/__init__.py +0 -3
  48. acp/server/lowlevel/helper_types.py +0 -9
  49. acp/server/lowlevel/server.py +0 -643
  50. acp/server/models.py +0 -17
  51. acp/server/session.py +0 -315
  52. acp/server/sse.py +0 -175
  53. acp/server/stdio.py +0 -83
  54. acp/server/websocket.py +0 -61
  55. acp/shared/__init__.py +0 -0
  56. acp/shared/context.py +0 -14
  57. acp/shared/exceptions.py +0 -14
  58. acp/shared/memory.py +0 -87
  59. acp/shared/progress.py +0 -40
  60. acp/shared/session.py +0 -413
  61. acp/shared/version.py +0 -3
  62. acp/types.py +0 -1258
  63. acp_sdk-0.0.6.dist-info/METADATA +0 -46
  64. acp_sdk-0.0.6.dist-info/RECORD +0 -57
  65. acp_sdk-0.0.6.dist-info/entry_points.txt +0 -2
  66. acp_sdk-0.0.6.dist-info/licenses/LICENSE +0 -22
  67. {acp/client → acp_sdk}/__init__.py +0 -0
  68. {acp → acp_sdk}/py.typed +0 -0
  69. {acp_sdk-0.0.6.dist-info → acp_sdk-1.0.0rc1.dist-info}/WHEEL +0 -0
@@ -1,23 +0,0 @@
1
- from .base import Resource
2
- from .resource_manager import ResourceManager
3
- from .templates import ResourceTemplate
4
- from .types import (
5
- BinaryResource,
6
- DirectoryResource,
7
- FileResource,
8
- FunctionResource,
9
- HttpResource,
10
- TextResource,
11
- )
12
-
13
- __all__ = [
14
- "Resource",
15
- "TextResource",
16
- "BinaryResource",
17
- "FunctionResource",
18
- "FileResource",
19
- "HttpResource",
20
- "DirectoryResource",
21
- "ResourceTemplate",
22
- "ResourceManager",
23
- ]
@@ -1,48 +0,0 @@
1
- """Base classes and interfaces for FastMCP resources."""
2
-
3
- import abc
4
- from typing import Annotated
5
-
6
- from pydantic import (
7
- AnyUrl,
8
- BaseModel,
9
- ConfigDict,
10
- Field,
11
- UrlConstraints,
12
- ValidationInfo,
13
- field_validator,
14
- )
15
-
16
-
17
- class Resource(BaseModel, abc.ABC):
18
- """Base class for all resources."""
19
-
20
- model_config = ConfigDict(validate_default=True)
21
-
22
- uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(
23
- default=..., description="URI of the resource"
24
- )
25
- name: str | None = Field(description="Name of the resource", default=None)
26
- description: str | None = Field(
27
- description="Description of the resource", default=None
28
- )
29
- mime_type: str = Field(
30
- default="text/plain",
31
- description="MIME type of the resource content",
32
- pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
33
- )
34
-
35
- @field_validator("name", mode="before")
36
- @classmethod
37
- def set_default_name(cls, name: str | None, info: ValidationInfo) -> str:
38
- """Set default name from URI if not provided."""
39
- if name:
40
- return name
41
- if uri := info.data.get("uri"):
42
- return str(uri)
43
- raise ValueError("Either name or uri must be provided")
44
-
45
- @abc.abstractmethod
46
- async def read(self) -> str | bytes:
47
- """Read the resource content."""
48
- pass
@@ -1,94 +0,0 @@
1
- """Resource manager functionality."""
2
-
3
- from typing import Callable
4
-
5
- from pydantic import AnyUrl
6
-
7
- from acp.server.highlevel.resources.base import Resource
8
- from acp.server.highlevel.resources.templates import ResourceTemplate
9
- from acp.server.highlevel.utilities.logging import get_logger
10
-
11
- logger = get_logger(__name__)
12
-
13
-
14
- class ResourceManager:
15
- """Manages FastMCP resources."""
16
-
17
- def __init__(self, warn_on_duplicate_resources: bool = True):
18
- self._resources: dict[str, Resource] = {}
19
- self._templates: dict[str, ResourceTemplate] = {}
20
- self.warn_on_duplicate_resources = warn_on_duplicate_resources
21
-
22
- def add_resource(self, resource: Resource) -> Resource:
23
- """Add a resource to the manager.
24
-
25
- Args:
26
- resource: A Resource instance to add
27
-
28
- Returns:
29
- The added resource. If a resource with the same URI already exists,
30
- returns the existing resource.
31
- """
32
- logger.debug(
33
- "Adding resource",
34
- extra={
35
- "uri": resource.uri,
36
- "type": type(resource).__name__,
37
- "resource_name": resource.name,
38
- },
39
- )
40
- existing = self._resources.get(str(resource.uri))
41
- if existing:
42
- if self.warn_on_duplicate_resources:
43
- logger.warning(f"Resource already exists: {resource.uri}")
44
- return existing
45
- self._resources[str(resource.uri)] = resource
46
- return resource
47
-
48
- def add_template(
49
- self,
50
- fn: Callable,
51
- uri_template: str,
52
- name: str | None = None,
53
- description: str | None = None,
54
- mime_type: str | None = None,
55
- ) -> ResourceTemplate:
56
- """Add a template from a function."""
57
- template = ResourceTemplate.from_function(
58
- fn,
59
- uri_template=uri_template,
60
- name=name,
61
- description=description,
62
- mime_type=mime_type,
63
- )
64
- self._templates[template.uri_template] = template
65
- return template
66
-
67
- async def get_resource(self, uri: AnyUrl | str) -> Resource | None:
68
- """Get resource by URI, checking concrete resources first, then templates."""
69
- uri_str = str(uri)
70
- logger.debug("Getting resource", extra={"uri": uri_str})
71
-
72
- # First check concrete resources
73
- if resource := self._resources.get(uri_str):
74
- return resource
75
-
76
- # Then check templates
77
- for template in self._templates.values():
78
- if params := template.matches(uri_str):
79
- try:
80
- return await template.create_resource(uri_str, params)
81
- except Exception as e:
82
- raise ValueError(f"Error creating resource from template: {e}")
83
-
84
- raise ValueError(f"Unknown resource: {uri}")
85
-
86
- def list_resources(self) -> list[Resource]:
87
- """List all registered resources."""
88
- logger.debug("Listing resources", extra={"count": len(self._resources)})
89
- return list(self._resources.values())
90
-
91
- def list_templates(self) -> list[ResourceTemplate]:
92
- """List all registered templates."""
93
- logger.debug("Listing templates", extra={"count": len(self._templates)})
94
- return list(self._templates.values())
@@ -1,80 +0,0 @@
1
- """Resource template functionality."""
2
-
3
- import inspect
4
- import re
5
- from typing import Any, Callable
6
-
7
- from pydantic import BaseModel, Field, TypeAdapter, validate_call
8
-
9
- from acp.server.highlevel.resources.types import FunctionResource, Resource
10
-
11
-
12
- class ResourceTemplate(BaseModel):
13
- """A template for dynamically creating resources."""
14
-
15
- uri_template: str = Field(
16
- description="URI template with parameters (e.g. weather://{city}/current)"
17
- )
18
- name: str = Field(description="Name of the resource")
19
- description: str | None = Field(description="Description of what the resource does")
20
- mime_type: str = Field(
21
- default="text/plain", description="MIME type of the resource content"
22
- )
23
- fn: Callable = Field(exclude=True)
24
- parameters: dict = Field(description="JSON schema for function parameters")
25
-
26
- @classmethod
27
- def from_function(
28
- cls,
29
- fn: Callable,
30
- uri_template: str,
31
- name: str | None = None,
32
- description: str | None = None,
33
- mime_type: str | None = None,
34
- ) -> "ResourceTemplate":
35
- """Create a template from a function."""
36
- func_name = name or fn.__name__
37
- if func_name == "<lambda>":
38
- raise ValueError("You must provide a name for lambda functions")
39
-
40
- # Get schema from TypeAdapter - will fail if function isn't properly typed
41
- parameters = TypeAdapter(fn).json_schema()
42
-
43
- # ensure the arguments are properly cast
44
- fn = validate_call(fn)
45
-
46
- return cls(
47
- uri_template=uri_template,
48
- name=func_name,
49
- description=description or fn.__doc__ or "",
50
- mime_type=mime_type or "text/plain",
51
- fn=fn,
52
- parameters=parameters,
53
- )
54
-
55
- def matches(self, uri: str) -> dict[str, Any] | None:
56
- """Check if URI matches template and extract parameters."""
57
- # Convert template to regex pattern
58
- pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
59
- match = re.match(f"^{pattern}$", uri)
60
- if match:
61
- return match.groupdict()
62
- return None
63
-
64
- async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
65
- """Create a resource from the template with the given parameters."""
66
- try:
67
- # Call function and check if result is a coroutine
68
- result = self.fn(**params)
69
- if inspect.iscoroutine(result):
70
- result = await result
71
-
72
- return FunctionResource(
73
- uri=uri, # type: ignore
74
- name=self.name,
75
- description=self.description,
76
- mime_type=self.mime_type,
77
- fn=lambda: result, # Capture result in closure
78
- )
79
- except Exception as e:
80
- raise ValueError(f"Error creating resource from template: {e}")
@@ -1,185 +0,0 @@
1
- """Concrete resource implementations."""
2
-
3
- import inspect
4
- import json
5
- from collections.abc import Callable
6
- from pathlib import Path
7
- from typing import Any
8
-
9
- import anyio
10
- import anyio.to_thread
11
- import httpx
12
- import pydantic.json
13
- import pydantic_core
14
- from pydantic import Field, ValidationInfo
15
-
16
- from acp.server.highlevel.resources.base import Resource
17
-
18
-
19
- class TextResource(Resource):
20
- """A resource that reads from a string."""
21
-
22
- text: str = Field(description="Text content of the resource")
23
-
24
- async def read(self) -> str:
25
- """Read the text content."""
26
- return self.text
27
-
28
-
29
- class BinaryResource(Resource):
30
- """A resource that reads from bytes."""
31
-
32
- data: bytes = Field(description="Binary content of the resource")
33
-
34
- async def read(self) -> bytes:
35
- """Read the binary content."""
36
- return self.data
37
-
38
-
39
- class FunctionResource(Resource):
40
- """A resource that defers data loading by wrapping a function.
41
-
42
- The function is only called when the resource is read, allowing for lazy loading
43
- of potentially expensive data. This is particularly useful when listing resources,
44
- as the function won't be called until the resource is actually accessed.
45
-
46
- The function can return:
47
- - str for text content (default)
48
- - bytes for binary content
49
- - other types will be converted to JSON
50
- """
51
-
52
- fn: Callable[[], Any] = Field(exclude=True)
53
-
54
- async def read(self) -> str | bytes:
55
- """Read the resource by calling the wrapped function."""
56
- try:
57
- result = (
58
- await self.fn() if inspect.iscoroutinefunction(self.fn) else self.fn()
59
- )
60
- if isinstance(result, Resource):
61
- return await result.read()
62
- if isinstance(result, bytes):
63
- return result
64
- if isinstance(result, str):
65
- return result
66
- try:
67
- return json.dumps(pydantic_core.to_jsonable_python(result))
68
- except (TypeError, pydantic_core.PydanticSerializationError):
69
- # If JSON serialization fails, try str()
70
- return str(result)
71
- except Exception as e:
72
- raise ValueError(f"Error reading resource {self.uri}: {e}")
73
-
74
-
75
- class FileResource(Resource):
76
- """A resource that reads from a file.
77
-
78
- Set is_binary=True to read file as binary data instead of text.
79
- """
80
-
81
- path: Path = Field(description="Path to the file")
82
- is_binary: bool = Field(
83
- default=False,
84
- description="Whether to read the file as binary data",
85
- )
86
- mime_type: str = Field(
87
- default="text/plain",
88
- description="MIME type of the resource content",
89
- )
90
-
91
- @pydantic.field_validator("path")
92
- @classmethod
93
- def validate_absolute_path(cls, path: Path) -> Path:
94
- """Ensure path is absolute."""
95
- if not path.is_absolute():
96
- raise ValueError("Path must be absolute")
97
- return path
98
-
99
- @pydantic.field_validator("is_binary")
100
- @classmethod
101
- def set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool:
102
- """Set is_binary based on mime_type if not explicitly set."""
103
- if is_binary:
104
- return True
105
- mime_type = info.data.get("mime_type", "text/plain")
106
- return not mime_type.startswith("text/")
107
-
108
- async def read(self) -> str | bytes:
109
- """Read the file content."""
110
- try:
111
- if self.is_binary:
112
- return await anyio.to_thread.run_sync(self.path.read_bytes)
113
- return await anyio.to_thread.run_sync(self.path.read_text)
114
- except Exception as e:
115
- raise ValueError(f"Error reading file {self.path}: {e}")
116
-
117
-
118
- class HttpResource(Resource):
119
- """A resource that reads from an HTTP endpoint."""
120
-
121
- url: str = Field(description="URL to fetch content from")
122
- mime_type: str = Field(
123
- default="application/json", description="MIME type of the resource content"
124
- )
125
-
126
- async def read(self) -> str | bytes:
127
- """Read the HTTP content."""
128
- async with httpx.AsyncClient() as client:
129
- response = await client.get(self.url)
130
- response.raise_for_status()
131
- return response.text
132
-
133
-
134
- class DirectoryResource(Resource):
135
- """A resource that lists files in a directory."""
136
-
137
- path: Path = Field(description="Path to the directory")
138
- recursive: bool = Field(
139
- default=False, description="Whether to list files recursively"
140
- )
141
- pattern: str | None = Field(
142
- default=None, description="Optional glob pattern to filter files"
143
- )
144
- mime_type: str = Field(
145
- default="application/json", description="MIME type of the resource content"
146
- )
147
-
148
- @pydantic.field_validator("path")
149
- @classmethod
150
- def validate_absolute_path(cls, path: Path) -> Path:
151
- """Ensure path is absolute."""
152
- if not path.is_absolute():
153
- raise ValueError("Path must be absolute")
154
- return path
155
-
156
- def list_files(self) -> list[Path]:
157
- """List files in the directory."""
158
- if not self.path.exists():
159
- raise FileNotFoundError(f"Directory not found: {self.path}")
160
- if not self.path.is_dir():
161
- raise NotADirectoryError(f"Not a directory: {self.path}")
162
-
163
- try:
164
- if self.pattern:
165
- return (
166
- list(self.path.glob(self.pattern))
167
- if not self.recursive
168
- else list(self.path.rglob(self.pattern))
169
- )
170
- return (
171
- list(self.path.glob("*"))
172
- if not self.recursive
173
- else list(self.path.rglob("*"))
174
- )
175
- except Exception as e:
176
- raise ValueError(f"Error listing directory {self.path}: {e}")
177
-
178
- async def read(self) -> str: # Always returns JSON string
179
- """Read the directory listing."""
180
- try:
181
- files = await anyio.to_thread.run_sync(self.list_files)
182
- file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]
183
- return json.dumps({"files": file_list}, indent=2)
184
- except Exception as e:
185
- raise ValueError(f"Error reading directory {self.path}: {e}")