Use one stable preview URL and document live provider matrix
This commit is contained in:
parent
0eccc5f89f
commit
55cf58cef5
4 changed files with 51 additions and 22 deletions
30
README.md
30
README.md
|
|
@ -82,6 +82,36 @@
|
||||||
|
|
||||||
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
|
<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
|
## 🦾 Demonstration
|
||||||
|
|
||||||
### 🛠️ Standard Assistant Workflows
|
### 🛠️ Standard Assistant Workflows
|
||||||
|
|
|
||||||
|
|
@ -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")
|
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")
|
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")
|
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
|
return false
|
||||||
}
|
}
|
||||||
wantsPreview := strings.Contains(lower, "preview") || strings.Contains(lower, "site") || strings.Contains(lower, "website") || strings.Contains(lower, "app") || strings.Contains(lower, "build")
|
if !wantsURL && !(wantsRecent && wantsHost) && !wantsStatus {
|
||||||
if !wantsPreview {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return len(al.recentPreviewInfos(agent)) > 0
|
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")
|
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")
|
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")
|
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
|
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 {
|
if !hints {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -665,7 +665,9 @@ func (m *Manager) PublishPreview(root, entry, slug string) (string, string, stri
|
||||||
if existing, ok := m.previews[slug]; ok {
|
if existing, ok := m.previews[slug]; ok {
|
||||||
createdAt = existing.CreatedAt
|
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()
|
m.mu.Unlock()
|
||||||
|
|
||||||
tailscaleBase, localBase := m.previewBases()
|
tailscaleBase, localBase := m.previewBases()
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ type HostPreviewTool struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const recentPreviewsStatePath = "state/recent_previews.json"
|
const recentPreviewsStatePath = "state/recent_previews.json"
|
||||||
|
const defaultHostedPreviewSlug = "site"
|
||||||
|
|
||||||
func NewHostPreviewTool(workspace string, restrict bool, publish PreviewPublisher) *HostPreviewTool {
|
func NewHostPreviewTool(workspace string, restrict bool, publish PreviewPublisher) *HostPreviewTool {
|
||||||
return &HostPreviewTool{workspace: workspace, restrict: restrict, publish: publish}
|
return &HostPreviewTool{workspace: workspace, restrict: restrict, publish: publish}
|
||||||
|
|
@ -41,7 +42,7 @@ func (t *HostPreviewTool) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *HostPreviewTool) Description() 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 {
|
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)
|
entry = strings.TrimSpace(entry)
|
||||||
slug, _ := args["slug"].(string)
|
slug, _ := args["slug"].(string)
|
||||||
slug = strings.TrimSpace(slug)
|
slug = strings.TrimSpace(slug)
|
||||||
|
if slug == "" {
|
||||||
|
slug = defaultHostedPreviewSlug
|
||||||
|
}
|
||||||
|
|
||||||
root := resolved
|
root := resolved
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
|
|
@ -106,6 +110,10 @@ func (t *HostPreviewTool) Execute(ctx context.Context, args map[string]any) *Too
|
||||||
entry = "index.html"
|
entry = "index.html"
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
root = filepath.Dir(resolved)
|
||||||
if entry == "" {
|
if entry == "" {
|
||||||
entry = filepath.Base(resolved)
|
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 {
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
previews, err := LoadRecentPreviews(workspace)
|
updated := []HostedPreview{*preview}
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data, err := json.MarshalIndent(updated, "", " ")
|
data, err := json.MarshalIndent(updated, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue