Skip to content

Commit 25dc4a7

Browse files
feat: Python Farcaster Plugin (#221)
* feat: add python farcaster plugin Co-Authored-By: Agus Armellini Fischer <[email protected]> * docs: add README and pyproject.toml for farcaster plugin Co-Authored-By: Agus Armellini Fischer <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Agus Armellini Fischer <[email protected]>
1 parent 351f5a2 commit 25dc4a7

File tree

5 files changed

+197
-0
lines changed

5 files changed

+197
-0
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Goat Farcaster Plugin 🐐 - Python
2+
3+
Farcaster plugin for Goat. Allows you to create tools for interacting with the Farcaster social protocol through the Neynar API.
4+
5+
## Installation
6+
```bash
7+
pip install goat-sdk-plugin-farcaster
8+
```
9+
10+
## Setup
11+
12+
```python
13+
from goat_plugins.farcaster import farcaster
14+
15+
plugin = farcaster({
16+
"api_key": "your_neynar_api_key"
17+
})
18+
```
19+
20+
## Features
21+
22+
- Full Farcaster protocol support through Neynar API
23+
- Cast creation and interaction
24+
- Thread and conversation management
25+
- Search functionality
26+
- Authentication via Signer UUID
27+
- Proper error handling
28+
- Python async/await support
29+
- Type hints with Pydantic models
30+
31+
## API Reference
32+
33+
### Plugin Configuration
34+
35+
| Parameter | Type | Description |
36+
|-----------|------|-------------|
37+
| api_key | str | Your Neynar API key |
38+
| base_url | str | (Optional) Custom API base URL |
39+
40+
## Goat
41+
42+
<div align="center">
43+
Go out and eat some grass.
44+
45+
[Docs](https://ohmygoat.dev) | [Examples](https://github.com/goat-sdk/goat/tree/main/typescript/examples) | [Discord](https://discord.gg/goat-sdk)</div>
46+
47+
## Goat 🐐
48+
Goat 🐐 (Great Onchain Agent Toolkit) is an open-source library enabling AI agents to interact with blockchain protocols and smart contracts via their own wallets.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
from goat.classes.plugin_base import PluginBase
4+
from .service import FarcasterService
5+
6+
7+
@dataclass
8+
class FarcasterPluginOptions:
9+
api_key: str
10+
base_url: Optional[str] = None
11+
12+
13+
class FarcasterPlugin(PluginBase):
14+
def __init__(self, options: FarcasterPluginOptions):
15+
super().__init__("farcaster", [FarcasterService(options.api_key, options.base_url)])
16+
17+
def supports_chain(self, chain) -> bool:
18+
# farcaster is chain-agnostic
19+
return True
20+
21+
22+
def farcaster(options: FarcasterPluginOptions) -> FarcasterPlugin:
23+
return FarcasterPlugin(options)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from pydantic import BaseModel, Field
2+
from typing import Optional
3+
4+
5+
class GetCastParameters(BaseModel):
6+
identifier: str = Field(..., description="Cast URL or hash identifier")
7+
type: str = Field("hash", description="Type of identifier (url or hash)")
8+
9+
10+
class PublishCastParameters(BaseModel):
11+
signer_uuid: str = Field(..., description="Unique ID of the signer publishing the cast")
12+
text: str = Field(..., description="Contents of the cast")
13+
parent: Optional[str] = Field(None, description="Parent cast hash if this is a reply")
14+
channel_id: Optional[str] = Field(None, description="Channel ID if posting to a specific channel")
15+
16+
17+
class SearchCastsParameters(BaseModel):
18+
query: str = Field(..., description="Text query to find matching casts")
19+
limit: Optional[int] = Field(20, description="Max results to retrieve")
20+
21+
22+
class GetConversationParameters(BaseModel):
23+
identifier: str = Field(..., description="Cast URL or hash identifier")
24+
type: str = Field("hash", description="Type of identifier (url or hash)")
25+
reply_depth: Optional[int] = Field(2, description="Depth of replies to fetch (0-5)")
26+
limit: Optional[int] = Field(20, description="Max results in conversation")
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import aiohttp
2+
from goat.decorators.tool import Tool
3+
from .parameters import (
4+
GetCastParameters,
5+
PublishCastParameters,
6+
SearchCastsParameters,
7+
GetConversationParameters
8+
)
9+
10+
11+
class FarcasterService:
12+
def __init__(self, api_key: str, base_url: str = "https://api.neynar.com/v2/farcaster"):
13+
self.api_key = api_key
14+
self.base_url = base_url
15+
16+
@Tool({"description": "Get a cast by its URL or hash", "parameters_schema": GetCastParameters})
17+
async def get_cast(self, parameters: dict):
18+
url = f"{self.base_url}/cast?identifier={parameters['identifier']}&type={parameters['type']}"
19+
return await self._make_request("GET", url)
20+
21+
@Tool({"description": "Publish a new cast", "parameters_schema": PublishCastParameters})
22+
async def publish_cast(self, parameters: dict):
23+
url = f"{self.base_url}/cast"
24+
return await self._make_request("POST", url, json={
25+
"signer_uuid": parameters['signer_uuid'],
26+
"text": parameters['text'],
27+
"parent": parameters.get('parent'),
28+
"channel_id": parameters.get('channel_id'),
29+
})
30+
31+
@Tool({"description": "Search for casts", "parameters_schema": SearchCastsParameters})
32+
async def search_casts(self, parameters: dict):
33+
url = f"{self.base_url}/cast/search"
34+
return await self._make_request("GET", url, params={
35+
"q": parameters['query'],
36+
"limit": parameters.get('limit', 20)
37+
})
38+
39+
@Tool({"description": "Get a conversation by its URL or hash", "parameters_schema": GetConversationParameters})
40+
async def get_conversation(self, parameters: dict):
41+
url = f"{self.base_url}/cast/conversation"
42+
return await self._make_request("GET", url, params={
43+
"identifier": parameters['identifier'],
44+
"type": parameters['type'],
45+
"reply_depth": parameters.get('reply_depth', 2),
46+
"limit": parameters.get('limit', 20),
47+
})
48+
49+
async def _make_request(self, method, url, **kwargs):
50+
headers = kwargs.pop("headers", {})
51+
headers["x-api-key"] = self.api_key
52+
headers["content-type"] = "application/json"
53+
async with aiohttp.ClientSession() as session:
54+
async with session.request(method, url, headers=headers, **kwargs) as response:
55+
if not response.ok:
56+
raise Exception(f"HTTP error! status: {response.status}, text: {await response.text()}")
57+
return await response.json()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
[tool.poetry]
2+
name = "goat-sdk-plugin-farcaster"
3+
version = "0.1.1"
4+
description = "Goat plugin for Farcaster"
5+
authors = ["Andrea Villa <[email protected]>"]
6+
readme = "README.md"
7+
keywords = ["goat", "sdk", "web3", "agents", "ai"]
8+
homepage = "https://ohmygoat.dev/"
9+
repository = "https://github.com/goat-sdk/goat"
10+
packages = [
11+
{ include = "goat_plugins/farcaster" },
12+
]
13+
14+
[tool.poetry.dependencies]
15+
python = "^3.10"
16+
aiohttp = "^3.8.6"
17+
goat-sdk = "^0.1.1"
18+
19+
[tool.poetry.group.test.dependencies]
20+
pytest = "^8.3.4"
21+
pytest-asyncio = "^0.25.0"
22+
23+
[tool.poetry.urls]
24+
"Bug Tracker" = "https://github.com/goat-sdk/goat/issues"
25+
26+
[tool.pytest.ini_options]
27+
addopts = [
28+
"--import-mode=importlib",
29+
]
30+
pythonpath = "src"
31+
asyncio_default_fixture_loop_scope = "function"
32+
33+
[build-system]
34+
requires = ["poetry-core"]
35+
build-backend = "poetry.core.masonry.api"
36+
37+
[tool.poetry.group.dev.dependencies]
38+
ruff = "^0.8.6"
39+
goat-sdk = { path = "../../goat-sdk", develop = true }
40+
41+
[tool.ruff]
42+
line-length = 120
43+
target-version = "py312"

0 commit comments

Comments
 (0)