Use one stable preview URL and document live provider matrix

This commit is contained in:
Bernardo 2026-03-14 13:37:48 +01:00
parent 0eccc5f89f
commit 55cf58cef5
4 changed files with 51 additions and 22 deletions

View file

@ -82,6 +82,36 @@
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
## 🔌 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://<host>: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

View file

@ -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
}

View file

@ -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()

View file

@ -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