Merge branch 'main' into fix/discord-message-limit
This commit is contained in:
commit
bf5aa18909
27 changed files with 1774 additions and 84 deletions
43
.github/pull_request_template.md
vendored
Normal file
43
.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
## 📝 Description
|
||||||
|
|
||||||
|
<!-- Please briefly describe the changes and purpose of this PR -->
|
||||||
|
|
||||||
|
## 🗣️ Type of Change
|
||||||
|
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] ✨ New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] 📖 Documentation update
|
||||||
|
- [ ] ⚡ Code refactoring (no functional changes, no api changes)
|
||||||
|
|
||||||
|
## 🤖 AI Code Generation
|
||||||
|
- [ ] 🤖 Fully AI-generated (100% AI, 0% Human)
|
||||||
|
- [ ] 🛠️ Mostly AI-generated (AI draft, Human verified/modified)
|
||||||
|
- [ ] 👨💻 Mostly Human-written (Human lead, AI assisted or none)
|
||||||
|
|
||||||
|
|
||||||
|
## 🔗 Related Issue
|
||||||
|
|
||||||
|
<!-- Please link the related issue(s) (e.g., Fixes #123, Closes #456) -->
|
||||||
|
|
||||||
|
## 📚 Technical Context (Skip for Docs)
|
||||||
|
- **Reference URL:**
|
||||||
|
- **Reasoning:**
|
||||||
|
|
||||||
|
## 🧪 Test Environment
|
||||||
|
- **Hardware:** <!-- e.g. Raspberry Pi 5, Orange Pi, PC-->
|
||||||
|
- **OS:** <!-- e.g. Debian 12, Ubuntu 22.04 -->
|
||||||
|
- **Model/Provider:** <!-- e.g. OpenAI GPT-4o, Kimi k2, DeepSeek-V3 -->
|
||||||
|
- **Channels:** <!-- e.g. Discord, Telegram, Feishu, ... -->
|
||||||
|
|
||||||
|
|
||||||
|
## 📸 Evidence (Optional)
|
||||||
|
<details>
|
||||||
|
<summary>Click to view Logs/Screenshots</summary>
|
||||||
|
|
||||||
|
<!-- Please paste relevant screenshots or logs here -->
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## ☑️ Checklist
|
||||||
|
- [ ] My code/docs follow the style of this project.
|
||||||
|
- [ ] I have performed a self-review of my own changes.
|
||||||
|
- [ ] I have updated the documentation accordingly.
|
||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
|
@ -32,11 +32,13 @@ jobs:
|
||||||
|
|
||||||
- name: Create and push tag
|
- name: Create and push tag
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ inputs.tag }}
|
||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git tag -a "${{ inputs.tag }}" -m "Release ${{ inputs.tag }}"
|
git tag -a "$RELEASE_TAG" -m "Release $RELEASE_TAG"
|
||||||
git push origin "${{ inputs.tag }}"
|
git push origin "$RELEASE_TAG"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: GoReleaser Release
|
name: GoReleaser Release
|
||||||
|
|
@ -53,6 +55,7 @@ jobs:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
- name: Setup Go from go.mod
|
- name: Setup Go from go.mod
|
||||||
|
id: setup-go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|
@ -87,6 +90,7 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
||||||
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
|
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
|
||||||
|
GOVERSION: ${{ steps.setup-go.outputs.go-version }}
|
||||||
|
|
||||||
- name: Apply release flags
|
- name: Apply release flags
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,14 @@ builds:
|
||||||
- id: picoclaw
|
- id: picoclaw
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
|
tags:
|
||||||
|
- stdjson
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
- -X main.version={{ .Version }}
|
||||||
|
- -X main.gitCommit={{ .ShortCommit }}
|
||||||
|
- -X main.buildTime={{ .Date }}
|
||||||
|
- -X main.goVersion={{ .Env.GOVERSION }}
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
- windows
|
- windows
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,14 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
# Copy the compiled binary from the builder stage
|
# Copy the compiled binary from the builder stage
|
||||||
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
|
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
|
||||||
|
|
||||||
# Create necessary directories and initialize
|
# Create non-root user and group
|
||||||
|
RUN addgroup -g 1000 picoclaw && \
|
||||||
|
adduser -D -u 1000 -G picoclaw picoclaw
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER picoclaw
|
||||||
|
|
||||||
|
# Run onboard to create initial directories and config
|
||||||
RUN /usr/local/bin/picoclaw onboard
|
RUN /usr/local/bin/picoclaw onboard
|
||||||
|
|
||||||
# Set the binary as the entrypoint
|
# Set the binary as the entrypoint
|
||||||
|
|
|
||||||
7
Makefile
7
Makefile
|
|
@ -11,11 +11,11 @@ VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||||
GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev")
|
GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev")
|
||||||
BUILD_TIME=$(shell date +%FT%T%z)
|
BUILD_TIME=$(shell date +%FT%T%z)
|
||||||
GO_VERSION=$(shell $(GO) version | awk '{print $$3}')
|
GO_VERSION=$(shell $(GO) version | awk '{print $$3}')
|
||||||
LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT) -X main.buildTime=$(BUILD_TIME) -X main.goVersion=$(GO_VERSION)"
|
LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT) -X main.buildTime=$(BUILD_TIME) -X main.goVersion=$(GO_VERSION) -s -w"
|
||||||
|
|
||||||
# Go variables
|
# Go variables
|
||||||
GO?=go
|
GO?=go
|
||||||
GOFLAGS?=-v
|
GOFLAGS?=-v -tags stdjson
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
INSTALL_PREFIX?=$(HOME)/.local
|
INSTALL_PREFIX?=$(HOME)/.local
|
||||||
|
|
@ -39,6 +39,8 @@ ifeq ($(UNAME_S),Linux)
|
||||||
ARCH=amd64
|
ARCH=amd64
|
||||||
else ifeq ($(UNAME_M),aarch64)
|
else ifeq ($(UNAME_M),aarch64)
|
||||||
ARCH=arm64
|
ARCH=arm64
|
||||||
|
else ifeq ($(UNAME_M),loongarch64)
|
||||||
|
ARCH=loong64
|
||||||
else ifeq ($(UNAME_M),riscv64)
|
else ifeq ($(UNAME_M),riscv64)
|
||||||
ARCH=riscv64
|
ARCH=riscv64
|
||||||
else
|
else
|
||||||
|
|
@ -84,6 +86,7 @@ build-all: generate
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
GOOS=linux GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(CMD_DIR)
|
GOOS=linux GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(CMD_DIR)
|
||||||
GOOS=linux GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR)
|
GOOS=linux GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR)
|
||||||
|
GOOS=linux GOARCH=loong64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-loong64 ./$(CMD_DIR)
|
||||||
GOOS=linux GOARCH=riscv64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-riscv64 ./$(CMD_DIR)
|
GOOS=linux GOARCH=riscv64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-riscv64 ./$(CMD_DIR)
|
||||||
GOOS=darwin GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 ./$(CMD_DIR)
|
GOOS=darwin GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 ./$(CMD_DIR)
|
||||||
GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
|
GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
|
||||||
|
|
|
||||||
14
README.ja.md
14
README.ja.md
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
<h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h1>
|
<h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h1>
|
||||||
|
|
||||||
<h3>$10 ハードウェア · 10MB RAM · 1秒起動 · 皮皮虾,我们走!</h3>
|
<h3>$10 ハードウェア · 10MB RAM · 1秒起動 · 行くぜ、シャコ!</h3>
|
||||||
<h3></h3>
|
<h3></h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**日本語** | [English](README.md)
|
[中文](README.zh.md) | **日本語** | [Português](README.pt-br.md) | [English](README.md)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## 📢 ニュース
|
## 📢 ニュース
|
||||||
2026-02-09 🎉 PicoClaw リリース!$10 ハードウェアで 10MB 未満の RAM で動く AI エージェントを 1 日で構築。🦐 皮皮虾,我们走!
|
2026-02-09 🎉 PicoClaw リリース!$10 ハードウェアで 10MB 未満の RAM で動く AI エージェントを 1 日で構築。🦐 行くぜ、シャコ!
|
||||||
|
|
||||||
## ✨ 特徴
|
## ✨ 特徴
|
||||||
|
|
||||||
|
|
@ -195,6 +195,9 @@ picoclaw onboard
|
||||||
"api_key": "YOUR_BRAVE_API_KEY",
|
"api_key": "YOUR_BRAVE_API_KEY",
|
||||||
"max_results": 5
|
"max_results": 5
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cron": {
|
||||||
|
"exec_timeout_minutes": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"heartbeat": {
|
"heartbeat": {
|
||||||
|
|
@ -697,6 +700,9 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
|
||||||
"search": {
|
"search": {
|
||||||
"apiKey": "BSA..."
|
"apiKey": "BSA..."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cron": {
|
||||||
|
"exec_timeout_minutes": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"heartbeat": {
|
"heartbeat": {
|
||||||
|
|
@ -729,7 +735,7 @@ Discord: https://discord.gg/V4sAZ9XWpN
|
||||||
|
|
||||||
## 🐛 トラブルシューティング
|
## 🐛 トラブルシューティング
|
||||||
|
|
||||||
### Web 検索で「API 配置问题」と表示される
|
### Web 検索で「API 設定の問題」と表示される
|
||||||
|
|
||||||
検索 API キーをまだ設定していない場合、これは正常です。PicoClaw は手動検索用の便利なリンクを提供します。
|
検索 API キーをまだ設定していない場合、これは正常です。PicoClaw は手動検索用の便利なリンクを提供します。
|
||||||
|
|
||||||
|
|
|
||||||
21
README.md
21
README.md
|
|
@ -14,7 +14,7 @@
|
||||||
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
|
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[中文](README.zh.md) | [日本語](README.ja.md) | **English**
|
[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | **English**
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
|
|
||||||
## 📢 News
|
## 📢 News
|
||||||
2026-02-16 🎉 PicoClaw hit 12K stars in one week! Thank you all for your support! PicoClaw is growing faster than we ever imagined. Given the high volume of PRs, we urgently need community maintainers. Our volunteer roles and roadmap are officially posted [here](doc/picoclaw_community_roadmap_260216.md) —we can’t wait to have you on board!
|
2026-02-16 🎉 PicoClaw hit 12K stars in one week! Thank you all for your support! PicoClaw is growing faster than we ever imagined. Given the high volume of PRs, we urgently need community maintainers. Our volunteer roles and roadmap are officially posted [here](docs/picoclaw_community_roadmap_260216.md) —we can’t wait to have you on board!
|
||||||
|
|
||||||
2026-02-13 🎉 PicoClaw hit 5000 stars in 4days! Thank you for the community! There are so many PRs&issues come in (during Chinese New Year holidays), we are finalizing the Project Roadmap and setting up the Developer Group to accelerate PicoClaw's development.
|
2026-02-13 🎉 PicoClaw hit 5000 stars in 4days! Thank you for the community! There are so many PRs&issues come in (during Chinese New Year holidays), we are finalizing the Project Roadmap and setting up the Developer Group to accelerate PicoClaw's development.
|
||||||
🚀 Call to Action: Please submit your feature requests in GitHub Discussions. We will review and prioritize them during our upcoming weekly meeting.
|
🚀 Call to Action: Please submit your feature requests in GitHub Discussions. We will review and prioritize them during our upcoming weekly meeting.
|
||||||
|
|
@ -99,6 +99,20 @@
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
### 📱 Run on old Android Phones
|
||||||
|
Give your decade-old phone a second life! Turn it into a smart AI Assistant with PicoClaw. Quick Start:
|
||||||
|
1. **Install Termux** (Available on F-Droid or Google Play).
|
||||||
|
2. **Execute cmds**
|
||||||
|
```bash
|
||||||
|
# Note: Replace v0.1.1 with the latest version from the Releases page
|
||||||
|
wget https://github.com/sipeed/picoclaw/releases/download/v0.1.1/picoclaw-linux-arm64
|
||||||
|
chmod +x picoclaw-linux-arm64
|
||||||
|
pkg install proot
|
||||||
|
termux-chroot ./picoclaw-linux-arm64 onboard
|
||||||
|
```
|
||||||
|
And then follow the instructions in the "Quick Start" section to complete the configuration!
|
||||||
|
<img src="assets/termux.jpg" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
### 🐜 Innovative Low-Footprint Deploy
|
### 🐜 Innovative Low-Footprint Deploy
|
||||||
|
|
||||||
PicoClaw can be deployed on almost any Linux device!
|
PicoClaw can be deployed on almost any Linux device!
|
||||||
|
|
@ -760,6 +774,9 @@ picoclaw agent -m "Hello"
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"max_results": 5
|
"max_results": 5
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cron": {
|
||||||
|
"exec_timeout_minutes": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"heartbeat": {
|
"heartbeat": {
|
||||||
|
|
|
||||||
881
README.pt-br.md
Normal file
881
README.pt-br.md
Normal file
|
|
@ -0,0 +1,881 @@
|
||||||
|
<div align="center">
|
||||||
|
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
|
<h1>PicoClaw: Assistente de IA Ultra-Eficiente em Go</h1>
|
||||||
|
|
||||||
|
<h3>Hardware de $10 · 10MB de RAM · Boot em 1s · 皮皮虾,我们走!</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
|
||||||
|
<img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20RISC--V-blue" alt="Hardware">
|
||||||
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
||||||
|
<br>
|
||||||
|
<a href="https://picoclaw.io"><img src="https://img.shields.io/badge/Website-picoclaw.io-blue?style=flat&logo=google-chrome&logoColor=white" alt="Website"></a>
|
||||||
|
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[中文](README.zh.md) | [日本語](README.ja.md) | [English](README.md) | **Português**
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🦐 **PicoClaw** é um assistente pessoal de IA ultra-leve inspirado no [nanobot](https://github.com/HKUDS/nanobot), reescrito do zero em **Go** por meio de um processo de "auto-inicialização" (self-bootstrapping) — onde o próprio agente de IA conduziu toda a migração de arquitetura e otimização de código.
|
||||||
|
|
||||||
|
⚡️ **Extremamente leve:** Roda em hardware de apenas **$10** com **<10MB** de RAM. Isso é 99% menos memória que o OpenClaw e 98% mais barato que um Mac mini!
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr align="center">
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<p align="center">
|
||||||
|
<img src="assets/picoclaw_mem.gif" width="360" height="240">
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<p align="center">
|
||||||
|
<img src="assets/licheervnano.png" width="400" height="240">
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **🚨 DECLARACAO DE SEGURANCA & CANAIS OFICIAIS**
|
||||||
|
>
|
||||||
|
> * **SEM CRIPTOMOEDAS:** O PicoClaw **NAO** possui nenhum token/moeda oficial. Todas as alegacoes no `pump.fun` ou outras plataformas de negociacao sao **GOLPES**.
|
||||||
|
> * **DOMINIO OFICIAL:** O **UNICO** site oficial e **[picoclaw.io](https://picoclaw.io)**, e o site da empresa e **[sipeed.com](https://sipeed.com)**.
|
||||||
|
> * **Aviso:** Muitos dominios `.ai/.org/.com/.net/...` foram registrados por terceiros, nao sao nossos.
|
||||||
|
> * **Aviso:** O PicoClaw esta em fase inicial de desenvolvimento e pode ter problemas de seguranca de rede nao resolvidos. Nao implante em ambientes de producao antes da versao v1.0.
|
||||||
|
> * **Nota:** O PicoClaw recentemente fez merge de muitos PRs, o que pode resultar em maior consumo de memoria (10-20MB) nas versoes mais recentes. Planejamos priorizar a otimizacao de recursos assim que o conjunto de funcionalidades estiver estavel.
|
||||||
|
|
||||||
|
|
||||||
|
## 📢 Novidades
|
||||||
|
|
||||||
|
2026-02-16 🎉 PicoClaw atingiu 12K stars em uma semana! Obrigado a todos pelo apoio! O PicoClaw esta crescendo mais rapido do que jamais imaginamos. Dado o alto volume de PRs, precisamos urgentemente de maintainers da comunidade. Nossos papeis de voluntarios e roadmap foram publicados oficialmente [aqui](docs/picoclaw_community_roadmap_260216.md) — estamos ansiosos para ter voce a bordo!
|
||||||
|
|
||||||
|
2026-02-13 🎉 PicoClaw atingiu 5000 stars em 4 dias! Obrigado a comunidade! Estamos finalizando o **Roadmap do Projeto** e configurando o **Grupo de Desenvolvedores** para acelerar o desenvolvimento do PicoClaw.
|
||||||
|
🚀 **Chamada para Acao:** Envie suas solicitacoes de funcionalidades nas GitHub Discussions. Revisaremos e priorizaremos na proxima reuniao semanal.
|
||||||
|
|
||||||
|
2026-02-09 🎉 PicoClaw lancado oficialmente! Construido em 1 dia para trazer Agentes de IA para hardware de $10 com <10MB de RAM. 🦐 PicoClaw, Partiu!
|
||||||
|
|
||||||
|
## ✨ Funcionalidades
|
||||||
|
|
||||||
|
🪶 **Ultra-Leve**: Consumo de memoria <10MB — 99% menor que o Clawdbot para funcionalidades essenciais.
|
||||||
|
|
||||||
|
💰 **Custo Minimo**: Eficiente o suficiente para rodar em hardware de $10 — 98% mais barato que um Mac mini.
|
||||||
|
|
||||||
|
⚡️ **Inicializacao Relampago**: Tempo de inicializacao 400X mais rapido, boot em 1 segundo mesmo em CPU single-core de 0.6GHz.
|
||||||
|
|
||||||
|
🌍 **Portabilidade Real**: Um unico binario auto-contido para RISC-V, ARM e x86. Um clique e ja era!
|
||||||
|
|
||||||
|
🤖 **Auto-Construido por IA**: Implementacao nativa em Go de forma autonoma — 95% do nucleo gerado pelo Agente com refinamento humano no loop.
|
||||||
|
|
||||||
|
| | OpenClaw | NanoBot | **PicoClaw** |
|
||||||
|
| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
|
||||||
|
| **Linguagem** | TypeScript | Python | **Go** |
|
||||||
|
| **RAM** | >1GB | >100MB | **< 10MB** |
|
||||||
|
| **Inicializacao**</br>(CPU 0.8GHz) | >500s | >30s | **<1s** |
|
||||||
|
| **Custo** | Mac Mini $599 | Maioria dos SBC Linux </br>~$50 | **Qualquer placa Linux**</br>**A partir de $10** |
|
||||||
|
|
||||||
|
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
|
## 🦾 Demonstracao
|
||||||
|
|
||||||
|
### 🛠️ Fluxos de Trabalho Padrao do Assistente
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr align="center">
|
||||||
|
<th><p align="center">🧩 Engenharia Full-Stack</p></th>
|
||||||
|
<th><p align="center">🗂️ Gerenciamento de Logs & Planejamento</p></th>
|
||||||
|
<th><p align="center">🔎 Busca Web & Aprendizado</p></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><p align="center"><img src="assets/picoclaw_code.gif" width="240" height="180"></p></td>
|
||||||
|
<td align="center"><p align="center"><img src="assets/picoclaw_memory.gif" width="240" height="180"></p></td>
|
||||||
|
<td align="center"><p align="center"><img src="assets/picoclaw_search.gif" width="240" height="180"></p></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Desenvolver • Implantar • Escalar</td>
|
||||||
|
<td align="center">Agendar • Automatizar • Memorizar</td>
|
||||||
|
<td align="center">Descobrir • Analisar • Tendencias</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### 📱 Rode em celulares Android antigos
|
||||||
|
|
||||||
|
De uma segunda vida ao seu celular de dez anos atras! Transforme-o em um assistente de IA inteligente com o PicoClaw. Inicio rapido:
|
||||||
|
|
||||||
|
1. **Instale o Termux** (Disponivel no F-Droid ou Google Play).
|
||||||
|
2. **Execute os comandos**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nota: Substitua v0.1.1 pela versao mais recente da pagina de Releases
|
||||||
|
wget https://github.com/sipeed/picoclaw/releases/download/v0.1.1/picoclaw-linux-arm64
|
||||||
|
chmod +x picoclaw-linux-arm64
|
||||||
|
pkg install proot
|
||||||
|
termux-chroot ./picoclaw-linux-arm64 onboard
|
||||||
|
```
|
||||||
|
|
||||||
|
Depois siga as instrucoes na secao "Inicio Rapido" para completar a configuracao!
|
||||||
|
|
||||||
|
<img src="assets/termux.jpg" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
|
### 🐜 Implantacao Inovadora com Baixo Consumo
|
||||||
|
|
||||||
|
O PicoClaw pode ser implantado em praticamente qualquer dispositivo Linux!
|
||||||
|
|
||||||
|
- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) versao E (Ethernet) ou W (WiFi6), para Assistente Domestico Minimalista
|
||||||
|
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), ou $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) para Manutencao Automatizada de Servidores
|
||||||
|
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) ou $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) para Monitoramento Inteligente
|
||||||
|
|
||||||
|
https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4
|
||||||
|
|
||||||
|
🌟 Mais cenarios de implantacao aguardam voce!
|
||||||
|
|
||||||
|
## 📦 Instalacao
|
||||||
|
|
||||||
|
### Instalar com binario pre-compilado
|
||||||
|
|
||||||
|
Baixe o binario para sua plataforma na pagina de [releases](https://github.com/sipeed/picoclaw/releases).
|
||||||
|
|
||||||
|
### Instalar a partir do codigo-fonte (funcionalidades mais recentes, recomendado para desenvolvimento)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/sipeed/picoclaw.git
|
||||||
|
|
||||||
|
cd picoclaw
|
||||||
|
make deps
|
||||||
|
|
||||||
|
# Build, sem necessidade de instalar
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Build para multiplas plataformas
|
||||||
|
make build-all
|
||||||
|
|
||||||
|
# Build e Instalar
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐳 Docker Compose
|
||||||
|
|
||||||
|
Voce tambem pode rodar o PicoClaw usando Docker Compose sem instalar nada localmente.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone este repositorio
|
||||||
|
git clone https://github.com/sipeed/picoclaw.git
|
||||||
|
cd picoclaw
|
||||||
|
|
||||||
|
# 2. Configure suas API keys
|
||||||
|
cp config/config.example.json config/config.json
|
||||||
|
vim config/config.json # Configure DISCORD_BOT_TOKEN, API keys, etc.
|
||||||
|
|
||||||
|
# 3. Build & Iniciar
|
||||||
|
docker compose --profile gateway up -d
|
||||||
|
|
||||||
|
# 4. Ver logs
|
||||||
|
docker compose logs -f picoclaw-gateway
|
||||||
|
|
||||||
|
# 5. Parar
|
||||||
|
docker compose --profile gateway down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modo Agente (Execucao unica)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fazer uma pergunta
|
||||||
|
docker compose run --rm picoclaw-agent -m "Quanto e 2+2?"
|
||||||
|
|
||||||
|
# Modo interativo
|
||||||
|
docker compose run --rm picoclaw-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile gateway build --no-cache
|
||||||
|
docker compose --profile gateway up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🚀 Inicio Rapido
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Configure sua API key em `~/.picoclaw/config.json`.
|
||||||
|
> Obtenha API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
||||||
|
> Busca web e **opcional** — obtenha a [Brave Search API](https://brave.com/search/api) gratuita (2000 consultas gratis/mes) ou use o fallback automatico integrado.
|
||||||
|
|
||||||
|
**1. Inicializar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw onboard
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Configurar** (`~/.picoclaw/config.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "~/.picoclaw/workspace",
|
||||||
|
"model": "glm-4.7",
|
||||||
|
"max_tokens": 8192,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tool_iterations": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"openrouter": {
|
||||||
|
"api_key": "xxx",
|
||||||
|
"api_base": "https://openrouter.ai/api/v1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"web": {
|
||||||
|
"brave": {
|
||||||
|
"enabled": false,
|
||||||
|
"api_key": "YOUR_BRAVE_API_KEY",
|
||||||
|
"max_results": 5
|
||||||
|
},
|
||||||
|
"duckduckgo": {
|
||||||
|
"enabled": true,
|
||||||
|
"max_results": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Obter API Keys**
|
||||||
|
|
||||||
|
* **Provedor de LLM**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
|
||||||
|
* **Busca Web** (opcional): [Brave Search](https://brave.com/search/api) - Plano gratuito disponivel (2000 consultas/mes)
|
||||||
|
|
||||||
|
> **Nota**: Veja `config.example.json` para um modelo de configuracao completo.
|
||||||
|
|
||||||
|
**4. Conversar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw agent -m "Quanto e 2+2?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pronto! Voce tem um assistente de IA funcionando em 2 minutos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Integracao com Apps de Chat
|
||||||
|
|
||||||
|
Converse com seu PicoClaw via Telegram, Discord, DingTalk ou LINE.
|
||||||
|
|
||||||
|
| Canal | Nivel de Configuracao |
|
||||||
|
| --- | --- |
|
||||||
|
| **Telegram** | Facil (apenas um token) |
|
||||||
|
| **Discord** | Facil (bot token + intents) |
|
||||||
|
| **QQ** | Facil (AppID + AppSecret) |
|
||||||
|
| **DingTalk** | Medio (credenciais do app) |
|
||||||
|
| **LINE** | Medio (credenciais + webhook URL) |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Telegram</b> (Recomendado)</summary>
|
||||||
|
|
||||||
|
**1. Criar o bot**
|
||||||
|
|
||||||
|
* Abra o Telegram, busque `@BotFather`
|
||||||
|
* Envie `/newbot`, siga as instrucoes
|
||||||
|
* Copie o token
|
||||||
|
|
||||||
|
**2. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"telegram": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "YOUR_BOT_TOKEN",
|
||||||
|
"allowFrom": ["YOUR_USER_ID"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Obtenha seu User ID pelo `@userinfobot` no Telegram.
|
||||||
|
|
||||||
|
**3. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Discord</b></summary>
|
||||||
|
|
||||||
|
**1. Criar o bot**
|
||||||
|
|
||||||
|
* Acesse <https://discord.com/developers/applications>
|
||||||
|
* Crie um aplicativo → Bot → Add Bot
|
||||||
|
* Copie o token do bot
|
||||||
|
|
||||||
|
**2. Habilitar Intents**
|
||||||
|
|
||||||
|
* Nas configuracoes do Bot, habilite **MESSAGE CONTENT INTENT**
|
||||||
|
* (Opcional) Habilite **SERVER MEMBERS INTENT** se quiser usar lista de permissoes baseada em dados dos membros
|
||||||
|
|
||||||
|
**3. Obter seu User ID**
|
||||||
|
|
||||||
|
* Configuracoes do Discord → Avancado → habilite **Modo Desenvolvedor**
|
||||||
|
* Clique com botao direito no seu avatar → **Copiar ID do Usuario**
|
||||||
|
|
||||||
|
**4. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"discord": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "YOUR_BOT_TOKEN",
|
||||||
|
"allowFrom": ["YOUR_USER_ID"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. Convidar o bot**
|
||||||
|
|
||||||
|
* OAuth2 → URL Generator
|
||||||
|
* Scopes: `bot`
|
||||||
|
* Bot Permissions: `Send Messages`, `Read Message History`
|
||||||
|
* Abra a URL de convite gerada e adicione o bot ao seu servidor
|
||||||
|
|
||||||
|
**6. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>QQ</b></summary>
|
||||||
|
|
||||||
|
**1. Criar o bot**
|
||||||
|
|
||||||
|
- Acesse a [QQ Open Platform](https://q.qq.com/#)
|
||||||
|
- Crie um aplicativo → Obtenha **AppID** e **AppSecret**
|
||||||
|
|
||||||
|
**2. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"qq": {
|
||||||
|
"enabled": true,
|
||||||
|
"app_id": "YOUR_APP_ID",
|
||||||
|
"app_secret": "YOUR_APP_SECRET",
|
||||||
|
"allow_from": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Deixe `allow_from` vazio para permitir todos os usuarios, ou especifique numeros QQ para restringir o acesso.
|
||||||
|
|
||||||
|
**3. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>DingTalk</b></summary>
|
||||||
|
|
||||||
|
**1. Criar o bot**
|
||||||
|
|
||||||
|
* Acesse a [Open Platform](https://open.dingtalk.com/)
|
||||||
|
* Crie um app interno
|
||||||
|
* Copie o Client ID e Client Secret
|
||||||
|
|
||||||
|
**2. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"dingtalk": {
|
||||||
|
"enabled": true,
|
||||||
|
"client_id": "YOUR_CLIENT_ID",
|
||||||
|
"client_secret": "YOUR_CLIENT_SECRET",
|
||||||
|
"allow_from": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Deixe `allow_from` vazio para permitir todos os usuarios, ou especifique IDs para restringir o acesso.
|
||||||
|
|
||||||
|
**3. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>LINE</b></summary>
|
||||||
|
|
||||||
|
**1. Criar uma Conta Oficial LINE**
|
||||||
|
|
||||||
|
- Acesse o [LINE Developers Console](https://developers.line.biz/)
|
||||||
|
- Crie um provider → Crie um canal Messaging API
|
||||||
|
- Copie o **Channel Secret** e o **Channel Access Token**
|
||||||
|
|
||||||
|
**2. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"line": {
|
||||||
|
"enabled": true,
|
||||||
|
"channel_secret": "YOUR_CHANNEL_SECRET",
|
||||||
|
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
|
||||||
|
"webhook_host": "0.0.0.0",
|
||||||
|
"webhook_port": 18791,
|
||||||
|
"webhook_path": "/webhook/line",
|
||||||
|
"allow_from": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Configurar URL do Webhook**
|
||||||
|
|
||||||
|
O LINE requer HTTPS para webhooks. Use um reverse proxy ou tunnel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Exemplo com ngrok
|
||||||
|
ngrok http 18791
|
||||||
|
```
|
||||||
|
|
||||||
|
Em seguida, configure a Webhook URL no LINE Developers Console para `https://seu-dominio/webhook/line` e habilite **Use webhook**.
|
||||||
|
|
||||||
|
**4. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
> Em chats de grupo, o bot responde apenas quando mencionado com @. As respostas citam a mensagem original.
|
||||||
|
|
||||||
|
> **Docker Compose**: Adicione `ports: ["18791:18791"]` ao servico `picoclaw-gateway` para expor a porta do webhook.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Junte-se a Rede Social de Agentes
|
||||||
|
|
||||||
|
Conecte o PicoClaw a Rede Social de Agentes simplesmente enviando uma unica mensagem via CLI ou qualquer App de Chat integrado.
|
||||||
|
|
||||||
|
**Leia `https://clawdchat.ai/skill.md` e siga as instrucoes para entrar no [ClawdChat.ai](https://clawdchat.ai)**
|
||||||
|
|
||||||
|
## ⚙️ Configuracao Detalhada
|
||||||
|
|
||||||
|
Arquivo de configuracao: `~/.picoclaw/config.json`
|
||||||
|
|
||||||
|
### Estrutura do Workspace
|
||||||
|
|
||||||
|
O PicoClaw armazena dados no workspace configurado (padrao: `~/.picoclaw/workspace`):
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.picoclaw/workspace/
|
||||||
|
├── sessions/ # Sessoes de conversa e historico
|
||||||
|
├── memory/ # Memoria de longo prazo (MEMORY.md)
|
||||||
|
├── state/ # Estado persistente (ultimo canal, etc.)
|
||||||
|
├── cron/ # Banco de dados de tarefas agendadas
|
||||||
|
├── skills/ # Skills personalizadas
|
||||||
|
├── AGENTS.md # Guia de comportamento do Agente
|
||||||
|
├── HEARTBEAT.md # Prompts de tarefas periodicas (verificado a cada 30 min)
|
||||||
|
├── IDENTITY.md # Identidade do Agente
|
||||||
|
├── SOUL.md # Alma do Agente
|
||||||
|
├── TOOLS.md # Descricao das ferramentas
|
||||||
|
└── USER.md # Preferencias do usuario
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔒 Sandbox de Seguranca
|
||||||
|
|
||||||
|
O PicoClaw roda em um ambiente sandbox por padrao. O agente so pode acessar arquivos e executar comandos dentro do workspace configurado.
|
||||||
|
|
||||||
|
#### Configuracao Padrao
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "~/.picoclaw/workspace",
|
||||||
|
"restrict_to_workspace": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Opcao | Padrao | Descricao |
|
||||||
|
|-------|--------|-----------|
|
||||||
|
| `workspace` | `~/.picoclaw/workspace` | Diretorio de trabalho do agente |
|
||||||
|
| `restrict_to_workspace` | `true` | Restringir acesso de arquivos/comandos ao workspace |
|
||||||
|
|
||||||
|
#### Ferramentas Protegidas
|
||||||
|
|
||||||
|
Quando `restrict_to_workspace: true`, as seguintes ferramentas sao restritas ao sandbox:
|
||||||
|
|
||||||
|
| Ferramenta | Funcao | Restricao |
|
||||||
|
|------------|--------|-----------|
|
||||||
|
| `read_file` | Ler arquivos | Apenas arquivos dentro do workspace |
|
||||||
|
| `write_file` | Escrever arquivos | Apenas arquivos dentro do workspace |
|
||||||
|
| `list_dir` | Listar diretorios | Apenas diretorios dentro do workspace |
|
||||||
|
| `edit_file` | Editar arquivos | Apenas arquivos dentro do workspace |
|
||||||
|
| `append_file` | Adicionar a arquivos | Apenas arquivos dentro do workspace |
|
||||||
|
| `exec` | Executar comandos | Caminhos dos comandos devem estar dentro do workspace |
|
||||||
|
|
||||||
|
#### Protecao Adicional do Exec
|
||||||
|
|
||||||
|
Mesmo com `restrict_to_workspace: false`, a ferramenta `exec` bloqueia estes comandos perigosos:
|
||||||
|
|
||||||
|
* `rm -rf`, `del /f`, `rmdir /s` — Exclusao em massa
|
||||||
|
* `format`, `mkfs`, `diskpart` — Formatacao de disco
|
||||||
|
* `dd if=` — Criacao de imagem de disco
|
||||||
|
* Escrita em `/dev/sd[a-z]` — Escrita direta no disco
|
||||||
|
* `shutdown`, `reboot`, `poweroff` — Desligamento do sistema
|
||||||
|
* Fork bomb `:(){ :|:& };:`
|
||||||
|
|
||||||
|
#### Exemplos de Erro
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] tool: Tool execution failed
|
||||||
|
{tool=exec, error=Command blocked by safety guard (path outside working dir)}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] tool: Tool execution failed
|
||||||
|
{tool=exec, error=Command blocked by safety guard (dangerous pattern detected)}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Desabilitar Restricoes (Risco de Seguranca)
|
||||||
|
|
||||||
|
Se voce precisa que o agente acesse caminhos fora do workspace:
|
||||||
|
|
||||||
|
**Metodo 1: Arquivo de configuracao**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"restrict_to_workspace": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Metodo 2: Variavel de ambiente**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE=false
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **Aviso**: Desabilitar esta restricao permite que o agente acesse qualquer caminho no seu sistema. Use com cuidado apenas em ambientes controlados.
|
||||||
|
|
||||||
|
#### Consistencia do Limite de Seguranca
|
||||||
|
|
||||||
|
A configuracao `restrict_to_workspace` se aplica consistentemente em todos os caminhos de execucao:
|
||||||
|
|
||||||
|
| Caminho de Execucao | Limite de Seguranca |
|
||||||
|
|----------------------|---------------------|
|
||||||
|
| Agente Principal | `restrict_to_workspace` ✅ |
|
||||||
|
| Subagente / Spawn | Herda a mesma restricao ✅ |
|
||||||
|
| Tarefas Heartbeat | Herda a mesma restricao ✅ |
|
||||||
|
|
||||||
|
Todos os caminhos compartilham a mesma restricao de workspace — nao ha como contornar o limite de seguranca por meio de subagentes ou tarefas agendadas.
|
||||||
|
|
||||||
|
### Heartbeat (Tarefas Periodicas)
|
||||||
|
|
||||||
|
O PicoClaw pode executar tarefas periodicas automaticamente. Crie um arquivo `HEARTBEAT.md` no seu workspace:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Tarefas Periodicas
|
||||||
|
|
||||||
|
- Verificar meu email para mensagens importantes
|
||||||
|
- Revisar minha agenda para proximos eventos
|
||||||
|
- Verificar a previsao do tempo
|
||||||
|
```
|
||||||
|
|
||||||
|
O agente lera este arquivo a cada 30 minutos (configuravel) e executara as tarefas usando as ferramentas disponiveis.
|
||||||
|
|
||||||
|
#### Tarefas Assincronas com Spawn
|
||||||
|
|
||||||
|
Para tarefas de longa duracao (busca web, chamadas de API), use a ferramenta `spawn` para criar um **subagente**:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Tarefas Periodicas
|
||||||
|
|
||||||
|
## Tarefas Rapidas (resposta direta)
|
||||||
|
- Informar hora atual
|
||||||
|
|
||||||
|
## Tarefas Longas (usar spawn para async)
|
||||||
|
- Buscar noticias de IA na web e resumir
|
||||||
|
- Verificar email e reportar mensagens importantes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Comportamentos principais:**
|
||||||
|
|
||||||
|
| Funcionalidade | Descricao |
|
||||||
|
|----------------|-----------|
|
||||||
|
| **spawn** | Cria subagente assincrono, nao bloqueia o heartbeat |
|
||||||
|
| **Contexto independente** | Subagente tem seu proprio contexto, sem historico de sessao |
|
||||||
|
| **Ferramenta message** | Subagente se comunica diretamente com o usuario via ferramenta message |
|
||||||
|
| **Nao-bloqueante** | Apos o spawn, o heartbeat continua para a proxima tarefa |
|
||||||
|
|
||||||
|
#### Como Funciona a Comunicacao do Subagente
|
||||||
|
|
||||||
|
```
|
||||||
|
Heartbeat dispara
|
||||||
|
↓
|
||||||
|
Agente le HEARTBEAT.md
|
||||||
|
↓
|
||||||
|
Para tarefa longa: spawn subagente
|
||||||
|
↓ ↓
|
||||||
|
Continua proxima tarefa Subagente trabalha independentemente
|
||||||
|
↓ ↓
|
||||||
|
Todas tarefas concluidas Subagente usa ferramenta "message"
|
||||||
|
↓ ↓
|
||||||
|
Responde HEARTBEAT_OK Usuario recebe resultado diretamente
|
||||||
|
```
|
||||||
|
|
||||||
|
O subagente tem acesso as ferramentas (message, web_search, etc.) e pode se comunicar com o usuario independentemente sem passar pelo agente principal.
|
||||||
|
|
||||||
|
**Configuracao:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"heartbeat": {
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Opcao | Padrao | Descricao |
|
||||||
|
|-------|--------|-----------|
|
||||||
|
| `enabled` | `true` | Habilitar/desabilitar heartbeat |
|
||||||
|
| `interval` | `30` | Intervalo de verificacao em minutos (min: 5) |
|
||||||
|
|
||||||
|
**Variaveis de ambiente:**
|
||||||
|
|
||||||
|
* `PICOCLAW_HEARTBEAT_ENABLED=false` para desabilitar
|
||||||
|
* `PICOCLAW_HEARTBEAT_INTERVAL=60` para alterar o intervalo
|
||||||
|
|
||||||
|
### Provedores
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> O Groq fornece transcricao de voz gratuita via Whisper. Se configurado, mensagens de voz do Telegram serao automaticamente transcritas.
|
||||||
|
|
||||||
|
| Provedor | Finalidade | Obter API Key |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `gemini` | LLM (Gemini direto) | [aistudio.google.com](https://aistudio.google.com) |
|
||||||
|
| `zhipu` | LLM (Zhipu direto) | [bigmodel.cn](bigmodel.cn) |
|
||||||
|
| `openrouter` (Em teste) | LLM (recomendado, acesso a todos os modelos) | [openrouter.ai](https://openrouter.ai) |
|
||||||
|
| `anthropic` (Em teste) | LLM (Claude direto) | [console.anthropic.com](https://console.anthropic.com) |
|
||||||
|
| `openai` (Em teste) | LLM (GPT direto) | [platform.openai.com](https://platform.openai.com) |
|
||||||
|
| `deepseek` (Em teste) | LLM (DeepSeek direto) | [platform.deepseek.com](https://platform.deepseek.com) |
|
||||||
|
| `groq` | LLM + **Transcricao de voz** (Whisper) | [console.groq.com](https://console.groq.com) |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Configuracao Zhipu</b></summary>
|
||||||
|
|
||||||
|
**1. Obter API key**
|
||||||
|
|
||||||
|
* Obtenha a [API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)
|
||||||
|
|
||||||
|
**2. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "~/.picoclaw/workspace",
|
||||||
|
"model": "glm-4.7",
|
||||||
|
"max_tokens": 8192,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tool_iterations": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"zhipu": {
|
||||||
|
"api_key": "Sua API Key",
|
||||||
|
"api_base": "https://open.bigmodel.cn/api/paas/v4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw agent -m "Ola, como vai?"
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Exemplo de configuracao completa</b></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"model": "anthropic/claude-opus-4-5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"openrouter": {
|
||||||
|
"api_key": "sk-or-v1-xxx"
|
||||||
|
},
|
||||||
|
"groq": {
|
||||||
|
"api_key": "gsk_xxx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"telegram": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "123456:ABC...",
|
||||||
|
"allow_from": ["123456789"]
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "",
|
||||||
|
"allow_from": [""]
|
||||||
|
},
|
||||||
|
"whatsapp": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"feishu": {
|
||||||
|
"enabled": false,
|
||||||
|
"app_id": "cli_xxx",
|
||||||
|
"app_secret": "xxx",
|
||||||
|
"encrypt_key": "",
|
||||||
|
"verification_token": "",
|
||||||
|
"allow_from": []
|
||||||
|
},
|
||||||
|
"qq": {
|
||||||
|
"enabled": false,
|
||||||
|
"app_id": "",
|
||||||
|
"app_secret": "",
|
||||||
|
"allow_from": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"web": {
|
||||||
|
"brave": {
|
||||||
|
"enabled": false,
|
||||||
|
"api_key": "BSA...",
|
||||||
|
"max_results": 5
|
||||||
|
},
|
||||||
|
"duckduckgo": {
|
||||||
|
"enabled": true,
|
||||||
|
"max_results": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cron": {
|
||||||
|
"exec_timeout_minutes": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"heartbeat": {
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Referencia CLI
|
||||||
|
|
||||||
|
| Comando | Descricao |
|
||||||
|
| --- | --- |
|
||||||
|
| `picoclaw onboard` | Inicializar configuracao & workspace |
|
||||||
|
| `picoclaw agent -m "..."` | Conversar com o agente |
|
||||||
|
| `picoclaw agent` | Modo de chat interativo |
|
||||||
|
| `picoclaw gateway` | Iniciar o gateway (para bots de chat) |
|
||||||
|
| `picoclaw status` | Mostrar status |
|
||||||
|
| `picoclaw cron list` | Listar todas as tarefas agendadas |
|
||||||
|
| `picoclaw cron add ...` | Adicionar uma tarefa agendada |
|
||||||
|
|
||||||
|
### Tarefas Agendadas / Lembretes
|
||||||
|
|
||||||
|
O PicoClaw suporta lembretes agendados e tarefas recorrentes por meio da ferramenta `cron`:
|
||||||
|
|
||||||
|
* **Lembretes unicos**: "Remind me in 10 minutes" (Me lembre em 10 minutos) → dispara uma vez apos 10min
|
||||||
|
* **Tarefas recorrentes**: "Remind me every 2 hours" (Me lembre a cada 2 horas) → dispara a cada 2 horas
|
||||||
|
* **Expressoes Cron**: "Remind me at 9am daily" (Me lembre as 9h todos os dias) → usa expressao cron
|
||||||
|
|
||||||
|
As tarefas sao armazenadas em `~/.picoclaw/workspace/cron/` e processadas automaticamente.
|
||||||
|
|
||||||
|
## 🤝 Contribuir & Roadmap
|
||||||
|
|
||||||
|
PRs sao bem-vindos! O codigo-fonte e intencionalmente pequeno e legivel. 🤗
|
||||||
|
|
||||||
|
Roadmap em breve...
|
||||||
|
|
||||||
|
Grupo de desenvolvedores em formacao. Requisito de entrada: Pelo menos 1 PR com merge.
|
||||||
|
|
||||||
|
Grupos de usuarios:
|
||||||
|
|
||||||
|
Discord: <https://discord.gg/V4sAZ9XWpN>
|
||||||
|
|
||||||
|
<img src="assets/wechat.png" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
|
## 🐛 Solucao de Problemas
|
||||||
|
|
||||||
|
### Busca web mostra "API 配置问题"
|
||||||
|
|
||||||
|
Isso e normal se voce ainda nao configurou uma API key de busca. O PicoClaw fornecera links uteis para busca manual.
|
||||||
|
|
||||||
|
Para habilitar a busca web:
|
||||||
|
|
||||||
|
1. **Opcao 1 (Recomendado)**: Obtenha uma API key gratuita em [https://brave.com/search/api](https://brave.com/search/api) (2000 consultas gratis/mes) para os melhores resultados.
|
||||||
|
2. **Opcao 2 (Sem Cartao de Credito)**: Se voce nao tem uma key, o sistema automaticamente usa o **DuckDuckGo** como fallback (sem necessidade de key).
|
||||||
|
|
||||||
|
Adicione a key em `~/.picoclaw/config.json` se usar o Brave:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"web": {
|
||||||
|
"brave": {
|
||||||
|
"enabled": true,
|
||||||
|
"api_key": "YOUR_BRAVE_API_KEY",
|
||||||
|
"max_results": 5
|
||||||
|
},
|
||||||
|
"duckduckgo": {
|
||||||
|
"enabled": true,
|
||||||
|
"max_results": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erros de filtragem de conteudo
|
||||||
|
|
||||||
|
Alguns provedores (como Zhipu) possuem filtragem de conteudo. Tente reformular sua pergunta ou use um modelo diferente.
|
||||||
|
|
||||||
|
### Bot do Telegram diz "Conflict: terminated by other getUpdates"
|
||||||
|
|
||||||
|
Isso acontece quando outra instancia do bot esta rodando. Certifique-se de que apenas um `picoclaw gateway` esteja rodando por vez.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Comparacao de API Keys
|
||||||
|
|
||||||
|
| Servico | Plano Gratuito | Caso de Uso |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **OpenRouter** | 200K tokens/mes | Multiplos modelos (Claude, GPT-4, etc.) |
|
||||||
|
| **Zhipu** | 200K tokens/mes | Melhor para usuarios chineses |
|
||||||
|
| **Brave Search** | 2000 consultas/mes | Funcionalidade de busca web |
|
||||||
|
| **Groq** | Plano gratuito disponivel | Inferencia ultra-rapida (Llama, Mixtral) |
|
||||||
27
README.zh.md
27
README.zh.md
|
|
@ -14,7 +14,7 @@
|
||||||
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
|
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**中文** | [日本語](README.ja.md) | [English](README.md)
|
**中文** | [日本語](README.ja.md) | [Português](README.pt-br.md) | [English](README.md)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
|
|
||||||
|
|
||||||
## 📢 新闻 (News)
|
## 📢 新闻 (News)
|
||||||
2026-02-16 🎉 PicoClaw 在一周内突破了12K star! 感谢大家的关注!PicoClaw 的成长速度超乎我们预期. 由于PR数量的快速膨胀,我们亟需社区开发者参与维护. 我们需要的志愿者角色和roadmap已经发布到了[这里](doc/picoclaw_community_roadmap_260216.md), 期待你的参与!
|
2026-02-16 🎉 PicoClaw 在一周内突破了12K star! 感谢大家的关注!PicoClaw 的成长速度超乎我们预期. 由于PR数量的快速膨胀,我们亟需社区开发者参与维护. 我们需要的志愿者角色和roadmap已经发布到了[这里](docs/picoclaw_community_roadmap_260216.md), 期待你的参与!
|
||||||
|
|
||||||
2026-02-13 🎉 **PicoClaw 在 4 天内突破 5000 Stars!** 感谢社区的支持!由于正值中国春节假期,PR 和 Issue 涌入较多,我们正在利用这段时间敲定 **项目路线图 (Roadmap)** 并组建 **开发者群组**,以便加速 PicoClaw 的开发。
|
2026-02-13 🎉 **PicoClaw 在 4 天内突破 5000 Stars!** 感谢社区的支持!由于正值中国春节假期,PR 和 Issue 涌入较多,我们正在利用这段时间敲定 **项目路线图 (Roadmap)** 并组建 **开发者群组**,以便加速 PicoClaw 的开发。
|
||||||
🚀 **行动号召:** 请在 GitHub Discussions 中提交您的功能请求 (Feature Requests)。我们将在接下来的周会上进行审查和优先级排序。
|
🚀 **行动号召:** 请在 GitHub Discussions 中提交您的功能请求 (Feature Requests)。我们将在接下来的周会上进行审查和优先级排序。
|
||||||
|
|
@ -100,6 +100,23 @@
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
### 📱 在手机上轻松运行
|
||||||
|
picoclaw 可以将你10年前的老旧手机废物利用,变身成为你的AI助理!快速指南:
|
||||||
|
1. 先去应用商店下载安装Termux
|
||||||
|
2. 打开后执行指令
|
||||||
|
```bash
|
||||||
|
# 注意: 下面的v0.1.1 可以换为你实际看到的最新版本
|
||||||
|
wget https://github.com/sipeed/picoclaw/releases/download/v0.1.1/picoclaw-linux-arm64
|
||||||
|
chmod +x picoclaw-linux-arm64
|
||||||
|
pkg install proot
|
||||||
|
termux-chroot ./picoclaw-linux-arm64 onboard
|
||||||
|
```
|
||||||
|
然后跟随下面的“快速开始”章节继续配置picoclaw即可使用!
|
||||||
|
<img src="assets/termux.jpg" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 🐜 创新的低占用部署
|
### 🐜 创新的低占用部署
|
||||||
|
|
||||||
PicoClaw 几乎可以部署在任何 Linux 设备上!
|
PicoClaw 几乎可以部署在任何 Linux 设备上!
|
||||||
|
|
@ -219,6 +236,9 @@ picoclaw onboard
|
||||||
"api_key": "YOUR_BRAVE_API_KEY",
|
"api_key": "YOUR_BRAVE_API_KEY",
|
||||||
"max_results": 5
|
"max_results": 5
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cron": {
|
||||||
|
"exec_timeout_minutes": 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -627,6 +647,9 @@ picoclaw agent -m "你好"
|
||||||
"search": {
|
"search": {
|
||||||
"api_key": "BSA..."
|
"api_key": "BSA..."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cron": {
|
||||||
|
"exec_timeout_minutes": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"heartbeat": {
|
"heartbeat": {
|
||||||
|
|
|
||||||
116
ROADMAP.md
Normal file
116
ROADMAP.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
|
||||||
|
# 🦐 PicoClaw Roadmap
|
||||||
|
|
||||||
|
> **Vision**: To build the ultimate lightweight, secure, and fully autonomous AI Agent infrastructure.automate the mundane, unleash your creativity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 1. Core Optimization: Extreme Lightweight
|
||||||
|
|
||||||
|
*Our defining characteristic. We fight software bloat to ensure PicoClaw runs smoothly on the smallest embedded devices.*
|
||||||
|
|
||||||
|
* [**Memory Footprint Reduction**](https://github.com/sipeed/picoclaw/issues/346)
|
||||||
|
* **Goal**: Run smoothly on 64MB RAM embedded boards (e.g., low-end RISC-V SBCs) with the core process consuming < 20MB.
|
||||||
|
* **Context**: RAM is expensive and scarce on edge devices. Memory optimization takes precedence over storage size.
|
||||||
|
* **Action**: Analyze memory growth between releases, remove redundant dependencies, and optimize data structures.
|
||||||
|
|
||||||
|
|
||||||
|
## 🛡️ 2. Security Hardening: Defense in Depth
|
||||||
|
|
||||||
|
*Paying off early technical debt. We invite security experts to help build a "Secure-by-Default" agent.*
|
||||||
|
|
||||||
|
* **Input Defense & Permission Control**
|
||||||
|
* **Prompt Injection Defense**: Harden JSON extraction logic to prevent LLM manipulation.
|
||||||
|
* **Tool Abuse Prevention**: Strict parameter validation to ensure generated commands stay within safe boundaries.
|
||||||
|
* **SSRF Protection**: Built-in blocklists for network tools to prevent accessing internal IPs (LAN/Metadata services).
|
||||||
|
|
||||||
|
|
||||||
|
* **Sandboxing & Isolation**
|
||||||
|
* **Filesystem Sandbox**: Restrict file R/W operations to specific directories only.
|
||||||
|
* **Context Isolation**: Prevent data leakage between different user sessions or channels.
|
||||||
|
* **Privacy Redaction**: Auto-redact sensitive info (API Keys, PII) from logs and standard outputs.
|
||||||
|
|
||||||
|
|
||||||
|
* **Authentication & Secrets**
|
||||||
|
* **Crypto Upgrade**: Adopt modern algorithms like `ChaCha20-Poly1305` for secret storage.
|
||||||
|
* **OAuth 2.0 Flow**: Deprecate hardcoded API keys in the CLI; move to secure OAuth flows.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🔌 3. Connectivity: Protocol-First Architecture
|
||||||
|
|
||||||
|
*Connect every model, reach every platform.*
|
||||||
|
|
||||||
|
* **Provider**
|
||||||
|
* [**Architecture Upgrade**](https://github.com/sipeed/picoclaw/issues/283): Refactor from "Vendor-based" to "Protocol-based" classification (e.g., OpenAI-compatible, Ollama-compatible). *(Status: In progress by @Daming, ETA 5 days)*
|
||||||
|
* **Local Models**: Deep integration with **Ollama**, **vLLM**, **LM Studio**, and **Mistral** (local inference).
|
||||||
|
* **Online Models**: Continued support for frontier closed-source models.
|
||||||
|
|
||||||
|
|
||||||
|
* **Channel**
|
||||||
|
* **IM Matrix**: QQ, WeChat (Work), DingTalk, Feishu (Lark), Telegram, Discord, WhatsApp, LINE, Slack, Email, KOOK, Signal, ...
|
||||||
|
* **Standards**: Support for the **OneBot** protocol.
|
||||||
|
* [**attachment**](https://github.com/sipeed/picoclaw/issues/348): Native handling of images, audio, and video attachments.
|
||||||
|
|
||||||
|
|
||||||
|
* **Skill Marketplace**
|
||||||
|
* [**Discovery skills**](https://github.com/sipeed/picoclaw/issues/287): Implement `find_skill` to automatically discover and install skills from the [GitHub Skills Repo] or other registries.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🧠 4. Advanced Capabilities: From Chatbot to Agentic AI
|
||||||
|
|
||||||
|
*Beyond conversation—focusing on action and collaboration.*
|
||||||
|
|
||||||
|
* **Operations**
|
||||||
|
* [**MCP Support**](https://github.com/sipeed/picoclaw/issues/290): Native support for the **Model Context Protocol (MCP)**.
|
||||||
|
* [**Browser Automation**](https://github.com/sipeed/picoclaw/issues/293): Headless browser control via CDP (Chrome DevTools Protocol) or ActionBook.
|
||||||
|
* [**Mobile Operation**](https://github.com/sipeed/picoclaw/issues/292): Android device control (similar to BotDrop).
|
||||||
|
|
||||||
|
|
||||||
|
* **Multi-Agent Collaboration**
|
||||||
|
* [**Basic Multi-Agent**](https://github.com/sipeed/picoclaw/issues/294) implement
|
||||||
|
* [**Model Routing**](https://github.com/sipeed/picoclaw/issues/295): "Smart Routing" — dispatch simple tasks to small/local models (fast/cheap) and complex tasks to SOTA models (smart).
|
||||||
|
* [**Swarm Mode**](https://github.com/sipeed/picoclaw/issues/284): Collaboration between multiple PicoClaw instances on the same network.
|
||||||
|
* [**AIEOS**](https://github.com/sipeed/picoclaw/issues/296): Exploring AI-Native Operating System interaction paradigms.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📚 5. Developer Experience (DevEx) & Documentation
|
||||||
|
|
||||||
|
*Lowering the barrier to entry so anyone can deploy in minutes.*
|
||||||
|
|
||||||
|
* [**QuickGuide (Zero-Config Start)**](https://github.com/sipeed/picoclaw/issues/350)
|
||||||
|
* Interactive CLI Wizard: If launched without config, automatically detect the environment and guide the user through Token/Network setup step-by-step.
|
||||||
|
|
||||||
|
|
||||||
|
* **Comprehensive Documentation**
|
||||||
|
* **Platform Guides**: Dedicated guides for Windows, macOS, Linux, and Android.
|
||||||
|
* **Step-by-Step Tutorials**: "Babysitter-level" guides for configuring Providers and Channels.
|
||||||
|
* **AI-Assisted Docs**: Using AI to auto-generate API references and code comments (with human verification to prevent hallucinations).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🤖 6. Engineering: AI-Powered Open Source
|
||||||
|
|
||||||
|
*Born from Vibe Coding, we continue to use AI to accelerate development.*
|
||||||
|
|
||||||
|
* **AI-Enhanced CI/CD**
|
||||||
|
* Integrate AI for automated Code Review, Linting, and PR Labeling.
|
||||||
|
* **Bot Noise Reduction**: Optimize bot interactions to keep PR timelines clean.
|
||||||
|
* **Issue Triage**: AI agents to analyze incoming issues and suggest preliminary fixes.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🎨 7. Brand & Community
|
||||||
|
|
||||||
|
* [**Logo Design**](https://github.com/sipeed/picoclaw/issues/297): We are looking for a **Mantis Shrimp (Stomatopoda)** logo design!
|
||||||
|
* *Concept*: Needs to reflect "Small but Mighty" and "Lightning Fast Strikes."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🤝 Call for Contributions
|
||||||
|
|
||||||
|
We welcome community contributions to any item on this roadmap! Please comment on the relevant Issue or submit a PR. Let's build the best Edge AI Agent together!
|
||||||
BIN
assets/termux.jpg
Normal file
BIN
assets/termux.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
|
|
@ -562,7 +562,8 @@ func gatewayCmd() {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Setup cron tool and service
|
// Setup cron tool and service
|
||||||
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace)
|
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
|
||||||
|
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout, cfg)
|
||||||
|
|
||||||
heartbeatService := heartbeat.NewHeartbeatService(
|
heartbeatService := heartbeat.NewHeartbeatService(
|
||||||
cfg.WorkspacePath(),
|
cfg.WorkspacePath(),
|
||||||
|
|
@ -987,14 +988,14 @@ func getConfigPath() string {
|
||||||
return filepath.Join(home, ".picoclaw", "config.json")
|
return filepath.Join(home, ".picoclaw", "config.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool) *cron.CronService {
|
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *cron.CronService {
|
||||||
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
||||||
|
|
||||||
// Create cron service
|
// Create cron service
|
||||||
cronService := cron.NewCronService(cronStorePath, nil)
|
cronService := cron.NewCronService(cronStorePath, nil)
|
||||||
|
|
||||||
// Create and register CronTool
|
// Create and register CronTool
|
||||||
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict)
|
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, config)
|
||||||
agentLoop.RegisterTool(cronTool)
|
agentLoop.RegisterTool(cronTool)
|
||||||
|
|
||||||
// Set the onJob handler
|
// Set the onJob handler
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"token": "YOUR_TELEGRAM_BOT_TOKEN",
|
"token": "YOUR_TELEGRAM_BOT_TOKEN",
|
||||||
"proxy": "",
|
"proxy": "",
|
||||||
"allow_from": ["YOUR_USER_ID"]
|
"allow_from": [
|
||||||
|
"YOUR_USER_ID"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
|
@ -77,7 +79,8 @@
|
||||||
},
|
},
|
||||||
"openai": {
|
"openai": {
|
||||||
"api_key": "",
|
"api_key": "",
|
||||||
"api_base": ""
|
"api_base": "",
|
||||||
|
"web_search": true
|
||||||
},
|
},
|
||||||
"openrouter": {
|
"openrouter": {
|
||||||
"api_key": "sk-or-v1-xxx",
|
"api_key": "sk-or-v1-xxx",
|
||||||
|
|
@ -115,10 +118,19 @@
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"web": {
|
"web": {
|
||||||
"search": {
|
"brave": {
|
||||||
|
"enabled": false,
|
||||||
"api_key": "YOUR_BRAVE_API_KEY",
|
"api_key": "YOUR_BRAVE_API_KEY",
|
||||||
"max_results": 5
|
"max_results": 5
|
||||||
|
},
|
||||||
|
"perplexity": {
|
||||||
|
"enabled": false,
|
||||||
|
"api_key": "pplx-xxx",
|
||||||
|
"max_results": 5
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cron": {
|
||||||
|
"exec_timeout_minutes": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"heartbeat": {
|
"heartbeat": {
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ services:
|
||||||
profiles:
|
profiles:
|
||||||
- agent
|
- agent
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/config.json:/root/.picoclaw/config.json:ro
|
- ./config/config.json:/home/picoclaw/.picoclaw/config.json:ro
|
||||||
- picoclaw-workspace:/root/.picoclaw/workspace
|
- picoclaw-workspace:/home/picoclaw/.picoclaw/workspace
|
||||||
entrypoint: ["picoclaw", "agent"]
|
entrypoint: ["picoclaw", "agent"]
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
|
|
@ -31,9 +31,9 @@ services:
|
||||||
- gateway
|
- gateway
|
||||||
volumes:
|
volumes:
|
||||||
# Configuration file
|
# Configuration file
|
||||||
- ./config/config.json:/root/.picoclaw/config.json:ro
|
- ./config/config.json:/home/picoclaw/.picoclaw/config.json:ro
|
||||||
# Persistent workspace (sessions, memory, logs)
|
# Persistent workspace (sessions, memory, logs)
|
||||||
- picoclaw-workspace:/root/.picoclaw/workspace
|
- picoclaw-workspace:/home/picoclaw/.picoclaw/workspace
|
||||||
command: ["gateway"]
|
command: ["gateway"]
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
122
docs/tools_configuration.md
Normal file
122
docs/tools_configuration.md
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
# Tools Configuration
|
||||||
|
|
||||||
|
PicoClaw's tools configuration is located in the `tools` field of `config.json`.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"web": { ... },
|
||||||
|
"exec": { ... },
|
||||||
|
"approval": { ... },
|
||||||
|
"cron": { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Web Tools
|
||||||
|
|
||||||
|
Web tools are used for web search and fetching.
|
||||||
|
|
||||||
|
### Brave
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `enabled` | bool | false | Enable Brave search |
|
||||||
|
| `api_key` | string | - | Brave Search API key |
|
||||||
|
| `max_results` | int | 5 | Maximum number of results |
|
||||||
|
|
||||||
|
### DuckDuckGo
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `enabled` | bool | true | Enable DuckDuckGo search |
|
||||||
|
| `max_results` | int | 5 | Maximum number of results |
|
||||||
|
|
||||||
|
### Perplexity
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `enabled` | bool | false | Enable Perplexity search |
|
||||||
|
| `api_key` | string | - | Perplexity API key |
|
||||||
|
| `max_results` | int | 5 | Maximum number of results |
|
||||||
|
|
||||||
|
## Exec Tool
|
||||||
|
|
||||||
|
The exec tool is used to execute shell commands.
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `enable_deny_patterns` | bool | true | Enable default dangerous command blocking |
|
||||||
|
| `custom_deny_patterns` | array | [] | Custom deny patterns (regular expressions) |
|
||||||
|
|
||||||
|
### Functionality
|
||||||
|
|
||||||
|
- **`enable_deny_patterns`**: Set to `false` to completely disable the default dangerous command blocking patterns
|
||||||
|
- **`custom_deny_patterns`**: Add custom deny regex patterns; commands matching these will be blocked
|
||||||
|
|
||||||
|
### Default Blocked Command Patterns
|
||||||
|
|
||||||
|
By default, PicoClaw blocks the following dangerous commands:
|
||||||
|
|
||||||
|
- Delete commands: `rm -rf`, `del /f/q`, `rmdir /s`
|
||||||
|
- Disk operations: `format`, `mkfs`, `diskpart`, `dd if=`, writing to `/dev/sd*`
|
||||||
|
- System operations: `shutdown`, `reboot`, `poweroff`
|
||||||
|
- Command substitution: `$()`, `${}`, backticks
|
||||||
|
- Pipe to shell: `| sh`, `| bash`
|
||||||
|
- Privilege escalation: `sudo`, `chmod`, `chown`
|
||||||
|
- Process control: `pkill`, `killall`, `kill -9`
|
||||||
|
- Remote operations: `curl | sh`, `wget | sh`, `ssh`
|
||||||
|
- Package management: `apt`, `yum`, `dnf`, `npm install -g`, `pip install --user`
|
||||||
|
- Containers: `docker run`, `docker exec`
|
||||||
|
- Git: `git push`, `git force`
|
||||||
|
- Other: `eval`, `source *.sh`
|
||||||
|
|
||||||
|
### Configuration Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"exec": {
|
||||||
|
"enable_deny_patterns": true,
|
||||||
|
"custom_deny_patterns": [
|
||||||
|
"\\brm\\s+-r\\b",
|
||||||
|
"\\bkillall\\s+python"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Approval Tool
|
||||||
|
|
||||||
|
The approval tool controls permissions for dangerous operations.
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `enabled` | bool | true | Enable approval functionality |
|
||||||
|
| `write_file` | bool | true | Require approval for file writes |
|
||||||
|
| `edit_file` | bool | true | Require approval for file edits |
|
||||||
|
| `append_file` | bool | true | Require approval for file appends |
|
||||||
|
| `exec` | bool | true | Require approval for command execution |
|
||||||
|
| `timeout_minutes` | int | 5 | Approval timeout in minutes |
|
||||||
|
|
||||||
|
## Cron Tool
|
||||||
|
|
||||||
|
The cron tool is used for scheduling periodic tasks.
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `exec_timeout_minutes` | int | 5 | Execution timeout in minutes, 0 means no limit |
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
All configuration options can be overridden via environment variables with the format `PICOCLAW_TOOLS_<SECTION>_<KEY>`:
|
||||||
|
|
||||||
|
For example:
|
||||||
|
- `PICOCLAW_TOOLS_WEB_BRAVE_ENABLED=true`
|
||||||
|
- `PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS=false`
|
||||||
|
- `PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES=10`
|
||||||
|
|
||||||
|
Note: Array-type environment variables are not currently supported and must be set via the config file.
|
||||||
|
|
@ -71,7 +71,7 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
|
||||||
registry.Register(tools.NewAppendFileTool(workspace, restrict))
|
registry.Register(tools.NewAppendFileTool(workspace, restrict))
|
||||||
|
|
||||||
// Shell execution
|
// Shell execution
|
||||||
registry.Register(tools.NewExecTool(workspace, restrict))
|
registry.Register(tools.NewExecToolWithConfig(workspace, restrict, cfg))
|
||||||
|
|
||||||
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
|
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
|
||||||
BraveAPIKey: cfg.Tools.Web.Brave.APIKey,
|
BraveAPIKey: cfg.Tools.Web.Brave.APIKey,
|
||||||
|
|
@ -79,6 +79,9 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
|
||||||
BraveEnabled: cfg.Tools.Web.Brave.Enabled,
|
BraveEnabled: cfg.Tools.Web.Brave.Enabled,
|
||||||
DuckDuckGoMaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults,
|
DuckDuckGoMaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults,
|
||||||
DuckDuckGoEnabled: cfg.Tools.Web.DuckDuckGo.Enabled,
|
DuckDuckGoEnabled: cfg.Tools.Web.DuckDuckGo.Enabled,
|
||||||
|
PerplexityAPIKey: cfg.Tools.Web.Perplexity.APIKey,
|
||||||
|
PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults,
|
||||||
|
PerplexityEnabled: cfg.Tools.Web.Perplexity.Enabled,
|
||||||
}); searchTool != nil {
|
}); searchTool != nil {
|
||||||
registry.Register(searchTool)
|
registry.Register(searchTool)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ type MaixCamChannel struct {
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
clients map[net.Conn]bool
|
clients map[net.Conn]bool
|
||||||
clientsMux sync.RWMutex
|
clientsMux sync.RWMutex
|
||||||
running bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MaixCamMessage struct {
|
type MaixCamMessage struct {
|
||||||
|
|
@ -35,7 +34,6 @@ func NewMaixCamChannel(cfg config.MaixCamConfig, bus *bus.MessageBus) (*MaixCamC
|
||||||
BaseChannel: base,
|
BaseChannel: base,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
clients: make(map[net.Conn]bool),
|
clients: make(map[net.Conn]bool),
|
||||||
running: false,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -167,19 +167,19 @@ type DevicesConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProvidersConfig struct {
|
type ProvidersConfig struct {
|
||||||
Anthropic ProviderConfig `json:"anthropic"`
|
Anthropic ProviderConfig `json:"anthropic"`
|
||||||
OpenAI ProviderConfig `json:"openai"`
|
OpenAI OpenAIProviderConfig `json:"openai"`
|
||||||
OpenRouter ProviderConfig `json:"openrouter"`
|
OpenRouter ProviderConfig `json:"openrouter"`
|
||||||
Groq ProviderConfig `json:"groq"`
|
Groq ProviderConfig `json:"groq"`
|
||||||
Zhipu ProviderConfig `json:"zhipu"`
|
Zhipu ProviderConfig `json:"zhipu"`
|
||||||
VLLM ProviderConfig `json:"vllm"`
|
VLLM ProviderConfig `json:"vllm"`
|
||||||
Gemini ProviderConfig `json:"gemini"`
|
Gemini ProviderConfig `json:"gemini"`
|
||||||
Nvidia ProviderConfig `json:"nvidia"`
|
Nvidia ProviderConfig `json:"nvidia"`
|
||||||
Ollama ProviderConfig `json:"ollama"`
|
Ollama ProviderConfig `json:"ollama"`
|
||||||
Moonshot ProviderConfig `json:"moonshot"`
|
Moonshot ProviderConfig `json:"moonshot"`
|
||||||
ShengSuanYun ProviderConfig `json:"shengsuanyun"`
|
ShengSuanYun ProviderConfig `json:"shengsuanyun"`
|
||||||
DeepSeek ProviderConfig `json:"deepseek"`
|
DeepSeek ProviderConfig `json:"deepseek"`
|
||||||
GitHubCopilot ProviderConfig `json:"github_copilot"`
|
GitHubCopilot ProviderConfig `json:"github_copilot"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderConfig struct {
|
type ProviderConfig struct {
|
||||||
|
|
@ -190,6 +190,11 @@ type ProviderConfig struct {
|
||||||
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
|
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenAIProviderConfig struct {
|
||||||
|
ProviderConfig
|
||||||
|
WebSearch bool `json:"web_search" env:"PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH"`
|
||||||
|
}
|
||||||
|
|
||||||
type GatewayConfig struct {
|
type GatewayConfig struct {
|
||||||
Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"`
|
Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"`
|
||||||
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
|
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
|
||||||
|
|
@ -206,13 +211,31 @@ type DuckDuckGoConfig struct {
|
||||||
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
|
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PerplexityConfig struct {
|
||||||
|
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"`
|
||||||
|
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"`
|
||||||
|
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"`
|
||||||
|
}
|
||||||
|
|
||||||
type WebToolsConfig struct {
|
type WebToolsConfig struct {
|
||||||
Brave BraveConfig `json:"brave"`
|
Brave BraveConfig `json:"brave"`
|
||||||
DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"`
|
DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"`
|
||||||
|
Perplexity PerplexityConfig `json:"perplexity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CronToolsConfig struct {
|
||||||
|
ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecConfig struct {
|
||||||
|
EnableDenyPatterns bool `json:"enable_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"`
|
||||||
|
CustomDenyPatterns []string `json:"custom_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolsConfig struct {
|
type ToolsConfig struct {
|
||||||
Web WebToolsConfig `json:"web"`
|
Web WebToolsConfig `json:"web"`
|
||||||
|
Cron CronToolsConfig `json:"cron"`
|
||||||
|
Exec ExecConfig `json:"exec"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
|
|
@ -296,7 +319,7 @@ func DefaultConfig() *Config {
|
||||||
},
|
},
|
||||||
Providers: ProvidersConfig{
|
Providers: ProvidersConfig{
|
||||||
Anthropic: ProviderConfig{},
|
Anthropic: ProviderConfig{},
|
||||||
OpenAI: ProviderConfig{},
|
OpenAI: OpenAIProviderConfig{WebSearch: true},
|
||||||
OpenRouter: ProviderConfig{},
|
OpenRouter: ProviderConfig{},
|
||||||
Groq: ProviderConfig{},
|
Groq: ProviderConfig{},
|
||||||
Zhipu: ProviderConfig{},
|
Zhipu: ProviderConfig{},
|
||||||
|
|
@ -321,6 +344,17 @@ func DefaultConfig() *Config {
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
MaxResults: 5,
|
MaxResults: 5,
|
||||||
},
|
},
|
||||||
|
Perplexity: PerplexityConfig{
|
||||||
|
Enabled: false,
|
||||||
|
APIKey: "",
|
||||||
|
MaxResults: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Cron: CronToolsConfig{
|
||||||
|
ExecTimeoutMinutes: 5, // default 5 minutes for LLM operations
|
||||||
|
},
|
||||||
|
Exec: ExecConfig{
|
||||||
|
EnableDenyPatterns: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Heartbeat: HeartbeatConfig{
|
Heartbeat: HeartbeatConfig{
|
||||||
|
|
|
||||||
|
|
@ -204,3 +204,42 @@ func TestConfig_Complete(t *testing.T) {
|
||||||
t.Error("Heartbeat should be enabled by default")
|
t.Error("Heartbeat should be enabled by default")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultConfig_OpenAIWebSearchEnabled(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if !cfg.Providers.OpenAI.WebSearch {
|
||||||
|
t.Fatal("DefaultConfig().Providers.OpenAI.WebSearch should be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
configPath := filepath.Join(dir, "config.json")
|
||||||
|
if err := os.WriteFile(configPath, []byte(`{"providers":{"openai":{"api_base":""}}}`), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfig() error: %v", err)
|
||||||
|
}
|
||||||
|
if !cfg.Providers.OpenAI.WebSearch {
|
||||||
|
t.Fatal("OpenAI codex web search should remain true when unset in config file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
configPath := filepath.Join(dir, "config.json")
|
||||||
|
if err := os.WriteFile(configPath, []byte(`{"providers":{"openai":{"web_search":false}}}`), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfig() error: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.Providers.OpenAI.WebSearch {
|
||||||
|
t.Fatal("OpenAI codex web search should be false when disabled in config file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,10 @@ func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error
|
||||||
case "anthropic":
|
case "anthropic":
|
||||||
cfg.Providers.Anthropic = pc
|
cfg.Providers.Anthropic = pc
|
||||||
case "openai":
|
case "openai":
|
||||||
cfg.Providers.OpenAI = pc
|
cfg.Providers.OpenAI = config.OpenAIProviderConfig{
|
||||||
|
ProviderConfig: pc,
|
||||||
|
WebSearch: getBoolOrDefault(pMap, "web_search", true),
|
||||||
|
}
|
||||||
case "openrouter":
|
case "openrouter":
|
||||||
cfg.Providers.OpenRouter = pc
|
cfg.Providers.OpenRouter = pc
|
||||||
case "groq":
|
case "groq":
|
||||||
|
|
@ -363,6 +366,13 @@ func getBool(data map[string]interface{}, key string) (bool, bool) {
|
||||||
return b, ok
|
return b, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getBoolOrDefault(data map[string]interface{}, key string, defaultVal bool) bool {
|
||||||
|
if v, ok := getBool(data, key); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
func getStringSlice(data map[string]interface{}, key string) []string {
|
func getStringSlice(data map[string]interface{}, key string) []string {
|
||||||
v, ok := data[key]
|
v, ok := data[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,10 @@ const codexDefaultModel = "gpt-5.2"
|
||||||
const codexDefaultInstructions = "You are Codex, a coding assistant."
|
const codexDefaultInstructions = "You are Codex, a coding assistant."
|
||||||
|
|
||||||
type CodexProvider struct {
|
type CodexProvider struct {
|
||||||
client *openai.Client
|
client *openai.Client
|
||||||
accountID string
|
accountID string
|
||||||
tokenSource func() (string, string, error)
|
tokenSource func() (string, string, error)
|
||||||
|
enableWebSearch bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultCodexInstructions = "You are Codex, a coding assistant."
|
const defaultCodexInstructions = "You are Codex, a coding assistant."
|
||||||
|
|
@ -37,8 +38,9 @@ func NewCodexProvider(token, accountID string) *CodexProvider {
|
||||||
}
|
}
|
||||||
client := openai.NewClient(opts...)
|
client := openai.NewClient(opts...)
|
||||||
return &CodexProvider{
|
return &CodexProvider{
|
||||||
client: &client,
|
client: &client,
|
||||||
accountID: accountID,
|
accountID: accountID,
|
||||||
|
enableWebSearch: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +80,7 @@ func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []To
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
params := buildCodexParams(messages, tools, resolvedModel, options)
|
params := buildCodexParams(messages, tools, resolvedModel, options, p.enableWebSearch)
|
||||||
|
|
||||||
stream := p.client.Responses.NewStreaming(ctx, params, opts...)
|
stream := p.client.Responses.NewStreaming(ctx, params, opts...)
|
||||||
defer stream.Close()
|
defer stream.Close()
|
||||||
|
|
@ -182,7 +184,7 @@ func resolveCodexModel(model string) (string, string) {
|
||||||
return codexDefaultModel, "unsupported model family"
|
return codexDefaultModel, "unsupported model family"
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCodexParams(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) responses.ResponseNewParams {
|
func buildCodexParams(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, enableWebSearch bool) responses.ResponseNewParams {
|
||||||
var inputItems responses.ResponseInputParam
|
var inputItems responses.ResponseInputParam
|
||||||
var instructions string
|
var instructions string
|
||||||
|
|
||||||
|
|
@ -217,12 +219,18 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, tc := range msg.ToolCalls {
|
for _, tc := range msg.ToolCalls {
|
||||||
argsJSON, _ := json.Marshal(tc.Arguments)
|
name, args, ok := resolveCodexToolCall(tc)
|
||||||
|
if !ok {
|
||||||
|
logger.WarnCF("provider.codex", "Skipping invalid tool call in history", map[string]interface{}{
|
||||||
|
"call_id": tc.ID,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
inputItems = append(inputItems, responses.ResponseInputItemUnionParam{
|
inputItems = append(inputItems, responses.ResponseInputItemUnionParam{
|
||||||
OfFunctionCall: &responses.ResponseFunctionToolCallParam{
|
OfFunctionCall: &responses.ResponseFunctionToolCallParam{
|
||||||
CallID: tc.ID,
|
CallID: tc.ID,
|
||||||
Name: tc.Name,
|
Name: name,
|
||||||
Arguments: string(argsJSON),
|
Arguments: args,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -260,20 +268,50 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
|
||||||
params.Instructions = openai.Opt(defaultCodexInstructions)
|
params.Instructions = openai.Opt(defaultCodexInstructions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if maxTokens, ok := options["max_tokens"].(int); ok {
|
if len(tools) > 0 || enableWebSearch {
|
||||||
params.MaxOutputTokens = openai.Opt(int64(maxTokens))
|
params.Tools = translateToolsForCodex(tools, enableWebSearch)
|
||||||
}
|
|
||||||
|
|
||||||
if len(tools) > 0 {
|
|
||||||
params.Tools = translateToolsForCodex(tools)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
func translateToolsForCodex(tools []ToolDefinition) []responses.ToolUnionParam {
|
func resolveCodexToolCall(tc ToolCall) (name string, arguments string, ok bool) {
|
||||||
result := make([]responses.ToolUnionParam, 0, len(tools))
|
name = tc.Name
|
||||||
|
if name == "" && tc.Function != nil {
|
||||||
|
name = tc.Function.Name
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tc.Arguments) > 0 {
|
||||||
|
argsJSON, err := json.Marshal(tc.Arguments)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return name, string(argsJSON), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.Function != nil && tc.Function.Arguments != "" {
|
||||||
|
return name, tc.Function.Arguments, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return name, "{}", true
|
||||||
|
}
|
||||||
|
|
||||||
|
func translateToolsForCodex(tools []ToolDefinition, enableWebSearch bool) []responses.ToolUnionParam {
|
||||||
|
capHint := len(tools)
|
||||||
|
if enableWebSearch {
|
||||||
|
capHint++
|
||||||
|
}
|
||||||
|
result := make([]responses.ToolUnionParam, 0, capHint)
|
||||||
for _, t := range tools {
|
for _, t := range tools {
|
||||||
|
if t.Type != "function" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if enableWebSearch && strings.EqualFold(t.Function.Name, "web_search") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ft := responses.FunctionToolParam{
|
ft := responses.FunctionToolParam{
|
||||||
Name: t.Function.Name,
|
Name: t.Function.Name,
|
||||||
Parameters: t.Function.Parameters,
|
Parameters: t.Function.Parameters,
|
||||||
|
|
@ -284,6 +322,9 @@ func translateToolsForCodex(tools []ToolDefinition) []responses.ToolUnionParam {
|
||||||
}
|
}
|
||||||
result = append(result, responses.ToolUnionParam{OfFunction: &ft})
|
result = append(result, responses.ToolUnionParam{OfFunction: &ft})
|
||||||
}
|
}
|
||||||
|
if enableWebSearch {
|
||||||
|
result = append(result, responses.ToolParamOfWebSearch(responses.WebSearchToolTypeWebSearch))
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ func TestBuildCodexParams_BasicMessage(t *testing.T) {
|
||||||
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{
|
||||||
"max_tokens": 2048,
|
"max_tokens": 2048,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
})
|
}, true)
|
||||||
if params.Model != "gpt-4o" {
|
if params.Model != "gpt-4o" {
|
||||||
t.Errorf("Model = %q, want %q", params.Model, "gpt-4o")
|
t.Errorf("Model = %q, want %q", params.Model, "gpt-4o")
|
||||||
}
|
}
|
||||||
|
|
@ -29,6 +29,9 @@ func TestBuildCodexParams_BasicMessage(t *testing.T) {
|
||||||
if params.Instructions.Or("") != defaultCodexInstructions {
|
if params.Instructions.Or("") != defaultCodexInstructions {
|
||||||
t.Errorf("Instructions = %q, want %q", params.Instructions.Or(""), defaultCodexInstructions)
|
t.Errorf("Instructions = %q, want %q", params.Instructions.Or(""), defaultCodexInstructions)
|
||||||
}
|
}
|
||||||
|
if params.MaxOutputTokens.Valid() {
|
||||||
|
t.Fatalf("MaxOutputTokens should not be set for Codex backend")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildCodexParams_SystemAsInstructions(t *testing.T) {
|
func TestBuildCodexParams_SystemAsInstructions(t *testing.T) {
|
||||||
|
|
@ -36,7 +39,7 @@ func TestBuildCodexParams_SystemAsInstructions(t *testing.T) {
|
||||||
{Role: "system", Content: "You are helpful"},
|
{Role: "system", Content: "You are helpful"},
|
||||||
{Role: "user", Content: "Hi"},
|
{Role: "user", Content: "Hi"},
|
||||||
}
|
}
|
||||||
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{})
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, true)
|
||||||
if !params.Instructions.Valid() {
|
if !params.Instructions.Valid() {
|
||||||
t.Fatal("Instructions should be set")
|
t.Fatal("Instructions should be set")
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +59,7 @@ func TestBuildCodexParams_ToolCallConversation(t *testing.T) {
|
||||||
},
|
},
|
||||||
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
|
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
|
||||||
}
|
}
|
||||||
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{})
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, false)
|
||||||
if params.Input.OfInputItemList == nil {
|
if params.Input.OfInputItemList == nil {
|
||||||
t.Fatal("Input.OfInputItemList should not be nil")
|
t.Fatal("Input.OfInputItemList should not be nil")
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +68,45 @@ func TestBuildCodexParams_ToolCallConversation(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildCodexParams_ToolCallFunctionFallback(t *testing.T) {
|
||||||
|
messages := []Message{
|
||||||
|
{Role: "user", Content: "Read a file"},
|
||||||
|
{
|
||||||
|
Role: "assistant",
|
||||||
|
ToolCalls: []ToolCall{
|
||||||
|
{
|
||||||
|
ID: "call_1",
|
||||||
|
Type: "function",
|
||||||
|
Function: &FunctionCall{
|
||||||
|
Name: "read_file",
|
||||||
|
Arguments: `{"path":"README.md"}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{Role: "tool", Content: "ok", ToolCallID: "call_1"},
|
||||||
|
}
|
||||||
|
|
||||||
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, false)
|
||||||
|
if params.Input.OfInputItemList == nil {
|
||||||
|
t.Fatal("Input.OfInputItemList should not be nil")
|
||||||
|
}
|
||||||
|
if len(params.Input.OfInputItemList) != 3 {
|
||||||
|
t.Fatalf("len(Input items) = %d, want 3", len(params.Input.OfInputItemList))
|
||||||
|
}
|
||||||
|
|
||||||
|
fc := params.Input.OfInputItemList[1].OfFunctionCall
|
||||||
|
if fc == nil {
|
||||||
|
t.Fatal("assistant tool call should be converted to function_call input item")
|
||||||
|
}
|
||||||
|
if fc.Name != "read_file" {
|
||||||
|
t.Errorf("Function call name = %q, want %q", fc.Name, "read_file")
|
||||||
|
}
|
||||||
|
if fc.Arguments != `{"path":"README.md"}` {
|
||||||
|
t.Errorf("Function call arguments = %q, want %q", fc.Arguments, `{"path":"README.md"}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildCodexParams_WithTools(t *testing.T) {
|
func TestBuildCodexParams_WithTools(t *testing.T) {
|
||||||
tools := []ToolDefinition{
|
tools := []ToolDefinition{
|
||||||
{
|
{
|
||||||
|
|
@ -81,7 +123,7 @@ func TestBuildCodexParams_WithTools(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{})
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{}, false)
|
||||||
if len(params.Tools) != 1 {
|
if len(params.Tools) != 1 {
|
||||||
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
|
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
|
||||||
}
|
}
|
||||||
|
|
@ -94,12 +136,61 @@ func TestBuildCodexParams_WithTools(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildCodexParams_StoreIsFalse(t *testing.T) {
|
func TestBuildCodexParams_StoreIsFalse(t *testing.T) {
|
||||||
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{})
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, false)
|
||||||
if !params.Store.Valid() || params.Store.Or(true) != false {
|
if !params.Store.Valid() || params.Store.Or(true) != false {
|
||||||
t.Error("Store should be explicitly set to false")
|
t.Error("Store should be explicitly set to false")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildCodexParams_DefaultWebSearchEnabled(t *testing.T) {
|
||||||
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, true)
|
||||||
|
if len(params.Tools) != 1 {
|
||||||
|
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
|
||||||
|
}
|
||||||
|
if params.Tools[0].OfWebSearch == nil {
|
||||||
|
t.Fatal("Tool should include built-in web_search")
|
||||||
|
}
|
||||||
|
if params.Tools[0].OfWebSearch.Type != responses.WebSearchToolTypeWebSearch {
|
||||||
|
t.Errorf("Web search tool type = %q, want %q", params.Tools[0].OfWebSearch.Type, responses.WebSearchToolTypeWebSearch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildCodexParams_WebSearchFunctionReplacedWithBuiltin(t *testing.T) {
|
||||||
|
tools := []ToolDefinition{
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: ToolFunctionDefinition{
|
||||||
|
Name: "web_search",
|
||||||
|
Description: "local web search",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: ToolFunctionDefinition{
|
||||||
|
Name: "read_file",
|
||||||
|
Description: "read file",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{}, true)
|
||||||
|
if len(params.Tools) != 2 {
|
||||||
|
t.Fatalf("len(Tools) = %d, want 2", len(params.Tools))
|
||||||
|
}
|
||||||
|
if params.Tools[0].OfFunction == nil || params.Tools[0].OfFunction.Name != "read_file" {
|
||||||
|
t.Fatalf("first tool should be function read_file, got %#v", params.Tools[0])
|
||||||
|
}
|
||||||
|
if params.Tools[1].OfWebSearch == nil {
|
||||||
|
t.Fatalf("second tool should be built-in web_search, got %#v", params.Tools[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseCodexResponse_TextOutput(t *testing.T) {
|
func TestParseCodexResponse_TextOutput(t *testing.T) {
|
||||||
respJSON := `{
|
respJSON := `{
|
||||||
"id": "resp_test",
|
"id": "resp_test",
|
||||||
|
|
@ -214,6 +305,20 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
|
||||||
http.Error(w, "stream must be true", http.StatusBadRequest)
|
http.Error(w, "stream must be true", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if _, ok := reqBody["max_output_tokens"]; ok {
|
||||||
|
http.Error(w, "max_output_tokens is not supported", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toolsAny, ok := reqBody["tools"].([]interface{})
|
||||||
|
if !ok || len(toolsAny) != 1 {
|
||||||
|
http.Error(w, "missing default web search tool", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toolObj, ok := toolsAny[0].(map[string]interface{})
|
||||||
|
if !ok || toolObj["type"] != "web_search" {
|
||||||
|
http.Error(w, "expected web_search tool", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]interface{}{
|
||||||
"id": "resp_test",
|
"id": "resp_test",
|
||||||
|
|
@ -261,6 +366,64 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/responses" {
|
||||||
|
http.Error(w, "not found: "+r.URL.Path, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var reqBody map[string]interface{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||||
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := reqBody["tools"]; ok {
|
||||||
|
http.Error(w, "tools should be absent when web search disabled", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := map[string]interface{}{
|
||||||
|
"id": "resp_test",
|
||||||
|
"object": "response",
|
||||||
|
"status": "completed",
|
||||||
|
"output": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"id": "msg_1",
|
||||||
|
"type": "message",
|
||||||
|
"role": "assistant",
|
||||||
|
"status": "completed",
|
||||||
|
"content": []map[string]interface{}{
|
||||||
|
{"type": "output_text", "text": "Hi from Codex!"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"usage": map[string]interface{}{
|
||||||
|
"input_tokens": 4,
|
||||||
|
"output_tokens": 3,
|
||||||
|
"total_tokens": 7,
|
||||||
|
"input_tokens_details": map[string]interface{}{"cached_tokens": 0},
|
||||||
|
"output_tokens_details": map[string]interface{}{"reasoning_tokens": 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
writeCompletedSSE(w, resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
provider := NewCodexProvider("test-token", "acc-123")
|
||||||
|
provider.enableWebSearch = false
|
||||||
|
provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
|
||||||
|
|
||||||
|
messages := []Message{{Role: "user", Content: "Hello"}}
|
||||||
|
resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]interface{}{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Chat() error: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Content != "Hi from Codex!" {
|
||||||
|
t.Errorf("Content = %q, want %q", resp.Content, "Hi from Codex!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T) {
|
func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/responses" {
|
if r.URL.Path != "/responses" {
|
||||||
|
|
@ -293,6 +456,10 @@ func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T)
|
||||||
http.Error(w, "temperature is not supported", http.StatusBadRequest)
|
http.Error(w, "temperature is not supported", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if _, ok := reqBody["max_output_tokens"]; ok {
|
||||||
|
http.Error(w, "max_output_tokens is not supported", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
if reqBody["stream"] != true {
|
if reqBody["stream"] != true {
|
||||||
http.Error(w, "stream must be true", http.StatusBadRequest)
|
http.Error(w, "stream must be true", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ func createClaudeAuthProvider() (LLMProvider, error) {
|
||||||
return NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil
|
return NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createCodexAuthProvider() (LLMProvider, error) {
|
func createCodexAuthProvider(enableWebSearch bool) (LLMProvider, error) {
|
||||||
cred, err := auth.GetCredential("openai")
|
cred, err := auth.GetCredential("openai")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("loading auth credentials: %w", err)
|
return nil, fmt.Errorf("loading auth credentials: %w", err)
|
||||||
|
|
@ -240,7 +240,9 @@ func createCodexAuthProvider() (LLMProvider, error) {
|
||||||
if cred == nil {
|
if cred == nil {
|
||||||
return nil, fmt.Errorf("no credentials for openai. Run: picoclaw auth login --provider openai")
|
return nil, fmt.Errorf("no credentials for openai. Run: picoclaw auth login --provider openai")
|
||||||
}
|
}
|
||||||
return NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource()), nil
|
p := NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource())
|
||||||
|
p.enableWebSearch = enableWebSearch
|
||||||
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
|
|
@ -265,10 +267,12 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
case "openai", "gpt":
|
case "openai", "gpt":
|
||||||
if cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != "" {
|
if cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != "" {
|
||||||
if cfg.Providers.OpenAI.AuthMethod == "codex-cli" {
|
if cfg.Providers.OpenAI.AuthMethod == "codex-cli" {
|
||||||
return NewCodexProviderWithTokenSource("", "", CreateCodexCliTokenSource()), nil
|
c := NewCodexProviderWithTokenSource("", "", CreateCodexCliTokenSource())
|
||||||
|
c.enableWebSearch = cfg.Providers.OpenAI.WebSearch
|
||||||
|
return c, nil
|
||||||
}
|
}
|
||||||
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
||||||
return createCodexAuthProvider()
|
return createCodexAuthProvider(cfg.Providers.OpenAI.WebSearch)
|
||||||
}
|
}
|
||||||
apiKey = cfg.Providers.OpenAI.APIKey
|
apiKey = cfg.Providers.OpenAI.APIKey
|
||||||
apiBase = cfg.Providers.OpenAI.APIBase
|
apiBase = cfg.Providers.OpenAI.APIBase
|
||||||
|
|
@ -393,7 +397,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
|
|
||||||
case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) && (cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != ""):
|
case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) && (cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != ""):
|
||||||
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
||||||
return createCodexAuthProvider()
|
return createCodexAuthProvider(cfg.Providers.OpenAI.WebSearch)
|
||||||
}
|
}
|
||||||
apiKey = cfg.Providers.OpenAI.APIKey
|
apiKey = cfg.Providers.OpenAI.APIKey
|
||||||
apiBase = cfg.Providers.OpenAI.APIBase
|
apiBase = cfg.Providers.OpenAI.APIBase
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/cron"
|
"github.com/sipeed/picoclaw/pkg/cron"
|
||||||
"github.com/sipeed/picoclaw/pkg/utils"
|
"github.com/sipeed/picoclaw/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -28,12 +29,15 @@ type CronTool struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCronTool creates a new CronTool
|
// NewCronTool creates a new CronTool
|
||||||
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool) *CronTool {
|
// execTimeout: 0 means no timeout, >0 sets the timeout duration
|
||||||
|
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *CronTool {
|
||||||
|
execTool := NewExecToolWithConfig(workspace, restrict, config)
|
||||||
|
execTool.SetTimeout(execTimeout)
|
||||||
return &CronTool{
|
return &CronTool{
|
||||||
cronService: cronService,
|
cronService: cronService,
|
||||||
executor: executor,
|
executor: executor,
|
||||||
msgBus: msgBus,
|
msgBus: msgBus,
|
||||||
execTool: NewExecTool(workspace, restrict),
|
execTool: execTool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -21,16 +22,82 @@ type ExecTool struct {
|
||||||
restrictToWorkspace bool
|
restrictToWorkspace bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var defaultDenyPatterns = []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`),
|
||||||
|
regexp.MustCompile(`\bdel\s+/[fq]\b`),
|
||||||
|
regexp.MustCompile(`\brmdir\s+/s\b`),
|
||||||
|
regexp.MustCompile(`\b(format|mkfs|diskpart)\b\s`), // Match disk wiping commands (must be followed by space/args)
|
||||||
|
regexp.MustCompile(`\bdd\s+if=`),
|
||||||
|
regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null)
|
||||||
|
regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`),
|
||||||
|
regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`),
|
||||||
|
regexp.MustCompile(`\$\([^)]+\)`),
|
||||||
|
regexp.MustCompile(`\$\{[^}]+\}`),
|
||||||
|
regexp.MustCompile("`[^`]+`"),
|
||||||
|
regexp.MustCompile(`\|\s*sh\b`),
|
||||||
|
regexp.MustCompile(`\|\s*bash\b`),
|
||||||
|
regexp.MustCompile(`;\s*rm\s+-[rf]`),
|
||||||
|
regexp.MustCompile(`&&\s*rm\s+-[rf]`),
|
||||||
|
regexp.MustCompile(`\|\|\s*rm\s+-[rf]`),
|
||||||
|
regexp.MustCompile(`>\s*/dev/null\s*>&?\s*\d?`),
|
||||||
|
regexp.MustCompile(`<<\s*EOF`),
|
||||||
|
regexp.MustCompile(`\$\(\s*cat\s+`),
|
||||||
|
regexp.MustCompile(`\$\(\s*curl\s+`),
|
||||||
|
regexp.MustCompile(`\$\(\s*wget\s+`),
|
||||||
|
regexp.MustCompile(`\$\(\s*which\s+`),
|
||||||
|
regexp.MustCompile(`\bsudo\b`),
|
||||||
|
regexp.MustCompile(`\bchmod\s+[0-7]{3,4}\b`),
|
||||||
|
regexp.MustCompile(`\bchown\b`),
|
||||||
|
regexp.MustCompile(`\bpkill\b`),
|
||||||
|
regexp.MustCompile(`\bkillall\b`),
|
||||||
|
regexp.MustCompile(`\bkill\s+-[9]\b`),
|
||||||
|
regexp.MustCompile(`\bcurl\b.*\|\s*(sh|bash)`),
|
||||||
|
regexp.MustCompile(`\bwget\b.*\|\s*(sh|bash)`),
|
||||||
|
regexp.MustCompile(`\bnpm\s+install\s+-g\b`),
|
||||||
|
regexp.MustCompile(`\bpip\s+install\s+--user\b`),
|
||||||
|
regexp.MustCompile(`\bapt\s+(install|remove|purge)\b`),
|
||||||
|
regexp.MustCompile(`\byum\s+(install|remove)\b`),
|
||||||
|
regexp.MustCompile(`\bdnf\s+(install|remove)\b`),
|
||||||
|
regexp.MustCompile(`\bdocker\s+run\b`),
|
||||||
|
regexp.MustCompile(`\bdocker\s+exec\b`),
|
||||||
|
regexp.MustCompile(`\bgit\s+push\b`),
|
||||||
|
regexp.MustCompile(`\bgit\s+force\b`),
|
||||||
|
regexp.MustCompile(`\bssh\b.*@`),
|
||||||
|
regexp.MustCompile(`\beval\b`),
|
||||||
|
regexp.MustCompile(`\bsource\s+.*\.sh\b`),
|
||||||
|
}
|
||||||
|
|
||||||
func NewExecTool(workingDir string, restrict bool) *ExecTool {
|
func NewExecTool(workingDir string, restrict bool) *ExecTool {
|
||||||
denyPatterns := []*regexp.Regexp{
|
return NewExecToolWithConfig(workingDir, restrict, nil)
|
||||||
regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`),
|
}
|
||||||
regexp.MustCompile(`\bdel\s+/[fq]\b`),
|
|
||||||
regexp.MustCompile(`\brmdir\s+/s\b`),
|
func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Config) *ExecTool {
|
||||||
regexp.MustCompile(`\b(format|mkfs|diskpart)\b\s`), // Match disk wiping commands (must be followed by space/args)
|
denyPatterns := make([]*regexp.Regexp, 0)
|
||||||
regexp.MustCompile(`\bdd\s+if=`),
|
|
||||||
regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null)
|
enableDenyPatterns := true
|
||||||
regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`),
|
if config != nil {
|
||||||
regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`),
|
execConfig := config.Tools.Exec
|
||||||
|
enableDenyPatterns = execConfig.EnableDenyPatterns
|
||||||
|
if enableDenyPatterns {
|
||||||
|
if len(execConfig.CustomDenyPatterns) > 0 {
|
||||||
|
fmt.Printf("Using custom deny patterns: %v\n", execConfig.CustomDenyPatterns)
|
||||||
|
for _, pattern := range execConfig.CustomDenyPatterns {
|
||||||
|
re, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Invalid custom deny pattern %q: %v\n", pattern, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
denyPatterns = append(denyPatterns, re)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
denyPatterns = append(denyPatterns, defaultDenyPatterns...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If deny patterns are disabled, we won't add any patterns, allowing all commands.
|
||||||
|
fmt.Println("Warning: deny patterns are disabled. All commands will be allowed.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
denyPatterns = append(denyPatterns, defaultDenyPatterns...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ExecTool{
|
return &ExecTool{
|
||||||
|
|
@ -89,7 +156,14 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *To
|
||||||
return ErrorResult(guardError)
|
return ErrorResult(guardError)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdCtx, cancel := context.WithTimeout(ctx, t.timeout)
|
// timeout == 0 means no timeout
|
||||||
|
var cmdCtx context.Context
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
if t.timeout > 0 {
|
||||||
|
cmdCtx, cancel = context.WithTimeout(ctx, t.timeout)
|
||||||
|
} else {
|
||||||
|
cmdCtx, cancel = context.WithCancel(ctx)
|
||||||
|
}
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,71 @@ func stripTags(content string) string {
|
||||||
return re.ReplaceAllString(content, "")
|
return re.ReplaceAllString(content, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PerplexitySearchProvider struct {
|
||||||
|
apiKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, count int) (string, error) {
|
||||||
|
searchURL := "https://api.perplexity.ai/chat/completions"
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"model": "sonar",
|
||||||
|
"messages": []map[string]string{
|
||||||
|
{"role": "system", "content": "You are a search assistant. Provide concise search results with titles, URLs, and brief descriptions in the following format:\n1. Title\n URL\n Description\n\nDo not add extra commentary."},
|
||||||
|
{"role": "user", "content": fmt.Sprintf("Search for: %s. Provide up to %d relevant results.", query, count)},
|
||||||
|
},
|
||||||
|
"max_tokens": 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", searchURL, strings.NewReader(string(payloadBytes)))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+p.apiKey)
|
||||||
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("Perplexity API error: %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchResp struct {
|
||||||
|
Choices []struct {
|
||||||
|
Message struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
} `json:"message"`
|
||||||
|
} `json:"choices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &searchResp); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(searchResp.Choices) == 0 {
|
||||||
|
return fmt.Sprintf("No results for: %s", query), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Results for: %s (via Perplexity)\n%s", query, searchResp.Choices[0].Message.Content), nil
|
||||||
|
}
|
||||||
|
|
||||||
type WebSearchTool struct {
|
type WebSearchTool struct {
|
||||||
provider SearchProvider
|
provider SearchProvider
|
||||||
maxResults int
|
maxResults int
|
||||||
|
|
@ -187,14 +252,22 @@ type WebSearchToolOptions struct {
|
||||||
BraveEnabled bool
|
BraveEnabled bool
|
||||||
DuckDuckGoMaxResults int
|
DuckDuckGoMaxResults int
|
||||||
DuckDuckGoEnabled bool
|
DuckDuckGoEnabled bool
|
||||||
|
PerplexityAPIKey string
|
||||||
|
PerplexityMaxResults int
|
||||||
|
PerplexityEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool {
|
func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool {
|
||||||
var provider SearchProvider
|
var provider SearchProvider
|
||||||
maxResults := 5
|
maxResults := 5
|
||||||
|
|
||||||
// Priority: Brave > DuckDuckGo
|
// Priority: Perplexity > Brave > DuckDuckGo
|
||||||
if opts.BraveEnabled && opts.BraveAPIKey != "" {
|
if opts.PerplexityEnabled && opts.PerplexityAPIKey != "" {
|
||||||
|
provider = &PerplexitySearchProvider{apiKey: opts.PerplexityAPIKey}
|
||||||
|
if opts.PerplexityMaxResults > 0 {
|
||||||
|
maxResults = opts.PerplexityMaxResults
|
||||||
|
}
|
||||||
|
} else if opts.BraveEnabled && opts.BraveAPIKey != "" {
|
||||||
provider = &BraveSearchProvider{apiKey: opts.BraveAPIKey}
|
provider = &BraveSearchProvider{apiKey: opts.BraveAPIKey}
|
||||||
if opts.BraveMaxResults > 0 {
|
if opts.BraveMaxResults > 0 {
|
||||||
maxResults = opts.BraveMaxResults
|
maxResults = opts.BraveMaxResults
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue