Skip to content

Commit 51e6fe5

Browse files
authored
feat(agent-framework): introduced @adk.tool decorator to register tools declaratively (#79)
2 parents 8bbef8d + 71e02e2 commit 51e6fe5

File tree

10 files changed

+1158
-40
lines changed

10 files changed

+1158
-40
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ GCP__TAGS=flare-ai,http-server,https-server
4343

4444
# API key for using Google Gemini (https://aistudio.google.com/app/apikey).
4545
AGENT__GEMINI_API_KEY="your_api_key"
46+
GOOGLE_API_KEY="your_api_key"
4647

4748
# Gemini model to use (e.g. gemini-2.5-flash, gemini-2.5-pro)
4849
# AGENT__GEMINI_MODEL="gemini-2.5-flash"

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ dependencies = [
2929
"pydantic-ai>=0.0.46",
3030
"structlog>=25.2.0",
3131
"tenacity>=8.2.3,<9.0.0",
32-
"web3>=7.10.0"
32+
"web3>=7.10.0",
33+
"pillow>=11.3.0",
34+
"pymupdf>=1.26.1",
35+
"pytesseract>=0.3.13",
36+
"google-adk>=1.8.0"
3337
]
3438

3539
[project.urls]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# flare_ai_kit/adk/__init__.py
2+
3+
from ..tool import tool # pyright: ignore[reportUnknownvariableType]
4+
5+
# This exposes `adk.tool` to users who import `adk`
6+
__all__ = ["tool"]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# agent/adk_agent.py
2+
from google.adk.agents import Agent
3+
4+
from flare_ai_kit.agent.settings import AgentSettings
5+
from flare_ai_kit.agent.tool import TOOL_REGISTRY
6+
7+
settings = AgentSettings()
8+
9+
agent = Agent(
10+
name="Flare ADK Agent",
11+
model=settings.gemini_model, # unwrap SecretStr
12+
tools=TOOL_REGISTRY,
13+
)
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
from eth_typing import ChecksumAddress
2+
from httpx import HTTPStatusError, RequestError, TimeoutException
3+
from web3.contract.async_contract import AsyncContractFunction
4+
from web3.types import TxParams
5+
6+
from flare_ai_kit.agent import adk
7+
from flare_ai_kit.common import AbiError, ExplorerError
8+
9+
# --- Flare Network ---
10+
11+
12+
@adk.tool
13+
async def check_balance(address: str) -> float:
14+
"""
15+
Check the balance of a given Flare address in FLR.
16+
"""
17+
from flare_ai_kit.ecosystem.flare import Flare
18+
from flare_ai_kit.ecosystem.settings import EcosystemSettings
19+
20+
settings = EcosystemSettings()
21+
flare = Flare(settings)
22+
return await flare.check_balance(address)
23+
24+
25+
@adk.tool
26+
async def check_connection() -> bool:
27+
"""
28+
Check the connection status to the configured RPC endpoint.
29+
30+
Returns:
31+
True if connected, False otherwise.
32+
33+
"""
34+
from flare_ai_kit.ecosystem.flare import Flare
35+
from flare_ai_kit.ecosystem.settings import EcosystemSettings
36+
37+
settings = EcosystemSettings()
38+
flare = Flare(settings)
39+
return await flare.check_connection()
40+
41+
42+
@adk.tool
43+
async def build_transaction(
44+
function_call: AsyncContractFunction, from_addr: ChecksumAddress
45+
) -> TxParams | None:
46+
"""Builds a transaction with dynamic gas and nonce parameters."""
47+
from flare_ai_kit.ecosystem.flare import Flare
48+
from flare_ai_kit.ecosystem.settings import EcosystemSettings
49+
50+
settings = EcosystemSettings()
51+
flare = Flare(settings)
52+
return await flare.build_transaction(function_call, from_addr)
53+
54+
55+
@adk.tool
56+
async def sign_and_send_transaction(tx: TxParams) -> str | None:
57+
"""
58+
Sign and send a transaction to the network.
59+
60+
Args:
61+
tx (TxParams): Transaction parameters to be sent
62+
63+
Returns:
64+
str: Transaction hash of the sent transaction
65+
66+
Raises:
67+
ValueError: If account is not initialized
68+
69+
"""
70+
from flare_ai_kit.ecosystem.flare import Flare
71+
from flare_ai_kit.ecosystem.settings import EcosystemSettings
72+
73+
settings = EcosystemSettings()
74+
flare = Flare(settings)
75+
return await flare.sign_and_send_transaction(tx)
76+
77+
78+
@adk.tool
79+
async def create_send_flr_tx(
80+
from_address: str, to_address: str, amount: float
81+
) -> TxParams:
82+
"""
83+
Create a transaction to send FLR tokens.
84+
85+
Args:
86+
from_address (str): Sender address
87+
to_address (str): Recipient address
88+
amount (float): Amount of FLR to send
89+
90+
Returns:
91+
TxParams: Transaction parameters for sending FLR
92+
93+
Raises:
94+
ValueError: If account does not exist
95+
96+
"""
97+
from flare_ai_kit.ecosystem.flare import Flare
98+
from flare_ai_kit.ecosystem.settings import EcosystemSettings
99+
100+
settings = EcosystemSettings()
101+
flare = Flare(settings)
102+
return await flare.create_send_flr_tx(from_address, to_address, amount)
103+
104+
105+
# --- FTSO Protocol ---
106+
107+
108+
@adk.tool
109+
async def get_ftso_latest_price(feed_name: str) -> float:
110+
"""
111+
Retrieves the latest price for a single feed.
112+
113+
Args:
114+
feed_name: The human-readable feed name (e.g., "BTC/USD").
115+
category: The feed category (default: CRYPTO i.e. "01").
116+
117+
Returns:
118+
The latest price as a float, adjusted for decimals.
119+
Returns 0.0 if the price or decimals returned by the contract are zero,
120+
which might indicate an invalid or unprovided feed.
121+
122+
Raises:
123+
FtsoV2Error: If the category is invalid, feed name cannot be converted
124+
or the contract call fails.
125+
126+
"""
127+
from flare_ai_kit.ecosystem.protocols.ftsov2 import FtsoV2
128+
from flare_ai_kit.ecosystem.settings import EcosystemSettings
129+
130+
settings = EcosystemSettings()
131+
ftso = await FtsoV2.create(settings)
132+
if not ftso:
133+
raise ValueError("FtsoV2 instance not fully initialized. Use FtsoV2.create().")
134+
135+
return await ftso.get_latest_price(feed_name)
136+
137+
138+
@adk.tool
139+
async def get_ftso_latest_prices(feed_names: list[str]) -> list[float]:
140+
"""
141+
Retrieves the latest prices for multiple feeds within the same category.
142+
143+
Args:
144+
feed_names: A list of human-readable feed names.
145+
category: The feed category for all requested feeds (default: "01").
146+
147+
Returns:
148+
A list of prices as floats, corresponding to the order of `feed_names`.
149+
Individual prices will be 0.0 if the contract returned zero values.
150+
151+
Raises:
152+
FtsoV2Error: If the category is invalid, feed name cannot be converted
153+
or the contract call fails.
154+
155+
"""
156+
from flare_ai_kit.ecosystem.protocols.ftsov2 import FtsoV2
157+
from flare_ai_kit.ecosystem.settings import EcosystemSettings
158+
159+
settings = EcosystemSettings()
160+
ftso = await FtsoV2.create(settings)
161+
if not ftso:
162+
raise ValueError("FtsoV2 instance not fully initialized. Use FtsoV2.create().")
163+
164+
return await ftso.get_latest_prices(feed_names)
165+
166+
167+
# --- Explorer ---
168+
169+
170+
@adk.tool
171+
async def get_contract_abi(contract_address: str) -> list[dict[str, str]]:
172+
"""
173+
Asynchronously get the ABI for a contract from the Chain Explorer API.
174+
175+
Args:
176+
contract_address: Address of the contract.
177+
178+
Returns:
179+
list[dict]: Contract ABI parsed from the JSON string response.
180+
181+
Raises:
182+
ValueError: If the ABI string in the response is not valid JSON
183+
or if the underlying API request fails.
184+
(Exceptions from _get): RequestError, TimeoutException, HTTPStatusError
185+
186+
"""
187+
from flare_ai_kit.ecosystem.explorer import BlockExplorer
188+
from flare_ai_kit.ecosystem.settings import EcosystemSettings
189+
190+
settings = EcosystemSettings()
191+
explorer = BlockExplorer(settings)
192+
try:
193+
async with explorer:
194+
return await explorer.get_contract_abi(contract_address)
195+
except (HTTPStatusError, RequestError, TimeoutException) as e:
196+
raise ExplorerError(f"Failed to fetch contract ABI: {e}")
197+
except AbiError as e:
198+
raise ValueError(f"Invalid ABI response for contract {contract_address}: {e}")
199+
200+
201+
# --- Social: X (Twitter) ---
202+
203+
204+
@adk.tool
205+
async def post_to_x(content: str) -> bool:
206+
"""Posts a message to X (Twitter)."""
207+
from flare_ai_kit.social.settings import SocialSettings
208+
from flare_ai_kit.social.x import XClient
209+
210+
settings = SocialSettings() # type: ignore[call-arg]
211+
x_client = XClient(settings)
212+
if not x_client.is_configured:
213+
raise ValueError(
214+
"XClient is not configured. Ensure API keys are set in the environment."
215+
)
216+
217+
return await x_client.post_tweet(content)
218+
219+
220+
# --- Social: Telegram ---
221+
222+
223+
@adk.tool
224+
async def send_telegram_message(chat_id: str, message: str) -> bool:
225+
"""
226+
Sends a message to a Telegram chat.
227+
228+
This is an asynchronous operation.
229+
230+
Args:
231+
chat_id: The unique identifier for the target chat
232+
(e.g., '@channelname' or a user ID).
233+
text: The text of the message to send.
234+
235+
Returns:
236+
True if the message was sent successfully, False otherwise.
237+
238+
"""
239+
from flare_ai_kit.social.settings import SocialSettings
240+
from flare_ai_kit.social.telegram import TelegramClient
241+
242+
settings = SocialSettings() # type: ignore[call-arg]
243+
telegram_client = TelegramClient(settings)
244+
if not telegram_client.is_configured:
245+
raise ValueError(
246+
"TelegramClient is not configured. Ensure API token is set in the environment."
247+
)
248+
249+
return await telegram_client.send_message(chat_id, message)

src/flare_ai_kit/agent/settings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class AgentSettings(BaseSettings):
1212
env_file=".env",
1313
extra="ignore",
1414
)
15-
gemini_api_key: SecretStr = Field(
16-
...,
15+
gemini_api_key: SecretStr | None = Field(
16+
default=None,
1717
description="API key for using Google Gemini (https://aistudio.google.com/app/apikey).",
1818
)
1919
gemini_model: str = Field(

src/flare_ai_kit/agent/tool.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import inspect
2+
from collections.abc import Callable
3+
from functools import wraps
4+
from typing import Any, TypeVar, cast
5+
6+
from google.adk.tools import FunctionTool, LongRunningFunctionTool
7+
8+
RT = TypeVar("RT") # Return type
9+
# ToolUnion = FunctionTool
10+
TOOL_REGISTRY: list[Any] = []
11+
12+
13+
def tool(func: Callable[..., RT]) -> Callable[..., RT]:
14+
"""
15+
Decorator to register a function as a Gemini-compatible ADK tool.
16+
Automatically wraps async functions using LongRunningFunctionTool,
17+
and sync functions using FunctionTool.
18+
"""
19+
is_async = inspect.iscoroutinefunction(func)
20+
21+
@wraps(func)
22+
def wrapper(*args: Any, **kwargs: Any) -> Any:
23+
print(f"[ADK TOOL] Calling {func.__name__} with args={args}, kwargs={kwargs}")
24+
return func(*args, **kwargs)
25+
26+
# Choose appropriate wrapper
27+
tool_obj = (
28+
LongRunningFunctionTool(func=func) if is_async else FunctionTool(func=func)
29+
)
30+
31+
TOOL_REGISTRY.append(tool_obj)
32+
return cast("Callable[..., RT]", wrapper)

src/flare_ai_kit/ecosystem/settings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ class EcosystemSettings(BaseSettings):
106106
description="dApp contract addresses on each supported network.",
107107
)
108108
da_layer_base_url: HttpUrl = Field(
109-
HttpUrl("https://flr-data-availability.flare.network/api/v1/"),
109+
default=HttpUrl("https://flr-data-availability.flare.network/api/v1/"),
110110
description="Flare Data Availability Layer API base URL.",
111111
)
112112
da_layer_api_key: SecretStr | None = Field(
113-
None,
113+
default=None,
114114
description="Optional API key for Flare Data Availability Layer.",
115115
)

0 commit comments

Comments
 (0)