From 55cf58cef5adb8f12286001a5d84c76c81e44b6f Mon Sep 17 00:00:00 2001 From: Bernardo Date: Sat, 14 Mar 2026 13:37:48 +0100 Subject: [PATCH] Use one stable preview URL and document live provider matrix --- README.md | 30 ++++++++++++++++++++++++++++++ pkg/agent/loop.go | 13 ++++++++----- pkg/channels/manager.go | 4 +++- pkg/tools/preview.go | 26 ++++++++++---------------- 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 5cf9f6143..bd4745e00 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,36 @@ PicoClaw +## 🔌 Provider Matrix + +The stack is provider-agnostic, but a practical low-cost deployment can split work by capability instead of forcing one model to do everything. + +### Example deployment matrix (Orange Pi / RISC-V) + +| Capability | Provider / API | Model / Path | Notes | +| ---------- | -------------- | ------------ | ----- | +| Chat / main conversation | OpenRouter | `openrouter/free` | Default text model path | +| Text fallback 1 | OpenRouter | `openai/gpt-oss-20b:free` | Free text fallback | +| Text fallback 2 | DeepSeek API | `deepseek-chat` | Extra text fallback | +| Text fallback 3 | Gemini API | `gemini-2.5-flash` | Last text fallback | +| Image understanding | Gemini API | `gemini-2.5-flash` | Used for photo / image turns | +| Voice transcription | ElevenLabs | `scribe_v1` | First speech-to-text provider | +| Voice transcription fallback 1 | Gemini API | `gemini-2.5-flash` | Speech understanding fallback | +| Voice transcription fallback 2 | Groq API | Whisper-compatible STT | Final transcription fallback | +| Voice reply synthesis | ElevenLabs | TTS API | Primary voice-note output | +| Voice reply synthesis fallback | Gemini API | `gemini-2.5-flash-preview-tts` | Fallback speech output | +| Gmail / Calendar / Drive actions | Google Workspace APIs via `gws` CLI | Gmail, Calendar, Drive | Executed as tool calls, not normal chat completions | +| Telegram transport | Telegram Bot API | Bot token | Messaging channel | +| WhatsApp transport | WhatsApp native (`whatsmeow`) | local session store | Not Meta Cloud API | +| Preview hosting | Built-in preview server | `http://:3002/preview/site/` | One stable preview URL can be reused across site updates | + +### Why split providers by capability? + +- Text chat can stay on cheap or free routing. +- Image turns can go directly to a vision-capable model. +- Voice can use STT/TTS providers that are better than a general-purpose chat model. +- Workspace actions like Gmail or Drive are usually better handled as tool/API calls than as LLM-only reasoning. + ## 🦾 Demonstration ### 🛠️ Standard Assistant Workflows diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 3b526cd82..c57a9e72a 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -2979,11 +2979,12 @@ func (al *AgentLoop) shouldRecallRecentPreview(lower string, agent *AgentInstanc wantsURL := strings.Contains(lower, "url") || strings.Contains(lower, "urls") || strings.Contains(lower, "link") || strings.Contains(lower, "links") wantsRecent := strings.Contains(lower, "recent") || strings.Contains(lower, "latest") || strings.Contains(lower, "most recent") || strings.Contains(lower, "current") wantsHost := strings.Contains(lower, "host") || strings.Contains(lower, "serve") || strings.Contains(lower, "open") - if !wantsURL && !(wantsRecent && wantsHost) { + wantsPreview := strings.Contains(lower, "preview") || strings.Contains(lower, "site") || strings.Contains(lower, "website") || strings.Contains(lower, "app") || strings.Contains(lower, "build") + wantsStatus := strings.Contains(lower, "status") || strings.Contains(lower, "going") || strings.Contains(lower, "howd") || strings.Contains(lower, "hows") || strings.Contains(lower, "how's") + if !wantsPreview { return false } - wantsPreview := strings.Contains(lower, "preview") || strings.Contains(lower, "site") || strings.Contains(lower, "website") || strings.Contains(lower, "app") || strings.Contains(lower, "build") - if !wantsPreview { + if !wantsURL && !(wantsRecent && wantsHost) && !wantsStatus { return false } return len(al.recentPreviewInfos(agent)) > 0 @@ -2996,10 +2997,12 @@ func (al *AgentLoop) shouldRecallRecentPreviewFromConversation(lower string, age wantsURL := strings.Contains(lower, "url") || strings.Contains(lower, "urls") || strings.Contains(lower, "link") || strings.Contains(lower, "links") wantsRecent := strings.Contains(lower, "recent") || strings.Contains(lower, "latest") || strings.Contains(lower, "most recent") || strings.Contains(lower, "current") wantsHost := strings.Contains(lower, "host") || strings.Contains(lower, "serve") || strings.Contains(lower, "open") - if !wantsURL && !(wantsRecent && wantsHost) { + wantsPreview := strings.Contains(lower, "preview") || strings.Contains(lower, "site") || strings.Contains(lower, "website") || strings.Contains(lower, "app") || strings.Contains(lower, "build") + wantsStatus := wantsPreview && (strings.Contains(lower, "status") || strings.Contains(lower, "going") || strings.Contains(lower, "howd") || strings.Contains(lower, "hows") || strings.Contains(lower, "how's")) + if !wantsURL && !(wantsRecent && wantsHost) && !wantsStatus { return false } - hints := strings.Contains(lower, "talked") || strings.Contains(lower, "previous") || strings.Contains(lower, "built") || strings.Contains(lower, "that site") || strings.Contains(lower, "that website") || wantsRecent + hints := strings.Contains(lower, "talked") || strings.Contains(lower, "previous") || strings.Contains(lower, "built") || strings.Contains(lower, "that site") || strings.Contains(lower, "that website") || wantsRecent || wantsStatus if !hints { return false } diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index a99e167a4..c09b20b41 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -665,7 +665,9 @@ func (m *Manager) PublishPreview(root, entry, slug string) (string, string, stri if existing, ok := m.previews[slug]; ok { createdAt = existing.CreatedAt } - m.previews[slug] = previewMount{Root: absRoot, Entry: entry, CreatedAt: createdAt, UpdatedAt: now} + m.previews = map[string]previewMount{ + slug: {Root: absRoot, Entry: entry, CreatedAt: createdAt, UpdatedAt: now}, + } m.mu.Unlock() tailscaleBase, localBase := m.previewBases() diff --git a/pkg/tools/preview.go b/pkg/tools/preview.go index 8c6cb38dd..0409661d8 100644 --- a/pkg/tools/preview.go +++ b/pkg/tools/preview.go @@ -31,6 +31,7 @@ type HostPreviewTool struct { } const recentPreviewsStatePath = "state/recent_previews.json" +const defaultHostedPreviewSlug = "site" func NewHostPreviewTool(workspace string, restrict bool, publish PreviewPublisher) *HostPreviewTool { return &HostPreviewTool{workspace: workspace, restrict: restrict, publish: publish} @@ -41,7 +42,7 @@ func (t *HostPreviewTool) Name() string { } func (t *HostPreviewTool) Description() string { - return "Host a local site/app directory on the built-in preview server and return Tailscale/local URLs for review." + return "Host a local site/app directory on the built-in preview server and return one stable Tailscale/local preview URL for review." } func (t *HostPreviewTool) Parameters() map[string]any { @@ -90,6 +91,9 @@ func (t *HostPreviewTool) Execute(ctx context.Context, args map[string]any) *Too entry = strings.TrimSpace(entry) slug, _ := args["slug"].(string) slug = strings.TrimSpace(slug) + if slug == "" { + slug = defaultHostedPreviewSlug + } root := resolved if info.IsDir() { @@ -106,6 +110,10 @@ func (t *HostPreviewTool) Execute(ctx context.Context, args map[string]any) *Too entry = "index.html" } } else { + ext := strings.ToLower(filepath.Ext(resolved)) + if ext != ".html" && ext != ".htm" { + return ErrorResult("path must be a site directory or an HTML entry file") + } root = filepath.Dir(resolved) if entry == "" { entry = filepath.Base(resolved) @@ -172,21 +180,7 @@ func SaveRecentPreview(workspace string, preview *HostedPreview) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } - previews, err := LoadRecentPreviews(workspace) - if err != nil { - return err - } - updated := make([]HostedPreview, 0, len(previews)+1) - updated = append(updated, *preview) - for _, item := range previews { - if strings.TrimSpace(item.Slug) == strings.TrimSpace(preview.Slug) { - continue - } - updated = append(updated, item) - if len(updated) >= 8 { - break - } - } + updated := []HostedPreview{*preview} data, err := json.MarshalIndent(updated, "", " ") if err != nil { return err