Merge branch 'main' of github.com:mosir/picoclaw into fix/atomic-file-writes
This commit is contained in:
commit
7a2d353d0f
33 changed files with 274 additions and 184 deletions
|
|
@ -66,7 +66,6 @@ linters:
|
||||||
- testifylint
|
- testifylint
|
||||||
- thelper
|
- thelper
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
|
||||||
- usestdlibvars
|
- usestdlibvars
|
||||||
- usetesting
|
- usetesting
|
||||||
- wastedassign
|
- wastedassign
|
||||||
|
|
@ -152,6 +151,9 @@ linters:
|
||||||
- gocognit
|
- gocognit
|
||||||
- gocyclo
|
- gocyclo
|
||||||
path: _test\.go$
|
path: _test\.go$
|
||||||
|
- linters:
|
||||||
|
- nolintlint
|
||||||
|
path: 'pkg/tools/(i2c\.go|spi\.go)$'
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
max-issues-per-linter: 0
|
max-issues-per-linter: 0
|
||||||
|
|
|
||||||
8
Makefile
8
Makefile
|
|
@ -14,7 +14,7 @@ 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) -s -w"
|
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?=CGO_ENABLED=0 go
|
||||||
GOFLAGS?=-v -tags stdjson
|
GOFLAGS?=-v -tags stdjson
|
||||||
|
|
||||||
# Golangci-lint
|
# Golangci-lint
|
||||||
|
|
@ -144,6 +144,10 @@ fmt:
|
||||||
lint:
|
lint:
|
||||||
@$(GOLANGCI_LINT) run
|
@$(GOLANGCI_LINT) run
|
||||||
|
|
||||||
|
## fix: Fix linting issues
|
||||||
|
fix:
|
||||||
|
@$(GOLANGCI_LINT) run --fix
|
||||||
|
|
||||||
## deps: Download dependencies
|
## deps: Download dependencies
|
||||||
deps:
|
deps:
|
||||||
@$(GO) mod download
|
@$(GO) mod download
|
||||||
|
|
@ -169,7 +173,7 @@ help:
|
||||||
@echo " make [target]"
|
@echo " make [target]"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## / /'
|
@grep -E '^## ' $(MAKEFILE_LIST) | sort | awk -F': ' '{printf " %-16s %s\n", substr($$1, 4), $$2}'
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Examples:"
|
@echo "Examples:"
|
||||||
@echo " make build # Build for current platform"
|
@echo " make build # Build for current platform"
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ picoclaw onboard
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model": "gpt4"
|
"model_name": "gpt4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ picoclaw onboard
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model": "gpt4"
|
"model_name": "gpt4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@
|
||||||
<br>
|
<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://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>
|
<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>
|
||||||
|
<br>
|
||||||
|
<a href="./assets/wechat.png"><img src="https://img.shields.io/badge/WeChat-Group-41d56b?style=flat&logo=wechat&logoColor=white"></a>
|
||||||
|
<a href="https://discord.gg/V4sAZ9XWpN"><img src="https://img.shields.io/badge/Discord-Community-4c60eb?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | **English**
|
[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | **English**
|
||||||
|
|
@ -219,7 +222,7 @@ picoclaw onboard
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"workspace": "~/.picoclaw/workspace",
|
"workspace": "~/.picoclaw/workspace",
|
||||||
"model": "gpt4",
|
"model_name": "gpt4",
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"max_tool_iterations": 20
|
"max_tool_iterations": 20
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ picoclaw onboard
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model": "gpt4"
|
"model_name": "gpt4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ picoclaw onboard
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model": "gpt4"
|
"model_name": "gpt4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ picoclaw onboard
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"workspace": "~/.picoclaw/workspace",
|
"workspace": "~/.picoclaw/workspace",
|
||||||
"model": "gpt4",
|
"model_name": "gpt4",
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"max_tool_iterations": 20
|
"max_tool_iterations": 20
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 147 KiB |
|
|
@ -56,7 +56,7 @@ func agentCmd() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if modelOverride != "" {
|
if modelOverride != "" {
|
||||||
cfg.Agents.Defaults.Model = modelOverride
|
cfg.Agents.Defaults.ModelName = modelOverride
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, modelID, err := providers.CreateProvider(cfg)
|
provider, modelID, err := providers.CreateProvider(cfg)
|
||||||
|
|
@ -66,7 +66,7 @@ func agentCmd() {
|
||||||
}
|
}
|
||||||
// Use the resolved model ID from provider creation
|
// Use the resolved model ID from provider creation
|
||||||
if modelID != "" {
|
if modelID != "" {
|
||||||
cfg.Agents.Defaults.Model = modelID
|
cfg.Agents.Defaults.ModelName = modelID
|
||||||
}
|
}
|
||||||
|
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ func authLoginOpenAI(useDeviceCode bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update default model to use OpenAI
|
// Update default model to use OpenAI
|
||||||
appCfg.Agents.Defaults.Model = "gpt-5.2"
|
appCfg.Agents.Defaults.ModelName = "gpt-5.2"
|
||||||
|
|
||||||
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
|
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
|
||||||
fmt.Printf("Warning: could not update config: %v\n", err)
|
fmt.Printf("Warning: could not update config: %v\n", err)
|
||||||
|
|
@ -218,7 +218,7 @@ func authLoginGoogleAntigravity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update default model
|
// Update default model
|
||||||
appCfg.Agents.Defaults.Model = "gemini-flash"
|
appCfg.Agents.Defaults.ModelName = "gemini-flash"
|
||||||
|
|
||||||
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
|
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
|
||||||
fmt.Printf("Warning: could not update config: %v\n", err)
|
fmt.Printf("Warning: could not update config: %v\n", err)
|
||||||
|
|
@ -292,7 +292,7 @@ func authLoginPasteToken(provider string) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Update default model
|
// Update default model
|
||||||
appCfg.Agents.Defaults.Model = "claude-sonnet-4.6"
|
appCfg.Agents.Defaults.ModelName = "claude-sonnet-4.6"
|
||||||
case "openai":
|
case "openai":
|
||||||
appCfg.Providers.OpenAI.AuthMethod = "token"
|
appCfg.Providers.OpenAI.AuthMethod = "token"
|
||||||
// Update ModelList
|
// Update ModelList
|
||||||
|
|
@ -312,7 +312,7 @@ func authLoginPasteToken(provider string) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Update default model
|
// Update default model
|
||||||
appCfg.Agents.Defaults.Model = "gpt-5.2"
|
appCfg.Agents.Defaults.ModelName = "gpt-5.2"
|
||||||
}
|
}
|
||||||
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
|
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
|
||||||
fmt.Printf("Warning: could not update config: %v\n", err)
|
fmt.Printf("Warning: could not update config: %v\n", err)
|
||||||
|
|
@ -320,7 +320,7 @@ func authLoginPasteToken(provider string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Token saved for %s!\n", provider)
|
fmt.Printf("Token saved for %s!\n", provider)
|
||||||
fmt.Printf("Default model set to: %s\n", appCfg.Agents.Defaults.Model)
|
fmt.Printf("Default model set to: %s\n", appCfg.Agents.Defaults.GetModelName())
|
||||||
}
|
}
|
||||||
|
|
||||||
func authLogoutCmd() {
|
func authLogoutCmd() {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ func gatewayCmd() {
|
||||||
}
|
}
|
||||||
// Use the resolved model ID from provider creation
|
// Use the resolved model ID from provider creation
|
||||||
if modelID != "" {
|
if modelID != "" {
|
||||||
cfg.Agents.Defaults.Model = modelID
|
cfg.Agents.Defaults.ModelName = modelID
|
||||||
}
|
}
|
||||||
|
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ func statusCmd() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
fmt.Printf("Model: %s\n", cfg.Agents.Defaults.Model)
|
fmt.Printf("Model: %s\n", cfg.Agents.Defaults.GetModelName())
|
||||||
|
|
||||||
hasOpenRouter := cfg.Providers.OpenRouter.APIKey != ""
|
hasOpenRouter := cfg.Providers.OpenRouter.APIKey != ""
|
||||||
hasAnthropic := cfg.Providers.Anthropic.APIKey != ""
|
hasAnthropic := cfg.Providers.Anthropic.APIKey != ""
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"workspace": "~/.picoclaw/workspace",
|
"workspace": "~/.picoclaw/workspace",
|
||||||
"restrict_to_workspace": true,
|
"restrict_to_workspace": true,
|
||||||
"model": "gpt4",
|
"model_name": "gpt4",
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"max_tool_iterations": 20
|
"max_tool_iterations": 20
|
||||||
|
|
|
||||||
|
|
@ -288,25 +288,6 @@ func (cb *ContextBuilder) AddAssistantMessage(
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cb *ContextBuilder) loadSkills() string {
|
|
||||||
allSkills := cb.skillsLoader.ListSkills()
|
|
||||||
if len(allSkills) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var skillNames []string
|
|
||||||
for _, s := range allSkills {
|
|
||||||
skillNames = append(skillNames, s.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
content := cb.skillsLoader.LoadSkillsForContext(skillNames)
|
|
||||||
if content == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return "# Skill Definitions\n\n" + content
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSkillsInfo returns information about loaded skills.
|
// GetSkillsInfo returns information about loaded skills.
|
||||||
func (cb *ContextBuilder) GetSkillsInfo() map[string]any {
|
func (cb *ContextBuilder) GetSkillsInfo() map[string]any {
|
||||||
allSkills := cb.skillsLoader.ListSkills()
|
allSkills := cb.skillsLoader.ListSkills()
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ func resolveAgentModel(agentCfg *config.AgentConfig, defaults *config.AgentDefau
|
||||||
if agentCfg != nil && agentCfg.Model != nil && strings.TrimSpace(agentCfg.Model.Primary) != "" {
|
if agentCfg != nil && agentCfg.Model != nil && strings.TrimSpace(agentCfg.Model.Primary) != "" {
|
||||||
return strings.TrimSpace(agentCfg.Model.Primary)
|
return strings.TrimSpace(agentCfg.Model.Primary)
|
||||||
}
|
}
|
||||||
return defaults.Model
|
return defaults.GetModelName()
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveAgentFallbacks resolves the fallback models for an agent.
|
// resolveAgentFallbacks resolves the fallback models for an agent.
|
||||||
|
|
|
||||||
|
|
@ -628,6 +628,7 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
assistantMsg := providers.Message{
|
assistantMsg := providers.Message{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: response.Content,
|
Content: response.Content,
|
||||||
|
ReasoningContent: response.ReasoningContent,
|
||||||
}
|
}
|
||||||
for _, tc := range normalizedToolCalls {
|
for _, tc := range normalizedToolCalls {
|
||||||
argumentsJSON, _ := json.Marshal(tc.Arguments)
|
argumentsJSON, _ := json.Marshal(tc.Arguments)
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ func (c *cmd) Show(ctx context.Context, message telego.Message) error {
|
||||||
switch args {
|
switch args {
|
||||||
case "model":
|
case "model":
|
||||||
response = fmt.Sprintf("Current Model: %s (Provider: %s)",
|
response = fmt.Sprintf("Current Model: %s (Provider: %s)",
|
||||||
c.config.Agents.Defaults.Model,
|
c.config.Agents.Defaults.GetModelName(),
|
||||||
c.config.Agents.Defaults.Provider)
|
c.config.Agents.Defaults.Provider)
|
||||||
case "channel":
|
case "channel":
|
||||||
response = "Current Channel: telegram"
|
response = "Current Channel: telegram"
|
||||||
|
|
@ -120,7 +120,7 @@ func (c *cmd) List(ctx context.Context, message telego.Message) error {
|
||||||
provider = "configured default"
|
provider = "configured default"
|
||||||
}
|
}
|
||||||
response = fmt.Sprintf("Configured Model: %s\nProvider: %s\n\nTo change models, update config.yaml",
|
response = fmt.Sprintf("Configured Model: %s\nProvider: %s\n\nTo change models, update config.yaml",
|
||||||
c.config.Agents.Defaults.Model, provider)
|
c.config.Agents.Defaults.GetModelName(), provider)
|
||||||
|
|
||||||
case "channels":
|
case "channels":
|
||||||
var enabled []string
|
var enabled []string
|
||||||
|
|
|
||||||
|
|
@ -571,61 +571,6 @@ func (c *WeComAppChannel) sendTextMessage(ctx context.Context, accessToken, user
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendMarkdownMessage sends a markdown message to a user
|
|
||||||
func (c *WeComAppChannel) sendMarkdownMessage(ctx context.Context, accessToken, userID, content string) error {
|
|
||||||
apiURL := fmt.Sprintf("%s/cgi-bin/message/send?access_token=%s", wecomAPIBase, accessToken)
|
|
||||||
|
|
||||||
msg := WeComMarkdownMessage{
|
|
||||||
ToUser: userID,
|
|
||||||
MsgType: "markdown",
|
|
||||||
AgentID: c.config.AgentID,
|
|
||||||
}
|
|
||||||
msg.Markdown.Content = content
|
|
||||||
|
|
||||||
jsonData, err := json.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use configurable timeout (default 5 seconds)
|
|
||||||
timeout := c.config.ReplyTimeout
|
|
||||||
if timeout <= 0 {
|
|
||||||
timeout = 5
|
|
||||||
}
|
|
||||||
|
|
||||||
reqCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, apiURL, bytes.NewBuffer(jsonData))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: time.Duration(timeout) * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to send message: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendResp WeComSendMessageResponse
|
|
||||||
if err := json.Unmarshal(body, &sendResp); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if sendResp.ErrCode != 0 {
|
|
||||||
return fmt.Errorf("API error: %s (code: %d)", sendResp.ErrMsg, sendResp.ErrCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleHealth handles health check requests
|
// handleHealth handles health check requests
|
||||||
func (c *WeComAppChannel) handleHealth(w http.ResponseWriter, r *http.Request) {
|
func (c *WeComAppChannel) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
status := map[string]any{
|
status := map[string]any{
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,8 @@ type AgentDefaults struct {
|
||||||
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
|
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
|
||||||
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
|
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
|
||||||
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
|
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
|
||||||
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
|
ModelName string `json:"model_name,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"`
|
||||||
|
Model string `json:"model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` // Deprecated: use model_name instead
|
||||||
ModelFallbacks []string `json:"model_fallbacks,omitempty"`
|
ModelFallbacks []string `json:"model_fallbacks,omitempty"`
|
||||||
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
|
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
|
||||||
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
|
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
|
||||||
|
|
@ -179,6 +180,15 @@ type AgentDefaults struct {
|
||||||
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetModelName returns the effective model name for the agent defaults.
|
||||||
|
// It prefers the new "model_name" field but falls back to "model" for backward compatibility.
|
||||||
|
func (d *AgentDefaults) GetModelName() string {
|
||||||
|
if d.ModelName != "" {
|
||||||
|
return d.ModelName
|
||||||
|
}
|
||||||
|
return d.Model
|
||||||
|
}
|
||||||
|
|
||||||
type ChannelsConfig struct {
|
type ChannelsConfig struct {
|
||||||
WhatsApp WhatsAppConfig `json:"whatsapp"`
|
WhatsApp WhatsAppConfig `json:"whatsapp"`
|
||||||
Telegram TelegramConfig `json:"telegram"`
|
Telegram TelegramConfig `json:"telegram"`
|
||||||
|
|
@ -499,6 +509,20 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-scan the JSON to check how many model_list entries the user provided.
|
||||||
|
// Go's JSON decoder reuses existing slice backing-array elements rather than
|
||||||
|
// zero-initializing them, so fields absent from the user's JSON (e.g. api_base)
|
||||||
|
// would silently inherit values from the DefaultConfig template at the same
|
||||||
|
// index position. We only reset cfg.ModelList when the user actually provides
|
||||||
|
// entries; when count is 0 we keep DefaultConfig's built-in list as fallback.
|
||||||
|
var tmp Config
|
||||||
|
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(tmp.ModelList) > 0 {
|
||||||
|
cfg.ModelList = nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(data, cfg); err != nil {
|
if err := json.Unmarshal(data, cfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
|
||||||
|
|
||||||
// Get user's configured provider and model
|
// Get user's configured provider and model
|
||||||
userProvider := strings.ToLower(cfg.Agents.Defaults.Provider)
|
userProvider := strings.ToLower(cfg.Agents.Defaults.Provider)
|
||||||
userModel := cfg.Agents.Defaults.Model
|
userModel := cfg.Agents.Defaults.GetModelName()
|
||||||
|
|
||||||
p := cfg.Providers
|
p := cfg.Providers
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -114,6 +115,137 @@ func TestGetModelConfig_Concurrent(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgentDefaults_GetModelName_BackwardCompat(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
defaults AgentDefaults
|
||||||
|
wantName string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "new model_name field only",
|
||||||
|
defaults: AgentDefaults{ModelName: "new-model"},
|
||||||
|
wantName: "new-model",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "old model field only",
|
||||||
|
defaults: AgentDefaults{Model: "legacy-model"},
|
||||||
|
wantName: "legacy-model",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both fields - model_name takes precedence",
|
||||||
|
defaults: AgentDefaults{ModelName: "new-model", Model: "old-model"},
|
||||||
|
wantName: "new-model",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := tt.defaults.GetModelName(); got != tt.wantName {
|
||||||
|
t.Errorf("GetModelName() = %q, want %q", got, tt.wantName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgentDefaults_JSON_BackwardCompat(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
json string
|
||||||
|
wantName string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "new model_name field",
|
||||||
|
json: `{"model_name": "gpt4"}`,
|
||||||
|
wantName: "gpt4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "old model field",
|
||||||
|
json: `{"model": "gpt4"}`,
|
||||||
|
wantName: "gpt4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both fields - model_name wins",
|
||||||
|
json: `{"model_name": "new", "model": "old"}`,
|
||||||
|
wantName: "new",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var defaults AgentDefaults
|
||||||
|
if err := json.Unmarshal([]byte(tt.json), &defaults); err != nil {
|
||||||
|
t.Fatalf("Unmarshal error: %v", err)
|
||||||
|
}
|
||||||
|
if got := defaults.GetModelName(); got != tt.wantName {
|
||||||
|
t.Errorf("GetModelName() = %q, want %q", got, tt.wantName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFullConfig_JSON_BackwardCompat(t *testing.T) {
|
||||||
|
// Test complete config with both old and new formats
|
||||||
|
oldFormat := `{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "~/.picoclaw/workspace",
|
||||||
|
"model": "gpt4",
|
||||||
|
"max_tokens": 4096
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model_list": [
|
||||||
|
{
|
||||||
|
"model_name": "gpt4",
|
||||||
|
"model": "openai/gpt-4o",
|
||||||
|
"api_key": "test-key"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
newFormat := `{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "~/.picoclaw/workspace",
|
||||||
|
"model_name": "gpt4",
|
||||||
|
"max_tokens": 4096
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model_list": [
|
||||||
|
{
|
||||||
|
"model_name": "gpt4",
|
||||||
|
"model": "openai/gpt-4o",
|
||||||
|
"api_key": "test-key"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
for name, jsonStr := range map[string]string{
|
||||||
|
"old format (model)": oldFormat,
|
||||||
|
"new format (model_name)": newFormat,
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
cfg := &Config{}
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), cfg); err != nil {
|
||||||
|
t.Fatalf("Unmarshal error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that GetModelName returns correct value
|
||||||
|
if got := cfg.Agents.Defaults.GetModelName(); got != "gpt4" {
|
||||||
|
t.Errorf("GetModelName() = %q, want %q", got, "gpt4")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that GetModelConfig works
|
||||||
|
modelCfg, err := cfg.GetModelConfig("gpt4")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetModelConfig error: %v", err)
|
||||||
|
}
|
||||||
|
if modelCfg.Model != "openai/gpt-4o" {
|
||||||
|
t.Errorf("Model = %q, want %q", modelCfg.Model, "openai/gpt-4o")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestModelConfig_Validate(t *testing.T) {
|
func TestModelConfig_Validate(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ var usbClassToCapability = map[string]string{
|
||||||
|
|
||||||
type USBMonitor struct {
|
type USBMonitor struct {
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
cancel context.CancelFunc
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,10 @@ func ConvertConfig(data map[string]any) (*config.Config, []string, error) {
|
||||||
|
|
||||||
if agents, ok := getMap(data, "agents"); ok {
|
if agents, ok := getMap(data, "agents"); ok {
|
||||||
if defaults, ok := getMap(agents, "defaults"); ok {
|
if defaults, ok := getMap(agents, "defaults"); ok {
|
||||||
if v, ok := getString(defaults, "model"); ok {
|
// Prefer model_name, fallback to model for backward compatibility
|
||||||
|
if v, ok := getString(defaults, "model_name"); ok {
|
||||||
|
cfg.Agents.Defaults.ModelName = v
|
||||||
|
} else if v, ok := getString(defaults, "model"); ok {
|
||||||
cfg.Agents.Defaults.Model = v
|
cfg.Agents.Defaults.Model = v
|
||||||
}
|
}
|
||||||
if v, ok := getFloat(defaults, "max_tokens"); ok {
|
if v, ok := getFloat(defaults, "max_tokens"); ok {
|
||||||
|
|
|
||||||
|
|
@ -404,64 +404,6 @@ type antigravityJSONResponse struct {
|
||||||
} `json:"usageMetadata"`
|
} `json:"usageMetadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AntigravityProvider) parseJSONResponse(body []byte) (*LLMResponse, error) {
|
|
||||||
var resp antigravityJSONResponse
|
|
||||||
if err := json.Unmarshal(body, &resp); err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing antigravity response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp.Candidates) == 0 {
|
|
||||||
return nil, fmt.Errorf("antigravity: no candidates in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
candidate := resp.Candidates[0]
|
|
||||||
var contentParts []string
|
|
||||||
var toolCalls []ToolCall
|
|
||||||
|
|
||||||
for _, part := range candidate.Content.Parts {
|
|
||||||
if part.Text != "" {
|
|
||||||
contentParts = append(contentParts, part.Text)
|
|
||||||
}
|
|
||||||
if part.FunctionCall != nil {
|
|
||||||
argumentsJSON, _ := json.Marshal(part.FunctionCall.Args)
|
|
||||||
toolCalls = append(toolCalls, ToolCall{
|
|
||||||
ID: fmt.Sprintf("call_%s_%d", part.FunctionCall.Name, time.Now().UnixNano()),
|
|
||||||
Name: part.FunctionCall.Name,
|
|
||||||
Arguments: part.FunctionCall.Args,
|
|
||||||
Function: &FunctionCall{
|
|
||||||
Name: part.FunctionCall.Name,
|
|
||||||
Arguments: string(argumentsJSON),
|
|
||||||
ThoughtSignature: extractPartThoughtSignature(part.ThoughtSignature, part.ThoughtSignatureSnake),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finishReason := "stop"
|
|
||||||
if len(toolCalls) > 0 {
|
|
||||||
finishReason = "tool_calls"
|
|
||||||
}
|
|
||||||
if candidate.FinishReason == "MAX_TOKENS" {
|
|
||||||
finishReason = "length"
|
|
||||||
}
|
|
||||||
|
|
||||||
var usage *UsageInfo
|
|
||||||
if resp.UsageMetadata.TotalTokenCount > 0 {
|
|
||||||
usage = &UsageInfo{
|
|
||||||
PromptTokens: resp.UsageMetadata.PromptTokenCount,
|
|
||||||
CompletionTokens: resp.UsageMetadata.CandidatesTokenCount,
|
|
||||||
TotalTokens: resp.UsageMetadata.TotalTokenCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &LLMResponse{
|
|
||||||
Content: strings.Join(contentParts, ""),
|
|
||||||
ToolCalls: toolCalls,
|
|
||||||
FinishReason: finishReason,
|
|
||||||
Usage: usage,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *AntigravityProvider) parseSSEResponse(body string) (*LLMResponse, error) {
|
func (p *AntigravityProvider) parseSSEResponse(body string) (*LLMResponse, error) {
|
||||||
var contentParts []string
|
var contentParts []string
|
||||||
var toolCalls []ToolCall
|
var toolCalls []ToolCall
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ type providerSelection struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
|
func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
|
||||||
model := cfg.Agents.Defaults.Model
|
model := cfg.Agents.Defaults.GetModelName()
|
||||||
providerName := strings.ToLower(cfg.Agents.Defaults.Provider)
|
providerName := strings.ToLower(cfg.Agents.Defaults.Provider)
|
||||||
lowerModel := strings.ToLower(model)
|
lowerModel := strings.ToLower(model)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,6 @@ func successRun(content string) func(ctx context.Context, provider, model string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func failRun(err error) func(ctx context.Context, provider, model string) (*LLMResponse, error) {
|
|
||||||
return func(ctx context.Context, provider, model string) (*LLMResponse, error) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFallback_SingleCandidate_Success(t *testing.T) {
|
func TestFallback_SingleCandidate_Success(t *testing.T) {
|
||||||
ct := NewCooldownTracker()
|
ct := NewCooldownTracker()
|
||||||
fc := NewFallbackChain(ct)
|
fc := NewFallbackChain(ct)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import (
|
||||||
// The old providers config is automatically converted to model_list during config loading.
|
// The old providers config is automatically converted to model_list during config loading.
|
||||||
// Returns the provider, the model ID to use, and any error.
|
// Returns the provider, the model ID to use, and any error.
|
||||||
func CreateProvider(cfg *config.Config) (LLMProvider, string, error) {
|
func CreateProvider(cfg *config.Config) (LLMProvider, string, error) {
|
||||||
model := cfg.Agents.Defaults.Model
|
model := cfg.Agents.Defaults.GetModelName()
|
||||||
|
|
||||||
// Ensure model_list is populated (should be done by LoadConfig, but handle edge cases)
|
// Ensure model_list is populated (should be done by LoadConfig, but handle edge cases)
|
||||||
if len(cfg.ModelList) == 0 && cfg.HasProvidersConfig() {
|
if len(cfg.ModelList) == 0 && cfg.HasProvidersConfig() {
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
Choices []struct {
|
Choices []struct {
|
||||||
Message struct {
|
Message struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
ReasoningContent string `json:"reasoning_content"`
|
||||||
ToolCalls []struct {
|
ToolCalls []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
@ -222,6 +223,7 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
|
|
||||||
return &LLMResponse{
|
return &LLMResponse{
|
||||||
Content: choice.Message.Content,
|
Content: choice.Message.Content,
|
||||||
|
ReasoningContent: choice.Message.ReasoningContent,
|
||||||
ToolCalls: toolCalls,
|
ToolCalls: toolCalls,
|
||||||
FinishReason: choice.FinishReason,
|
FinishReason: choice.FinishReason,
|
||||||
Usage: apiResponse.Usage,
|
Usage: apiResponse.Usage,
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,50 @@ func TestProviderChat_ParsesToolCalls(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProviderChat_ParsesReasoningContent(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
resp := map[string]any{
|
||||||
|
"choices": []map[string]any{
|
||||||
|
{
|
||||||
|
"message": map[string]any{
|
||||||
|
"content": "The answer is 2",
|
||||||
|
"reasoning_content": "Let me think step by step... 1+1=2",
|
||||||
|
"tool_calls": []map[string]any{
|
||||||
|
{
|
||||||
|
"id": "call_1",
|
||||||
|
"type": "function",
|
||||||
|
"function": map[string]any{
|
||||||
|
"name": "calculator",
|
||||||
|
"arguments": "{\"expr\":\"1+1\"}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"finish_reason": "tool_calls",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
p := NewProvider("key", server.URL, "")
|
||||||
|
out, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "1+1=?"}}, nil, "kimi-k2.5", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Chat() error = %v", err)
|
||||||
|
}
|
||||||
|
if out.ReasoningContent != "Let me think step by step... 1+1=2" {
|
||||||
|
t.Fatalf("ReasoningContent = %q, want %q", out.ReasoningContent, "Let me think step by step... 1+1=2")
|
||||||
|
}
|
||||||
|
if out.Content != "The answer is 2" {
|
||||||
|
t.Fatalf("Content = %q, want %q", out.Content, "The answer is 2")
|
||||||
|
}
|
||||||
|
if len(out.ToolCalls) != 1 {
|
||||||
|
t.Fatalf("len(ToolCalls) = %d, want 1", len(out.ToolCalls))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProviderChat_HTTPError(t *testing.T) {
|
func TestProviderChat_HTTPError(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) {
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ type FunctionCall struct {
|
||||||
|
|
||||||
type LLMResponse struct {
|
type LLMResponse struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||||
FinishReason string `json:"finish_reason"`
|
FinishReason string `json:"finish_reason"`
|
||||||
Usage *UsageInfo `json:"usage,omitempty"`
|
Usage *UsageInfo `json:"usage,omitempty"`
|
||||||
|
|
@ -40,6 +41,7 @@ type UsageInfo struct {
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,13 +117,19 @@ func (t *I2CTool) detect() *ToolResult {
|
||||||
return SilentResult(fmt.Sprintf("Found %d I2C bus(es):\n%s", len(buses), string(result)))
|
return SilentResult(fmt.Sprintf("Found %d I2C bus(es):\n%s", len(buses), string(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper functions for I2C operations (used by platform-specific implementations)
|
||||||
|
|
||||||
// isValidBusID checks that a bus identifier is a simple number (prevents path injection)
|
// isValidBusID checks that a bus identifier is a simple number (prevents path injection)
|
||||||
|
//
|
||||||
|
//nolint:unused // Used by i2c_linux.go
|
||||||
func isValidBusID(id string) bool {
|
func isValidBusID(id string) bool {
|
||||||
matched, _ := regexp.MatchString(`^\d+$`, id)
|
matched, _ := regexp.MatchString(`^\d+$`, id)
|
||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseI2CAddress extracts and validates an I2C address from args
|
// parseI2CAddress extracts and validates an I2C address from args
|
||||||
|
//
|
||||||
|
//nolint:unused // Used by i2c_linux.go
|
||||||
func parseI2CAddress(args map[string]any) (int, *ToolResult) {
|
func parseI2CAddress(args map[string]any) (int, *ToolResult) {
|
||||||
addrFloat, ok := args["address"].(float64)
|
addrFloat, ok := args["address"].(float64)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -137,6 +143,8 @@ func parseI2CAddress(args map[string]any) (int, *ToolResult) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseI2CBus extracts and validates an I2C bus from args
|
// parseI2CBus extracts and validates an I2C bus from args
|
||||||
|
//
|
||||||
|
//nolint:unused // Used by i2c_linux.go
|
||||||
func parseI2CBus(args map[string]any) (string, *ToolResult) {
|
func parseI2CBus(args map[string]any) (string, *ToolResult) {
|
||||||
bus, ok := args["bus"].(string)
|
bus, ok := args["bus"].(string)
|
||||||
if !ok || bus == "" {
|
if !ok || bus == "" {
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,11 @@ func (t *SPITool) list() *ToolResult {
|
||||||
return SilentResult(fmt.Sprintf("Found %d SPI device(s):\n%s", len(devices), string(result)))
|
return SilentResult(fmt.Sprintf("Found %d SPI device(s):\n%s", len(devices), string(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function for SPI operations (used by platform-specific implementations)
|
||||||
|
|
||||||
// parseSPIArgs extracts and validates common SPI parameters
|
// parseSPIArgs extracts and validates common SPI parameters
|
||||||
|
//
|
||||||
|
//nolint:unused // Used by spi_linux.go
|
||||||
func parseSPIArgs(args map[string]any) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
|
func parseSPIArgs(args map[string]any) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
|
||||||
dev, ok := args["device"].(string)
|
dev, ok := args["device"].(string)
|
||||||
if !ok || dev == "" {
|
if !ok || dev == "" {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue