Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/api_types.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,11 @@ class WalletPoolPosition(BaseModel):
depositedTokens: Dict[str, float] # address to token amount


class Suggestion(BaseModel):
display: str # short label shown in the UI (2-5 words)
prompt: str # full detailed message sent to the agent when clicked


class UserMessage(BaseModel):
type: Literal["user"] = "user"
message: str
Expand Down
26 changes: 12 additions & 14 deletions server/fastapi_server.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@
AgentType,
Portfolio,
Message,
Suggestion,
TokenMetadata,
SolanaVerifyRequest,
Context,
Expand DownExpand Up@@ -595,7 +596,7 @@ async def handle_suggestions_request(
portfolio: Portfolio,
token_metadata_repo: TokenMetadataRepo,
suggestions_model: ChatOpenAI,
) -> List[str]:
) -> List[Suggestion]:
# Get tools from agent config and format them
tools = create_investor_agent_toolkit() + create_analytics_agent_toolkit(
token_metadata_repo
Expand All@@ -621,21 +622,18 @@ async def handle_suggestions_request(
content = content.strip()

try:
# First try parsing as JSON
suggestions = json.loads(content)
if isinstance(suggestions, list):
parsed = json.loads(content)
if isinstance(parsed, list):
suggestions = []
for item in parsed:
if isinstance(item, dict) and "display" in item and "prompt" in item:
suggestions.append(Suggestion(display=item["display"], prompt=item["prompt"]))
elif isinstance(item, str):
# Graceful fallback: treat legacy string as both display and prompt
suggestions.append(Suggestion(display=item, prompt=item))
return suggestions
except json.JSONDecodeError:
# If JSON parsing fails, try parsing as string array
try:
# Remove any JSON-like syntax and split by comma
cleaned = content.strip("[]")
# Split by comma and remove quotes
suggestions = [item.strip().strip("'\"") for item in cleaned.split(",")]
return suggestions
except Exception as e:
logging.error(f"Error parsing suggestions string: {e}")
return []
logging.error(f"Error parsing suggestions JSON: {content}")

return []

Expand Down
26 changes: 15 additions & 11 deletions templates/suggestions.jinja2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
You are a context-aware suggestion system for OpenGradient's DeFi platform. Based on conversation history and user wallet data, generate 3-4 highly relevant next-step suggestions displayed as clickable buttons.

Each suggestion has two parts:
- "display": short label shown on the button (2-5 words, action-oriented)
- "prompt": the full, detailed message sent to the agent when the button is clicked (1-3 sentences with specific context, tokens, or chains)

CRITICAL RULES:
1. Return ONLY a valid JSON array of strings: ["Suggestion 1", "Suggestion 2", "Suggestion 3", "Suggestion 4"]
1. Return ONLY a valid JSON array of objects: [{"display": "...", "prompt": "..."}, ...]
2. Don't wrap your response in quotes or anything else, just return the array
3. Each suggestion must be 2-5 words, action-oriented, and specific
4. NO explanations, preambles, or additional text
5. ONLY suggest actions supported by the available tools listed below
6. DON'T suggest an action that has already been taken
7. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc
3. "display" must be 2-5 words, action-oriented, and specific
4. "prompt" must be a complete, detailed question or instruction the agent can act on immediately
5. NO explanations, preambles, or additional text outside the JSON array
6. ONLY suggest actions supported by the available tools listed below
7. DON'T suggest an action that has already been taken
8. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc

AVAILABLE TOOLS:
{{ tools }}
Expand All@@ -24,11 +29,10 @@ Always consider:
- For tokens that the user is interested in, suggest buying only if the token is on solana. e.g. id is "solana:TOKEN_B"

EXAMPLE RESPONSES:
- After yield discussion: ["Yield on USDC", "Yield on SOL", "Yield on BONK", "Get price history for SOL", etc]
- After token mention: ["Evaluate BONK risk", "Top holders of BONK", "Buy TOKEN_B", etc]
- After market question: ["Analyze ETH trend", "Get Ethereum TVL", "Portfolio volatility", "TVL trends on Ethereum", "Portfolio summary", etc]
- After risk concern: ["Analyze BONK risk", "Analyze BONK price trend", etc]
- After trending tokens on CHAIN_A: ["Evaluate TOKEN_A risk", "Top holders of TOKEN_A", "Evaluate TOKEN_B risk", etc]
- After yield discussion: [{"display": "Yield on SOL", "prompt": "What are the best current yield opportunities for my SOL holdings, including APR and risk level?"}, {"display": "Yield on BONK", "prompt": "Show me yield options for BONK including staking and liquidity pool APRs."}, ...]
- After token mention: [{"display": "Evaluate BONK risk", "prompt": "Run a full risk evaluation on BONK, covering contract security, liquidity depth, and holder concentration."}, {"display": "Top BONK holders", "prompt": "Who are the top holders of BONK and what percentage of supply do they control?"}, ...]
- After market question: [{"display": "Analyze ETH trend", "prompt": "Analyze Ethereum's price trend over the past 30 days, including volume, momentum, and key support and resistance levels."}, {"display": "Get Ethereum TVL", "prompt": "Show me the historical TVL trend for Ethereum DeFi over the past 90 days."}, ...]
- After risk concern: [{"display": "Analyze BONK risk", "prompt": "Evaluate BONK for smart contract risks, rug pull indicators, and unusual whale activity."}, ...]

User's Solana wallet tokens: {{ tokens }}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/api_types.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,11 @@ class WalletPoolPosition(BaseModel):
depositedTokens: Dict[str, float] # address to token amount


class Suggestion(BaseModel):
display: str # short label shown in the UI (2-5 words)
prompt: str # full detailed message sent to the agent when clicked


class UserMessage(BaseModel):
type: Literal["user"] = "user"
message: str
Expand Down
26 changes: 12 additions & 14 deletions server/fastapi_server.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@
AgentType,
Portfolio,
Message,
Suggestion,
TokenMetadata,
SolanaVerifyRequest,
Context,
Expand DownExpand Up@@ -595,7 +596,7 @@ async def handle_suggestions_request(
portfolio: Portfolio,
token_metadata_repo: TokenMetadataRepo,
suggestions_model: ChatOpenAI,
) -> List[str]:
) -> List[Suggestion]:
# Get tools from agent config and format them
tools = create_investor_agent_toolkit() + create_analytics_agent_toolkit(
token_metadata_repo
Expand All@@ -621,21 +622,18 @@ async def handle_suggestions_request(
content = content.strip()

try:
# First try parsing as JSON
suggestions = json.loads(content)
if isinstance(suggestions, list):
parsed = json.loads(content)
if isinstance(parsed, list):
suggestions = []
for item in parsed:
if isinstance(item, dict) and "display" in item and "prompt" in item:
suggestions.append(Suggestion(display=item["display"], prompt=item["prompt"]))
elif isinstance(item, str):
# Graceful fallback: treat legacy string as both display and prompt
suggestions.append(Suggestion(display=item, prompt=item))
return suggestions
except json.JSONDecodeError:
# If JSON parsing fails, try parsing as string array
try:
# Remove any JSON-like syntax and split by comma
cleaned = content.strip("[]")
# Split by comma and remove quotes
suggestions = [item.strip().strip("'\"") for item in cleaned.split(",")]
return suggestions
except Exception as e:
logging.error(f"Error parsing suggestions string: {e}")
return []
logging.error(f"Error parsing suggestions JSON: {content}")

return []

Expand Down
26 changes: 15 additions & 11 deletions templates/suggestions.jinja2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
You are a context-aware suggestion system for OpenGradient's DeFi platform. Based on conversation history and user wallet data, generate 3-4 highly relevant next-step suggestions displayed as clickable buttons.

Each suggestion has two parts:
- "display": short label shown on the button (2-5 words, action-oriented)
- "prompt": the full, detailed message sent to the agent when the button is clicked (1-3 sentences with specific context, tokens, or chains)

CRITICAL RULES:
1. Return ONLY a valid JSON array of strings: ["Suggestion 1", "Suggestion 2", "Suggestion 3", "Suggestion 4"]
1. Return ONLY a valid JSON array of objects: [{"display": "...", "prompt": "..."}, ...]
2. Don't wrap your response in quotes or anything else, just return the array
3. Each suggestion must be 2-5 words, action-oriented, and specific
4. NO explanations, preambles, or additional text
5. ONLY suggest actions supported by the available tools listed below
6. DON'T suggest an action that has already been taken
7. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc
3. "display" must be 2-5 words, action-oriented, and specific
4. "prompt" must be a complete, detailed question or instruction the agent can act on immediately
5. NO explanations, preambles, or additional text outside the JSON array
6. ONLY suggest actions supported by the available tools listed below
7. DON'T suggest an action that has already been taken
8. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc

AVAILABLE TOOLS:
{{ tools }}
Expand All@@ -24,11 +29,10 @@ Always consider:
- For tokens that the user is interested in, suggest buying only if the token is on solana. e.g. id is "solana:TOKEN_B"

EXAMPLE RESPONSES:
- After yield discussion: ["Yield on USDC", "Yield on SOL", "Yield on BONK", "Get price history for SOL", etc]
- After token mention: ["Evaluate BONK risk", "Top holders of BONK", "Buy TOKEN_B", etc]
- After market question: ["Analyze ETH trend", "Get Ethereum TVL", "Portfolio volatility", "TVL trends on Ethereum", "Portfolio summary", etc]
- After risk concern: ["Analyze BONK risk", "Analyze BONK price trend", etc]
- After trending tokens on CHAIN_A: ["Evaluate TOKEN_A risk", "Top holders of TOKEN_A", "Evaluate TOKEN_B risk", etc]
- After yield discussion: [{"display": "Yield on SOL", "prompt": "What are the best current yield opportunities for my SOL holdings, including APR and risk level?"}, {"display": "Yield on BONK", "prompt": "Show me yield options for BONK including staking and liquidity pool APRs."}, ...]
- After token mention: [{"display": "Evaluate BONK risk", "prompt": "Run a full risk evaluation on BONK, covering contract security, liquidity depth, and holder concentration."}, {"display": "Top BONK holders", "prompt": "Who are the top holders of BONK and what percentage of supply do they control?"}, ...]
- After market question: [{"display": "Analyze ETH trend", "prompt": "Analyze Ethereum's price trend over the past 30 days, including volume, momentum, and key support and resistance levels."}, {"display": "Get Ethereum TVL", "prompt": "Show me the historical TVL trend for Ethereum DeFi over the past 90 days."}, ...]
- After risk concern: [{"display": "Analyze BONK risk", "prompt": "Evaluate BONK for smart contract risks, rug pull indicators, and unusual whale activity."}, ...]

User's Solana wallet tokens: {{ tokens }}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/api_types.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,11 @@ class WalletPoolPosition(BaseModel):
depositedTokens: Dict[str, float] # address to token amount


class Suggestion(BaseModel):
display: str # short label shown in the UI (2-5 words)
prompt: str # full detailed message sent to the agent when clicked


class UserMessage(BaseModel):
type: Literal["user"] = "user"
message: str
Expand Down
26 changes: 12 additions & 14 deletions server/fastapi_server.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@
AgentType,
Portfolio,
Message,
Suggestion,
TokenMetadata,
SolanaVerifyRequest,
Context,
Expand DownExpand Up@@ -595,7 +596,7 @@ async def handle_suggestions_request(
portfolio: Portfolio,
token_metadata_repo: TokenMetadataRepo,
suggestions_model: ChatOpenAI,
) -> List[str]:
) -> List[Suggestion]:
# Get tools from agent config and format them
tools = create_investor_agent_toolkit() + create_analytics_agent_toolkit(
token_metadata_repo
Expand All@@ -621,21 +622,18 @@ async def handle_suggestions_request(
content = content.strip()

try:
# First try parsing as JSON
suggestions = json.loads(content)
if isinstance(suggestions, list):
parsed = json.loads(content)
if isinstance(parsed, list):
suggestions = []
for item in parsed:
if isinstance(item, dict) and "display" in item and "prompt" in item:
suggestions.append(Suggestion(display=item["display"], prompt=item["prompt"]))
elif isinstance(item, str):
# Graceful fallback: treat legacy string as both display and prompt
suggestions.append(Suggestion(display=item, prompt=item))
return suggestions
except json.JSONDecodeError:
# If JSON parsing fails, try parsing as string array
try:
# Remove any JSON-like syntax and split by comma
cleaned = content.strip("[]")
# Split by comma and remove quotes
suggestions = [item.strip().strip("'\"") for item in cleaned.split(",")]
return suggestions
except Exception as e:
logging.error(f"Error parsing suggestions string: {e}")
return []
logging.error(f"Error parsing suggestions JSON: {content}")

return []

Expand Down
26 changes: 15 additions & 11 deletions templates/suggestions.jinja2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
You are a context-aware suggestion system for OpenGradient's DeFi platform. Based on conversation history and user wallet data, generate 3-4 highly relevant next-step suggestions displayed as clickable buttons.

Each suggestion has two parts:
- "display": short label shown on the button (2-5 words, action-oriented)
- "prompt": the full, detailed message sent to the agent when the button is clicked (1-3 sentences with specific context, tokens, or chains)

CRITICAL RULES:
1. Return ONLY a valid JSON array of strings: ["Suggestion 1", "Suggestion 2", "Suggestion 3", "Suggestion 4"]
1. Return ONLY a valid JSON array of objects: [{"display": "...", "prompt": "..."}, ...]
2. Don't wrap your response in quotes or anything else, just return the array
3. Each suggestion must be 2-5 words, action-oriented, and specific
4. NO explanations, preambles, or additional text
5. ONLY suggest actions supported by the available tools listed below
6. DON'T suggest an action that has already been taken
7. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc
3. "display" must be 2-5 words, action-oriented, and specific
4. "prompt" must be a complete, detailed question or instruction the agent can act on immediately
5. NO explanations, preambles, or additional text outside the JSON array
6. ONLY suggest actions supported by the available tools listed below
7. DON'T suggest an action that has already been taken
8. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc

AVAILABLE TOOLS:
{{ tools }}
Expand All@@ -24,11 +29,10 @@ Always consider:
- For tokens that the user is interested in, suggest buying only if the token is on solana. e.g. id is "solana:TOKEN_B"

EXAMPLE RESPONSES:
- After yield discussion: ["Yield on USDC", "Yield on SOL", "Yield on BONK", "Get price history for SOL", etc]
- After token mention: ["Evaluate BONK risk", "Top holders of BONK", "Buy TOKEN_B", etc]
- After market question: ["Analyze ETH trend", "Get Ethereum TVL", "Portfolio volatility", "TVL trends on Ethereum", "Portfolio summary", etc]
- After risk concern: ["Analyze BONK risk", "Analyze BONK price trend", etc]
- After trending tokens on CHAIN_A: ["Evaluate TOKEN_A risk", "Top holders of TOKEN_A", "Evaluate TOKEN_B risk", etc]
- After yield discussion: [{"display": "Yield on SOL", "prompt": "What are the best current yield opportunities for my SOL holdings, including APR and risk level?"}, {"display": "Yield on BONK", "prompt": "Show me yield options for BONK including staking and liquidity pool APRs."}, ...]
- After token mention: [{"display": "Evaluate BONK risk", "prompt": "Run a full risk evaluation on BONK, covering contract security, liquidity depth, and holder concentration."}, {"display": "Top BONK holders", "prompt": "Who are the top holders of BONK and what percentage of supply do they control?"}, ...]
- After market question: [{"display": "Analyze ETH trend", "prompt": "Analyze Ethereum's price trend over the past 30 days, including volume, momentum, and key support and resistance levels."}, {"display": "Get Ethereum TVL", "prompt": "Show me the historical TVL trend for Ethereum DeFi over the past 90 days."}, ...]
- After risk concern: [{"display": "Analyze BONK risk", "prompt": "Evaluate BONK for smart contract risks, rug pull indicators, and unusual whale activity."}, ...]

User's Solana wallet tokens: {{ tokens }}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/api_types.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,11 @@ class WalletPoolPosition(BaseModel):
depositedTokens: Dict[str, float] # address to token amount


class Suggestion(BaseModel):
display: str # short label shown in the UI (2-5 words)
prompt: str # full detailed message sent to the agent when clicked


class UserMessage(BaseModel):
type: Literal["user"] = "user"
message: str
Expand Down
26 changes: 12 additions & 14 deletions server/fastapi_server.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@
AgentType,
Portfolio,
Message,
Suggestion,
TokenMetadata,
SolanaVerifyRequest,
Context,
Expand DownExpand Up@@ -595,7 +596,7 @@ async def handle_suggestions_request(
portfolio: Portfolio,
token_metadata_repo: TokenMetadataRepo,
suggestions_model: ChatOpenAI,
) -> List[str]:
) -> List[Suggestion]:
# Get tools from agent config and format them
tools = create_investor_agent_toolkit() + create_analytics_agent_toolkit(
token_metadata_repo
Expand All@@ -621,21 +622,18 @@ async def handle_suggestions_request(
content = content.strip()

try:
# First try parsing as JSON
suggestions = json.loads(content)
if isinstance(suggestions, list):
parsed = json.loads(content)
if isinstance(parsed, list):
suggestions = []
for item in parsed:
if isinstance(item, dict) and "display" in item and "prompt" in item:
suggestions.append(Suggestion(display=item["display"], prompt=item["prompt"]))
elif isinstance(item, str):
# Graceful fallback: treat legacy string as both display and prompt
suggestions.append(Suggestion(display=item, prompt=item))
return suggestions
except json.JSONDecodeError:
# If JSON parsing fails, try parsing as string array
try:
# Remove any JSON-like syntax and split by comma
cleaned = content.strip("[]")
# Split by comma and remove quotes
suggestions = [item.strip().strip("'\"") for item in cleaned.split(",")]
return suggestions
except Exception as e:
logging.error(f"Error parsing suggestions string: {e}")
return []
logging.error(f"Error parsing suggestions JSON: {content}")

return []

Expand Down
26 changes: 15 additions & 11 deletions templates/suggestions.jinja2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
You are a context-aware suggestion system for OpenGradient's DeFi platform. Based on conversation history and user wallet data, generate 3-4 highly relevant next-step suggestions displayed as clickable buttons.

Each suggestion has two parts:
- "display": short label shown on the button (2-5 words, action-oriented)
- "prompt": the full, detailed message sent to the agent when the button is clicked (1-3 sentences with specific context, tokens, or chains)

CRITICAL RULES:
1. Return ONLY a valid JSON array of strings: ["Suggestion 1", "Suggestion 2", "Suggestion 3", "Suggestion 4"]
1. Return ONLY a valid JSON array of objects: [{"display": "...", "prompt": "..."}, ...]
2. Don't wrap your response in quotes or anything else, just return the array
3. Each suggestion must be 2-5 words, action-oriented, and specific
4. NO explanations, preambles, or additional text
5. ONLY suggest actions supported by the available tools listed below
6. DON'T suggest an action that has already been taken
7. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc
3. "display" must be 2-5 words, action-oriented, and specific
4. "prompt" must be a complete, detailed question or instruction the agent can act on immediately
5. NO explanations, preambles, or additional text outside the JSON array
6. ONLY suggest actions supported by the available tools listed below
7. DON'T suggest an action that has already been taken
8. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc

AVAILABLE TOOLS:
{{ tools }}
Expand All@@ -24,11 +29,10 @@ Always consider:
- For tokens that the user is interested in, suggest buying only if the token is on solana. e.g. id is "solana:TOKEN_B"

EXAMPLE RESPONSES:
- After yield discussion: ["Yield on USDC", "Yield on SOL", "Yield on BONK", "Get price history for SOL", etc]
- After token mention: ["Evaluate BONK risk", "Top holders of BONK", "Buy TOKEN_B", etc]
- After market question: ["Analyze ETH trend", "Get Ethereum TVL", "Portfolio volatility", "TVL trends on Ethereum", "Portfolio summary", etc]
- After risk concern: ["Analyze BONK risk", "Analyze BONK price trend", etc]
- After trending tokens on CHAIN_A: ["Evaluate TOKEN_A risk", "Top holders of TOKEN_A", "Evaluate TOKEN_B risk", etc]
- After yield discussion: [{"display": "Yield on SOL", "prompt": "What are the best current yield opportunities for my SOL holdings, including APR and risk level?"}, {"display": "Yield on BONK", "prompt": "Show me yield options for BONK including staking and liquidity pool APRs."}, ...]
- After token mention: [{"display": "Evaluate BONK risk", "prompt": "Run a full risk evaluation on BONK, covering contract security, liquidity depth, and holder concentration."}, {"display": "Top BONK holders", "prompt": "Who are the top holders of BONK and what percentage of supply do they control?"}, ...]
- After market question: [{"display": "Analyze ETH trend", "prompt": "Analyze Ethereum's price trend over the past 30 days, including volume, momentum, and key support and resistance levels."}, {"display": "Get Ethereum TVL", "prompt": "Show me the historical TVL trend for Ethereum DeFi over the past 90 days."}, ...]
- After risk concern: [{"display": "Analyze BONK risk", "prompt": "Evaluate BONK for smart contract risks, rug pull indicators, and unusual whale activity."}, ...]

User's Solana wallet tokens: {{ tokens }}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/api_types.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,11 @@ class WalletPoolPosition(BaseModel):
depositedTokens: Dict[str, float] # address to token amount


class Suggestion(BaseModel):
display: str # short label shown in the UI (2-5 words)
prompt: str # full detailed message sent to the agent when clicked


class UserMessage(BaseModel):
type: Literal["user"] = "user"
message: str
Expand Down
26 changes: 12 additions & 14 deletions server/fastapi_server.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@
AgentType,
Portfolio,
Message,
Suggestion,
TokenMetadata,
SolanaVerifyRequest,
Context,
Expand DownExpand Up@@ -595,7 +596,7 @@ async def handle_suggestions_request(
portfolio: Portfolio,
token_metadata_repo: TokenMetadataRepo,
suggestions_model: ChatOpenAI,
) -> List[str]:
) -> List[Suggestion]:
# Get tools from agent config and format them
tools = create_investor_agent_toolkit() + create_analytics_agent_toolkit(
token_metadata_repo
Expand All@@ -621,21 +622,18 @@ async def handle_suggestions_request(
content = content.strip()

try:
# First try parsing as JSON
suggestions = json.loads(content)
if isinstance(suggestions, list):
parsed = json.loads(content)
if isinstance(parsed, list):
suggestions = []
for item in parsed:
if isinstance(item, dict) and "display" in item and "prompt" in item:
suggestions.append(Suggestion(display=item["display"], prompt=item["prompt"]))
elif isinstance(item, str):
# Graceful fallback: treat legacy string as both display and prompt
suggestions.append(Suggestion(display=item, prompt=item))
return suggestions
except json.JSONDecodeError:
# If JSON parsing fails, try parsing as string array
try:
# Remove any JSON-like syntax and split by comma
cleaned = content.strip("[]")
# Split by comma and remove quotes
suggestions = [item.strip().strip("'\"") for item in cleaned.split(",")]
return suggestions
except Exception as e:
logging.error(f"Error parsing suggestions string: {e}")
return []
logging.error(f"Error parsing suggestions JSON: {content}")

return []

Expand Down
26 changes: 15 additions & 11 deletions templates/suggestions.jinja2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
You are a context-aware suggestion system for OpenGradient's DeFi platform. Based on conversation history and user wallet data, generate 3-4 highly relevant next-step suggestions displayed as clickable buttons.

Each suggestion has two parts:
- "display": short label shown on the button (2-5 words, action-oriented)
- "prompt": the full, detailed message sent to the agent when the button is clicked (1-3 sentences with specific context, tokens, or chains)

CRITICAL RULES:
1. Return ONLY a valid JSON array of strings: ["Suggestion 1", "Suggestion 2", "Suggestion 3", "Suggestion 4"]
1. Return ONLY a valid JSON array of objects: [{"display": "...", "prompt": "..."}, ...]
2. Don't wrap your response in quotes or anything else, just return the array
3. Each suggestion must be 2-5 words, action-oriented, and specific
4. NO explanations, preambles, or additional text
5. ONLY suggest actions supported by the available tools listed below
6. DON'T suggest an action that has already been taken
7. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc
3. "display" must be 2-5 words, action-oriented, and specific
4. "prompt" must be a complete, detailed question or instruction the agent can act on immediately
5. NO explanations, preambles, or additional text outside the JSON array
6. ONLY suggest actions supported by the available tools listed below
7. DON'T suggest an action that has already been taken
8. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc

AVAILABLE TOOLS:
{{ tools }}
Expand All@@ -24,11 +29,10 @@ Always consider:
- For tokens that the user is interested in, suggest buying only if the token is on solana. e.g. id is "solana:TOKEN_B"

EXAMPLE RESPONSES:
- After yield discussion: ["Yield on USDC", "Yield on SOL", "Yield on BONK", "Get price history for SOL", etc]
- After token mention: ["Evaluate BONK risk", "Top holders of BONK", "Buy TOKEN_B", etc]
- After market question: ["Analyze ETH trend", "Get Ethereum TVL", "Portfolio volatility", "TVL trends on Ethereum", "Portfolio summary", etc]
- After risk concern: ["Analyze BONK risk", "Analyze BONK price trend", etc]
- After trending tokens on CHAIN_A: ["Evaluate TOKEN_A risk", "Top holders of TOKEN_A", "Evaluate TOKEN_B risk", etc]
- After yield discussion: [{"display": "Yield on SOL", "prompt": "What are the best current yield opportunities for my SOL holdings, including APR and risk level?"}, {"display": "Yield on BONK", "prompt": "Show me yield options for BONK including staking and liquidity pool APRs."}, ...]
- After token mention: [{"display": "Evaluate BONK risk", "prompt": "Run a full risk evaluation on BONK, covering contract security, liquidity depth, and holder concentration."}, {"display": "Top BONK holders", "prompt": "Who are the top holders of BONK and what percentage of supply do they control?"}, ...]
- After market question: [{"display": "Analyze ETH trend", "prompt": "Analyze Ethereum's price trend over the past 30 days, including volume, momentum, and key support and resistance levels."}, {"display": "Get Ethereum TVL", "prompt": "Show me the historical TVL trend for Ethereum DeFi over the past 90 days."}, ...]
- After risk concern: [{"display": "Analyze BONK risk", "prompt": "Evaluate BONK for smart contract risks, rug pull indicators, and unusual whale activity."}, ...]

User's Solana wallet tokens: {{ tokens }}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/api_types.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,11 @@ class WalletPoolPosition(BaseModel):
depositedTokens: Dict[str, float] # address to token amount


class Suggestion(BaseModel):
display: str # short label shown in the UI (2-5 words)
prompt: str # full detailed message sent to the agent when clicked


class UserMessage(BaseModel):
type: Literal["user"] = "user"
message: str
Expand Down
26 changes: 12 additions & 14 deletions server/fastapi_server.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@
AgentType,
Portfolio,
Message,
Suggestion,
TokenMetadata,
SolanaVerifyRequest,
Context,
Expand DownExpand Up@@ -595,7 +596,7 @@ async def handle_suggestions_request(
portfolio: Portfolio,
token_metadata_repo: TokenMetadataRepo,
suggestions_model: ChatOpenAI,
) -> List[str]:
) -> List[Suggestion]:
# Get tools from agent config and format them
tools = create_investor_agent_toolkit() + create_analytics_agent_toolkit(
token_metadata_repo
Expand All@@ -621,21 +622,18 @@ async def handle_suggestions_request(
content = content.strip()

try:
# First try parsing as JSON
suggestions = json.loads(content)
if isinstance(suggestions, list):
parsed = json.loads(content)
if isinstance(parsed, list):
suggestions = []
for item in parsed:
if isinstance(item, dict) and "display" in item and "prompt" in item:
suggestions.append(Suggestion(display=item["display"], prompt=item["prompt"]))
elif isinstance(item, str):
# Graceful fallback: treat legacy string as both display and prompt
suggestions.append(Suggestion(display=item, prompt=item))
return suggestions
except json.JSONDecodeError:
# If JSON parsing fails, try parsing as string array
try:
# Remove any JSON-like syntax and split by comma
cleaned = content.strip("[]")
# Split by comma and remove quotes
suggestions = [item.strip().strip("'\"") for item in cleaned.split(",")]
return suggestions
except Exception as e:
logging.error(f"Error parsing suggestions string: {e}")
return []
logging.error(f"Error parsing suggestions JSON: {content}")

return []

Expand Down
26 changes: 15 additions & 11 deletions templates/suggestions.jinja2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
You are a context-aware suggestion system for OpenGradient's DeFi platform. Based on conversation history and user wallet data, generate 3-4 highly relevant next-step suggestions displayed as clickable buttons.

Each suggestion has two parts:
- "display": short label shown on the button (2-5 words, action-oriented)
- "prompt": the full, detailed message sent to the agent when the button is clicked (1-3 sentences with specific context, tokens, or chains)

CRITICAL RULES:
1. Return ONLY a valid JSON array of strings: ["Suggestion 1", "Suggestion 2", "Suggestion 3", "Suggestion 4"]
1. Return ONLY a valid JSON array of objects: [{"display": "...", "prompt": "..."}, ...]
2. Don't wrap your response in quotes or anything else, just return the array
3. Each suggestion must be 2-5 words, action-oriented, and specific
4. NO explanations, preambles, or additional text
5. ONLY suggest actions supported by the available tools listed below
6. DON'T suggest an action that has already been taken
7. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc
3. "display" must be 2-5 words, action-oriented, and specific
4. "prompt" must be a complete, detailed question or instruction the agent can act on immediately
5. NO explanations, preambles, or additional text outside the JSON array
6. ONLY suggest actions supported by the available tools listed below
7. DON'T suggest an action that has already been taken
8. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc

AVAILABLE TOOLS:
{{ tools }}
Expand All@@ -24,11 +29,10 @@ Always consider:
- For tokens that the user is interested in, suggest buying only if the token is on solana. e.g. id is "solana:TOKEN_B"

EXAMPLE RESPONSES:
- After yield discussion: ["Yield on USDC", "Yield on SOL", "Yield on BONK", "Get price history for SOL", etc]
- After token mention: ["Evaluate BONK risk", "Top holders of BONK", "Buy TOKEN_B", etc]
- After market question: ["Analyze ETH trend", "Get Ethereum TVL", "Portfolio volatility", "TVL trends on Ethereum", "Portfolio summary", etc]
- After risk concern: ["Analyze BONK risk", "Analyze BONK price trend", etc]
- After trending tokens on CHAIN_A: ["Evaluate TOKEN_A risk", "Top holders of TOKEN_A", "Evaluate TOKEN_B risk", etc]
- After yield discussion: [{"display": "Yield on SOL", "prompt": "What are the best current yield opportunities for my SOL holdings, including APR and risk level?"}, {"display": "Yield on BONK", "prompt": "Show me yield options for BONK including staking and liquidity pool APRs."}, ...]
- After token mention: [{"display": "Evaluate BONK risk", "prompt": "Run a full risk evaluation on BONK, covering contract security, liquidity depth, and holder concentration."}, {"display": "Top BONK holders", "prompt": "Who are the top holders of BONK and what percentage of supply do they control?"}, ...]
- After market question: [{"display": "Analyze ETH trend", "prompt": "Analyze Ethereum's price trend over the past 30 days, including volume, momentum, and key support and resistance levels."}, {"display": "Get Ethereum TVL", "prompt": "Show me the historical TVL trend for Ethereum DeFi over the past 90 days."}, ...]
- After risk concern: [{"display": "Analyze BONK risk", "prompt": "Evaluate BONK for smart contract risks, rug pull indicators, and unusual whale activity."}, ...]

User's Solana wallet tokens: {{ tokens }}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/api_types.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,11 @@ class WalletPoolPosition(BaseModel):
depositedTokens: Dict[str, float] # address to token amount


class Suggestion(BaseModel):
display: str # short label shown in the UI (2-5 words)
prompt: str # full detailed message sent to the agent when clicked


class UserMessage(BaseModel):
type: Literal["user"] = "user"
message: str
Expand Down
26 changes: 12 additions & 14 deletions server/fastapi_server.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@
AgentType,
Portfolio,
Message,
Suggestion,
TokenMetadata,
SolanaVerifyRequest,
Context,
Expand DownExpand Up@@ -595,7 +596,7 @@ async def handle_suggestions_request(
portfolio: Portfolio,
token_metadata_repo: TokenMetadataRepo,
suggestions_model: ChatOpenAI,
) -> List[str]:
) -> List[Suggestion]:
# Get tools from agent config and format them
tools = create_investor_agent_toolkit() + create_analytics_agent_toolkit(
token_metadata_repo
Expand All@@ -621,21 +622,18 @@ async def handle_suggestions_request(
content = content.strip()

try:
# First try parsing as JSON
suggestions = json.loads(content)
if isinstance(suggestions, list):
parsed = json.loads(content)
if isinstance(parsed, list):
suggestions = []
for item in parsed:
if isinstance(item, dict) and "display" in item and "prompt" in item:
suggestions.append(Suggestion(display=item["display"], prompt=item["prompt"]))
elif isinstance(item, str):
# Graceful fallback: treat legacy string as both display and prompt
suggestions.append(Suggestion(display=item, prompt=item))
return suggestions
except json.JSONDecodeError:
# If JSON parsing fails, try parsing as string array
try:
# Remove any JSON-like syntax and split by comma
cleaned = content.strip("[]")
# Split by comma and remove quotes
suggestions = [item.strip().strip("'\"") for item in cleaned.split(",")]
return suggestions
except Exception as e:
logging.error(f"Error parsing suggestions string: {e}")
return []
logging.error(f"Error parsing suggestions JSON: {content}")

return []

Expand Down
26 changes: 15 additions & 11 deletions templates/suggestions.jinja2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
You are a context-aware suggestion system for OpenGradient's DeFi platform. Based on conversation history and user wallet data, generate 3-4 highly relevant next-step suggestions displayed as clickable buttons.

Each suggestion has two parts:
- "display": short label shown on the button (2-5 words, action-oriented)
- "prompt": the full, detailed message sent to the agent when the button is clicked (1-3 sentences with specific context, tokens, or chains)

CRITICAL RULES:
1. Return ONLY a valid JSON array of strings: ["Suggestion 1", "Suggestion 2", "Suggestion 3", "Suggestion 4"]
1. Return ONLY a valid JSON array of objects: [{"display": "...", "prompt": "..."}, ...]
2. Don't wrap your response in quotes or anything else, just return the array
3. Each suggestion must be 2-5 words, action-oriented, and specific
4. NO explanations, preambles, or additional text
5. ONLY suggest actions supported by the available tools listed below
6. DON'T suggest an action that has already been taken
7. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc
3. "display" must be 2-5 words, action-oriented, and specific
4. "prompt" must be a complete, detailed question or instruction the agent can act on immediately
5. NO explanations, preambles, or additional text outside the JSON array
6. ONLY suggest actions supported by the available tools listed below
7. DON'T suggest an action that has already been taken
8. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc

AVAILABLE TOOLS:
{{ tools }}
Expand All@@ -24,11 +29,10 @@ Always consider:
- For tokens that the user is interested in, suggest buying only if the token is on solana. e.g. id is "solana:TOKEN_B"

EXAMPLE RESPONSES:
- After yield discussion: ["Yield on USDC", "Yield on SOL", "Yield on BONK", "Get price history for SOL", etc]
- After token mention: ["Evaluate BONK risk", "Top holders of BONK", "Buy TOKEN_B", etc]
- After market question: ["Analyze ETH trend", "Get Ethereum TVL", "Portfolio volatility", "TVL trends on Ethereum", "Portfolio summary", etc]
- After risk concern: ["Analyze BONK risk", "Analyze BONK price trend", etc]
- After trending tokens on CHAIN_A: ["Evaluate TOKEN_A risk", "Top holders of TOKEN_A", "Evaluate TOKEN_B risk", etc]
- After yield discussion: [{"display": "Yield on SOL", "prompt": "What are the best current yield opportunities for my SOL holdings, including APR and risk level?"}, {"display": "Yield on BONK", "prompt": "Show me yield options for BONK including staking and liquidity pool APRs."}, ...]
- After token mention: [{"display": "Evaluate BONK risk", "prompt": "Run a full risk evaluation on BONK, covering contract security, liquidity depth, and holder concentration."}, {"display": "Top BONK holders", "prompt": "Who are the top holders of BONK and what percentage of supply do they control?"}, ...]
- After market question: [{"display": "Analyze ETH trend", "prompt": "Analyze Ethereum's price trend over the past 30 days, including volume, momentum, and key support and resistance levels."}, {"display": "Get Ethereum TVL", "prompt": "Show me the historical TVL trend for Ethereum DeFi over the past 90 days."}, ...]
- After risk concern: [{"display": "Analyze BONK risk", "prompt": "Evaluate BONK for smart contract risks, rug pull indicators, and unusual whale activity."}, ...]

User's Solana wallet tokens: {{ tokens }}

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/api_types.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,11 @@ class WalletPoolPosition(BaseModel):
depositedTokens: Dict[str, float] # address to token amount


class Suggestion(BaseModel):
display: str # short label shown in the UI (2-5 words)
prompt: str # full detailed message sent to the agent when clicked


class UserMessage(BaseModel):
type: Literal["user"] = "user"
message: str
Expand Down
26 changes: 12 additions & 14 deletions server/fastapi_server.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@
AgentType,
Portfolio,
Message,
Suggestion,
TokenMetadata,
SolanaVerifyRequest,
Context,
Expand DownExpand Up@@ -595,7 +596,7 @@ async def handle_suggestions_request(
portfolio: Portfolio,
token_metadata_repo: TokenMetadataRepo,
suggestions_model: ChatOpenAI,
) -> List[str]:
) -> List[Suggestion]:
# Get tools from agent config and format them
tools = create_investor_agent_toolkit() + create_analytics_agent_toolkit(
token_metadata_repo
Expand All@@ -621,21 +622,18 @@ async def handle_suggestions_request(
content = content.strip()

try:
# First try parsing as JSON
suggestions = json.loads(content)
if isinstance(suggestions, list):
parsed = json.loads(content)
if isinstance(parsed, list):
suggestions = []
for item in parsed:
if isinstance(item, dict) and "display" in item and "prompt" in item:
suggestions.append(Suggestion(display=item["display"], prompt=item["prompt"]))
elif isinstance(item, str):
# Graceful fallback: treat legacy string as both display and prompt
suggestions.append(Suggestion(display=item, prompt=item))
return suggestions
except json.JSONDecodeError:
# If JSON parsing fails, try parsing as string array
try:
# Remove any JSON-like syntax and split by comma
cleaned = content.strip("[]")
# Split by comma and remove quotes
suggestions = [item.strip().strip("'\"") for item in cleaned.split(",")]
return suggestions
except Exception as e:
logging.error(f"Error parsing suggestions string: {e}")
return []
logging.error(f"Error parsing suggestions JSON: {content}")

return []

Expand Down
26 changes: 15 additions & 11 deletions templates/suggestions.jinja2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
You are a context-aware suggestion system for OpenGradient's DeFi platform. Based on conversation history and user wallet data, generate 3-4 highly relevant next-step suggestions displayed as clickable buttons.

Each suggestion has two parts:
- "display": short label shown on the button (2-5 words, action-oriented)
- "prompt": the full, detailed message sent to the agent when the button is clicked (1-3 sentences with specific context, tokens, or chains)

CRITICAL RULES:
1. Return ONLY a valid JSON array of strings: ["Suggestion 1", "Suggestion 2", "Suggestion 3", "Suggestion 4"]
1. Return ONLY a valid JSON array of objects: [{"display": "...", "prompt": "..."}, ...]
2. Don't wrap your response in quotes or anything else, just return the array
3. Each suggestion must be 2-5 words, action-oriented, and specific
4. NO explanations, preambles, or additional text
5. ONLY suggest actions supported by the available tools listed below
6. DON'T suggest an action that has already been taken
7. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc
3. "display" must be 2-5 words, action-oriented, and specific
4. "prompt" must be a complete, detailed question or instruction the agent can act on immediately
5. NO explanations, preambles, or additional text outside the JSON array
6. ONLY suggest actions supported by the available tools listed below
7. DON'T suggest an action that has already been taken
8. Don't recommend analyze_price_trend() or get_coingecko_current_price() on stablecoins like USDC, USDT, etc

AVAILABLE TOOLS:
{{ tools }}
Expand All@@ -24,11 +29,10 @@ Always consider:
- For tokens that the user is interested in, suggest buying only if the token is on solana. e.g. id is "solana:TOKEN_B"

EXAMPLE RESPONSES:
- After yield discussion: ["Yield on USDC", "Yield on SOL", "Yield on BONK", "Get price history for SOL", etc]
- After token mention: ["Evaluate BONK risk", "Top holders of BONK", "Buy TOKEN_B", etc]
- After market question: ["Analyze ETH trend", "Get Ethereum TVL", "Portfolio volatility", "TVL trends on Ethereum", "Portfolio summary", etc]
- After risk concern: ["Analyze BONK risk", "Analyze BONK price trend", etc]
- After trending tokens on CHAIN_A: ["Evaluate TOKEN_A risk", "Top holders of TOKEN_A", "Evaluate TOKEN_B risk", etc]
- After yield discussion: [{"display": "Yield on SOL", "prompt": "What are the best current yield opportunities for my SOL holdings, including APR and risk level?"}, {"display": "Yield on BONK", "prompt": "Show me yield options for BONK including staking and liquidity pool APRs."}, ...]
- After token mention: [{"display": "Evaluate BONK risk", "prompt": "Run a full risk evaluation on BONK, covering contract security, liquidity depth, and holder concentration."}, {"display": "Top BONK holders", "prompt": "Who are the top holders of BONK and what percentage of supply do they control?"}, ...]
- After market question: [{"display": "Analyze ETH trend", "prompt": "Analyze Ethereum's price trend over the past 30 days, including volume, momentum, and key support and resistance levels."}, {"display": "Get Ethereum TVL", "prompt": "Show me the historical TVL trend for Ethereum DeFi over the past 90 days."}, ...]
- After risk concern: [{"display": "Analyze BONK risk", "prompt": "Evaluate BONK for smart contract risks, rug pull indicators, and unusual whale activity."}, ...]

User's Solana wallet tokens: {{ tokens }}

Expand Down