468 lines
17 KiB
Python
468 lines
17 KiB
Python
"""
|
||||
|
|
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} 格式",
|
|||
|
|
)
|