""" Claude AI 集成 Skill 负责与 Claude API 的交互和内容分析 Enhanced with configurable API URL support """ import json import logging from datetime import datetime from typing import Dict, Any, Optional, List, Union try: from ..dependency_manager import get_dependency_manager from ..claude_api_client import ClaudeAPIClient from ..config_validation import ClaudeAPIConfig # Try to import anthropic with graceful degradation dependency_manager = get_dependency_manager() Anthropic = dependency_manager.get_class_from_module('anthropic', 'Anthropic') AsyncAnthropic = dependency_manager.get_class_from_module('anthropic', 'AsyncAnthropic') except ImportError: # Fallback for backward compatibility try: from anthropic import Anthropic, AsyncAnthropic from claude_api_client import ClaudeAPIClient from config_validation import ClaudeAPIConfig except ImportError: AsyncAnthropic = None Anthropic = None ClaudeAPIClient = None ClaudeAPIConfig = None from ..agent_core import Skill, SkillType, SkillResult, CommandContext from ..api_response_validation import validate_api_response from ..error_handling import ( ErrorHandler, APIError, ConfigurationError, ValidationError, ErrorContext, get_error_handler, ) from ..input_validation import command_input_validator class ClaudeAnalyzeSkill(Skill): """Claude 日记分析 Skill""" def __init__(self) -> None: super().__init__( name="claude_analyze", skill_type=SkillType.ANALYZE, description="使用 Claude 分析日记内容并提取关键信息", ) self.client: Optional[ClaudeAPIClient] = None self.error_handler = get_error_handler() def _get_analysis_prompt(self, categories: List[str]) -> str: """ 获取分析 prompt Args: categories: 分类列表 Returns: prompt 字符串 """ categories_str: str = "、".join(categories) if categories else "技术学习、项目管理、个人成长" return f"""你是一个专业的日记分析助手。请分析以下日记内容,并按照指定的格式提取关键信息。 分析要求: 1. 提取经验和见解(Experiences):日记中提到的重要经验、发现或见解 2. 提取学到的知识(Lessons Learned):具体学到的知识点、最佳实践或原则 3. 提取待办事项(Action Items):需要采取行动的任务或改进项 4. 提取问题和挑战(Problems):遇到的问题、挑战或障碍 5. 提取成就和进展(Achievements):完成的工作、达成的目标或进展 6. 提取改进建议(Improvements):可以改进的方向或优化建议 分类类别:{categories_str} 请以 JSON 格式返回结果,结构如下: {{ "experiences": [ {{ "title": "标题", "content": "详细内容", "category": "分类", "priority": "high/medium/low" }} ], "lessons_learned": [ {{ "lesson": "学到的内容", "context": "背景信息", "application": "如何应用" }} ], "action_items": [ {{ "task": "任务描述", "priority": "high/medium/low", "deadline": "建议截止日期", "status": "new" }} ], "problems": [ {{ "problem": "问题描述", "impact": "影响程度", "proposed_solution": "建议方案" }} ], "achievements": [ {{ "achievement": "成就描述", "significance": "重要性", "evidence": "证据或细节" }} ], "improvements": [ {{ "area": "改进领域", "current_state": "当前状态", "suggested_change": "建议改进", "expected_benefit": "预期收益" }} ], "summary": "日记的总体总结" }} 日记内容: """ async def execute(self, context: CommandContext, **kwargs: Any) -> SkillResult: """ 分析日记内容 Args: context: 命令执行上下文 **kwargs: 包含以下参数 - journal_content: 日记内容 - api_key: Claude API 密钥 - model: 模型名称(默认 claude-3-5-sonnet-20241022) - categories: 分类列表(可选) Returns: SkillResult: 包含分析结果的结果 """ try: return await self._execute_analyze(context, **kwargs) except (ValidationError, ConfigurationError, APIError) as e: error_context = ErrorContext( component="claude_analyze", operation="analyze_journal", user_message="Failed to analyze journal content with Claude AI", technical_details=kwargs, ) error_response = self.error_handler.handle_error(e, error_context) return SkillResult( success=error_response["success"], error=error_response["error"], message=error_response["message"], ) except Exception as e: api_error = APIError( message=f"Unexpected error analyzing journal: {str(e)}", api_name="claude", cause=e, ) error_context = ErrorContext( component="claude_analyze", operation="analyze_journal", user_message="An unexpected error occurred while analyzing the journal", ) error_response = self.error_handler.handle_error(api_error, error_context) return SkillResult( success=error_response["success"], error=error_response["error"], message=error_response["message"], ) async def _execute_analyze( self, context: CommandContext, **kwargs: Any ) -> SkillResult: """Internal method that performs the actual analysis""" # Validate input parameters validated_kwargs = command_input_validator.validate_skill_input( 'claude_analyze', kwargs ) journal_content: str = validated_kwargs["journal_content"] # Handle both legacy and new configuration formats if 'claude_config' in validated_kwargs: # New format: ClaudeAPIConfig object claude_config = validated_kwargs['claude_config'] if not isinstance(claude_config, ClaudeAPIConfig): raise ConfigurationError( message="claude_config must be a ClaudeAPIConfig instance", config_key="claude_config" ) else: # Legacy format: individual parameters api_key: str = validated_kwargs["api_key"] model: str = validated_kwargs.get("model", "claude-3-5-sonnet-20241022") api_url: str = validated_kwargs.get("api_url", "https://api.anthropic.com") max_tokens: int = validated_kwargs.get("max_tokens", 4096) temperature: float = validated_kwargs.get("temperature", 0.7) # Create ClaudeAPIConfig from legacy parameters claude_config = ClaudeAPIConfig( api_key=api_key, model=model, api_url=api_url, max_tokens=max_tokens, temperature=temperature ) categories: List[str] = validated_kwargs.get("categories", []) if not ClaudeAPIClient: raise ConfigurationError( message="ClaudeAPIClient is not available. Please check your installation.", config_key="claude_api_client", ) # Initialize enhanced client try: client = ClaudeAPIClient(claude_config) except Exception as e: raise ConfigurationError( message=f"Failed to initialize Claude API client: {str(e)}", config_key="claude_client_init", cause=e ) # Construct prompt system_prompt: str = self._get_analysis_prompt(categories) user_message: str = journal_content self.logger.info(f"开始分析日记,模型: {claude_config.model}, API URL: {claude_config.api_url}") try: # Call Claude API using enhanced client message = await client.create_message( messages=[ {"role": "user", "content": f"{system_prompt}{user_message}"} ] ) except APIError: # Re-raise APIError as-is (already properly formatted) raise except Exception as e: raise APIError( message=f"Claude API call failed: {str(e)}", api_name="claude", cause=e ) # Validate API response validated_response = validate_api_response( message.model_dump() if hasattr(message, 'model_dump') else message.__dict__, "claude", "analyze" ) # Parse response response_text: str = message.content[0].text # Try to extract JSON try: # Find JSON block json_start: int = response_text.find("{") json_end: int = response_text.rfind("}") + 1 if json_start >= 0 and json_end > json_start: json_str: str = response_text[json_start:json_end] analysis_result: Dict[str, Any] = json.loads(json_str) else: # If no JSON found, return raw text analysis_result = { "raw_response": response_text, "parse_error": "无法解析 JSON 格式", } except json.JSONDecodeError as e: self.logger.warning(f"JSON 解析失败: {str(e)}") analysis_result = {"raw_response": response_text, "parse_error": str(e)} return SkillResult( success=True, data={ "analysis": analysis_result, "model": claude_config.model, "api_url": claude_config.api_url, "analyzed_at": datetime.now().isoformat(), "journal_length": len(journal_content), "api_response": validated_response, "client_info": client.get_client_info(), }, message="成功分析日记内容", ) class ClaudeTransformSkill(Skill): """Claude 内容转换 Skill""" def __init__(self) -> None: super().__init__( name="claude_transform", skill_type=SkillType.TRANSFORM, description="使用 Claude 转换和格式化内容", ) self.error_handler = get_error_handler() async def execute(self, context: CommandContext, **kwargs: Any) -> SkillResult: """ 转换内容格式 Args: context: 命令执行上下文 **kwargs: 包含以下参数 - content: 要转换的内容 - transform_type: 转换类型(markdown, html, summary 等) - api_key: Claude API 密钥 - model: 模型名称(可选) Returns: SkillResult: 包含转换结果的结果 """ try: return await self._execute_transform(context, **kwargs) except (ValidationError, ConfigurationError, APIError) as e: error_context = ErrorContext( component="claude_transform", operation="transform_content", user_message="Failed to transform content with Claude AI", technical_details=kwargs, ) error_response = self.error_handler.handle_error(e, error_context) return SkillResult( success=error_response["success"], error=error_response["error"], message=error_response["message"], ) except Exception as e: api_error = APIError( message=f"Unexpected error transforming content: {str(e)}", api_name="claude", cause=e, ) error_context = ErrorContext( component="claude_transform", operation="transform_content", user_message="An unexpected error occurred while transforming content", ) error_response = self.error_handler.handle_error(api_error, error_context) return SkillResult( success=error_response["success"], error=error_response["error"], message=error_response["message"], ) async def _execute_transform( self, context: CommandContext, **kwargs: Any ) -> SkillResult: """Internal method that performs the actual transformation""" content: Optional[str] = kwargs.get("content") transform_type: str = kwargs.get("transform_type", "markdown") if not content: raise ValidationError( message="Content is required for transformation", field_name="content", validation_rule="non_empty", ) # Handle both legacy and new configuration formats if 'claude_config' in kwargs: # New format: ClaudeAPIConfig object claude_config = kwargs['claude_config'] if not isinstance(claude_config, ClaudeAPIConfig): raise ConfigurationError( message="claude_config must be a ClaudeAPIConfig instance", config_key="claude_config" ) else: # Legacy format: individual parameters api_key: Optional[str] = kwargs.get("api_key") model: str = kwargs.get("model", "claude-3-5-sonnet-20241022") api_url: str = kwargs.get("api_url", "https://api.anthropic.com") max_tokens: int = kwargs.get("max_tokens", 4096) temperature: float = kwargs.get("temperature", 0.7) if not api_key: raise ConfigurationError( message="Claude API key is required", config_key="api_key" ) # Create ClaudeAPIConfig from legacy parameters claude_config = ClaudeAPIConfig( api_key=api_key, model=model, api_url=api_url, max_tokens=max_tokens, temperature=temperature ) if not ClaudeAPIClient: raise ConfigurationError( message="ClaudeAPIClient is not available. Please check your installation.", config_key="claude_api_client", ) # Initialize enhanced client try: client = ClaudeAPIClient(claude_config) except Exception as e: raise ConfigurationError( message=f"Failed to initialize Claude API client: {str(e)}", config_key="claude_client_init", cause=e ) # Build prompt based on transformation type prompts: Dict[str, str] = { "markdown": "请将以下内容转换为格式良好的 Markdown 格式:", "html": "请将以下内容转换为 HTML 格式:", "summary": "请为以下内容生成一个简洁的总结:", "outline": "请为以下内容生成一个结构化的大纲:", "checklist": "请将以下内容转换为检查清单格式:", } system_prompt: str = prompts.get(transform_type, "请转换以下内容:") self.logger.info(f"转换内容,类型: {transform_type}, 模型: {claude_config.model}") try: message = await client.create_message( messages=[{"role": "user", "content": f"{system_prompt}\n\n{content}"}] ) except APIError: # Re-raise APIError as-is (already properly formatted) raise except Exception as e: raise APIError( message=f"Claude API call failed: {str(e)}", api_name="claude", cause=e ) transformed_content: str = message.content[0].text return SkillResult( success=True, data={ "original_length": len(content), "transformed_length": len(transformed_content), "transform_type": transform_type, "transformed_content": transformed_content, "transformed_at": datetime.now().isoformat(), "model": claude_config.model, "api_url": claude_config.api_url, "client_info": client.get_client_info(), }, message=f"成功转换内容为 {transform_type} 格式", )