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
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
"""
|
||||
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} 格式",
|
||||
)
|
||||
Reference in New Issue
Block a user