Files
cherry-studio/resources/agents/claude_code_agent.py
Vaayne 6087cb687d feat: enhance agent logging system with structured events and improved UI
- Add structured logging for Claude assistant responses in agent.py
- Remove duplicate user query logging to prevent UI duplication
- Implement comprehensive message filtering and formatting in UI
- Add visual styling for different message types (errors, results, responses)
- Improve message content parsing with type-specific handlers
- Filter raw stdout/stderr logs from conversation display
- Add useCallback optimization for React Hook dependencies
- Fix ESLint and TypeScript issues in CherryAgentPage component
- Update test formatting and structure for consistency

This creates a clean conversation flow showing:
1. User prompts
2. Session initialization details
3. Claude assistant responses
4. Session results with cost/duration
5. Error messages with proper styling
2025-08-05 12:10:45 +08:00

181 lines
5.8 KiB
Python

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = "==3.10"
# dependencies = [
# "claude-code-sdk",
# ]
# ///
import argparse
import asyncio
import json
import logging
import os
from datetime import datetime, timezone
from claude_code_sdk import ClaudeCodeOptions, ClaudeSDKClient, Message
from claude_code_sdk.types import (
SystemMessage,
UserMessage,
ResultMessage,
AssistantMessage,
TextBlock,
ToolUseBlock,
ToolResultBlock
)
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def log_structured_event(event_type: str, data: dict):
"""Output structured log event as JSON to stdout for AgentExecutionService to parse."""
event = {
"__CHERRY_AGENT_LOG__": True,
"timestamp": datetime.now(timezone.utc) .isoformat(),
"event_type": event_type,
"data": data
}
print(json.dumps(event), flush=True)
def display_message(msg: Message):
"""Standardized message display function.
- UserMessage: "User: <content>"
- AssistantMessage: "Claude: <content>"
- SystemMessage: ignored
- ResultMessage: "Result ended" + cost if available
"""
if isinstance(msg, UserMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(f"User: {block.text}")
elif isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(block, ToolUseBlock):
print(f"Tool: {block}")
elif isinstance(block, ToolResultBlock):
print(f"Tool Result: {block}")
elif isinstance(msg, SystemMessage):
print(f"--- Started session: {msg.data.get('session_id', 'unknown')} ---")
pass
elif isinstance(msg, ResultMessage):
cost_info = f" (${msg.total_cost_usd:.4f})" if msg.total_cost_usd else ""
print(f"--- Finished session: {msg.session_id}{cost_info} ---")
pass
async def run_claude_query(prompt: str, opts: ClaudeCodeOptions = ClaudeCodeOptions()):
"""Initializes the Claude SDK client and handles the query-response loop."""
try:
# Log session initialization
log_structured_event("session_init", {
"system_prompt": opts.system_prompt,
"max_turns": opts.max_turns,
"permission_mode": opts.permission_mode,
"cwd": str(opts.cwd) if opts.cwd else None
})
# Note: User query is already logged by AgentExecutionService, no need to duplicate
async with ClaudeSDKClient(opts) as client:
await client.query(prompt)
async for msg in client.receive_response():
# Log structured events for important message types
if isinstance(msg, SystemMessage):
log_structured_event("session_started", {
"session_id": msg.data.get('session_id')
})
elif isinstance(msg, AssistantMessage):
# Log Claude's response content
text_content = []
for block in msg.content:
if isinstance(block, TextBlock):
text_content.append(block.text)
if text_content:
log_structured_event("assistant_response", {
"content": "\n".join(text_content)
})
elif isinstance(msg, ResultMessage):
log_structured_event("session_result", {
"session_id": msg.session_id,
"success": not msg.is_error,
"duration_ms": msg.duration_ms,
"num_turns": msg.num_turns,
"total_cost_usd": msg.total_cost_usd,
"usage": msg.usage
})
display_message(msg)
except Exception as e:
log_structured_event("error", {
"error_type": type(e).__name__,
"error_message": str(e)
})
logger.error(f"An error occurred: {e}")
async def main():
"""Parses command-line arguments and runs the Claude query."""
parser = argparse.ArgumentParser(description="Claude Code SDK Example")
parser.add_argument(
"--prompt",
"-p",
required=True,
help="User prompt",
)
parser.add_argument(
"--cwd",
type=str,
default=os.path.join(os.getcwd(), "sessions"),
help="Working directory for the session. Defaults to './sessions'.",
)
parser.add_argument(
"--system-prompt",
type=str,
default="You are a helpful assistant.",
help="System prompt",
)
parser.add_argument(
"--permission-mode",
type=str,
default="default",
choices=["default", "acceptEdits", "bypassPermissions"],
help="Permission mode for file edits.",
)
parser.add_argument(
"--max-turns",
type=int,
default=10,
help="Maximum number of conversation turns.",
)
parser.add_argument(
"--session-id",
"-s",
default=None,
help="The session ID to resume an existing session.",
)
args = parser.parse_args()
# Ensure the working directory exists
os.makedirs(args.cwd, exist_ok=True)
opts = ClaudeCodeOptions(
system_prompt=args.system_prompt,
max_turns=args.max_turns,
permission_mode=args.permission_mode,
cwd=args.cwd,
# resume=args.session_id,
continue_conversation=True
)
await run_claude_query(args.prompt, opts)
if __name__ == "__main__":
asyncio.run(main())