mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 09:11:03 +08:00
feat(agent): surface the AI500 index board in chat, tools, and sidebar
- provider/nofxos: GetAI500ListCached — 5min TTL cache with stale fallback on upstream failure; ResolveClient routes through the claw402 gateway when a wallet key is configured (user's claw402 model key -> CLAW402_WALLET_KEY env -> direct nofxos) - new GET /api/ai500 endpoint serving the score-sorted board - new get_ai500_list agent tool + prompt rule: when the user wants coin picks or creates a strategy without naming coins, consult AI500's high-scoring entries by default - web: AI500 sidebar panel (rank, score badge, gain since entry, 5min auto-refresh); clicking an entry asks the agent to analyze it
This commit is contained in:
122
agent/ai500.go
Normal file
122
agent/ai500.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"nofx/provider/nofxos"
|
||||
"nofx/store"
|
||||
)
|
||||
|
||||
const (
|
||||
ai500DefaultLimit = 20
|
||||
ai500MaxLimit = 100
|
||||
)
|
||||
|
||||
// fetchAI500ForTool is swappable in tests. It resolves a nofxos client
|
||||
// (routed through claw402 when a wallet key is available) and returns the
|
||||
// cached AI500 board.
|
||||
var fetchAI500ForTool = func(walletKey string) ([]nofxos.CoinData, error) {
|
||||
return nofxos.GetAI500ListCached(nofxos.ResolveClient(walletKey))
|
||||
}
|
||||
|
||||
// Claw402WalletKeyForStoreUser returns the wallet private key of the user's
|
||||
// enabled claw402 model, if any, so data requests can be routed through the
|
||||
// claw402 payment gateway on the user's own account.
|
||||
func Claw402WalletKeyForStoreUser(st *store.Store, storeUserID string) string {
|
||||
if st == nil {
|
||||
return ""
|
||||
}
|
||||
if strings.TrimSpace(storeUserID) == "" {
|
||||
storeUserID = "default"
|
||||
}
|
||||
models, err := st.AIModel().List(storeUserID)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, model := range models {
|
||||
if model == nil || !model.Enabled {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(model.Provider), "claw402") && len(model.APIKey) > 0 {
|
||||
return string(model.APIKey)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// AI500BoardEntry is the display shape for one AI500 constituent.
|
||||
type AI500BoardEntry struct {
|
||||
Pair string `json:"pair"`
|
||||
Score float64 `json:"score"`
|
||||
MaxScore float64 `json:"max_score"`
|
||||
IncreasePercent float64 `json:"increase_percent"`
|
||||
StartPrice float64 `json:"start_price"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
}
|
||||
|
||||
// AI500Board returns the AI500 constituents sorted by score (descending),
|
||||
// truncated to limit.
|
||||
func AI500Board(walletKey string, limit int) ([]AI500BoardEntry, error) {
|
||||
if limit <= 0 {
|
||||
limit = ai500DefaultLimit
|
||||
}
|
||||
if limit > ai500MaxLimit {
|
||||
limit = ai500MaxLimit
|
||||
}
|
||||
coins, err := fetchAI500ForTool(walletKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sorted := make([]nofxos.CoinData, len(coins))
|
||||
copy(sorted, coins)
|
||||
sort.SliceStable(sorted, func(i, j int) bool {
|
||||
return sorted[i].Score > sorted[j].Score
|
||||
})
|
||||
if len(sorted) > limit {
|
||||
sorted = sorted[:limit]
|
||||
}
|
||||
out := make([]AI500BoardEntry, 0, len(sorted))
|
||||
for _, coin := range sorted {
|
||||
out = append(out, AI500BoardEntry{
|
||||
Pair: coin.Pair,
|
||||
Score: coin.Score,
|
||||
MaxScore: coin.MaxScore,
|
||||
IncreasePercent: coin.IncreasePercent,
|
||||
StartPrice: coin.StartPrice,
|
||||
StartTime: coin.StartTime,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// toolGetAI500List exposes the AI500 board to the chat agent.
|
||||
func (a *Agent) toolGetAI500List(storeUserID, argsJSON string) string {
|
||||
var args struct {
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
if strings.TrimSpace(argsJSON) != "" {
|
||||
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
|
||||
return fmt.Sprintf(`{"error":"invalid arguments: %s"}`, err)
|
||||
}
|
||||
}
|
||||
|
||||
walletKey := Claw402WalletKeyForStoreUser(a.store, storeUserID)
|
||||
entries, err := AI500Board(walletKey, args.Limit)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`{"error":"failed to fetch AI500 list: %s"}`, err)
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"status": "ok",
|
||||
"count": len(entries),
|
||||
"coins": entries,
|
||||
"note": "AI500 is an AI-scored crypto index; score is 0-100, increase_percent is the gain since the coin entered the index.",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`{"error":"failed to serialize AI500 list: %s"}`, err)
|
||||
}
|
||||
return string(payload)
|
||||
}
|
||||
Reference in New Issue
Block a user