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
@@ -1,83 +0,0 @@
|
|
1
|
-
import inspect
|
2
|
-
from typing import TYPE_CHECKING, Any, Callable
|
3
|
-
|
4
|
-
from pydantic import BaseModel, Field
|
5
|
-
|
6
|
-
import acp.server.highlevel
|
7
|
-
from acp.server.highlevel.exceptions import ToolError
|
8
|
-
from acp.server.highlevel.utilities.func_metadata import FuncMetadata, func_metadata
|
9
|
-
|
10
|
-
if TYPE_CHECKING:
|
11
|
-
from acp.server.highlevel.server import Context
|
12
|
-
|
13
|
-
|
14
|
-
class Tool(BaseModel):
|
15
|
-
"""Internal tool registration info."""
|
16
|
-
|
17
|
-
fn: Callable = Field(exclude=True)
|
18
|
-
name: str = Field(description="Name of the tool")
|
19
|
-
description: str = Field(description="Description of what the tool does")
|
20
|
-
parameters: dict = Field(description="JSON schema for tool parameters")
|
21
|
-
fn_metadata: FuncMetadata = Field(
|
22
|
-
description="Metadata about the function including a pydantic model for tool"
|
23
|
-
" arguments"
|
24
|
-
)
|
25
|
-
is_async: bool = Field(description="Whether the tool is async")
|
26
|
-
context_kwarg: str | None = Field(
|
27
|
-
None, description="Name of the kwarg that should receive context"
|
28
|
-
)
|
29
|
-
|
30
|
-
@classmethod
|
31
|
-
def from_function(
|
32
|
-
cls,
|
33
|
-
fn: Callable,
|
34
|
-
name: str | None = None,
|
35
|
-
description: str | None = None,
|
36
|
-
context_kwarg: str | None = None,
|
37
|
-
) -> "Tool":
|
38
|
-
"""Create a Tool from a function."""
|
39
|
-
func_name = name or fn.__name__
|
40
|
-
|
41
|
-
if func_name == "<lambda>":
|
42
|
-
raise ValueError("You must provide a name for lambda functions")
|
43
|
-
|
44
|
-
func_doc = description or fn.__doc__ or ""
|
45
|
-
is_async = inspect.iscoroutinefunction(fn)
|
46
|
-
|
47
|
-
# Find context parameter if it exists
|
48
|
-
if context_kwarg is None:
|
49
|
-
sig = inspect.signature(fn)
|
50
|
-
for param_name, param in sig.parameters.items():
|
51
|
-
if param.annotation is acp.server.highlevel.Context:
|
52
|
-
context_kwarg = param_name
|
53
|
-
break
|
54
|
-
|
55
|
-
func_arg_metadata = func_metadata(
|
56
|
-
fn,
|
57
|
-
skip_names=[context_kwarg] if context_kwarg is not None else [],
|
58
|
-
)
|
59
|
-
parameters = func_arg_metadata.arg_model.model_json_schema()
|
60
|
-
|
61
|
-
return cls(
|
62
|
-
fn=fn,
|
63
|
-
name=func_name,
|
64
|
-
description=func_doc,
|
65
|
-
parameters=parameters,
|
66
|
-
fn_metadata=func_arg_metadata,
|
67
|
-
is_async=is_async,
|
68
|
-
context_kwarg=context_kwarg,
|
69
|
-
)
|
70
|
-
|
71
|
-
async def run(self, arguments: dict, context: "Context | None" = None) -> Any:
|
72
|
-
"""Run the tool with arguments."""
|
73
|
-
try:
|
74
|
-
return await self.fn_metadata.call_fn_with_arg_validation(
|
75
|
-
self.fn,
|
76
|
-
self.is_async,
|
77
|
-
arguments,
|
78
|
-
{self.context_kwarg: context}
|
79
|
-
if self.context_kwarg is not None
|
80
|
-
else None,
|
81
|
-
)
|
82
|
-
except Exception as e:
|
83
|
-
raise ToolError(f"Error executing tool {self.name}: {e}") from e
|
@@ -1,53 +0,0 @@
|
|
1
|
-
from collections.abc import Callable
|
2
|
-
from typing import TYPE_CHECKING, Any
|
3
|
-
|
4
|
-
from acp.server.highlevel.exceptions import ToolError
|
5
|
-
from acp.server.highlevel.tools.base import Tool
|
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 ToolManager:
|
15
|
-
"""Manages FastMCP tools."""
|
16
|
-
|
17
|
-
def __init__(self, warn_on_duplicate_tools: bool = True):
|
18
|
-
self._tools: dict[str, Tool] = {}
|
19
|
-
self.warn_on_duplicate_tools = warn_on_duplicate_tools
|
20
|
-
|
21
|
-
def get_tool(self, name: str) -> Tool | None:
|
22
|
-
"""Get tool by name."""
|
23
|
-
return self._tools.get(name)
|
24
|
-
|
25
|
-
def list_tools(self) -> list[Tool]:
|
26
|
-
"""List all registered tools."""
|
27
|
-
return list(self._tools.values())
|
28
|
-
|
29
|
-
def add_tool(
|
30
|
-
self,
|
31
|
-
fn: Callable,
|
32
|
-
name: str | None = None,
|
33
|
-
description: str | None = None,
|
34
|
-
) -> Tool:
|
35
|
-
"""Add a tool to the server."""
|
36
|
-
tool = Tool.from_function(fn, name=name, description=description)
|
37
|
-
existing = self._tools.get(tool.name)
|
38
|
-
if existing:
|
39
|
-
if self.warn_on_duplicate_tools:
|
40
|
-
logger.warning(f"Tool already exists: {tool.name}")
|
41
|
-
return existing
|
42
|
-
self._tools[tool.name] = tool
|
43
|
-
return tool
|
44
|
-
|
45
|
-
async def call_tool(
|
46
|
-
self, name: str, arguments: dict, context: "Context | None" = None
|
47
|
-
) -> Any:
|
48
|
-
"""Call a tool by name with arguments."""
|
49
|
-
tool = self.get_tool(name)
|
50
|
-
if not tool:
|
51
|
-
raise ToolError(f"Unknown tool: {name}")
|
52
|
-
|
53
|
-
return await tool.run(arguments, context=context)
|
@@ -1 +0,0 @@
|
|
1
|
-
"""FastMCP utility modules."""
|
@@ -1,210 +0,0 @@
|
|
1
|
-
import inspect
|
2
|
-
import json
|
3
|
-
from collections.abc import Awaitable, Callable, Sequence
|
4
|
-
from typing import (
|
5
|
-
Annotated,
|
6
|
-
Any,
|
7
|
-
ForwardRef,
|
8
|
-
)
|
9
|
-
|
10
|
-
from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, create_model
|
11
|
-
from pydantic._internal._typing_extra import eval_type_backport
|
12
|
-
from pydantic.fields import FieldInfo
|
13
|
-
from pydantic_core import PydanticUndefined
|
14
|
-
|
15
|
-
from acp.server.highlevel.exceptions import InvalidSignature
|
16
|
-
from acp.server.highlevel.utilities.logging import get_logger
|
17
|
-
|
18
|
-
logger = get_logger(__name__)
|
19
|
-
|
20
|
-
|
21
|
-
class ArgModelBase(BaseModel):
|
22
|
-
"""A model representing the arguments to a function."""
|
23
|
-
|
24
|
-
def model_dump_one_level(self) -> dict[str, Any]:
|
25
|
-
"""Return a dict of the model's fields, one level deep.
|
26
|
-
|
27
|
-
That is, sub-models etc are not dumped - they are kept as pydantic models.
|
28
|
-
"""
|
29
|
-
kwargs: dict[str, Any] = {}
|
30
|
-
for field_name in self.model_fields.keys():
|
31
|
-
kwargs[field_name] = getattr(self, field_name)
|
32
|
-
return kwargs
|
33
|
-
|
34
|
-
model_config = ConfigDict(
|
35
|
-
arbitrary_types_allowed=True,
|
36
|
-
)
|
37
|
-
|
38
|
-
|
39
|
-
class FuncMetadata(BaseModel):
|
40
|
-
arg_model: Annotated[type[ArgModelBase], WithJsonSchema(None)]
|
41
|
-
# We can add things in the future like
|
42
|
-
# - Maybe some args are excluded from attempting to parse from JSON
|
43
|
-
# - Maybe some args are special (like context) for dependency injection
|
44
|
-
|
45
|
-
async def call_fn_with_arg_validation(
|
46
|
-
self,
|
47
|
-
fn: Callable[..., Any] | Awaitable[Any],
|
48
|
-
fn_is_async: bool,
|
49
|
-
arguments_to_validate: dict[str, Any],
|
50
|
-
arguments_to_pass_directly: dict[str, Any] | None,
|
51
|
-
) -> Any:
|
52
|
-
"""Call the given function with arguments validated and injected.
|
53
|
-
|
54
|
-
Arguments are first attempted to be parsed from JSON, then validated against
|
55
|
-
the argument model, before being passed to the function.
|
56
|
-
"""
|
57
|
-
arguments_pre_parsed = self.pre_parse_json(arguments_to_validate)
|
58
|
-
arguments_parsed_model = self.arg_model.model_validate(arguments_pre_parsed)
|
59
|
-
arguments_parsed_dict = arguments_parsed_model.model_dump_one_level()
|
60
|
-
|
61
|
-
arguments_parsed_dict |= arguments_to_pass_directly or {}
|
62
|
-
|
63
|
-
if fn_is_async:
|
64
|
-
if isinstance(fn, Awaitable):
|
65
|
-
return await fn
|
66
|
-
return await fn(**arguments_parsed_dict)
|
67
|
-
if isinstance(fn, Callable):
|
68
|
-
return fn(**arguments_parsed_dict)
|
69
|
-
raise TypeError("fn must be either Callable or Awaitable")
|
70
|
-
|
71
|
-
def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
|
72
|
-
"""Pre-parse data from JSON.
|
73
|
-
|
74
|
-
Return a dict with same keys as input but with values parsed from JSON
|
75
|
-
if appropriate.
|
76
|
-
|
77
|
-
This is to handle cases like `["a", "b", "c"]` being passed in as JSON inside
|
78
|
-
a string rather than an actual list. Claude desktop is prone to this - in fact
|
79
|
-
it seems incapable of NOT doing this. For sub-models, it tends to pass
|
80
|
-
dicts (JSON objects) as JSON strings, which can be pre-parsed here.
|
81
|
-
"""
|
82
|
-
new_data = data.copy() # Shallow copy
|
83
|
-
for field_name, field_info in self.arg_model.model_fields.items():
|
84
|
-
if field_name not in data.keys():
|
85
|
-
continue
|
86
|
-
if isinstance(data[field_name], str):
|
87
|
-
try:
|
88
|
-
pre_parsed = json.loads(data[field_name])
|
89
|
-
except json.JSONDecodeError:
|
90
|
-
continue # Not JSON - skip
|
91
|
-
if isinstance(pre_parsed, str):
|
92
|
-
# This is likely that the raw value is e.g. `"hello"` which we
|
93
|
-
# Should really be parsed as '"hello"' in Python - but if we parse
|
94
|
-
# it as JSON it'll turn into just 'hello'. So we skip it.
|
95
|
-
continue
|
96
|
-
new_data[field_name] = pre_parsed
|
97
|
-
assert new_data.keys() == data.keys()
|
98
|
-
return new_data
|
99
|
-
|
100
|
-
model_config = ConfigDict(
|
101
|
-
arbitrary_types_allowed=True,
|
102
|
-
)
|
103
|
-
|
104
|
-
|
105
|
-
def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadata:
|
106
|
-
"""Given a function, return metadata including a pydantic model representing its
|
107
|
-
signature.
|
108
|
-
|
109
|
-
The use case for this is
|
110
|
-
```
|
111
|
-
meta = func_to_pyd(func)
|
112
|
-
validated_args = meta.arg_model.model_validate(some_raw_data_dict)
|
113
|
-
return func(**validated_args.model_dump_one_level())
|
114
|
-
```
|
115
|
-
|
116
|
-
**critically** it also provides pre-parse helper to attempt to parse things from
|
117
|
-
JSON.
|
118
|
-
|
119
|
-
Args:
|
120
|
-
func: The function to convert to a pydantic model
|
121
|
-
skip_names: A list of parameter names to skip. These will not be included in
|
122
|
-
the model.
|
123
|
-
Returns:
|
124
|
-
A pydantic model representing the function's signature.
|
125
|
-
"""
|
126
|
-
sig = _get_typed_signature(func)
|
127
|
-
params = sig.parameters
|
128
|
-
dynamic_pydantic_model_params: dict[str, Any] = {}
|
129
|
-
globalns = getattr(func, "__globals__", {})
|
130
|
-
for param in params.values():
|
131
|
-
if param.name.startswith("_"):
|
132
|
-
raise InvalidSignature(
|
133
|
-
f"Parameter {param.name} of {func.__name__} cannot start with '_'"
|
134
|
-
)
|
135
|
-
if param.name in skip_names:
|
136
|
-
continue
|
137
|
-
annotation = param.annotation
|
138
|
-
|
139
|
-
# `x: None` / `x: None = None`
|
140
|
-
if annotation is None:
|
141
|
-
annotation = Annotated[
|
142
|
-
None,
|
143
|
-
Field(
|
144
|
-
default=param.default
|
145
|
-
if param.default is not inspect.Parameter.empty
|
146
|
-
else PydanticUndefined
|
147
|
-
),
|
148
|
-
]
|
149
|
-
|
150
|
-
# Untyped field
|
151
|
-
if annotation is inspect.Parameter.empty:
|
152
|
-
annotation = Annotated[
|
153
|
-
Any,
|
154
|
-
Field(),
|
155
|
-
# 🤷
|
156
|
-
WithJsonSchema({"title": param.name, "type": "string"}),
|
157
|
-
]
|
158
|
-
|
159
|
-
field_info = FieldInfo.from_annotated_attribute(
|
160
|
-
_get_typed_annotation(annotation, globalns),
|
161
|
-
param.default
|
162
|
-
if param.default is not inspect.Parameter.empty
|
163
|
-
else PydanticUndefined,
|
164
|
-
)
|
165
|
-
dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info)
|
166
|
-
continue
|
167
|
-
|
168
|
-
arguments_model = create_model(
|
169
|
-
f"{func.__name__}Arguments",
|
170
|
-
**dynamic_pydantic_model_params,
|
171
|
-
__base__=ArgModelBase,
|
172
|
-
)
|
173
|
-
resp = FuncMetadata(arg_model=arguments_model)
|
174
|
-
return resp
|
175
|
-
|
176
|
-
|
177
|
-
def _get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
|
178
|
-
def try_eval_type(value, globalns, localns):
|
179
|
-
try:
|
180
|
-
return eval_type_backport(value, globalns, localns), True
|
181
|
-
except NameError:
|
182
|
-
return value, False
|
183
|
-
|
184
|
-
if isinstance(annotation, str):
|
185
|
-
annotation = ForwardRef(annotation)
|
186
|
-
annotation, status = try_eval_type(annotation, globalns, globalns)
|
187
|
-
|
188
|
-
# This check and raise could perhaps be skipped, and we (FastMCP) just call
|
189
|
-
# model_rebuild right before using it 🤷
|
190
|
-
if status is False:
|
191
|
-
raise InvalidSignature(f"Unable to evaluate type annotation {annotation}")
|
192
|
-
|
193
|
-
return annotation
|
194
|
-
|
195
|
-
|
196
|
-
def _get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
197
|
-
"""Get function signature while evaluating forward references"""
|
198
|
-
signature = inspect.signature(call)
|
199
|
-
globalns = getattr(call, "__globals__", {})
|
200
|
-
typed_params = [
|
201
|
-
inspect.Parameter(
|
202
|
-
name=param.name,
|
203
|
-
kind=param.kind,
|
204
|
-
default=param.default,
|
205
|
-
annotation=_get_typed_annotation(param.annotation, globalns),
|
206
|
-
)
|
207
|
-
for param in signature.parameters.values()
|
208
|
-
]
|
209
|
-
typed_signature = inspect.Signature(typed_params)
|
210
|
-
return typed_signature
|
@@ -1,43 +0,0 @@
|
|
1
|
-
"""Logging utilities for FastMCP."""
|
2
|
-
|
3
|
-
import logging
|
4
|
-
from typing import Literal
|
5
|
-
|
6
|
-
|
7
|
-
def get_logger(name: str) -> logging.Logger:
|
8
|
-
"""Get a logger nested under MCPnamespace.
|
9
|
-
|
10
|
-
Args:
|
11
|
-
name: the name of the logger, which will be prefixed with 'FastMCP.'
|
12
|
-
|
13
|
-
Returns:
|
14
|
-
a configured logger instance
|
15
|
-
"""
|
16
|
-
return logging.getLogger(name)
|
17
|
-
|
18
|
-
|
19
|
-
def configure_logging(
|
20
|
-
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
|
21
|
-
) -> None:
|
22
|
-
"""Configure logging for MCP.
|
23
|
-
|
24
|
-
Args:
|
25
|
-
level: the log level to use
|
26
|
-
"""
|
27
|
-
handlers = []
|
28
|
-
try:
|
29
|
-
from rich.console import Console
|
30
|
-
from rich.logging import RichHandler
|
31
|
-
|
32
|
-
handlers.append(RichHandler(console=Console(stderr=True), rich_tracebacks=True))
|
33
|
-
except ImportError:
|
34
|
-
pass
|
35
|
-
|
36
|
-
if not handlers:
|
37
|
-
handlers.append(logging.StreamHandler())
|
38
|
-
|
39
|
-
logging.basicConfig(
|
40
|
-
level=level,
|
41
|
-
format="%(message)s",
|
42
|
-
handlers=handlers,
|
43
|
-
)
|
@@ -1,54 +0,0 @@
|
|
1
|
-
"""Common types used across FastMCP."""
|
2
|
-
|
3
|
-
import base64
|
4
|
-
from pathlib import Path
|
5
|
-
|
6
|
-
from acp.types import ImageContent
|
7
|
-
|
8
|
-
|
9
|
-
class Image:
|
10
|
-
"""Helper class for returning images from tools."""
|
11
|
-
|
12
|
-
def __init__(
|
13
|
-
self,
|
14
|
-
path: str | Path | None = None,
|
15
|
-
data: bytes | None = None,
|
16
|
-
format: str | None = None,
|
17
|
-
):
|
18
|
-
if path is None and data is None:
|
19
|
-
raise ValueError("Either path or data must be provided")
|
20
|
-
if path is not None and data is not None:
|
21
|
-
raise ValueError("Only one of path or data can be provided")
|
22
|
-
|
23
|
-
self.path = Path(path) if path else None
|
24
|
-
self.data = data
|
25
|
-
self._format = format
|
26
|
-
self._mime_type = self._get_mime_type()
|
27
|
-
|
28
|
-
def _get_mime_type(self) -> str:
|
29
|
-
"""Get MIME type from format or guess from file extension."""
|
30
|
-
if self._format:
|
31
|
-
return f"image/{self._format.lower()}"
|
32
|
-
|
33
|
-
if self.path:
|
34
|
-
suffix = self.path.suffix.lower()
|
35
|
-
return {
|
36
|
-
".png": "image/png",
|
37
|
-
".jpg": "image/jpeg",
|
38
|
-
".jpeg": "image/jpeg",
|
39
|
-
".gif": "image/gif",
|
40
|
-
".webp": "image/webp",
|
41
|
-
}.get(suffix, "application/octet-stream")
|
42
|
-
return "image/png" # default for raw binary data
|
43
|
-
|
44
|
-
def to_image_content(self) -> ImageContent:
|
45
|
-
"""Convert to MCP ImageContent."""
|
46
|
-
if self.path:
|
47
|
-
with open(self.path, "rb") as f:
|
48
|
-
data = base64.b64encode(f.read()).decode()
|
49
|
-
elif self.data is not None:
|
50
|
-
data = base64.b64encode(self.data).decode()
|
51
|
-
else:
|
52
|
-
raise ValueError("No image data available")
|
53
|
-
|
54
|
-
return ImageContent(type="image", data=data, mimeType=self._mime_type)
|
acp/server/lowlevel/__init__.py
DELETED