Files
windyboy f7e54692a9 Initial project setup: Obsidian intelligent journal organizer
- 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
2025-12-31 17:55:10 +08:00

468 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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} 格式",
)