- Add core agent architecture with Command + Skill pattern - Implement Claude API integration for content analysis - Add Obsidian REST API integration for vault operations - Create conversational interface (v2.0) with natural language processing - Add comprehensive configuration management and validation - Include project documentation and developer guides - Set up testing framework with unit, integration, and property tests - Add Kiro specs for Claude API configuration and code quality improvements - Configure project steering files for development guidelines
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} 格式",
|
||
)
|