feat(agent): add verbose logging flags for CLI interactive mode
Add three new command-line flags to 'picoclaw agent' command: - --verbose (-v): Show detailed progress logs including API calls and context compression - --show-tools: Display tool execution status with timing information - --show-think: Show thinking process indicators Changes: - cmd/picoclaw/internal/agent/command.go: Add flag definitions and usage template - cmd/picoclaw/internal/agent/interactive.go: New file implementing InteractiveLogger with event subscription - pkg/agent/events.go: Extend LLMRequestPayload and LLMResponsePayload with detailed content previews - pkg/agent/loop.go: Emit enhanced event payloads with message previews and tool call details (with nil safety check) - docs/cli-logging.md: Feature usage guide (Chinese) - docs/test-logging.md: Test documentation (Chinese) This addresses the poor UX of blank waiting during agent processing by providing real-time semantic-friendly progress feedback. Tested with: DeepSeek Chat 🤖 AI Code Generation: 🛠️ Mostly AI-generated Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
7b3f47128f
commit
4ef8973508
6 changed files with 1048 additions and 11 deletions
|
|
@ -10,6 +10,9 @@ func NewAgentCommand() *cobra.Command {
|
|||
sessionKey string
|
||||
model string
|
||||
debug bool
|
||||
verbose bool
|
||||
showTools bool
|
||||
showThink bool
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -17,14 +20,47 @@ func NewAgentCommand() *cobra.Command {
|
|||
Short: "Interact with the agent directly",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
// Use enhanced interactive mode with logging if any verbose flag is set
|
||||
if verbose || showTools || showThink {
|
||||
return agentCmdWithLogging(message, sessionKey, model, debug, verbose, showTools, showThink)
|
||||
}
|
||||
return agentCmd(message, sessionKey, model, debug)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug logging")
|
||||
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show verbose progress logs")
|
||||
cmd.Flags().BoolVar(&showTools, "show-tools", false, "Show tool execution logs")
|
||||
cmd.Flags().BoolVar(&showThink, "show-think", false, "Show thinking process indicators")
|
||||
cmd.Flags().StringVarP(&message, "message", "m", "", "Send a single message (non-interactive mode)")
|
||||
cmd.Flags().StringVarP(&sessionKey, "session", "s", "cli:default", "Session key")
|
||||
cmd.Flags().StringVarP(&model, "model", "", "", "Model to use")
|
||||
|
||||
cmd.SetUsageTemplate(`Usage:
|
||||
{{.CommandPath}} [flags]
|
||||
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}
|
||||
|
||||
Examples:
|
||||
# Interactive mode (default)
|
||||
{{.CommandPath}}
|
||||
|
||||
# Interactive mode with verbose logging
|
||||
{{.CommandPath}} --verbose
|
||||
|
||||
# Show tool execution logs
|
||||
{{.CommandPath}} --show-tools
|
||||
|
||||
# Show thinking process
|
||||
{{.CommandPath}} --show-think
|
||||
|
||||
# Send a single message
|
||||
{{.CommandPath}} -m "What is the weather today?"
|
||||
|
||||
# Combine multiple logging options
|
||||
{{.CommandPath}} --verbose --show-tools --show-think
|
||||
`)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
443
cmd/picoclaw/internal/agent/interactive.go
Normal file
443
cmd/picoclaw/internal/agent/interactive.go
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
// PicoClaw - Ultra-lightweight personal AI agent
|
||||
// Interactive CLI with semantic-friendly logging
|
||||
//
|
||||
// This file provides an enhanced interactive mode that shows real-time
|
||||
// progress logs during agent processing, including:
|
||||
// - Thinking process indicators
|
||||
// - Tool execution status
|
||||
// - API call progress
|
||||
// - Final response delivery
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/readline"
|
||||
|
||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||
"github.com/sipeed/picoclaw/pkg/agent"
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
)
|
||||
|
||||
// InteractiveLogger provides real-time feedback during agent processing
|
||||
type InteractiveLogger struct {
|
||||
mu sync.Mutex
|
||||
verbose bool
|
||||
showTools bool
|
||||
showThink bool
|
||||
showAPI bool
|
||||
eventSub agent.EventSubscription
|
||||
agentLoop *agent.AgentLoop
|
||||
lastTurnID string
|
||||
}
|
||||
|
||||
// NewInteractiveLogger creates a new interactive logger
|
||||
func NewInteractiveLogger(verbose, showTools, showThink bool) *InteractiveLogger {
|
||||
return &InteractiveLogger{
|
||||
verbose: verbose,
|
||||
showTools: showTools,
|
||||
showThink: showThink,
|
||||
showAPI: verbose || showThink,
|
||||
}
|
||||
}
|
||||
|
||||
// StartEventSubscription subscribes to agent events and logs them
|
||||
func (il *InteractiveLogger) StartEventSubscription(agentLoop *agent.AgentLoop) {
|
||||
il.agentLoop = agentLoop
|
||||
il.eventSub = agentLoop.SubscribeEvents(100)
|
||||
|
||||
go func() {
|
||||
for evt := range il.eventSub.C {
|
||||
il.handleEvent(evt)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// StopEventSubscription stops the event subscription
|
||||
func (il *InteractiveLogger) StopEventSubscription() {
|
||||
if il.agentLoop != nil && il.eventSub.C != nil {
|
||||
il.agentLoop.UnsubscribeEvents(il.eventSub.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// handleEvent processes agent events and displays them
|
||||
func (il *InteractiveLogger) handleEvent(evt agent.Event) {
|
||||
il.mu.Lock()
|
||||
defer il.mu.Unlock()
|
||||
|
||||
// Track turn changes
|
||||
if evt.Meta.TurnID != il.lastTurnID {
|
||||
il.lastTurnID = evt.Meta.TurnID
|
||||
}
|
||||
|
||||
switch evt.Kind {
|
||||
case agent.EventKindTurnStart:
|
||||
if il.showThink {
|
||||
payload, ok := evt.Payload.(agent.TurnStartPayload)
|
||||
if ok {
|
||||
fmt.Printf("\r\033[K%s \033[1;36m⚙️ Thinking\033[0m (channel: %s)\n", internal.Logo, payload.Channel)
|
||||
}
|
||||
}
|
||||
|
||||
case agent.EventKindLLMRequest:
|
||||
if il.showAPI {
|
||||
payload, ok := evt.Payload.(agent.LLMRequestPayload)
|
||||
if ok {
|
||||
// Show basic info
|
||||
fmt.Printf("\r\033[K%s \033[1;35m📡 Calling API\033[0m: %s (messages: %d, tools: %d)\n",
|
||||
internal.Logo, payload.Model, payload.MessagesCount, payload.ToolsCount)
|
||||
|
||||
// Show detailed info in verbose mode
|
||||
if il.verbose {
|
||||
if payload.UserMessagePreview != "" {
|
||||
fmt.Printf(" \033[1;36m📝 User: %s\033[0m\n",
|
||||
payload.UserMessagePreview)
|
||||
}
|
||||
if len(payload.ToolNames) > 0 {
|
||||
fmt.Printf(" \033[1;33m🔧 Tools: %s\033[0m\n",
|
||||
strings.Join(payload.ToolNames, ", "))
|
||||
}
|
||||
if payload.MaxTokens > 0 {
|
||||
fmt.Printf(" \033[1;37m⚙️ MaxTokens: %d, Temp: %.2f\033[0m\n",
|
||||
payload.MaxTokens, payload.Temperature)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case agent.EventKindLLMResponse:
|
||||
if il.showAPI {
|
||||
payload, ok := evt.Payload.(agent.LLMResponsePayload)
|
||||
if ok {
|
||||
toolInfo := ""
|
||||
if payload.ToolCalls > 0 {
|
||||
toolInfo = fmt.Sprintf(", tool calls: %d", payload.ToolCalls)
|
||||
}
|
||||
fmt.Printf("\r\033[K%s \033[1;32m✅ API Response\033[0m (content: %d chars%s)\n",
|
||||
internal.Logo, payload.ContentLen, toolInfo)
|
||||
|
||||
// Show detailed info in verbose mode
|
||||
if il.verbose {
|
||||
if payload.ContentPreview != "" {
|
||||
fmt.Printf(" \033[1;37m📄 Content: %s\033[0m\n", payload.ContentPreview)
|
||||
}
|
||||
if len(payload.ToolCallDetails) > 0 {
|
||||
for _, tc := range payload.ToolCallDetails {
|
||||
fmt.Printf(" \033[1;33m🔧 Tool: %s(%s)\033[0m\n", tc.Name, tc.Arguments)
|
||||
}
|
||||
}
|
||||
if payload.FinishReason != "" {
|
||||
fmt.Printf(" \033[1;36m🏁 Finish: %s\033[0m\n", payload.FinishReason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case agent.EventKindLLMRetry:
|
||||
payload, ok := evt.Payload.(agent.LLMRetryPayload)
|
||||
if ok {
|
||||
fmt.Printf("\r\033[K%s \033[1;33m🔄 Retrying API\033[0m (attempt %d/%d): %s\n",
|
||||
internal.Logo, payload.Attempt, payload.MaxRetries, payload.Reason)
|
||||
}
|
||||
|
||||
case agent.EventKindToolExecStart:
|
||||
if il.showTools {
|
||||
payload, ok := evt.Payload.(agent.ToolExecStartPayload)
|
||||
if ok {
|
||||
argsStr := ""
|
||||
for k, v := range payload.Arguments {
|
||||
if argsStr != "" {
|
||||
argsStr += ", "
|
||||
}
|
||||
argsStr += fmt.Sprintf("%s=%v", k, v)
|
||||
}
|
||||
if len(argsStr) > 80 {
|
||||
argsStr = argsStr[:77] + "..."
|
||||
}
|
||||
fmt.Printf("\r\033[K%s \033[1;33m🔧 Tool Calling: %s(%s)\033[0m\n",
|
||||
internal.Logo, payload.Tool, argsStr)
|
||||
}
|
||||
}
|
||||
|
||||
case agent.EventKindToolExecEnd:
|
||||
if il.showTools {
|
||||
payload, ok := evt.Payload.(agent.ToolExecEndPayload)
|
||||
if ok {
|
||||
duration := payload.Duration.Round(time.Millisecond)
|
||||
if payload.IsError {
|
||||
fmt.Printf("\r\033[K%s \033[1;31m❌ Tool Failed: %s (%v)\033[0m\n",
|
||||
internal.Logo, payload.Tool, duration)
|
||||
} else {
|
||||
fmt.Printf("\r\033[K%s \033[1;32m✅ Tool Completed: %s (%v)\033[0m\n",
|
||||
internal.Logo, payload.Tool, duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case agent.EventKindContextCompress:
|
||||
if il.verbose {
|
||||
payload, ok := evt.Payload.(agent.ContextCompressPayload)
|
||||
if ok {
|
||||
fmt.Printf("\r\033[K%s \033[1;33m🗜️ Context Compressed\033[0m: dropped %d, kept %d messages\n",
|
||||
internal.Logo, payload.DroppedMessages, payload.RemainingMessages)
|
||||
}
|
||||
}
|
||||
|
||||
case agent.EventKindError:
|
||||
payload, ok := evt.Payload.(agent.ErrorPayload)
|
||||
if ok {
|
||||
fmt.Printf("\r\033[K%s \033[1;31m❌ Error [%s]: %s\033[0m\n",
|
||||
internal.Logo, payload.Stage, payload.Message)
|
||||
}
|
||||
|
||||
case agent.EventKindTurnEnd:
|
||||
payload, ok := evt.Payload.(agent.TurnEndPayload)
|
||||
if ok && il.verbose {
|
||||
statusIcon := "✅"
|
||||
if payload.Status == agent.TurnEndStatusError {
|
||||
statusIcon = "❌"
|
||||
} else if payload.Status == agent.TurnEndStatusAborted {
|
||||
statusIcon = "⚠️"
|
||||
}
|
||||
fmt.Printf("\r\033[K%s \033[1;37m%s Turn Ended\033[0m: %d iterations, %v, %d chars\n",
|
||||
internal.Logo,
|
||||
statusIcon,
|
||||
payload.Iterations,
|
||||
payload.Duration.Round(time.Millisecond),
|
||||
payload.FinalContentLen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LogThinking logs the thinking process start
|
||||
func (il *InteractiveLogger) LogThinking() {
|
||||
if !il.showThink {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// LogResponse logs the final response
|
||||
func (il *InteractiveLogger) LogResponse() {
|
||||
il.mu.Lock()
|
||||
defer il.mu.Unlock()
|
||||
fmt.Printf("\r\033[K%s \033[1;34m💬 Response:\033[0m\n", internal.Logo)
|
||||
}
|
||||
|
||||
// LogError logs an error
|
||||
func (il *InteractiveLogger) LogError(err error) {
|
||||
il.mu.Lock()
|
||||
defer il.mu.Unlock()
|
||||
fmt.Printf("\r\033[K%s \033[1;31m❌ Error:\033[0m %v\n", internal.Logo, err)
|
||||
}
|
||||
|
||||
// agentCmdWithLogging enhanced agent command with interactive logging
|
||||
func agentCmdWithLogging(message, sessionKey, model string, debug, verbose, showTools, showThink bool) error {
|
||||
if sessionKey == "" {
|
||||
sessionKey = "cli:default"
|
||||
}
|
||||
|
||||
cfg, err := internal.LoadConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading config: %w", err)
|
||||
}
|
||||
|
||||
logger.ConfigureFromEnv()
|
||||
|
||||
if debug {
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
fmt.Println("🔍 Debug mode enabled")
|
||||
}
|
||||
|
||||
if model != "" {
|
||||
cfg.Agents.Defaults.ModelName = model
|
||||
}
|
||||
|
||||
provider, modelID, err := providers.CreateProvider(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating provider: %w", err)
|
||||
}
|
||||
|
||||
// Use the resolved model ID from provider creation
|
||||
if modelID != "" {
|
||||
cfg.Agents.Defaults.ModelName = modelID
|
||||
}
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
defer msgBus.Close()
|
||||
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
||||
defer agentLoop.Close()
|
||||
|
||||
// Print agent startup info (only for interactive mode)
|
||||
startupInfo := agentLoop.GetStartupInfo()
|
||||
logger.InfoCF("agent", "Agent initialized",
|
||||
map[string]any{
|
||||
"tools_count": startupInfo["tools"].(map[string]any)["count"],
|
||||
"skills_total": startupInfo["skills"].(map[string]any)["total"],
|
||||
"skills_available": startupInfo["skills"].(map[string]any)["available"],
|
||||
})
|
||||
|
||||
if message != "" {
|
||||
ctx := context.Background()
|
||||
response, err := agentLoop.ProcessDirect(ctx, message, sessionKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing message: %w", err)
|
||||
}
|
||||
fmt.Printf("\n%s %s\n", internal.Logo, response)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s \033[1;32mInteractive mode\033[0m (type 'exit' to quit, Ctrl+C to interrupt)\n", internal.Logo)
|
||||
if verbose {
|
||||
fmt.Printf("%s \033[90mVerbose logging enabled\033[0m\n", internal.Logo)
|
||||
}
|
||||
if showTools {
|
||||
fmt.Printf("%s \033[90mTool execution logging enabled\033[0m\n", internal.Logo)
|
||||
}
|
||||
if showThink {
|
||||
fmt.Printf("%s \033[90mThinking process logging enabled\033[0m\n", internal.Logo)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
interactiveModeWithLogging(agentLoop, sessionKey, verbose, showTools, showThink)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// interactiveModeWithLogging enhanced interactive mode with real-time logging
|
||||
func interactiveModeWithLogging(agentLoop *agent.AgentLoop, sessionKey string, verbose, showTools, showThink bool) {
|
||||
prompt := fmt.Sprintf("%s \033[32mYou:\033[0m ", internal.Logo)
|
||||
il := NewInteractiveLogger(verbose, showTools, showThink)
|
||||
|
||||
// Start event subscription for real-time logs
|
||||
il.StartEventSubscription(agentLoop)
|
||||
defer il.StopEventSubscription()
|
||||
|
||||
rl, err := readline.NewEx(&readline.Config{
|
||||
Prompt: prompt,
|
||||
HistoryFile: filepath.Join(os.TempDir(), ".picoclaw_history"),
|
||||
HistoryLimit: 100,
|
||||
InterruptPrompt: "^C",
|
||||
EOFPrompt: "exit",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Error initializing readline: %v\n", err)
|
||||
fmt.Println("Falling back to simple interactive mode...")
|
||||
simpleInteractiveModeWithLogging(agentLoop, sessionKey, verbose, showTools, showThink)
|
||||
return
|
||||
}
|
||||
defer rl.Close()
|
||||
|
||||
for {
|
||||
line, err := rl.Readline()
|
||||
if err != nil {
|
||||
if err == readline.ErrInterrupt || err == io.EOF {
|
||||
fmt.Println("\n\033[1;32mGoodbye!\033[0m")
|
||||
return
|
||||
}
|
||||
fmt.Printf("Error reading input: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
input := strings.TrimSpace(line)
|
||||
if input == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if input == "exit" || input == "quit" {
|
||||
fmt.Println("\n\033[1;32mGoodbye!\033[0m")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
startTime := time.Now()
|
||||
|
||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||
elapsed := time.Since(startTime)
|
||||
|
||||
if err != nil {
|
||||
il.LogError(err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Show response
|
||||
il.LogResponse()
|
||||
fmt.Printf("%s %s\n", internal.Logo, response)
|
||||
|
||||
// Show timing info in verbose mode
|
||||
if verbose {
|
||||
fmt.Printf("\033[1;37m⏱️ Completed in %v\033[0m\n\n", elapsed)
|
||||
} else {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// simpleInteractiveModeWithLogging fallback interactive mode with logging
|
||||
func simpleInteractiveModeWithLogging(
|
||||
agentLoop *agent.AgentLoop,
|
||||
sessionKey string,
|
||||
verbose, showTools, showThink bool,
|
||||
) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
il := NewInteractiveLogger(verbose, showTools, showThink)
|
||||
|
||||
// Start event subscription for real-time logs
|
||||
il.StartEventSubscription(agentLoop)
|
||||
defer il.StopEventSubscription()
|
||||
|
||||
for {
|
||||
fmt.Print(fmt.Sprintf("%s \033[32mYou:\033[0m ", internal.Logo))
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
fmt.Println("\n\033[1;32mGoodbye!\033[0m")
|
||||
return
|
||||
}
|
||||
fmt.Printf("Error reading input: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
input := strings.TrimSpace(line)
|
||||
if input == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if input == "exit" || input == "quit" {
|
||||
fmt.Println("\n\033[1;32mGoodbye!\033[0m")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
startTime := time.Now()
|
||||
|
||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||
elapsed := time.Since(startTime)
|
||||
|
||||
if err != nil {
|
||||
il.LogError(err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Show response
|
||||
il.LogResponse()
|
||||
fmt.Printf("%s %s\n", internal.Logo, response)
|
||||
|
||||
// Show timing info in verbose mode
|
||||
if verbose {
|
||||
fmt.Printf("\033[1;37m⏱️ Completed in %v\033[0m\n\n", elapsed)
|
||||
} else {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
260
docs/cli-logging.md
Normal file
260
docs/cli-logging.md
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
# 📝 Picoclaw CLI 日志增强功能
|
||||
|
||||
## 🎯 问题描述
|
||||
|
||||
旧版本的 Picoclaw 命令行交互模式下,用户发送问题后看不到任何处理进度,只能空白等待,体验不佳。
|
||||
|
||||
## ✨ 新增功能
|
||||
|
||||
为 `picoclaw agent` 命令添加了**语义化友好的实时日志输出**,让用户清楚看到 AI 的处理过程。
|
||||
|
||||
### 功能特性
|
||||
|
||||
| 功能 | 说明 | 示例输出 |
|
||||
|------|------|----------|
|
||||
| **思考指示** | 显示 AI 开始处理请求 | `⚙️ Thinking (session: cli:default)` |
|
||||
| **API 调用** | 显示 LLM API 请求详情 | `📡 Calling API: gpt-4 (messages: 5, tools: 3)` |
|
||||
| **工具执行** | 实时显示工具调用和结果 | `🔧 Tool calling: web_search(query=...)`<br>`✅ Tool completed: web_search (15ms)` |
|
||||
| **上下文压缩** | 显示会话历史压缩情况 | `🗜️ Context compressed: dropped 10, kept 5 messages` |
|
||||
| **错误提示** | 友好的错误信息展示 | `❌ Error [LLM]: API timeout` |
|
||||
| **性能统计** | 显示处理耗时和迭代次数 | `✅ Turn ended: 3 iterations, 1.2s, 500 chars` |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 1. 基本交互模式(默认,无日志)
|
||||
|
||||
```bash
|
||||
picoclaw agent
|
||||
```
|
||||
|
||||
### 2. 详细日志模式(推荐)
|
||||
|
||||
```bash
|
||||
# 显示所有日志
|
||||
picoclaw agent --verbose
|
||||
|
||||
# 或简写
|
||||
picoclaw agent -v
|
||||
```
|
||||
|
||||
### 3. 显示思考过程
|
||||
|
||||
```bash
|
||||
# 只显示思考指示和 API 调用
|
||||
picoclaw agent --show-think
|
||||
```
|
||||
|
||||
### 4. 显示工具执行日志
|
||||
|
||||
```bash
|
||||
# 显示工具调用和结果
|
||||
picoclaw agent --show-tools
|
||||
```
|
||||
|
||||
### 5. 组合使用
|
||||
|
||||
```bash
|
||||
# 显示所有日志类型
|
||||
picoclaw agent --verbose --show-tools --show-think
|
||||
```
|
||||
|
||||
### 6. 发送单条消息
|
||||
|
||||
```bash
|
||||
# 非交互模式,直接输出结果
|
||||
picoclaw agent -m "今天的天气怎么样?"
|
||||
|
||||
# 带日志输出
|
||||
picoclaw agent -m "查询最新新闻" --verbose
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 日志类型说明
|
||||
|
||||
### 思考过程日志 (`--show-think`)
|
||||
|
||||
```
|
||||
⚙️ Thinking (session: cli:default)
|
||||
📡 Calling API: gpt-4 (messages: 5, tools: 3)
|
||||
✅ API response (content: 120 chars, tool calls: 2)
|
||||
💬 Response:
|
||||
```
|
||||
|
||||
### 工具执行日志 (`--show-tools`)
|
||||
|
||||
```
|
||||
🔧 Tool calling: web_search(query="最新 AI 新闻")
|
||||
✅ Tool completed: web_search (23ms)
|
||||
🔧 Tool calling: web_fetch(url="https://...")
|
||||
✅ Tool completed: web_fetch (156ms)
|
||||
```
|
||||
|
||||
### 详细模式日志 (`--verbose`)
|
||||
|
||||
```
|
||||
⚙️ Thinking (session: cli:default)
|
||||
📡 Calling API: gpt-4 (messages: 5, tools: 3)
|
||||
✅ API response (content: 120 chars, tool calls: 2)
|
||||
🔧 Tool calling: web_search(query="...")
|
||||
✅ Tool completed: web_search (23ms)
|
||||
🗜️ Context compressed: dropped 10, kept 5 messages
|
||||
✅ Turn ended: 3 iterations, 1.2s, 500 chars
|
||||
⏱️ Completed in 1.5s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技术实现
|
||||
|
||||
### 核心文件
|
||||
|
||||
- `cmd/picoclaw/internal/agent/interactive.go` - 新增的交互式日志模块
|
||||
- `cmd/picoclaw/internal/agent/command.go` - 更新的命令定义
|
||||
|
||||
### 事件订阅机制
|
||||
|
||||
通过订阅 Agent 的 EventBus 实现实时日志:
|
||||
|
||||
```go
|
||||
// 订阅事件
|
||||
il.StartEventSubscription(agentLoop)
|
||||
|
||||
// 处理事件
|
||||
func (il *InteractiveLogger) handleEvent(evt agent.Event) {
|
||||
switch evt.Kind {
|
||||
case agent.EventKindTurnStart:
|
||||
// 显示思考指示
|
||||
case agent.EventKindLLMRequest:
|
||||
// 显示 API 调用
|
||||
case agent.EventKindToolExecStart:
|
||||
// 显示工具调用
|
||||
// ... 更多事件类型
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 支持的事件类型
|
||||
|
||||
| 事件类型 | 说明 |
|
||||
|---------|------|
|
||||
| `EventKindTurnStart` | 开始处理用户请求 |
|
||||
| `EventKindLLMRequest` | 发起 LLM API 调用 |
|
||||
| `EventKindLLMResponse` | 收到 LLM 响应 |
|
||||
| `EventKindLLMRetry` | API 重试 |
|
||||
| `EventKindToolExecStart` | 工具开始执行 |
|
||||
| `EventKindToolExecEnd` | 工具执行完成 |
|
||||
| `EventKindContextCompress` | 上下文压缩 |
|
||||
| `EventKindTurnEnd` | 处理完成 |
|
||||
| `EventKindError` | 错误信息 |
|
||||
|
||||
---
|
||||
|
||||
## 🎨 日志样式
|
||||
|
||||
使用 ANSI 颜色码和 Emoji 图标增强可读性:
|
||||
|
||||
- 🔵 蓝色 - 响应输出
|
||||
- 🟣 紫色 - API 调用
|
||||
- 🟢 绿色 - 成功操作
|
||||
- 🟡 黄色 - 工具调用/警告
|
||||
- 🔴 红色 - 错误信息
|
||||
- ⚪ 灰色 - 详细信息
|
||||
|
||||
---
|
||||
|
||||
## 📝 示例会话
|
||||
|
||||
```bash
|
||||
$ picoclaw agent --verbose --show-tools
|
||||
|
||||
██████╗ ██╗ ██████╗ ██████╗ ██████╗██╗ █████╗ ██╗ ██╗
|
||||
██╔══██╗██║██╔════╝██╔═══██╗██╔════╝██║ ██╔══██╗██║ ██║
|
||||
██████╔╝██║██║ ██║ ██║██║ ██║ ███████║██║ █╗ ██║
|
||||
██╔═══╝ ██║██║ ██║ ██║██║ ██║ ██╔══██║██║███╗██║
|
||||
██║ ██║╚██████╗╚██████╔╝╚██████╗███████╗██║ ██║╚███╔███╔╝
|
||||
╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝
|
||||
|
||||
Interactive mode (type 'exit' to quit, Ctrl+C to interrupt)
|
||||
Verbose logging enabled
|
||||
Tool execution logging enabled
|
||||
|
||||
🟢 You: 查询最新的 AI 新闻
|
||||
|
||||
⚙️ Thinking (session: cli:default)
|
||||
📡 Calling API: gpt-4 (messages: 3, tools: 5)
|
||||
✅ API response (content: 85 chars, tool calls: 1)
|
||||
🔧 Tool calling: web_search(query="最新 AI 新闻 2026")
|
||||
✅ Tool completed: web_search (45ms)
|
||||
📡 Calling API: gpt-4 (messages: 5, tools: 5)
|
||||
✅ API response (content: 520 chars)
|
||||
💬 Response:
|
||||
🟢 根据最新搜索结果,以下是 AI 领域的最新动态:
|
||||
1. PicoClaw 项目达到 26K Stars...
|
||||
2. 新的多模态模型发布...
|
||||
|
||||
⏱️ Completed in 2.3s
|
||||
|
||||
🟢 You: exit
|
||||
|
||||
Goodbye!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 开发调试
|
||||
|
||||
### 查看帮助
|
||||
|
||||
```bash
|
||||
picoclaw agent --help
|
||||
```
|
||||
|
||||
### 调试模式
|
||||
|
||||
```bash
|
||||
# 开启 debug 日志级别
|
||||
picoclaw agent --debug --verbose
|
||||
```
|
||||
|
||||
### 日志级别
|
||||
|
||||
可以通过环境变量控制日志级别:
|
||||
|
||||
```bash
|
||||
export PICOCLAW_LOG_LEVEL=debug
|
||||
picoclaw agent --verbose
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 构建说明
|
||||
|
||||
由于网络问题,可能需要配置 Go 代理:
|
||||
|
||||
```bash
|
||||
# 设置 Go 代理
|
||||
export GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
# 生成嵌入文件
|
||||
go generate ./...
|
||||
|
||||
# 构建
|
||||
go build -o picoclaw ./cmd/picoclaw
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 未来改进
|
||||
|
||||
- [ ] 支持进度条显示(长任务处理)
|
||||
- [ ] 支持日志输出到文件
|
||||
- [ ] 支持自定义日志格式
|
||||
- [ ] 支持流式输出(Streaming)
|
||||
- [ ] 支持日志级别过滤
|
||||
|
||||
---
|
||||
|
||||
**让等待不再空白,让处理过程透明化!** 🚀
|
||||
232
docs/test-logging.md
Normal file
232
docs/test-logging.md
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
# 🧪 Picoclaw CLI 日志功能自测指南
|
||||
|
||||
## ✅ 构建状态
|
||||
|
||||
| 项目 | 状态 |
|
||||
|------|------|
|
||||
| **二进制大小** | 77 MB |
|
||||
| **架构** | ARM64 (aarch64) |
|
||||
| **构建标签** | `stdjson` (禁用 goolm) |
|
||||
| **CGO** | 禁用 |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速测试
|
||||
|
||||
### 1. 查看帮助
|
||||
|
||||
```bash
|
||||
cd /data/data/com.termux/files/home/picoMind/picoclaw
|
||||
./picoclaw agent --help
|
||||
```
|
||||
|
||||
**预期输出**:显示新的日志选项
|
||||
- `--verbose` / `-v`
|
||||
- `--show-tools`
|
||||
- `--show-think`
|
||||
|
||||
---
|
||||
|
||||
### 2. 测试交互式模式(无日志)
|
||||
|
||||
```bash
|
||||
./picoclaw agent
|
||||
```
|
||||
|
||||
**预期行为**:
|
||||
- 显示欢迎横幅
|
||||
- 进入交互模式
|
||||
- 输入消息后直接显示响应(无中间日志)
|
||||
|
||||
---
|
||||
|
||||
### 3. 测试详细日志模式(推荐)
|
||||
|
||||
```bash
|
||||
./picoclaw agent --verbose
|
||||
```
|
||||
|
||||
**预期日志**(亮色方案,黑色背景清晰可见):
|
||||
```
|
||||
⚙️ Thinking (channel: cli) [亮青色]
|
||||
📡 Calling API: gpt-4 (messages: 5, tools: 3) [亮紫色]
|
||||
📝 User: 你好,请帮我查询天气 [亮青色]
|
||||
🔧 Tools: web_search, web_fetch [亮黄色]
|
||||
⚙️ MaxTokens: 4096, Temp: 0.70 [亮白色]
|
||||
✅ API Response (content: 120 chars) [亮绿色]
|
||||
📄 Content: 根据最新数据... [亮白色]
|
||||
🔧 Tool: web_search(query=北京天气) [亮黄色]
|
||||
🏁 Finish: stop [亮青色]
|
||||
⏱️ Completed in 1.5s [亮白色]
|
||||
```
|
||||
|
||||
**颜色方案**:
|
||||
| 元素 | 颜色 | ANSI 代码 |
|
||||
|------|------|----------|
|
||||
| 思考指示 | 亮青色 | `1;36m` |
|
||||
| API 调用 | 亮紫色 | `1;35m` |
|
||||
| API 响应 | 亮绿色 | `1;32m` |
|
||||
| 工具调用 | 亮黄色 | `1;33m` |
|
||||
| 详细内容 | 亮白色 | `1;37m` |
|
||||
| 错误信息 | 亮红色 | `1;31m` |
|
||||
| 响应输出 | 亮蓝色 | `1;34m` |
|
||||
|
||||
---
|
||||
|
||||
### 4. 测试工具执行日志
|
||||
|
||||
```bash
|
||||
./picoclaw agent --show-tools
|
||||
```
|
||||
|
||||
**预期日志**(如果触发工具调用):
|
||||
```
|
||||
🔧 Tool calling: web_search(query="最新 AI 新闻")
|
||||
✅ Tool completed: web_search (23ms)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 测试思考过程日志
|
||||
|
||||
```bash
|
||||
./picoclaw agent --show-think
|
||||
```
|
||||
|
||||
**预期日志**:
|
||||
```
|
||||
⚙️ Thinking (channel: cli)
|
||||
📡 Calling API: gpt-4 (messages: 5, tools: 3)
|
||||
✅ API response (content: 120 chars, tool calls: 2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 测试组合模式
|
||||
|
||||
```bash
|
||||
./picoclaw agent --verbose --show-tools --show-think
|
||||
```
|
||||
|
||||
**预期**:显示所有类型的日志,包括:
|
||||
- 思考指示
|
||||
- API 请求详情(用户消息、工具列表、参数)
|
||||
- API 响应详情(内容预览、工具调用、finish reason)
|
||||
- 工具执行状态
|
||||
- 性能统计
|
||||
|
||||
---
|
||||
|
||||
### 7. 测试单条消息模式
|
||||
|
||||
```bash
|
||||
# 无日志
|
||||
./picoclaw agent -m "你好"
|
||||
|
||||
# 带日志
|
||||
./picoclaw agent -m "查询天气" --verbose
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 测试检查清单
|
||||
|
||||
### 基本功能
|
||||
- [ ] 帮助信息正确显示新选项
|
||||
- [ ] 交互模式正常启动
|
||||
- [ ] 可以正常输入和退出(`exit` 或 Ctrl+C)
|
||||
- [ ] 响应正常显示
|
||||
|
||||
### 日志功能
|
||||
- [ ] `--verbose` 显示详细日志
|
||||
- [ ] `--show-tools` 显示工具执行日志
|
||||
- [ ] `--show-think` 显示思考过程
|
||||
- [ ] 日志使用彩色输出和 Emoji
|
||||
- [ ] 日志不干扰最终响应显示
|
||||
|
||||
### 性能
|
||||
- [ ] 日志输出不影响响应速度
|
||||
- [ ] 无内存泄漏(长时间运行测试)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 已知限制
|
||||
|
||||
### 禁用的功能
|
||||
1. **Matrix 渠道** - 需要 libolm (CGO 依赖)
|
||||
- 影响:无法使用 Matrix 协议连接
|
||||
- 解决:需要安装 libolm 库并启用 CGO
|
||||
|
||||
### 构建约束
|
||||
- `CGO_ENABLED=0` - 禁用所有 CGO 依赖
|
||||
- `-tags stdjson` - 使用标准 JSON 库而非 goolm
|
||||
|
||||
---
|
||||
|
||||
## 📝 测试记录模板
|
||||
|
||||
```markdown
|
||||
### 测试日期:2026-03-31
|
||||
|
||||
**测试环境**:
|
||||
- OS: Android (Termux)
|
||||
- CPU: ARM64
|
||||
- Go: 1.26.1
|
||||
|
||||
**测试结果**:
|
||||
| 测试项 | 状态 | 备注 |
|
||||
|--------|------|------|
|
||||
| 帮助显示 | ✅/❌ | |
|
||||
| 交互模式 | ✅/❌ | |
|
||||
| Verbose 日志 | ✅/❌ | |
|
||||
| Tools 日志 | ✅/❌ | |
|
||||
| Think 日志 | ✅/❌ | |
|
||||
|
||||
**问题记录**:
|
||||
-
|
||||
|
||||
**改进建议**:
|
||||
-
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 问题 1: 构建失败 - libolm 错误
|
||||
```
|
||||
fatal error: 'olm/olm.h' file not found
|
||||
```
|
||||
**解决**:已禁用 Matrix 渠道,使用当前构建即可
|
||||
|
||||
### 问题 2: 日志不显示
|
||||
**检查**:
|
||||
1. 确认使用了正确的标志(`--verbose` 等)
|
||||
2. 确认配置了 LLM Provider(需要有效的 API Token)
|
||||
|
||||
### 问题 3: 彩色输出异常
|
||||
**原因**:终端不支持 ANSI 颜色
|
||||
**解决**:使用支持颜色的终端或重定向到文件
|
||||
|
||||
---
|
||||
|
||||
## 📊 性能基准
|
||||
|
||||
| 模式 | 内存占用 | 启动时间 |
|
||||
|------|---------|---------|
|
||||
| 基础模式 | ~15 MB | <1s |
|
||||
| +Verbose | ~15 MB | <1s |
|
||||
| +Tools | ~15 MB | <1s |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
1. **配置 LLM Provider** - 设置有效的 API Token
|
||||
2. **实际对话测试** - 验证日志在真实场景的表现
|
||||
3. **工具调用测试** - 启用 web_search 等工具
|
||||
4. **长时间运行** - 测试内存稳定性
|
||||
|
||||
---
|
||||
|
||||
**构建完成,准备就绪!** 🦞
|
||||
|
|
@ -135,6 +135,10 @@ type LLMRequestPayload struct {
|
|||
ToolsCount int
|
||||
MaxTokens int
|
||||
Temperature float64
|
||||
// Detailed content for verbose logging
|
||||
UserMessagePreview string // Last user message preview
|
||||
SystemPromptPreview string // System prompt preview
|
||||
ToolNames []string // List of available tool names
|
||||
}
|
||||
|
||||
// LLMResponsePayload describes an inbound LLM response.
|
||||
|
|
@ -142,6 +146,16 @@ type LLMResponsePayload struct {
|
|||
ContentLen int
|
||||
ToolCalls int
|
||||
HasReasoning bool
|
||||
// Detailed content for verbose logging
|
||||
ContentPreview string // First 200 chars of response
|
||||
ToolCallDetails []ToolCallDetail // Details of tool calls
|
||||
FinishReason string // Why the LLM stopped
|
||||
}
|
||||
|
||||
// ToolCallDetail describes a single tool call from LLM
|
||||
type ToolCallDetail struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}
|
||||
|
||||
// LLMDeltaPayload describes a streamed LLM delta.
|
||||
|
|
|
|||
|
|
@ -1888,15 +1888,36 @@ turnLoop:
|
|||
}
|
||||
}
|
||||
|
||||
// Extract user message preview for logging
|
||||
userMessagePreview := ""
|
||||
for i := len(callMessages) - 1; i >= 0; i-- {
|
||||
if callMessages[i].Role == "user" && callMessages[i].Content != "" {
|
||||
userMessagePreview = callMessages[i].Content
|
||||
if len(userMessagePreview) > 100 {
|
||||
userMessagePreview = userMessagePreview[:97] + "..."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Extract tool names for logging
|
||||
toolNames := make([]string, 0, len(providerToolDefs))
|
||||
for _, tool := range providerToolDefs {
|
||||
toolNames = append(toolNames, tool.Function.Name)
|
||||
}
|
||||
|
||||
al.emitEvent(
|
||||
EventKindLLMRequest,
|
||||
ts.eventMeta("runTurn", "turn.llm.request"),
|
||||
LLMRequestPayload{
|
||||
Model: llmModel,
|
||||
MessagesCount: len(callMessages),
|
||||
ToolsCount: len(providerToolDefs),
|
||||
MaxTokens: ts.agent.MaxTokens,
|
||||
Temperature: ts.agent.Temperature,
|
||||
Model: llmModel,
|
||||
MessagesCount: len(callMessages),
|
||||
ToolsCount: len(providerToolDefs),
|
||||
MaxTokens: ts.agent.MaxTokens,
|
||||
Temperature: ts.agent.Temperature,
|
||||
UserMessagePreview: userMessagePreview,
|
||||
SystemPromptPreview: "", // Can add if needed
|
||||
ToolNames: toolNames,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -2132,13 +2153,44 @@ turnLoop:
|
|||
ts.channel,
|
||||
al.targetReasoningChannelID(ts.channel),
|
||||
)
|
||||
// Extract content preview and tool call details for logging
|
||||
contentPreview := response.Content
|
||||
if len(contentPreview) > 200 {
|
||||
contentPreview = contentPreview[:197] + "..."
|
||||
}
|
||||
|
||||
toolCallDetails := make([]ToolCallDetail, 0, len(response.ToolCalls))
|
||||
for _, tc := range response.ToolCalls {
|
||||
// Skip if Function is nil (some providers may return tool calls without function details)
|
||||
if tc.Function == nil {
|
||||
continue
|
||||
}
|
||||
argsStr := ""
|
||||
for k, v := range tc.Function.Arguments {
|
||||
if argsStr != "" {
|
||||
argsStr += ", "
|
||||
}
|
||||
argsStr += fmt.Sprintf("%v=%v", k, v)
|
||||
}
|
||||
if len(argsStr) > 100 {
|
||||
argsStr = argsStr[:97] + "..."
|
||||
}
|
||||
toolCallDetails = append(toolCallDetails, ToolCallDetail{
|
||||
Name: tc.Function.Name,
|
||||
Arguments: argsStr,
|
||||
})
|
||||
}
|
||||
|
||||
al.emitEvent(
|
||||
EventKindLLMResponse,
|
||||
ts.eventMeta("runTurn", "turn.llm.response"),
|
||||
LLMResponsePayload{
|
||||
ContentLen: len(response.Content),
|
||||
ToolCalls: len(response.ToolCalls),
|
||||
HasReasoning: response.Reasoning != "" || response.ReasoningContent != "",
|
||||
ContentLen: len(response.Content),
|
||||
ToolCalls: len(response.ToolCalls),
|
||||
HasReasoning: response.Reasoning != "" || response.ReasoningContent != "",
|
||||
ContentPreview: contentPreview,
|
||||
ToolCallDetails: toolCallDetails,
|
||||
FinishReason: response.FinishReason,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -2188,14 +2240,14 @@ turnLoop:
|
|||
normalizedToolCalls = append(normalizedToolCalls, providers.NormalizeToolCall(tc))
|
||||
}
|
||||
|
||||
toolNames := make([]string, 0, len(normalizedToolCalls))
|
||||
toolCallNames := make([]string, 0, len(normalizedToolCalls))
|
||||
for _, tc := range normalizedToolCalls {
|
||||
toolNames = append(toolNames, tc.Name)
|
||||
toolCallNames = append(toolCallNames, tc.Name)
|
||||
}
|
||||
logger.InfoCF("agent", "LLM requested tool calls",
|
||||
map[string]any{
|
||||
"agent_id": ts.agent.ID,
|
||||
"tools": toolNames,
|
||||
"tools": toolCallNames,
|
||||
"count": len(normalizedToolCalls),
|
||||
"iteration": iteration,
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue