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

23 KiB
Raw Permalink Blame History

开发者指南

本指南说明如何扩展和自定义日记整理 Agent,包括新的错误处理模式、配置验证功能和故障排除指南。

架构概览

Agent 系统基于以下核心概念:

  • Agent:主控制器,负责管理 Commands 和 Skills
  • Command:用户可执行的命令,编排多个 Skills
  • Skill:原子化的功能单元,执行具体任务
  • SkillChain:多个 Skills 的有序执行链
  • ErrorHandler:集中式错误处理和日志记录
  • ConfigurationValidator:配置验证和环境变量扩展

核心类

Agent

from journal_organizer.agent_core import Agent

# 创建 Agent
agent = Agent("MyAgent", config={})

# 注册命令
agent.register_command(my_command)

# 执行命令
result = await agent.execute_command("command_name", args={})

Skill

所有 Skill 都继承自 Skill 基类:

from journal_organizer.agent_core import Skill, SkillType, SkillResult, CommandContext

class MySkill(Skill):
    def __init__(self):
        super().__init__(
            name="my_skill",
            skill_type=SkillType.ANALYZE,
            description="我的自定义 Skill"
        )
    
    async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
        # 实现您的逻辑
        try:
            result = do_something(**kwargs)
            return SkillResult(
                success=True,
                data=result,
                message="执行成功"
            )
        except Exception as e:
            return SkillResult(
                success=False,
                error=str(e),
                message="执行失败"
            )

Command

所有 Command 都继承自 Command 基类:

from journal_organizer.agent_core import Command, SkillResult, CommandContext

class MyCommand(Command):
    def __init__(self):
        super().__init__(
            name="my_command",
            description="我的自定义命令",
            aliases=["mc"]
        )
        # 注册 Skills
        self.register_skill(MySkill())
    
    async def execute(self, context: CommandContext) -> SkillResult:
        # 获取参数
        param1 = context.args.get('param1')
        
        # 执行 Skill
        skill = self.skills['my_skill']
        result = await skill.execute(context, param1=param1)
        
        return result

添加新的 Skill

步骤 1: 创建 Skill 类

skills/ 目录下创建一个新文件,例如 my_skill.py

from ..agent_core import Skill, SkillType, SkillResult, CommandContext

class MyCustomSkill(Skill):
    def __init__(self):
        super().__init__(
            name="my_custom_skill",
            skill_type=SkillType.TRANSFORM,
            description="执行自定义转换"
        )
    
    async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
        try:
            input_data = kwargs.get('input_data')
            
            # 您的自定义逻辑
            output_data = self._process(input_data)
            
            return SkillResult(
                success=True,
                data=output_data,
                message="处理完成"
            )
        except Exception as e:
            return SkillResult(
                success=False,
                error=str(e),
                message="处理失败"
            )
    
    def _process(self, data):
        # 实现处理逻辑
        return data

步骤 2: 在 Command 中使用 Skill

from ..skills.my_skill import MyCustomSkill

class MyCommand(Command):
    def __init__(self):
        super().__init__(name="my_command")
        self.register_skill(MyCustomSkill())
    
    async def execute(self, context: CommandContext) -> SkillResult:
        skill = self.skills['my_custom_skill']
        return await skill.execute(context, input_data="test")

添加新的 Command

步骤 1: 创建 Command 类

commands/ 目录下创建一个新文件,例如 my_command.py

from ..agent_core import Command, SkillResult, CommandContext
from ..skills.my_skill import MyCustomSkill

class MyCommand(Command):
    def __init__(self):
        super().__init__(
            name="my_command",
            description="我的自定义命令",
            aliases=["mc", "my-cmd"]
        )
        self.register_skill(MyCustomSkill())
    
    async def execute(self, context: CommandContext) -> SkillResult:
        # 获取参数
        param1 = context.args.get('param1')
        param2 = context.args.get('param2', 'default')
        
        # 执行 Skill
        skill = self.skills['my_custom_skill']
        result = await skill.execute(context, input_data=param1)
        
        return result

步骤 2: 在 Agent 中注册 Command

编辑 main.py_register_commands 方法:

def _register_commands(self) -> None:
    """注册所有命令"""
    self.agent.register_command(OrganizeCommand())
    self.agent.register_command(MyCommand())  # 添加新命令

步骤 3: 测试新命令

python -m journal_organizer my_command --param1 "value1"

使用 SkillChain

SkillChain 允许您按顺序执行多个 Skills:

from journal_organizer.agent_core import SkillChain

class MyCommand(Command):
    def __init__(self):
        super().__init__(name="my_command")
        
        # 创建 Skill 链
        chain = SkillChain("my_chain", "执行一系列操作")
        chain.add_skill(Skill1(), {"param1": "value1"})
        chain.add_skill(Skill2(), {"param2": "value2"})
        
        self.register_skill_chain(chain)
    
    async def execute(self, context: CommandContext) -> SkillResult:
        chain = self.skill_chains['my_chain']
        return await chain.execute(context)

异步编程

所有 Skills 和 Commands 都使用异步编程(async/await)。这允许并发执行多个操作。

基本示例

async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
    # 异步调用外部 API
    result = await self.call_external_api()
    return SkillResult(success=True, data=result)

async def call_external_api(self):
    # 使用 aiohttp 进行异步 HTTP 请求
    async with aiohttp.ClientSession() as session:
        async with session.get('https://api.example.com/data') as resp:
            return await resp.json()

错误处理

新的错误处理框架

系统现在使用集中式错误处理框架,提供一致的错误管理和日志记录:

from journal_organizer.error_handling import (
    ErrorHandler, 
    JournalOrganizerError, 
    ConfigurationError, 
    APIError, 
    ValidationError
)

# 创建错误处理器
import logging
logger = logging.getLogger("MySkill")
error_handler = ErrorHandler(logger)

# 处理 API 错误
try:
    result = await api_call()
except Exception as e:
    error_result = error_handler.handle_api_error(e, "claude", "analyze_text")
    return SkillResult(
        success=False,
        error=error_result["error"],
        message=error_result["message"]
    )

自定义异常类型

使用专门的异常类型来处理不同类型的错误:

from journal_organizer.error_handling import (
    JournalOrganizerError,
    ConfigurationError,
    APIError,
    ValidationError
)

# 配置错误
if not api_key:
    raise ConfigurationError("API key is required", context={"service": "claude"})

# API 错误
if response.status_code != 200:
    raise APIError(
        "API request failed", 
        api_name="obsidian", 
        status_code=response.status_code
    )

# 验证错误
if not validate_input(data):
    raise ValidationError("Invalid input format", field="date")

Skill 中的错误处理模式

在 Skill 中实现标准化的错误处理:

from journal_organizer.agent_core import Skill, SkillResult, CommandContext
from journal_organizer.error_handling import ErrorHandler, APIError, ValidationError

class MySkill(Skill):
    def __init__(self):
        super().__init__(name="my_skill")
        self.error_handler = ErrorHandler(self.logger)
    
    async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
        try:
            # 输入验证
            self._validate_inputs(**kwargs)
            
            # 执行主要逻辑
            result = await self._perform_operation(**kwargs)
            
            return SkillResult(success=True, data=result, message="操作成功")
            
        except ValidationError as e:
            error_result = self.error_handler.handle_validation_error(e, "input_data")
            return SkillResult(
                success=False,
                error=error_result["error"],
                message=error_result["message"]
            )
        except APIError as e:
            error_result = self.error_handler.handle_api_error(e, "external_service", "operation")
            return SkillResult(
                success=False,
                error=error_result["error"],
                message=error_result["message"]
            )
        except Exception as e:
            # 处理未预期的错误
            self.logger.error(f"Unexpected error in {self.name}: {str(e)}", exc_info=True)
            return SkillResult(
                success=False,
                error="Internal error occurred",
                message="操作失败,请检查日志"
            )
    
    def _validate_inputs(self, **kwargs):
        """验证输入参数"""
        required_params = ['param1', 'param2']
        for param in required_params:
            if param not in kwargs:
                raise ValidationError(f"Missing required parameter: {param}", field=param)
    
    async def _perform_operation(self, **kwargs):
        """执行主要操作"""
        # 实现您的逻辑
        pass

日志记录

使用内置的 logger 记录信息:

class MySkill(Skill):
    async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
        self.logger.debug("开始执行")
        self.logger.info("处理数据")
        self.logger.warning("可能的问题")
        self.logger.error("发生错误")
        
        return SkillResult(success=True)

配置管理

新的配置验证系统

系统现在包含强大的配置验证功能,支持类型检查、环境变量扩展和路径验证:

from journal_organizer.config_validation import (
    SystemConfig,
    ObsidianConfig,
    ClaudeConfig,
    validate_system_config
)

# 验证配置
try:
    config = validate_system_config(raw_config)
    print("配置验证成功")
except ValidationError as e:
    print(f"配置验证失败: {e}")

环境变量扩展

配置文件支持环境变量扩展:

# config.yaml
claude:
  api_key: "${ANTHROPIC_API_KEY}"  # 从环境变量读取
  model: "${CLAUDE_MODEL:-claude-3-5-sonnet-20241022}"  # 带默认值

obsidian:
  vault_path: "${OBSIDIAN_VAULT_PATH}"
  rest_api:
    api_key: "${OBSIDIAN_API_KEY}"

配置验证示例

from journal_organizer.config_validation import validate_obsidian_config, validate_claude_config

# 验证 Obsidian 配置
obsidian_config = {
    "vault_path": "/path/to/vault",
    "rest_api": {
        "url": "https://localhost:27123",
        "api_key": "your-key",
        "verify_ssl": False
    }
}

try:
    validated_config = validate_obsidian_config(obsidian_config)
    print("Obsidian 配置有效")
except ValidationError as e:
    print(f"Obsidian 配置错误: {e}")

# 验证 Claude 配置
claude_config = {
    "api_key": "sk-ant-...",
    "model": "claude-3-5-sonnet-20241022",
    "max_tokens": 4096
}

try:
    validated_config = validate_claude_config(claude_config)
    print("Claude 配置有效")
except ValidationError as e:
    print(f"Claude 配置错误: {e}")

在 Skill 中访问配置

async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
    config = context.config or {}
    
    # 安全地访问配置
    claude_config = config.get('claude', {})
    api_key = claude_config.get('api_key')
    
    if not api_key:
        raise ConfigurationError("Claude API key not configured")
    
    # 使用配置
    return SkillResult(success=True)

测试

单元测试示例

import pytest
from journal_organizer.agent_core import CommandContext

@pytest.mark.asyncio
async def test_my_skill():
    skill = MySkill()
    context = CommandContext(command_name="test")
    
    result = await skill.execute(context, input_data="test")
    
    assert result.success == True
    assert result.data is not None

运行测试

pytest tests/

性能优化

并发执行

import asyncio

async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
    # 并发执行多个操作
    results = await asyncio.gather(
        self.operation1(),
        self.operation2(),
        self.operation3()
    )
    
    return SkillResult(success=True, data=results)

缓存

from functools import lru_cache

class MySkill(Skill):
    @lru_cache(maxsize=128)
    def expensive_operation(self, key):
        # 缓存昂贵的操作
        return process(key)

最佳实践

现代 Python 模式

系统现在遵循现代 Python 最佳实践:

1. 使用 f-strings 进行字符串格式化

# ✅ 推荐:使用 f-strings
name = "用户"
message = f"欢迎 {name},当前时间是 {datetime.now()}"

# ❌ 避免:字符串连接
message = "欢迎 " + name + ",当前时间是 " + str(datetime.now())

2. 使用 pathlib 进行文件路径操作

from pathlib import Path

# ✅ 推荐:使用 pathlib
vault_path = Path(config['obsidian']['vault_path'])
daily_folder = vault_path / "Daily"
note_file = daily_folder / f"{date}.md"

# 检查文件是否存在
if note_file.exists():
    content = note_file.read_text(encoding='utf-8')

# ❌ 避免:使用 os.path
import os
note_file = os.path.join(vault_path, "Daily", f"{date}.md")

3. 使用 dataclasses 定义数据结构

from dataclasses import dataclass, field
from typing import Optional, List
from datetime import datetime

@dataclass
class SkillResult:
    success: bool
    data: Optional[Dict[str, Any]] = None
    error: Optional[str] = None
    message: str = ""
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
    
    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)

4. 使用类型提示

from typing import Dict, Any, Optional, List, Union

async def execute_skill(
    skill_name: str,
    context: CommandContext,
    **kwargs: Any
) -> SkillResult:
    """
    执行指定的 Skill
    
    Args:
        skill_name: Skill 名称
        context: 命令执行上下文
        **kwargs: Skill 参数
    
    Returns:
        SkillResult: 执行结果
    """
    pass

5. 使用异步上下文管理器

from contextlib import asynccontextmanager
import aiohttp

@asynccontextmanager
async def http_client(config: Dict[str, Any]):
    """HTTP 客户端上下文管理器"""
    connector = aiohttp.TCPConnector(
        ssl=False if not config.get('verify_ssl', True) else None
    )
    
    async with aiohttp.ClientSession(connector=connector) as session:
        try:
            yield session
        finally:
            await session.close()

# 使用示例
async def call_api():
    async with http_client(api_config) as client:
        async with client.get(url) as response:
            return await response.json()

通用最佳实践

  1. 单一职责:每个 Skill 只负责一个任务
  2. 错误处理:使用新的错误处理框架
  3. 日志记录:使用 logger 记录重要信息
  4. 配置驱动:使用配置验证系统
  5. 异步编程:充分利用异步特性提高性能
  6. 类型安全:使用类型提示和验证
  7. 文档:为您的 Skills 和 Commands 编写清晰的文档
  8. 测试:编写单元测试和属性测试确保代码质量
  9. 版本控制:使用 git 管理代码版本
  10. 代码格式化:使用 black 和 isort 保持代码风格一致

示例:完整的自定义 Skill

"""
自定义 Skill 示例:文本统计
"""

from ..agent_core import Skill, SkillType, SkillResult, CommandContext

class TextStatisticsSkill(Skill):
    """计算文本统计信息的 Skill"""
    
    def __init__(self):
        super().__init__(
            name="text_statistics",
            skill_type=SkillType.ANALYZE,
            description="计算文本的字数、词数、句数等统计信息"
        )
    
    async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
        """
        执行文本统计
        
        Args:
            context: 命令执行上下文
            **kwargs: 包含 text 参数
        
        Returns:
            SkillResult: 包含统计结果的结果
        """
        try:
            text = kwargs.get('text', '')
            
            if not text:
                return SkillResult(
                    success=False,
                    error="缺少文本参数",
                    message="未提供要统计的文本"
                )
            
            # 计算统计信息
            stats = {
                'char_count': len(text),
                'word_count': len(text.split()),
                'sentence_count': len(text.split('。')),
                'line_count': len(text.split('\n')),
                'avg_word_length': len(text) / len(text.split()) if text.split() else 0
            }
            
            self.logger.info(f"文本统计完成: {stats}")
            
            return SkillResult(
                success=True,
                data=stats,
                message="文本统计完成"
            )
        
        except Exception as e:
            self.logger.error(f"文本统计失败: {str(e)}")
            return SkillResult(
                success=False,
                error=str(e),
                message="文本统计异常"
            )

资源


故障排除指南

本节提供常见问题的解决方案和调试技巧。

常见问题

1. 导入错误 (ImportError)

问题: ImportError: attempted relative import with no known parent package

解决方案:

# 确保以模块方式运行
python -m journal_organizer --help

# 而不是直接运行
python main.py  # ❌ 错误方式

原因: 项目使用相对导入,需要作为包运行。

2. 配置文件问题

问题: ConfigurationError: Missing required configuration

解决方案:

  1. 检查配置文件是否存在:
ls -la config.yaml
  1. 验证配置格式:
python -c "
import yaml
with open('config.yaml', 'r') as f:
    config = yaml.safe_load(f)
    print('配置文件格式正确')
"
  1. 检查环境变量:
echo $ANTHROPIC_API_KEY
echo $OBSIDIAN_API_KEY

3. API 连接问题

问题: APIError: Failed to connect to Claude/Obsidian API

解决方案:

Claude API:

# 测试 API 密钥
curl -H "Authorization: Bearer $ANTHROPIC_API_KEY" \
     -H "Content-Type: application/json" \
     https://api.anthropic.com/v1/messages

Obsidian API:

# 检查 Obsidian Local REST API 插件状态
curl -k -H "Authorization: Bearer $OBSIDIAN_API_KEY" \
     https://localhost:27123/

4. 依赖项问题

问题: ModuleNotFoundError: No module named 'xxx'

解决方案:

# 检查依赖项状态
python -m journal_organizer check-deps

# 安装缺失的依赖项
pip install -r requirements.txt

# 安装可选依赖项
pip install pyyaml aiohttp anthropic

5. 权限问题

问题: PermissionError: [Errno 13] Permission denied

解决方案:

# 检查文件权限
ls -la config.yaml
ls -la /path/to/obsidian/vault

# 修复权限
chmod 644 config.yaml
chmod -R 755 /path/to/obsidian/vault

6. SSL 证书问题

问题: SSL: CERTIFICATE_VERIFY_FAILED

解决方案: 在配置文件中禁用 SSL 验证(仅用于本地开发):

obsidian:
  rest_api:
    verify_ssl: false

调试技巧

1. 启用详细日志

# 设置调试级别日志
python -m journal_organizer --log-level DEBUG organize

2. 使用 Python 调试器

# 在 Skill 中添加断点
import pdb; pdb.set_trace()

# 或使用 ipdb(更友好的界面)
import ipdb; ipdb.set_trace()

3. 检查配置加载

# 测试配置加载
from journal_organizer.main import JournalOrganizerAgent

agent = JournalOrganizerAgent("config.yaml")
print(f"配置: {agent.config}")

4. 测试单个 Skill

# 单独测试 Skill
import asyncio
from journal_organizer.skills.claude_skill import ClaudeAnalyzeSkill
from journal_organizer.agent_core import CommandContext

async def test_skill():
    skill = ClaudeAnalyzeSkill()
    context = CommandContext(command_name="test")
    result = await skill.execute(context, text="测试文本")
    print(f"结果: {result}")

asyncio.run(test_skill())

性能问题

1. 内存使用过高

诊断:

import psutil
import os

process = psutil.Process(os.getpid())
print(f"内存使用: {process.memory_info().rss / 1024 / 1024:.2f} MB")

解决方案:

  • 检查是否有内存泄漏
  • 使用 gc.collect() 强制垃圾回收
  • 限制并发操作数量

2. API 调用缓慢

诊断:

import time

start_time = time.time()
result = await api_call()
duration = time.time() - start_time
print(f"API 调用耗时: {duration:.2f} 秒")

解决方案:

  • 检查网络连接
  • 增加超时设置
  • 使用连接池
  • 实现重试机制

错误代码参考

错误代码 描述 解决方案
CONFIG_001 配置文件不存在 创建 config.yaml 文件
CONFIG_002 配置格式错误 检查 YAML/JSON 语法
CONFIG_003 缺少必需配置项 添加缺失的配置项
API_001 API 密钥无效 检查并更新 API 密钥
API_002 API 连接超时 检查网络连接和服务状态
API_003 API 限流 减少请求频率或升级 API 计划
SKILL_001 Skill 执行失败 检查 Skill 输入参数和依赖项
SKILL_002 Skill 超时 增加超时设置或优化 Skill 逻辑

获取帮助

如果问题仍然存在:

  1. 检查日志文件: logs/journal_organizer.log
  2. 运行诊断命令: python -m journal_organizer check-deps
  3. 查看详细错误: 使用 --log-level DEBUG
  4. 测试基本功能: 运行简单的命令如 listhelp

开发环境设置

推荐的开发工具

# 安装开发依赖
pip install pytest pytest-asyncio black isort mypy

# 代码格式化
black .
isort .

# 类型检查
mypy journal_organizer/

# 运行测试
pytest tests/

调试配置 (VS Code)

创建 .vscode/launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug Journal Organizer",
            "type": "python",
            "request": "launch",
            "module": "journal_organizer",
            "args": ["--log-level", "DEBUG", "organize"],
            "console": "integratedTerminal",
            "cwd": "${workspaceFolder}"
        }
    ]
}