feat: adapt FreeRide skill for PicoClaw
This commit is contained in:
parent
6ac2a46680
commit
cd41d44085
5 changed files with 340 additions and 9 deletions
|
|
@ -57,6 +57,11 @@ spec:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: picoclaw-secrets
|
name: picoclaw-secrets
|
||||||
key: telegram-token
|
key: telegram-token
|
||||||
|
- name: OPENROUTER_API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: picoclaw-secrets
|
||||||
|
key: OPENROUTER_API_KEY
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: picoclaw-data
|
- name: picoclaw-data
|
||||||
mountPath: /home/picoclaw/.picoclaw
|
mountPath: /home/picoclaw/.picoclaw
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,4 @@ stringData:
|
||||||
telegram-token: "YOUR_TELEGRAM_TOKEN_HERE"
|
telegram-token: "YOUR_TELEGRAM_TOKEN_HERE"
|
||||||
nvidia-api-key: "YOUR_NVIDIA_API_KEY_HERE"
|
nvidia-api-key: "YOUR_NVIDIA_API_KEY_HERE"
|
||||||
azure-api-key: "YOUR_AZURE_API_KEY_HERE"
|
azure-api-key: "YOUR_AZURE_API_KEY_HERE"
|
||||||
|
OPENROUTER_API_KEY: "YOUR_OPENROUTER_API_KEY_HERE"
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ type AgentLoop struct {
|
||||||
activeRequests sync.WaitGroup
|
activeRequests sync.WaitGroup
|
||||||
|
|
||||||
reloadFunc func() error
|
reloadFunc func() error
|
||||||
|
configPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// processOptions configures how a message is processed
|
// processOptions configures how a message is processed
|
||||||
|
|
@ -124,6 +125,7 @@ const (
|
||||||
|
|
||||||
func NewAgentLoop(
|
func NewAgentLoop(
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
|
configPath string,
|
||||||
msgBus *bus.MessageBus,
|
msgBus *bus.MessageBus,
|
||||||
provider providers.LLMProvider,
|
provider providers.LLMProvider,
|
||||||
) *AgentLoop {
|
) *AgentLoop {
|
||||||
|
|
@ -151,14 +153,15 @@ func NewAgentLoop(
|
||||||
|
|
||||||
eventBus := NewEventBus()
|
eventBus := NewEventBus()
|
||||||
al := &AgentLoop{
|
al := &AgentLoop{
|
||||||
bus: msgBus,
|
bus: msgBus,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
registry: registry,
|
configPath: configPath,
|
||||||
state: stateManager,
|
registry: registry,
|
||||||
eventBus: eventBus,
|
state: stateManager,
|
||||||
fallback: fallbackChain,
|
eventBus: eventBus,
|
||||||
cmdRegistry: commands.NewRegistry(commands.BuiltinDefinitions()),
|
fallback: fallbackChain,
|
||||||
steering: newSteeringQueue(parseSteeringMode(cfg.Agents.Defaults.SteeringMode)),
|
cmdRegistry: commands.NewRegistry(commands.BuiltinDefinitions()),
|
||||||
|
steering: newSteeringQueue(parseSteeringMode(cfg.Agents.Defaults.SteeringMode)),
|
||||||
}
|
}
|
||||||
|
|
||||||
al.agentCacheTTL = 24 * time.Hour
|
al.agentCacheTTL = 24 * time.Hour
|
||||||
|
|
@ -336,6 +339,10 @@ func registerSharedTools(
|
||||||
|
|
||||||
// Skill discovery and installation tools
|
// Skill discovery and installation tools
|
||||||
skills_enabled := cfg.Tools.IsToolEnabled("skills")
|
skills_enabled := cfg.Tools.IsToolEnabled("skills")
|
||||||
|
if skills_enabled {
|
||||||
|
agent.Tools.Register(tools.NewFreeRideTool(al.GetConfigPath(), al.GetReloadFunc()))
|
||||||
|
}
|
||||||
|
|
||||||
find_skills_enable := cfg.Tools.IsToolEnabled("find_skills")
|
find_skills_enable := cfg.Tools.IsToolEnabled("find_skills")
|
||||||
install_skills_enable := cfg.Tools.IsToolEnabled("install_skill")
|
install_skills_enable := cfg.Tools.IsToolEnabled("install_skill")
|
||||||
if skills_enabled && (find_skills_enable || install_skills_enable) {
|
if skills_enabled && (find_skills_enable || install_skills_enable) {
|
||||||
|
|
@ -1209,6 +1216,16 @@ func (al *AgentLoop) SetReloadFunc(fn func() error) {
|
||||||
al.reloadFunc = fn
|
al.reloadFunc = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetReloadFunc returns the current reload callback.
|
||||||
|
func (al *AgentLoop) GetReloadFunc() func() error {
|
||||||
|
return al.reloadFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigPath returns the path to the configuration file.
|
||||||
|
func (al *AgentLoop) GetConfigPath() string {
|
||||||
|
return al.configPath
|
||||||
|
}
|
||||||
|
|
||||||
var audioAnnotationRe = regexp.MustCompile(`\[(voice|audio)(?::[^\]]*)?\]`)
|
var audioAnnotationRe = regexp.MustCompile(`\[(voice|audio)(?::[^\]]*)?\]`)
|
||||||
|
|
||||||
// transcribeAudioInMessage resolves audio media refs, transcribes them, and
|
// transcribeAudioInMessage resolves audio media refs, transcribes them, and
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
agentLoop := agent.NewAgentLoop(cfg, configPath, msgBus, provider)
|
||||||
|
|
||||||
fmt.Println("\n📦 Agent Status:")
|
fmt.Println("\n📦 Agent Status:")
|
||||||
startupInfo := agentLoop.GetStartupInfo()
|
startupInfo := agentLoop.GetStartupInfo()
|
||||||
|
|
|
||||||
308
pkg/tools/freeride.go
Normal file
308
pkg/tools/freeride.go
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FreeRideTool adapts the FreeRide logic (from clawhub/free-ride) for PicoClaw.
|
||||||
|
// It manages OpenRouter's free models and configures them as fallbacks.
|
||||||
|
type FreeRideTool struct {
|
||||||
|
configPath string
|
||||||
|
reloadFunc func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFreeRideTool(configPath string, reloadFunc func() error) *FreeRideTool {
|
||||||
|
return &FreeRideTool{
|
||||||
|
configPath: configPath,
|
||||||
|
reloadFunc: reloadFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *FreeRideTool) Name() string {
|
||||||
|
return "freeride"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *FreeRideTool) Description() string {
|
||||||
|
return "FreeRide gives you unlimited free AI in PicoClaw by automatically managing OpenRouter's free models. " +
|
||||||
|
"Use 'auto' to configure best model + fallbacks, or 'list' to see available free models."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *FreeRideTool) Parameters() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"command": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"enum": []string{"auto", "list", "status"},
|
||||||
|
"description": "The command to run: 'auto' (configures models), 'list' (shows free models), 'status' (checks current setup)",
|
||||||
|
},
|
||||||
|
"limit": map[string]any{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "For 'list', how many models to show. For 'auto', how many fallbacks to configure.",
|
||||||
|
"default": 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"command"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type openRouterModel struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ContextLength int `json:"context_length"`
|
||||||
|
Pricing struct {
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
Completion string `json:"completion"`
|
||||||
|
} `json:"pricing"`
|
||||||
|
SupportedParameters []string `json:"supported_parameters"`
|
||||||
|
Created int64 `json:"created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *FreeRideTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
|
cmd, _ := args["command"].(string)
|
||||||
|
limit := 5
|
||||||
|
if l, ok := args["limit"].(float64); ok {
|
||||||
|
limit = int(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "list":
|
||||||
|
return t.handleList(ctx, limit)
|
||||||
|
case "auto":
|
||||||
|
return t.handleAuto(ctx, limit)
|
||||||
|
case "status":
|
||||||
|
return t.handleStatus()
|
||||||
|
default:
|
||||||
|
return ErrorResult(fmt.Sprintf("unknown command: %s", cmd))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *FreeRideTool) fetchFreeModels(ctx context.Context) ([]openRouterModel, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://openrouter.ai/api/v1/models", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("OpenRouter API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrapper struct {
|
||||||
|
Data []openRouterModel `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&wrapper); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var freeModels []openRouterModel
|
||||||
|
for _, m := range wrapper.Data {
|
||||||
|
if m.Pricing.Prompt == "0" || m.Pricing.Prompt == "0.0" || m.Pricing.Prompt == "0.00" {
|
||||||
|
freeModels = append(freeModels, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rank models
|
||||||
|
sort.Slice(freeModels, func(i, j int) bool {
|
||||||
|
return scoreModel(freeModels[i]) > scoreModel(freeModels[j])
|
||||||
|
})
|
||||||
|
|
||||||
|
return freeModels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scoreModel(m openRouterModel) float64 {
|
||||||
|
score := 0.0
|
||||||
|
|
||||||
|
// Context length (40%) - normalize against 128k
|
||||||
|
ctxScore := float64(m.ContextLength) / 128000.0
|
||||||
|
if ctxScore > 1.0 {
|
||||||
|
ctxScore = 1.0
|
||||||
|
}
|
||||||
|
score += ctxScore * 0.4
|
||||||
|
|
||||||
|
// Capabilities (30%) - tools, vision, prompt caching, etc.
|
||||||
|
capabilityScore := 0.0
|
||||||
|
for _, p := range m.SupportedParameters {
|
||||||
|
if p == "tools" {
|
||||||
|
capabilityScore += 0.5
|
||||||
|
}
|
||||||
|
if p == "response_format" {
|
||||||
|
capabilityScore += 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if capabilityScore > 1.0 {
|
||||||
|
capabilityScore = 1.0
|
||||||
|
}
|
||||||
|
score += capabilityScore * 0.3
|
||||||
|
|
||||||
|
// Recency (20%) - newer is better
|
||||||
|
// Normalize against 2 years ago
|
||||||
|
twoYearsAgo := time.Now().AddDate(-2, 0, 0).Unix()
|
||||||
|
now := time.Now().Unix()
|
||||||
|
if m.Created > twoYearsAgo {
|
||||||
|
recencyScore := float64(m.Created-twoYearsAgo) / float64(now-twoYearsAgo)
|
||||||
|
score += recencyScore * 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider Trust (10%) - hardcoded list of trusted names
|
||||||
|
trustNames := []string{"google", "meta", "nvidia", "mistral", "anthropic", "openai", "microsoft", "qwen", "deepseek"}
|
||||||
|
for _, name := range trustNames {
|
||||||
|
if strings.Contains(strings.ToLower(m.ID), name) {
|
||||||
|
score += 0.1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *FreeRideTool) handleList(ctx context.Context, limit int) *ToolResult {
|
||||||
|
models, err := t.fetchFreeModels(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(fmt.Errorf("failed to fetch models: %w", err).Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(models) == 0 {
|
||||||
|
return SilentResult("No free models found on OpenRouter.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("Found %d free models on OpenRouter (ranked by quality):\n\n", len(models)))
|
||||||
|
for i, m := range models {
|
||||||
|
if i >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%d. **%s** (%s)\n", i+1, m.Name, m.ID))
|
||||||
|
sb.WriteString(fmt.Sprintf(" Context: %d tokens | Score: %.2f\n", m.ContextLength, scoreModel(m)))
|
||||||
|
sb.WriteString(fmt.Sprintf(" Parameters: %s\n\n", strings.Join(m.SupportedParameters, ", ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
return SilentResult(sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *FreeRideTool) handleAuto(ctx context.Context, limit int) *ToolResult {
|
||||||
|
models, err := t.fetchFreeModels(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(fmt.Errorf("failed to fetch models: %w", err).Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(models) == 0 {
|
||||||
|
return ErrorResult("No free models found on OpenRouter.")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgObj, err := config.LoadConfig(t.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(fmt.Errorf("failed to load config: %w", err).Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Add models to ModelList if not present
|
||||||
|
var addedModels []string
|
||||||
|
for i, m := range models {
|
||||||
|
if i >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
modelName := strings.ReplaceAll(m.ID, "/", "-")
|
||||||
|
if !modelExists(cfgObj, modelName) {
|
||||||
|
mc := &config.ModelConfig{
|
||||||
|
ModelName: modelName,
|
||||||
|
Model: "openrouter/" + m.ID,
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
mc.SetAPIKey("env://OPENROUTER_API_KEY")
|
||||||
|
cfgObj.ModelList = append(cfgObj.ModelList, mc)
|
||||||
|
addedModels = append(addedModels, modelName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Set fallbacks for the default agent
|
||||||
|
if len(addedModels) > 0 {
|
||||||
|
// Update AgentDefaults fallbacks
|
||||||
|
cfgObj.Agents.Defaults.ModelFallbacks = append(cfgObj.Agents.Defaults.ModelFallbacks, addedModels...)
|
||||||
|
// Deduplicate fallbacks
|
||||||
|
cfgObj.Agents.Defaults.ModelFallbacks = uniqueStrings(cfgObj.Agents.Defaults.ModelFallbacks)
|
||||||
|
|
||||||
|
if err := config.SaveConfig(t.configPath, cfgObj); err != nil {
|
||||||
|
return ErrorResult(fmt.Errorf("failed to save config: %w", err).Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("Success! Added %d free models as fallbacks: %s.\n", len(addedModels), strings.Join(addedModels, ", "))
|
||||||
|
msg += "Re-loading configuration to apply changes..."
|
||||||
|
|
||||||
|
if t.reloadFunc != nil {
|
||||||
|
if err := t.reloadFunc(); err != nil {
|
||||||
|
return ErrorResult(fmt.Sprintf("%s\nFailed to reload: %v", msg, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SilentResult(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SilentResult("No new free models to add. Your configuration is up to date.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *FreeRideTool) handleStatus() *ToolResult {
|
||||||
|
cfgObj, err := config.LoadConfig(t.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(fmt.Errorf("failed to load config: %w", err).Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("FreeRide Status:\n")
|
||||||
|
sb.WriteString(fmt.Sprintf("- Primary Model: %s\n", cfgObj.Agents.Defaults.GetModelName()))
|
||||||
|
sb.WriteString(fmt.Sprintf("- Fallback Models: %s\n", strings.Join(cfgObj.Agents.Defaults.ModelFallbacks, ", ")))
|
||||||
|
|
||||||
|
// Check for OpenRouter models in fallbacks
|
||||||
|
openRouterCount := 0
|
||||||
|
for _, fb := range cfgObj.Agents.Defaults.ModelFallbacks {
|
||||||
|
if strings.Contains(strings.ToLower(fb), "openrouter") || isKnownOpenRouterAlias(cfgObj, fb) {
|
||||||
|
openRouterCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("- Managed Free Models: %d\n", openRouterCount))
|
||||||
|
|
||||||
|
return SilentResult(sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func modelExists(cfg *config.Config, modelName string) bool {
|
||||||
|
for _, m := range cfg.ModelList {
|
||||||
|
if m.ModelName == modelName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKnownOpenRouterAlias(cfg *config.Config, modelName string) bool {
|
||||||
|
for _, m := range cfg.ModelList {
|
||||||
|
if m.ModelName == modelName && strings.HasPrefix(m.Model, "openrouter/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueStrings(input []string) []string {
|
||||||
|
keys := make(map[string]bool)
|
||||||
|
list := []string{}
|
||||||
|
for _, entry := range input {
|
||||||
|
if _, value := keys[entry]; !value {
|
||||||
|
keys[entry] = true
|
||||||
|
list = append(list, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue