Skip to content

Commit f92d3fd

Browse files
Winston-503ethancjacksonaflament
authored
Feature AlphaSwarmTool & docs (#75)
* Add AlphaSwarmBaseTool * Clean up tools * Add __all__ to exchanges/__init__ * Make override optional * Add tests * From AlphaSwarmBaseTool to AlphaSwarmTool * From Tool to AlphaSwarmTool in examples * From dataclass to BaseModel * Clean up * Fix to_smolagents * Fix merging * Parse inputs_descriptions, introduce adapter class, more tests * From inputs to forward docstrings * Merging * Merging - update forecasting example * Use dict comprehension * Address comments * Rename into Base abc class * Improve docstrings * Add docs * Update tools.md * Tweak docs * Add services.md docs * Update docs/tools.md Co-authored-by: Arnaud Flament <[email protected]> * Update docs/tools.md Co-authored-by: Arnaud Flament <[email protected]> * Update docs/tools.md Co-authored-by: Arnaud Flament <[email protected]> * Update docs/tools.md Co-authored-by: Arnaud Flament <[email protected]> * Update docs/tools.md Co-authored-by: Arnaud Flament <[email protected]> * Update docs/services.md Co-authored-by: Arnaud Flament <[email protected]> * Update docs/services.md Co-authored-by: Arnaud Flament <[email protected]> * Update docs/services.md Co-authored-by: Arnaud Flament <[email protected]> * Update docs/services.md Co-authored-by: Arnaud Flament <[email protected]> * Update docs/tools.md Co-authored-by: Arnaud Flament <[email protected]> * Improve tool docs * Tweak examples definition * Merging * Clean up * Test hotfix --------- Co-authored-by: ethancjackson <[email protected]> Co-authored-by: Arnaud Flament <[email protected]>
1 parent 343ea89 commit f92d3fd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1078
-583
lines changed

CONTRIBUTING.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ Thank you for your interest in contributing to AlphaSwarm! This guide focuses on
66

77
Currently, we are accepting contributions in the following areas:
88

9-
- **Tools**: Located in `alphaswarm/tools/`
9+
- **Tools**: Located in `alphaswarm/tools/`, see [this guide](docs/tools.md) for more details
1010
- Utility functions and classes
1111
- Metrics collection and analysis
1212
- Helper tools and scripts
1313

14-
- **Services**: Located in `alphaswarm/services/`
14+
- **Services**: Located in `alphaswarm/services/`, see [this guide](docs/services.md) for more details
1515
- Service implementations
1616
- Client libraries
1717
- Service utilities
@@ -42,10 +42,11 @@ Currently, we are accepting contributions in the following areas:
4242
1. Update documentation if needed
4343
2. Add tests for new functionality
4444
3. Ensure the test suite passes
45-
4. Update the README.md if necessary
46-
5. Submit the pull request with a clear description of changes
47-
6. The pull request should pass all linters and static code checks [see](https://github.com/chain-ml/alphaswarm?tab=readme-ov-file#code-quality)
48-
7. The pull request needs to be reviewed and accepted by the core team
45+
4. Update the `README.md` and `.env.example` if necessary
46+
5. If you're introducing a new package, create an `__init__.py` file with appropriate imports
47+
6. Submit the pull request with a clear description of changes
48+
7. The pull request should pass all linters and static code checks, [see](https://github.com/chain-ml/alphaswarm?tab=readme-ov-file#code-quality)
49+
8. The pull request needs to be reviewed and accepted by the core team
4950

5051
## Questions or Need Help?
5152

alphaswarm/agent/agent.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
from datetime import datetime
33
from typing import Optional, Sequence
44

5-
from smolagents import CODE_SYSTEM_PROMPT, CodeAgent, LiteLLMModel, Tool
5+
from alphaswarm.core.tool import AlphaSwarmToolBase
6+
from alphaswarm.core.tool.tool import AlphaSwarmToSmolAgentsToolAdapter
7+
from smolagents import CODE_SYSTEM_PROMPT, CodeAgent, LiteLLMModel
68

79

810
class AlphaSwarmAgent:
911

1012
def __init__(
1113
self,
12-
tools: Sequence[Tool],
14+
tools: Sequence[AlphaSwarmToolBase],
1315
model_id: str = "anthropic/claude-3-5-sonnet-20241022",
1416
system_prompt: Optional[str] = None,
1517
hints: Optional[str] = None,
@@ -28,7 +30,7 @@ def __init__(
2830
system_prompt = system_prompt + "\n" + hints if hints else system_prompt
2931

3032
self._agent = CodeAgent(
31-
tools=list(tools),
33+
tools=[AlphaSwarmToSmolAgentsToolAdapter.adapt(tool) for tool in tools],
3234
model=LiteLLMModel(model_id=model_id),
3335
system_prompt=system_prompt,
3436
additional_authorized_imports=["json", "decimal"],

alphaswarm/core/tool/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .tool import AlphaSwarmToolBase
2+
3+
__all__ = ["AlphaSwarmToolBase"]

alphaswarm/core/tool/tool.py

+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import abc
2+
import inspect
3+
from textwrap import dedent
4+
from typing import Any, Dict, Optional, Sequence, Type, Union, get_args, get_origin, get_type_hints
5+
6+
from pydantic import BaseModel
7+
from smolagents import Tool
8+
9+
10+
class AlphaSwarmToolBase(abc.ABC):
11+
"""
12+
An AlphaSwarm Tool being used by AlphaSwarm Agents.
13+
"""
14+
15+
name: str
16+
"""
17+
The name of the tool.
18+
Will be automatically set to class name if not provided.
19+
"""
20+
21+
description: str
22+
"""
23+
The description of the tool, automatically set to docstring of the class if not provided.
24+
Will be automatically extended with the output type description if the forward() method returns a BaseModel
25+
and examples if any are provided.
26+
"""
27+
28+
examples: Sequence[str]
29+
"""
30+
Usage examples for the tool, could be any of: "when" to use the tool, "how" to use it, "what" to expect from it.
31+
Treat it as additional hints passed to the agent through the tool description.
32+
"""
33+
34+
inputs_descriptions: Dict[str, str]
35+
"""
36+
Mapping of forward() parameter names to their descriptions.
37+
Will be derived from the forward() method docstring if not provided.
38+
"""
39+
40+
output_type: Type
41+
"""forward() return type. Will derived from the function signature if not provided."""
42+
43+
def __init_subclass__(cls, **kwargs: Any) -> None:
44+
super().__init_subclass__(**kwargs)
45+
46+
cls.name = cls._construct_name()
47+
cls.inputs_descriptions = cls._construct_inputs_descriptions()
48+
cls.output_type = cls._construct_output_type()
49+
cls.description = cls._construct_description()
50+
51+
@abc.abstractmethod
52+
def forward(self, *args: Any, **kwargs: Any) -> Any:
53+
"""Execute the tool's core functionality.
54+
55+
This method must be implemented by subclasses to define the tool's behavior.
56+
Type hints and docstring are required and used to generate tool metadata.
57+
Arguments should be documented in the following format:
58+
59+
Args:
60+
param_name: Description of the first parameter
61+
param_name2: Description of the second parameter
62+
"""
63+
pass
64+
65+
@classmethod
66+
def _construct_name(cls) -> str:
67+
"""Construct the name of the tool - returns name attribute if provided, otherwise class name."""
68+
if "name" in cls.__dict__:
69+
return cls.name
70+
return cls.__name__
71+
72+
@classmethod
73+
def _construct_description(cls) -> str:
74+
"""
75+
Construct the full description of the tool, combining base description, output type description and examples.
76+
"""
77+
description_parts = [cls._get_base_description()]
78+
79+
output_type_description = cls._get_output_type_description()
80+
if output_type_description is not None:
81+
description_parts.append(output_type_description)
82+
if "examples" in cls.__dict__ and len(cls.examples) > 0:
83+
description_parts.append("\n".join(cls.examples))
84+
85+
return "\n\n".join(description_parts).strip()
86+
87+
@classmethod
88+
def _get_base_description(cls) -> str:
89+
"""Get the base description of the tool - returns description attribute if provided, otherwise docstring."""
90+
if "description" in cls.__dict__:
91+
return cls.description
92+
if cls.__doc__ is not None:
93+
return dedent(cls.__doc__).strip()
94+
95+
raise ValueError("Description of the tool must be provided either as a class attribute or docstring")
96+
97+
@classmethod
98+
def _get_output_type_description(cls) -> Optional[str]:
99+
"""Get a description of the return type schema when forward() returns a BaseModel."""
100+
101+
if issubclass(cls.output_type, BaseModel):
102+
# could add additional hints after the schema for AlphaSwarmToolInput class? or object docstring?
103+
return (
104+
f"Returns a {cls.output_type.__name__} object with the following schema:\n\n"
105+
f"{cls.output_type.model_json_schema()}"
106+
)
107+
108+
return None
109+
110+
@classmethod
111+
def _construct_inputs_descriptions(cls) -> Dict[str, str]:
112+
"""
113+
Construct the inputs descriptions
114+
Returns inputs_descriptions attribute if provided, otherwise extract from forward() method docstring.
115+
"""
116+
if "inputs_descriptions" in cls.__dict__:
117+
return cls.inputs_descriptions
118+
119+
forward_signature = inspect.signature(cls.forward)
120+
params = [param for param in forward_signature.parameters.keys() if param != "self"]
121+
122+
if not params:
123+
return {}
124+
125+
hints = get_type_hints(cls.forward)
126+
params_hints = {param: t for param, t in hints.items() if param != "return"}
127+
128+
missing_hints = [param for param in params if param not in params_hints]
129+
if missing_hints:
130+
raise ValueError(f"Missing type hints for forward() method parameters: {', '.join(missing_hints)}")
131+
132+
docstring = cls.forward.__doc__
133+
if not docstring:
134+
raise ValueError("Missing docstring for the forward() method. Must contain parameters descriptions.")
135+
136+
docstring = dedent(docstring).strip()
137+
lines = docstring.splitlines()
138+
139+
# find the Args/Parameters section
140+
section_start = None
141+
for i, line in enumerate(lines):
142+
if line.strip() in ("Args:", "Parameters:"):
143+
section_start = i + 1
144+
break
145+
146+
if section_start is None:
147+
raise ValueError("Missing Args/Parameters section in the forward() method docstring.")
148+
149+
inputs_descriptions: Dict[str, str] = {}
150+
151+
for line in lines[section_start:]:
152+
stripped_line = line.strip()
153+
154+
# expect each parameter line to follow: <param>: <description>
155+
if ":" not in stripped_line:
156+
continue
157+
158+
param_name, description = stripped_line.split(":", 1)
159+
param_name = param_name.strip()
160+
description = description.strip()
161+
inputs_descriptions[param_name] = description
162+
163+
missing_descriptions = [param for param in params if param not in inputs_descriptions]
164+
if missing_descriptions:
165+
raise ValueError(f"Missing description for parameters: {', '.join(missing_descriptions)}")
166+
167+
return inputs_descriptions
168+
169+
@classmethod
170+
def _construct_output_type(cls) -> Type:
171+
"""
172+
Construct the output type
173+
Returns output_type attribute if provided, otherwise forward() return type from type hints.
174+
"""
175+
if "output_type" in cls.__dict__:
176+
return cls.output_type
177+
178+
hints = get_type_hints(cls.forward)
179+
output_type = hints.get("return")
180+
if output_type is None:
181+
raise ValueError("Missing return type hint for the forward() method")
182+
183+
if not isinstance(output_type, type):
184+
raise RuntimeError("forward() output type hint is not a type")
185+
186+
return output_type
187+
188+
189+
class AlphaSwarmToSmolAgentsToolAdapter:
190+
"""Adapter class to convert AlphaSwarmToolBase instances to smolagents Tool instances."""
191+
192+
@classmethod
193+
def adapt(cls, alphaswarm_tool: AlphaSwarmToolBase) -> Tool:
194+
tool = Tool()
195+
196+
tool.name = alphaswarm_tool.name
197+
tool.description = alphaswarm_tool.description
198+
tool.inputs = cls._construct_smolagents_inputs(alphaswarm_tool)
199+
tool.output_type = cls._get_smolagents_type(alphaswarm_tool.output_type)
200+
tool.forward = alphaswarm_tool.forward
201+
202+
return tool
203+
204+
@classmethod
205+
def _construct_smolagents_inputs(cls, alphaswarm_tool: AlphaSwarmToolBase) -> Dict[str, Any]:
206+
hints = get_type_hints(alphaswarm_tool.forward)
207+
208+
inputs = {
209+
name: {"description": description, "type": cls._get_smolagents_type(hints[name])}
210+
for name, description in alphaswarm_tool.inputs_descriptions.items()
211+
}
212+
return inputs
213+
214+
@staticmethod
215+
def _get_smolagents_type(t: Type) -> str:
216+
types_to_smolagents_types = {
217+
str: "string",
218+
bool: "boolean",
219+
int: "integer",
220+
float: "number",
221+
type(None): "null",
222+
list: "array",
223+
}
224+
225+
# handling Optional[type]
226+
origin = get_origin(t)
227+
if origin is Union:
228+
args = get_args(t)
229+
non_none_args = [arg for arg in args if arg is not type(None)]
230+
if len(non_none_args) == 1:
231+
return types_to_smolagents_types.get(non_none_args[0], "object")
232+
233+
return types_to_smolagents_types.get(t, "object")

alphaswarm/services/alchemy/alchemy_client.py

+7-14
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,32 @@
99

1010
import requests
1111
from alphaswarm.services.api_exception import ApiException
12-
from pydantic import Field, field_validator
13-
from pydantic.dataclasses import dataclass
12+
from pydantic import BaseModel, Field, field_validator
1413

1514
logger = logging.getLogger(__name__)
1615

1716

18-
@dataclass
19-
class HistoricalPrice:
17+
class HistoricalPrice(BaseModel):
2018
value: Decimal
2119
timestamp: datetime
2220

2321

24-
@dataclass
25-
class HistoricalPriceBySymbol:
22+
class HistoricalPriceBySymbol(BaseModel):
2623
symbol: str
2724
data: List[HistoricalPrice]
2825

2926

30-
@dataclass
31-
class HistoricalPriceByAddress:
27+
class HistoricalPriceByAddress(BaseModel):
3228
address: str
3329
network: str
3430
data: List[HistoricalPrice]
3531

3632

37-
@dataclass
38-
class Metadata:
33+
class Metadata(BaseModel):
3934
block_timestamp: Annotated[str, Field(alias="blockTimestamp")]
4035

4136

42-
@dataclass
43-
class Transfer:
37+
class Transfer(BaseModel):
4438
"""Represents a token transfer transaction.
4539
4640
A Transfer object captures details about a single token transfer on the blockchain,
@@ -82,8 +76,7 @@ def convert_to_decimal(cls, value: str | int | float | Decimal) -> Decimal:
8276
return Decimal(str(value))
8377

8478

85-
@dataclass
86-
class Balance:
79+
class Balance(BaseModel):
8780
contract_address: Annotated[str, Field(validation_alias="contractAddress")]
8881
value: Annotated[Decimal, Field(validation_alias="tokenBalance", default=Decimal(0))]
8982
error: Annotated[Optional[str], Field(default=None)]

alphaswarm/services/cookiefun/cookiefun_client.py

+5-10
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import requests
77
from alphaswarm.config import Config
88
from alphaswarm.services.api_exception import ApiException
9-
from pydantic import Field
10-
from pydantic.dataclasses import dataclass
9+
from pydantic import BaseModel, Field
1110

1211
# Set up logging
1312
logger = logging.getLogger(__name__)
@@ -18,23 +17,20 @@ class Interval(str, Enum):
1817
SEVEN_DAYS = "_7Days"
1918

2019

21-
@dataclass
22-
class Contract:
20+
class Contract(BaseModel):
2321
chain: int = Field(default=0)
2422
contract_address: str = Field(default="", alias="contractAddress")
2523

2624

27-
@dataclass
28-
class Tweet:
25+
class Tweet(BaseModel):
2926
tweet_url: str = Field(default="", alias="tweetUrl")
3027
tweet_author_profile_image_url: str = Field(default="", alias="tweetAuthorProfileImageUrl")
3128
tweet_author_display_name: str = Field(default="", alias="tweetAuthorDisplayName")
3229
smart_engagement_points: int = Field(default=0, alias="smartEngagementPoints")
3330
impressions_count: int = Field(default=0, alias="impressionsCount")
3431

3532

36-
@dataclass
37-
class AgentMetrics:
33+
class AgentMetrics(BaseModel):
3834
contracts: List[Contract] = Field(default_factory=list)
3935
mindshare: float = Field(default=0.0)
4036
price: float = Field(default=0.0)
@@ -58,8 +54,7 @@ class AgentMetrics:
5854
top_tweets: List[Tweet] = Field(default_factory=list, alias="topTweets")
5955

6056

61-
@dataclass
62-
class PagedAgentsResponse:
57+
class PagedAgentsResponse(BaseModel):
6358
"""Response from the paged agents endpoint"""
6459

6560
data: List[AgentMetrics] = Field(default_factory=list)

0 commit comments

Comments
 (0)