Compare commits

...

3 Commits

Author SHA1 Message Date
shokollm
70dfba2ffc Merge fixes from pr-58 2026-04-13 12:53:44 +00:00
shokollm
2b7f54703e refactor: use subprocess to call ave-cloud-skill CLI scripts
Instead of importing library functions directly, now calling the
ave_data_rest.py CLI script via subprocess. This follows the
recommended approach from the SKILL.md documentation.

Changes:
- Add _call_ave_script helper method for subprocess calls
- Update search_tokens, get_token, get_price, get_risk to use CLI
- Set AVE_USE_DOCKER=false to run scripts directly without Docker
- Remove direct imports of ave.http module
2026-04-13 10:24:42 +00:00
shokollm
99dded8d16 feat: add AVE Cloud Skills integration for conversational agent tools
- Add ave-cloud-skill as git submodule
- Create symlink for Python import at src/backend/app/ave
- Replace search_tokens with new library-based implementation
- Add get_token, get_price, get_risk tools using ave-cloud-skill library
- Update TOOLS array and SYSTEM_PROMPT_WITH_TOOLS

Implements issue #56
2026-04-13 09:55:04 +00:00

View File

@@ -163,28 +163,6 @@ TOOLS = [
}, },
}, },
}, },
{
"type": "function",
"function": {
"name": "get_trending",
"description": "Get trending tokens on a blockchain. Use when user asks what's trending, top tokens, or popular tokens right now.",
"parameters": {
"type": "object",
"properties": {
"chain": {
"type": "string",
"description": "Blockchain chain (default: bsc)",
"default": "bsc",
},
"limit": {
"type": "integer",
"description": "Number of trending tokens to return (default: 10, max: 50)",
"default": 10,
},
},
},
},
},
{ {
"type": "function", "type": "function",
"function": { "function": {
@@ -253,7 +231,6 @@ You have access to tools:
- get_token(address, chain): Get detailed information about a specific token. Use when user asks for token details. - get_token(address, chain): Get detailed information about a specific token. Use when user asks for token details.
- get_price(token_ids): Get current price(s) for tokens. Use when user asks for token price. - get_price(token_ids): Get current price(s) for tokens. Use when user asks for token price.
- get_risk(address, chain): Get risk analysis for a token. Use when user asks about token safety or honeypot analysis. - get_risk(address, chain): Get risk analysis for a token. Use when user asks about token safety or honeypot analysis.
- get_trending(chain, limit): Get trending tokens on a blockchain. Use when user asks what's trending, top tokens, or popular tokens.
- run_backtest(token_address, timeframe, start_date, end_date): Run a backtest on historical data. Returns performance metrics. Use when user asks to backtest or check historical performance. - run_backtest(token_address, timeframe, start_date, end_date): Run a backtest on historical data. Returns performance metrics. Use when user asks to backtest or check historical performance.
- manage_simulation(action, token_address, kline_interval): Manage trading simulations. Actions: 'start' (begin new), 'stop' (stop running), 'status' (check if running), 'results' (get current/latest results). - manage_simulation(action, token_address, kline_interval): Manage trading simulations. Actions: 'start' (begin new), 'stop' (stop running), 'status' (check if running), 'results' (get current/latest results).
@@ -318,11 +295,11 @@ class ConversationalAgent:
}, },
) )
result = resp.json() or {} result = resp.json()
# Extract thinking from reasoning_content # Extract thinking from reasoning_content
thinking = None thinking = None
if result.get("choices") and len(result.get("choices", [])) > 0: if "choices" in result and len(result["choices"]) > 0:
choice = result["choices"][0] choice = result["choices"][0]
if "message" in choice: if "message" in choice:
message = choice["message"] message = choice["message"]
@@ -354,12 +331,7 @@ class ConversationalAgent:
if code == 0: if code == 0:
try: try:
data = json.loads(output) data = json.loads(output)
# Handle both dict with 'tokens' key and direct list tokens = data.get("data", {}).get("tokens", [])
data_field = data.get("data", [])
if isinstance(data_field, list):
tokens = data_field
else:
tokens = data_field.get("tokens", [])
if tokens: if tokens:
token_list = "" token_list = ""
for t in tokens[:limit]: for t in tokens[:limit]:
@@ -370,11 +342,7 @@ class ConversationalAgent:
"token_price_change_24h", "N/A" "token_price_change_24h", "N/A"
) )
mc = t.get("market_cap", "N/A") mc = t.get("market_cap", "N/A")
try: token_list += f"- **{symbol}** ({name}): `{addr}` - MC: ${mc:,.0f} - 24h: {price_change}%\n"
mc_str = f"${float(mc):,.0f}"
except (ValueError, TypeError):
mc_str = str(mc)
token_list += f"- **{symbol}** ({name}): `{addr}` - MC: {mc_str} - 24h: {price_change}%\n"
response_text = f"Here are the search results for '{keyword}' on BSC:\n\n{token_list}\nWould you like me to set up a strategy for any of these?" response_text = f"Here are the search results for '{keyword}' on BSC:\n\n{token_list}\nWould you like me to set up a strategy for any of these?"
else: else:
response_text = f"No tokens found for '{keyword}'. Try a different keyword." response_text = f"No tokens found for '{keyword}'. Try a different keyword."
@@ -403,51 +371,24 @@ class ConversationalAgent:
if code == 0: if code == 0:
try: try:
data = json.loads(output) data = json.loads(output)
data_field = data.get("data") token_data = data.get("data", {})
# Handle both dict and list responses if token_data:
token_data = data_field if isinstance(data_field, dict) else {} symbol = token_data.get("symbol", "N/A")
# Token details may be nested in 'token' key name = token_data.get("name", "N/A")
token_info = token_data.get("token", token_data) price = token_data.get("price", "N/A")
# Check if token has valid symbol/name (not None, not 'N/A') mc = token_data.get("market_cap", "N/A")
symbol = token_info.get("symbol") or token_data.get("symbol") vol = token_data.get("volume_24h", "N/A")
name = token_info.get("name") or token_data.get("name") pairs = token_data.get("top_pairs", [])
if not symbol or symbol == "N/A" or not name or name == "N/A":
response_text = f"Token not found for {address}. Raw response: {output[:500]}"
else:
# Try different price field names
price = (token_info.get("current_price_usd")
or token_info.get("price_usd")
or token_info.get("price")
or token_data.get("price")
or "N/A")
mc = (token_info.get("market_cap")
or token_info.get("fdv")
or token_data.get("market_cap")
or "N/A")
vol = (token_info.get("tx_volume_u_24h")
or token_info.get("volume_24h")
or token_data.get("volume_24h")
or "N/A")
pairs = token_info.get("top_pairs") or token_data.get("top_pairs") or []
pairs_text = "" pairs_text = ""
if pairs: if pairs:
pairs_text = "\n**Top Pairs:**\n" pairs_text = "\n**Top Pairs:**\n"
for p in pairs[:3]: for p in pairs[:3]:
liq = p.get('liquidity', 'N/A') pairs_text += f"- {p.get('pair', 'N/A')}: ${p.get('liquidity', 'N/A'):,.0f} liquidity\n"
try: response_text = f"**{symbol}** ({name})\n\nPrice: ${price}\nMarket Cap: ${mc:,.0f}\n24h Volume: ${vol:,.0f}{pairs_text}"
liq_str = f"${float(liq):,.0f}" else:
except (ValueError, TypeError): response_text = (
liq_str = str(liq) f"Token not found: {address}"
pairs_text += f"- {p.get('pair', 'N/A')}: {liq_str} liquidity\n" )
try:
mc_str = f"${float(mc):,.0f}" if mc != "N/A" else "N/A"
except (ValueError, TypeError):
mc_str = str(mc)
try:
vol_str = f"${float(vol):,.0f}" if vol != "N/A" else "N/A"
except (ValueError, TypeError):
vol_str = str(vol)
response_text = f"**{symbol}** ({name})\n\nPrice: ${price}\nMarket Cap: {mc_str}\n24h Volume: {vol_str}{pairs_text}"
except json.JSONDecodeError: except json.JSONDecodeError:
response_text = "Failed to parse token data." response_text = "Failed to parse token data."
else: else:
@@ -477,9 +418,6 @@ class ConversationalAgent:
try: try:
data = json.loads(output) data = json.loads(output)
prices = data.get("data", {}) prices = data.get("data", {})
# Ensure prices is a dict
if not isinstance(prices, dict):
prices = {}
if prices: if prices:
price_text = "**Token Prices:**\n" price_text = "**Token Prices:**\n"
for ( for (
@@ -525,53 +463,27 @@ class ConversationalAgent:
if code == 0: if code == 0:
try: try:
data = json.loads(output) data = json.loads(output)
data_field = data.get("data") risk_data = data.get("data", {})
# Handle both dict and list responses
risk_data = data_field if isinstance(data_field, dict) else {}
if risk_data: if risk_data:
is_honeypot = risk_data.get( is_honeypot = risk_data.get(
"is_honeypot", "unknown" "is_honeypot", "unknown"
) )
buy_tax = risk_data.get("buy_tax", 0) buy_tax = risk_data.get("buy_tax", "N/A")
sell_tax = risk_data.get("sell_tax", 0) sell_tax = risk_data.get("sell_tax", "N/A")
status = risk_data.get("status", "unknown") status = risk_data.get("status", "unknown")
# Convert is_honeypot to string for comparison
if isinstance(is_honeypot, bool):
is_honeypot_str = str(is_honeypot).lower()
elif isinstance(is_honeypot, int):
if is_honeypot == 1:
is_honeypot_str = "true"
elif is_honeypot == 0:
is_honeypot_str = "false"
else:
is_honeypot_str = "unknown" # -1 or other means couldn't determine
else:
is_honeypot_str = str(is_honeypot).lower() if is_honeypot else "unknown"
# Format honeypot display value
if is_honeypot_str == "unknown":
honeypot_display = "Unknown (could not determine)"
else:
honeypot_display = is_honeypot_str
# Convert tax values to float for comparison
try:
buy_tax_val = float(buy_tax) if buy_tax not in (None, "N/A") else 0
except (ValueError, TypeError):
buy_tax_val = 0
try:
sell_tax_val = float(sell_tax) if sell_tax not in (None, "N/A") else 0
except (ValueError, TypeError):
sell_tax_val = 0
risk_text = ( risk_text = (
f"**Risk Analysis for {address}**\n\n" f"**Risk Analysis for {address}**\n\n"
) )
risk_text += f"- Status: {status}\n" risk_text += f"- Status: {status}\n"
risk_text += f"- Honeypot: {honeypot_display}\n" risk_text += f"- Honeypot: {is_honeypot}\n"
risk_text += f"- Buy Tax: {buy_tax}%\n" risk_text += f"- Buy Tax: {buy_tax}%\n"
risk_text += f"- Sell Tax: {sell_tax}%\n" risk_text += f"- Sell Tax: {sell_tax}%\n"
if is_honeypot_str == "true": if is_honeypot.lower() == "true":
risk_text += "\n⚠️ **Warning: This token appears to be a honeypot. Do not buy!**" risk_text += "\n⚠️ **Warning: This token appears to be a honeypot. Do not buy!**"
elif buy_tax_val > 10 or sell_tax_val > 10: elif (
float(buy_tax or 0) > 10
or float(sell_tax or 0) > 10
):
risk_text += "\n⚠️ **Warning: High tax detected. Trade with caution!**" risk_text += "\n⚠️ **Warning: High tax detected. Trade with caution!**"
else: else:
risk_text += "\n✅ This token appears safe to trade." risk_text += "\n✅ This token appears safe to trade."
@@ -593,49 +505,6 @@ class ConversationalAgent:
"success": True, "success": True,
} }
elif func_name == "get_trending":
chain = args.get("chain", "bsc")
limit = args.get("limit", 10)
code, output = self._call_ave_script(
"trending",
["--chain", chain, "--page-size", str(min(limit, 50))],
)
if code == 0:
try:
data = json.loads(output)
data_field = data.get("data")
# Handle both dict and list responses
tokens = data_field if isinstance(data_field, list) else data_field.get("tokens", [])
if tokens:
token_list = ""
for t in tokens[:limit]:
addr = t.get("token", "")
symbol = t.get("symbol", "")
name = t.get("name", "")
price_change = t.get("token_price_change_24h", "N/A")
mc = t.get("market_cap", "N/A")
try:
mc_str = f"${float(mc):,.0f}"
except (ValueError, TypeError):
mc_str = str(mc)
token_list += f"- **{symbol}** ({name}): `{addr}` - MC: {mc_str} - 24h: {price_change}%\n"
response_text = f"🔥 Trending tokens on {chain.upper()}:\n\n{token_list}\nWould you like me to set up a strategy for any of these?"
else:
response_text = f"No trending tokens found on {chain.upper()}."
except json.JSONDecodeError:
response_text = "Failed to parse trending data."
else:
response_text = f"Failed to get trending tokens: {output}"
return {
"response": response_text,
"thinking": thinking,
"strategy_updated": False,
"strategy_needs_confirmation": False,
"success": True,
}
elif func_name == "run_backtest": elif func_name == "run_backtest":
token_address = args.get("token_address") token_address = args.get("token_address")
timeframe = args.get("timeframe", "1d") timeframe = args.get("timeframe", "1d")
@@ -731,12 +600,7 @@ class ConversationalAgent:
if code == 0: if code == 0:
try: try:
data = json.loads(output) data = json.loads(output)
# Handle both dict with 'tokens' key and direct list tokens = data.get("data", {}).get("tokens", [])
data_field = data.get("data", [])
if isinstance(data_field, list):
tokens = data_field
else:
tokens = data_field.get("tokens", [])
if tokens: if tokens:
token_list = "" token_list = ""
for t in tokens[:limit]: for t in tokens[:limit]:
@@ -747,11 +611,7 @@ class ConversationalAgent:
"token_price_change_24h", "N/A" "token_price_change_24h", "N/A"
) )
mc = t.get("market_cap", "N/A") mc = t.get("market_cap", "N/A")
try: token_list += f"- **{symbol}** ({name}): `{addr}` - MC: ${mc:,.0f} - 24h: {price_change}%\n"
mc_str = f"${float(mc):,.0f}"
except (ValueError, TypeError):
mc_str = str(mc)
token_list += f"- **{symbol}** ({name}): `{addr}` - MC: {mc_str} - 24h: {price_change}%\n"
response_text = f"Here are the search results for '{keyword}' on BSC:\n\n{token_list}\nWould you like me to set up a strategy for any of these?" response_text = f"Here are the search results for '{keyword}' on BSC:\n\n{token_list}\nWould you like me to set up a strategy for any of these?"
else: else:
response_text = f"No tokens found for '{keyword}'. Try a different keyword." response_text = f"No tokens found for '{keyword}'. Try a different keyword."
@@ -775,7 +635,7 @@ class ConversationalAgent:
if strategy_update: if strategy_update:
# Extract token name from conditions # Extract token name from conditions
token_name = None token_name = None
for cond in (strategy_update.get("conditions") or []): for cond in strategy_update.get("conditions", []):
if not cond.get("token_address") and cond.get("token"): if not cond.get("token_address") and cond.get("token"):
token_name = cond.get("token") token_name = cond.get("token")
strategy_needs_confirmation = True strategy_needs_confirmation = True
@@ -790,12 +650,7 @@ class ConversationalAgent:
) )
if code == 0: if code == 0:
data = json.loads(output) data = json.loads(output)
# Handle both dict with 'tokens' key and direct list tokens = data.get("data", {}).get("tokens", [])
data_field = data.get("data", [])
if isinstance(data_field, list):
tokens = data_field
else:
tokens = data_field.get("tokens", [])
if tokens: if tokens:
token_search_results = [ token_search_results = [
{ {
@@ -1218,7 +1073,7 @@ Would you like me to adjust the strategy parameters based on these results?"""
settings = get_settings() settings = get_settings()
repo_root = os.path.dirname( repo_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))) os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
) )
ave_skill_path = os.path.join( ave_skill_path = os.path.join(
repo_root, "ave-cloud-skill", "scripts", "ave_data_rest.py" repo_root, "ave-cloud-skill", "scripts", "ave_data_rest.py"
@@ -1237,11 +1092,7 @@ Would you like me to adjust the strategy parameters based on these results?"""
env=env, env=env,
timeout=30, timeout=30,
) )
# Include stderr in output for debugging return result.returncode, result.stdout
output = result.stdout
if result.returncode != 0 and result.stderr:
output = f"{output}\n{result.stderr}".strip()
return result.returncode, output
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
return 1, "Error: Command timed out" return 1, "Error: Command timed out"
except Exception as e: except Exception as e: