feat: Add slash command help system for conversational interface
Implement slash command help system as described in issue #57: - Add Tool Registry in backend with metadata for all available tools - Add command parser for '/' prefix in ConversationalAgent - Add slash command handling functions: - '/' shows list of all available tools - '/help' shows general help about Randebu - '/<tool-name>' shows detailed help for specific tool - Update frontend ChatInterface to detect '/' and show formatted help dropdown - Add keyboard navigation (Arrow keys, Tab, Enter, Escape) for slash menu
This commit is contained in:
@@ -18,6 +18,169 @@ from ...core.config import get_settings
|
|||||||
from ...db.models import Bot, Simulation
|
from ...db.models import Bot, Simulation
|
||||||
|
|
||||||
|
|
||||||
|
TOOL_REGISTRY = {
|
||||||
|
"randebu": [
|
||||||
|
{
|
||||||
|
"name": "backtest",
|
||||||
|
"description": "Run strategy backtest",
|
||||||
|
"category": "Randebu Built-in",
|
||||||
|
"command": "/backtest",
|
||||||
|
"details": {
|
||||||
|
"description": "Run a backtest to evaluate how the current trading strategy would have performed historically.",
|
||||||
|
"usage": "/backtest [token_address] [--timeframe 1d|4h|1h|15m] [--start YYYY-MM-DD] [--end YYYY-MM-DD]",
|
||||||
|
"example": "Run a backtest on PEPE for the last 30 days",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "simulate",
|
||||||
|
"description": "Start/stop simulation",
|
||||||
|
"category": "Randebu Built-in",
|
||||||
|
"command": "/simulate",
|
||||||
|
"details": {
|
||||||
|
"description": "Start or stop trading simulations that run on real-time klines.",
|
||||||
|
"usage": "/simulate start|stop|status|results [token_address]",
|
||||||
|
"example": "Start a simulation on PEPE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "strategy",
|
||||||
|
"description": "View/update strategy",
|
||||||
|
"category": "Randebu Built-in",
|
||||||
|
"command": "/strategy",
|
||||||
|
"details": {
|
||||||
|
"description": "View your current trading strategy or update it with new parameters.",
|
||||||
|
"usage": "Describe your strategy in plain English, e.g., 'Buy PEPE when price drops 5%'",
|
||||||
|
"example": "Buy PEPE when it drops 10% within 1 hour",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"ave": [
|
||||||
|
{
|
||||||
|
"name": "search",
|
||||||
|
"description": "Token search",
|
||||||
|
"category": "AVE Cloud Skills",
|
||||||
|
"command": "/search",
|
||||||
|
"details": {
|
||||||
|
"description": "Find tokens by keyword, symbol, or contract address on BSC.",
|
||||||
|
"usage": "search <keyword> [--chain bsc] [--limit 20]",
|
||||||
|
"example": "search PEPE\nsearch 0x1234... --chain bsc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "risk",
|
||||||
|
"description": "Honeypot detection",
|
||||||
|
"category": "AVE Cloud Skills",
|
||||||
|
"command": "/risk",
|
||||||
|
"details": {
|
||||||
|
"description": "Get risk analysis for a token contract including honeypot assessment.",
|
||||||
|
"usage": "risk <token_address> [--chain bsc]",
|
||||||
|
"example": "risk 0x6982508145454Ce125dDE157d8d64a26D53f60a2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "token",
|
||||||
|
"description": "Token details",
|
||||||
|
"category": "AVE Cloud Skills",
|
||||||
|
"command": "/token",
|
||||||
|
"details": {
|
||||||
|
"description": "Get detailed information about a specific token including price, market cap, and pairs.",
|
||||||
|
"usage": "token <address> [--chain bsc]",
|
||||||
|
"example": "token 0x6982508145454Ce125dDE157d8d64a26D53f60a2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "price",
|
||||||
|
"description": "Batch prices",
|
||||||
|
"category": "AVE Cloud Skills",
|
||||||
|
"command": "/price",
|
||||||
|
"details": {
|
||||||
|
"description": "Get current price(s) for multiple tokens.",
|
||||||
|
"usage": "price <token_id>,<token_id>,... (e.g., PEPE-bsc,TRUMP-bsc)",
|
||||||
|
"example": "price PEPE-bsc,TRUMP-bsc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool_registry() -> Dict[str, Any]:
|
||||||
|
"""Return the tool registry for slash command help."""
|
||||||
|
return TOOL_REGISTRY
|
||||||
|
|
||||||
|
|
||||||
|
def format_tools_list() -> str:
|
||||||
|
"""Format the tool registry as a help message."""
|
||||||
|
message = "📋 Available Tools\n\n"
|
||||||
|
|
||||||
|
for category in ["randebu", "ave"]:
|
||||||
|
tools = TOOL_REGISTRY.get(category, [])
|
||||||
|
if category == "randebu":
|
||||||
|
message += "🤖 Randebu Built-in:\n"
|
||||||
|
else:
|
||||||
|
message += "☁️ AVE Cloud Skills:\n"
|
||||||
|
|
||||||
|
for tool in tools:
|
||||||
|
message += f" • {tool['command']} - {tool['description']}\n"
|
||||||
|
message += "\n"
|
||||||
|
|
||||||
|
message = (
|
||||||
|
message.rstrip() + "\n\nType /<tool-name> for detailed help on a specific tool."
|
||||||
|
)
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
def format_tool_help(tool_name: str) -> str:
|
||||||
|
"""Format detailed help for a specific tool."""
|
||||||
|
tool_name = tool_name.lstrip("/")
|
||||||
|
|
||||||
|
for category in ["randebu", "ave"]:
|
||||||
|
for tool in TOOL_REGISTRY.get(category, []):
|
||||||
|
if tool["name"].lower() == tool_name.lower():
|
||||||
|
cat_label = (
|
||||||
|
"Randebu Built-in" if category == "randebu" else "AVE Cloud Skill"
|
||||||
|
)
|
||||||
|
details = tool["details"]
|
||||||
|
message = (
|
||||||
|
f"🔍 {tool['command']} - {details['description']} ({cat_label})\n\n"
|
||||||
|
)
|
||||||
|
message += f"**Description:** {details['description']}\n"
|
||||||
|
message += f"**Usage:** `{details['usage']}`\n"
|
||||||
|
message += f"**Example:**\n```\n{details['example']}\n```"
|
||||||
|
return message
|
||||||
|
|
||||||
|
return f"Tool '{tool_name}' not found. Type / to see all available tools."
|
||||||
|
|
||||||
|
|
||||||
|
def format_general_help() -> str:
|
||||||
|
"""Format general help about Randebu."""
|
||||||
|
return """🤖 **Randebu - AI Trading Assistant**
|
||||||
|
|
||||||
|
Randebu is your AI trading assistant that helps you manage your trading bots on BSC (Binance Smart Chain).
|
||||||
|
|
||||||
|
**Getting Started:**
|
||||||
|
1. Create a bot on the dashboard
|
||||||
|
2. Describe your trading strategy in plain English
|
||||||
|
3. Run backtests to validate your strategy
|
||||||
|
4. Start simulations to see live trading
|
||||||
|
|
||||||
|
**Example Strategies:**
|
||||||
|
- "Buy PEPE when it drops 5%"
|
||||||
|
- "Sell if price rises 10% within 1 hour"
|
||||||
|
- "Buy when volume spikes by 200%"
|
||||||
|
|
||||||
|
**Slash Commands:**
|
||||||
|
- `/` - Show all available tools
|
||||||
|
- `/help` - Show this help message
|
||||||
|
- `/<tool-name>` - Get help on a specific tool
|
||||||
|
|
||||||
|
**Natural Language:**
|
||||||
|
You can also just describe what you want in natural language. For example:
|
||||||
|
- "What's the price of PEPE?"
|
||||||
|
- "Run a backtest on 0x... token"
|
||||||
|
- "Start a simulation on TRUMP"
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_PROMPT = """You are a helpful AI trading assistant named Randebu. You help users manage their trading bots.
|
SYSTEM_PROMPT = """You are a helpful AI trading assistant named Randebu. You help users manage their trading bots.
|
||||||
|
|
||||||
IMPORTANT CHAIN LIMITATION:
|
IMPORTANT CHAIN LIMITATION:
|
||||||
@@ -253,6 +416,51 @@ class ConversationalAgent:
|
|||||||
# Extended thinking endpoint
|
# Extended thinking endpoint
|
||||||
self.thinking_endpoint = "https://api.minimax.io/v1/text/chatcompletion_v2"
|
self.thinking_endpoint = "https://api.minimax.io/v1/text/chatcompletion_v2"
|
||||||
|
|
||||||
|
def _handle_slash_command(self, user_message: str) -> Dict[str, Any]:
|
||||||
|
"""Handle slash command help requests.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_message: The slash command message (e.g., '/', '/help', '/search')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with 'response', 'thinking', and other fields
|
||||||
|
"""
|
||||||
|
cmd = user_message.strip().lower()
|
||||||
|
|
||||||
|
if cmd == "/":
|
||||||
|
return {
|
||||||
|
"response": format_tools_list(),
|
||||||
|
"thinking": None,
|
||||||
|
"strategy_updated": False,
|
||||||
|
"strategy_needs_confirmation": False,
|
||||||
|
"success": True,
|
||||||
|
}
|
||||||
|
elif cmd == "/help":
|
||||||
|
return {
|
||||||
|
"response": format_general_help(),
|
||||||
|
"thinking": None,
|
||||||
|
"strategy_updated": False,
|
||||||
|
"strategy_needs_confirmation": False,
|
||||||
|
"success": True,
|
||||||
|
}
|
||||||
|
elif cmd.startswith("/"):
|
||||||
|
tool_name = cmd[1:]
|
||||||
|
return {
|
||||||
|
"response": format_tool_help(tool_name),
|
||||||
|
"thinking": None,
|
||||||
|
"strategy_updated": False,
|
||||||
|
"strategy_needs_confirmation": False,
|
||||||
|
"success": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"response": "Unknown command. Type / for available tools or /help for general help.",
|
||||||
|
"thinking": None,
|
||||||
|
"strategy_updated": False,
|
||||||
|
"strategy_needs_confirmation": False,
|
||||||
|
"success": True,
|
||||||
|
}
|
||||||
|
|
||||||
def chat(
|
def chat(
|
||||||
self, user_message: str, conversation_history: List[Dict] = None
|
self, user_message: str, conversation_history: List[Dict] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
@@ -266,6 +474,10 @@ class ConversationalAgent:
|
|||||||
Dict with 'response', 'thinking', and 'strategy_updated'
|
Dict with 'response', 'thinking', and 'strategy_updated'
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Handle slash commands
|
||||||
|
if user_message.startswith("/"):
|
||||||
|
return self._handle_slash_command(user_message)
|
||||||
|
|
||||||
# Build messages array with system prompt and conversation history
|
# Build messages array with system prompt and conversation history
|
||||||
messages = [{"role": "system", "content": SYSTEM_PROMPT_WITH_TOOLS}]
|
messages = [{"role": "system", "content": SYSTEM_PROMPT_WITH_TOOLS}]
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,34 @@
|
|||||||
import type { ChatMessage } from '$lib/stores/chatStore';
|
import type { ChatMessage } from '$lib/stores/chatStore';
|
||||||
import { parseMarkdown, parseInlineElements, type InlineSegment } from '$lib/utils/markdown';
|
import { parseMarkdown, parseInlineElements, type InlineSegment } from '$lib/utils/markdown';
|
||||||
|
|
||||||
|
interface ToolItem {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOLS: { category: string; label: string; tools: ToolItem[] }[] = [
|
||||||
|
{
|
||||||
|
category: 'randebu',
|
||||||
|
label: '🤖 Randebu Built-in',
|
||||||
|
tools: [
|
||||||
|
{ name: 'backtest', description: 'Run strategy backtest', command: '/backtest' },
|
||||||
|
{ name: 'simulate', description: 'Start/stop simulation', command: '/simulate' },
|
||||||
|
{ name: 'strategy', description: 'View/update strategy', command: '/strategy' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'ave',
|
||||||
|
label: '☁️ AVE Cloud Skills',
|
||||||
|
tools: [
|
||||||
|
{ name: 'search', description: 'Token search', command: '/search' },
|
||||||
|
{ name: 'risk', description: 'Honeypot detection', command: '/risk' },
|
||||||
|
{ name: 'token', description: 'Token details', command: '/token' },
|
||||||
|
{ name: 'price', description: 'Batch prices', command: '/price' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bot: Bot | null;
|
bot: Bot | null;
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
@@ -26,9 +54,14 @@
|
|||||||
let messageInput = $state('');
|
let messageInput = $state('');
|
||||||
let chatContainer: HTMLDivElement;
|
let chatContainer: HTMLDivElement;
|
||||||
let expandedThinking: Record<string, boolean> = $state({});
|
let expandedThinking: Record<string, boolean> = $state({});
|
||||||
|
let showSlashMenu = $state(false);
|
||||||
|
let slashMenuPosition = $state({ top: 0, left: 0 });
|
||||||
|
let filteredTools = $state<ToolItem[]>([]);
|
||||||
|
let selectedIndex = $state(0);
|
||||||
|
|
||||||
function handleSend() {
|
function handleSend() {
|
||||||
if (!messageInput.trim()) return;
|
if (!messageInput.trim()) return;
|
||||||
|
showSlashMenu = false;
|
||||||
onSendMessage(messageInput);
|
onSendMessage(messageInput);
|
||||||
messageInput = '';
|
messageInput = '';
|
||||||
}
|
}
|
||||||
@@ -36,7 +69,57 @@
|
|||||||
function handleKeydown(e: KeyboardEvent) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSend();
|
if (showSlashMenu && filteredTools.length > 0) {
|
||||||
|
selectTool(filteredTools[selectedIndex]);
|
||||||
|
} else {
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'ArrowDown' && showSlashMenu) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, filteredTools.length - 1);
|
||||||
|
} else if (e.key === 'ArrowUp' && showSlashMenu) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||||
|
} else if (e.key === 'Escape' && showSlashMenu) {
|
||||||
|
showSlashMenu = false;
|
||||||
|
} else if (e.key === 'Tab' && showSlashMenu && filteredTools.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectTool(filteredTools[selectedIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput(e: Event) {
|
||||||
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
const value = target.value;
|
||||||
|
messageInput = value;
|
||||||
|
|
||||||
|
if (value.startsWith('/')) {
|
||||||
|
const query = value.slice(1).toLowerCase();
|
||||||
|
filteredTools = TOOLS.flatMap(t => t.tools).filter(tool =>
|
||||||
|
tool.name.toLowerCase().startsWith(query) ||
|
||||||
|
tool.command.toLowerCase().startsWith(query)
|
||||||
|
);
|
||||||
|
selectedIndex = 0;
|
||||||
|
showSlashMenu = filteredTools.length > 0;
|
||||||
|
|
||||||
|
if (showSlashMenu) {
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
slashMenuPosition = {
|
||||||
|
top: rect.top - 10,
|
||||||
|
left: rect.left
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showSlashMenu = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTool(tool: ToolItem) {
|
||||||
|
messageInput = tool.command + ' ';
|
||||||
|
showSlashMenu = false;
|
||||||
|
const textarea = document.querySelector('.input-container textarea') as HTMLTextAreaElement;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +157,17 @@
|
|||||||
}
|
}
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(e: MouseEvent) {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (!target.closest('.slash-menu') && !target.closest('.input-container textarea')) {
|
||||||
|
showSlashMenu = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:click={handleClickOutside} />
|
||||||
|
|
||||||
<div class="chat-interface">
|
<div class="chat-interface">
|
||||||
{#if showBotSelector && availableBots.length > 0}
|
{#if showBotSelector && availableBots.length > 0}
|
||||||
<div class="bot-selector">
|
<div class="bot-selector">
|
||||||
@@ -215,10 +307,32 @@
|
|||||||
|
|
||||||
{#if bot}
|
{#if bot}
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
|
{#if showSlashMenu && filteredTools.length > 0}
|
||||||
|
<div class="slash-menu" style="top: {slashMenuPosition.top}px; left: {slashMenuPosition.left}px;">
|
||||||
|
<div class="slash-menu-header">Available Commands</div>
|
||||||
|
{#each TOOLS as group}
|
||||||
|
{#if group.tools.some(t => filteredTools.includes(t))}
|
||||||
|
<div class="slash-menu-category">{group.label}</div>
|
||||||
|
{#each group.tools.filter(t => filteredTools.includes(t)) as tool, i}
|
||||||
|
<button
|
||||||
|
class="slash-menu-item"
|
||||||
|
class:selected={filteredTools.indexOf(tool) === selectedIndex}
|
||||||
|
onclick={() => selectTool(tool)}
|
||||||
|
>
|
||||||
|
<span class="slash-command">{tool.command}</span>
|
||||||
|
<span class="slash-description">{tool.description}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<div class="slash-menu-hint">Press Tab to select, Enter to send</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={messageInput}
|
bind:value={messageInput}
|
||||||
|
oninput={handleInput}
|
||||||
onkeydown={handleKeydown}
|
onkeydown={handleKeydown}
|
||||||
placeholder="Describe your trading strategy..."
|
placeholder="Describe your trading strategy... (or type / for commands)"
|
||||||
rows="1"
|
rows="1"
|
||||||
></textarea>
|
></textarea>
|
||||||
<button onclick={handleSend}>
|
<button onclick={handleSend}>
|
||||||
@@ -555,4 +669,76 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slash-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: rgba(20, 20, 20, 0.98);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 400px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slash-menu-header {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slash-menu-category {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #666;
|
||||||
|
padding: 0.5rem 0.75rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slash-menu-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.15s;
|
||||||
|
margin: 0.15rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slash-menu-item:hover,
|
||||||
|
.slash-menu-item.selected {
|
||||||
|
background: rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slash-command {
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slash-description {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slash-menu-hint {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #555;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user