- 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
403 lines
13 KiB
Python
403 lines
13 KiB
Python
"""
|
|
意图理解模块
|
|
使用 Claude 理解用户的自然语言输入,提取意图和参数
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, Any, Optional, List
|
|
|
|
from ..dependency_manager import get_dependency_manager, graceful_import
|
|
|
|
# Try to import anthropic with graceful degradation
|
|
dependency_manager = get_dependency_manager()
|
|
Anthropic = dependency_manager.get_class_from_module('anthropic', 'Anthropic')
|
|
|
|
|
|
@dataclass
|
|
class Intent:
|
|
"""用户意图"""
|
|
|
|
command: str # 对应的命令名称
|
|
parameters: Dict[str, Any] = field(default_factory=dict)
|
|
confidence: float = 1.0
|
|
clarification_needed: bool = False
|
|
clarification_question: Optional[str] = None
|
|
raw_understanding: str = "" # Claude 的原始理解
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Validate intent after initialization"""
|
|
if not self.command or not self.command.strip():
|
|
raise ValueError("command cannot be empty")
|
|
if not 0.0 <= self.confidence <= 1.0:
|
|
raise ValueError("confidence must be between 0.0 and 1.0")
|
|
if not isinstance(self.parameters, dict):
|
|
raise ValueError("parameters must be a dictionary")
|
|
|
|
|
|
class IntentUnderstanding:
|
|
"""意图理解器,使用 Claude 理解用户意图"""
|
|
|
|
def __init__(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
"""
|
|
初始化意图理解器
|
|
|
|
Args:
|
|
config: 配置字典
|
|
"""
|
|
self.config: Dict[str, Any] = config or {}
|
|
self.logger: logging.Logger = logging.getLogger("IntentUnderstanding")
|
|
|
|
# Initialize Anthropic client with dependency checking
|
|
if Anthropic is not None:
|
|
try:
|
|
self.client: Optional[Anthropic] = Anthropic()
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to initialize Anthropic client: {e}")
|
|
self.client = None
|
|
else:
|
|
self.client = None
|
|
self.logger.warning("Anthropic library not available - Claude-based understanding disabled")
|
|
|
|
# 定义支持的命令和它们的关键词
|
|
self.command_keywords: Dict[str, List[str]] = {
|
|
"organize": [
|
|
"整理",
|
|
"组织",
|
|
"分类",
|
|
"归纳",
|
|
"整理日记",
|
|
"organize",
|
|
"arrange",
|
|
"categorize",
|
|
],
|
|
"analyze": [
|
|
"分析",
|
|
"总结",
|
|
"统计",
|
|
"分类",
|
|
"分析日记",
|
|
"analyze",
|
|
"summarize",
|
|
"statistics",
|
|
],
|
|
"export": [
|
|
"导出",
|
|
"保存",
|
|
"生成",
|
|
"输出",
|
|
"导出为",
|
|
"export",
|
|
"save",
|
|
"generate",
|
|
"output",
|
|
],
|
|
"review": [
|
|
"回顾",
|
|
"查看",
|
|
"查询",
|
|
"搜索",
|
|
"浏览",
|
|
"review",
|
|
"view",
|
|
"search",
|
|
"browse",
|
|
],
|
|
}
|
|
|
|
# 定义参数提取规则
|
|
self.parameter_patterns: Dict[str, List[str]] = {
|
|
"date": [
|
|
r"(\d{4}[-/]\d{1,2}[-/]\d{1,2})", # YYYY-MM-DD 或 YYYY/M/D
|
|
r"(今天|明天|昨天|前天)", # 相对日期
|
|
r"(这周|本周|上周|下周)", # 周
|
|
r"(这个月|本月|上个月|下个月)", # 月
|
|
],
|
|
"category": [
|
|
r"(经验|教训|待办|问题|成就|改进)",
|
|
r"(experience|lesson|task|problem|achievement|improvement)",
|
|
],
|
|
"format": [r"(PDF|Excel|Word|Markdown|JSON)", r"(pdf|xlsx|docx|md|json)"],
|
|
}
|
|
|
|
async def understand(
|
|
self, user_message: str, context: Optional[Dict[str, Any]] = None
|
|
) -> Intent:
|
|
"""
|
|
理解用户的自然语言输入
|
|
|
|
Args:
|
|
user_message: 用户的输入消息
|
|
context: 上下文信息(如对话历史)
|
|
|
|
Returns:
|
|
Intent: 提取的意图
|
|
"""
|
|
self.logger.debug(f"理解用户消息: {user_message}")
|
|
|
|
try:
|
|
# 首先尝试本地模式匹配(快速路径)
|
|
intent = self._match_intent_locally(user_message)
|
|
if intent and intent.confidence > 0.8:
|
|
self.logger.debug(f"本地匹配成功: {intent.command}")
|
|
return intent
|
|
|
|
# 使用 Claude 进行更深入的理解
|
|
intent = await self._understand_with_claude(user_message, context)
|
|
|
|
return intent
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"意图理解失败: {str(e)}")
|
|
return Intent(
|
|
command="unknown",
|
|
clarification_needed=True,
|
|
clarification_question="抱歉,我没有理解您的意思。能否请您重新表述?",
|
|
)
|
|
|
|
def _match_intent_locally(self, user_message: str) -> Optional[Intent]:
|
|
"""
|
|
本地模式匹配,快速识别常见意图
|
|
|
|
Args:
|
|
user_message: 用户消息
|
|
|
|
Returns:
|
|
Intent 或 None
|
|
"""
|
|
message_lower: str = user_message.lower()
|
|
|
|
# 逐个检查命令关键词
|
|
for command, keywords in self.command_keywords.items():
|
|
for keyword in keywords:
|
|
if keyword in message_lower:
|
|
# 提取参数
|
|
parameters: Dict[str, Any] = self._extract_parameters_locally(
|
|
user_message
|
|
)
|
|
|
|
return Intent(
|
|
command=command, parameters=parameters, confidence=0.9
|
|
)
|
|
|
|
return None
|
|
|
|
def _extract_parameters_locally(self, user_message: str) -> Dict[str, Any]:
|
|
"""
|
|
本地提取参数
|
|
|
|
Args:
|
|
user_message: 用户消息
|
|
|
|
Returns:
|
|
提取的参数字典
|
|
"""
|
|
parameters: Dict[str, Any] = {}
|
|
|
|
# 提取日期
|
|
for pattern in self.parameter_patterns["date"]:
|
|
match: Optional[re.Match[str]] = re.search(pattern, user_message)
|
|
if match:
|
|
date_str: str = match.group(1)
|
|
parameters["date"] = self._normalize_date(date_str)
|
|
break
|
|
|
|
# 提取分类
|
|
for pattern in self.parameter_patterns["category"]:
|
|
match = re.search(pattern, user_message)
|
|
if match:
|
|
parameters["category"] = match.group(1)
|
|
break
|
|
|
|
# 提取格式
|
|
for pattern in self.parameter_patterns["format"]:
|
|
match = re.search(pattern, user_message)
|
|
if match:
|
|
parameters["format"] = match.group(1).lower()
|
|
break
|
|
|
|
return parameters
|
|
|
|
def _normalize_date(self, date_str: str) -> str:
|
|
"""
|
|
规范化日期字符串为 YYYY-MM-DD 格式
|
|
|
|
Args:
|
|
date_str: 日期字符串
|
|
|
|
Returns:
|
|
规范化的日期字符串
|
|
"""
|
|
today: datetime = datetime.now()
|
|
|
|
# 处理相对日期
|
|
if date_str == "今天":
|
|
return today.strftime("%Y-%m-%d")
|
|
elif date_str == "明天":
|
|
return (today + timedelta(days=1)).strftime("%Y-%m-%d")
|
|
elif date_str == "昨天":
|
|
return (today - timedelta(days=1)).strftime("%Y-%m-%d")
|
|
elif date_str == "前天":
|
|
return (today - timedelta(days=2)).strftime("%Y-%m-%d")
|
|
|
|
# 处理标准日期格式
|
|
try:
|
|
# 尝试 YYYY-MM-DD 或 YYYY/M/D 格式
|
|
for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%Y-%m-%d"]:
|
|
try:
|
|
parsed: datetime = datetime.strptime(
|
|
date_str.replace("/", "-"), fmt
|
|
)
|
|
return parsed.strftime("%Y-%m-%d")
|
|
except ValueError:
|
|
continue
|
|
except:
|
|
pass
|
|
|
|
return date_str
|
|
|
|
async def _understand_with_claude(
|
|
self, user_message: str, context: Optional[Dict[str, Any]] = None
|
|
) -> Intent:
|
|
"""
|
|
使用 Claude 理解用户意图
|
|
|
|
Args:
|
|
user_message: 用户消息
|
|
context: 上下文信息
|
|
|
|
Returns:
|
|
Intent: 提取的意图
|
|
"""
|
|
# Check if Claude is available
|
|
if self.client is None:
|
|
self.logger.warning("Claude client not available, falling back to local matching")
|
|
return Intent(
|
|
command="unknown",
|
|
clarification_needed=True,
|
|
clarification_question="抱歉,AI 理解功能暂时不可用。请使用更具体的命令,如 '整理今天的日记' 或 '分析本周内容'。",
|
|
)
|
|
|
|
# Construct prompt
|
|
prompt = self._build_understanding_prompt(user_message, context)
|
|
|
|
try:
|
|
# Call Claude
|
|
response = self.client.messages.create(
|
|
model="claude-3-5-sonnet-20241022",
|
|
max_tokens=500,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
)
|
|
|
|
# Parse response
|
|
response_text = response.content[0].text
|
|
self.logger.debug(f"Claude 响应: {response_text}")
|
|
|
|
return self._parse_claude_response(response_text, user_message)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Claude API call failed: {str(e)}")
|
|
# Fall back to local matching
|
|
local_intent = self._match_intent_locally(user_message)
|
|
if local_intent:
|
|
return local_intent
|
|
|
|
return Intent(
|
|
command="unknown",
|
|
clarification_needed=True,
|
|
clarification_question="抱歉,我在理解您的意图时遇到了问题。请尝试使用更具体的命令。",
|
|
)
|
|
|
|
def _build_understanding_prompt(
|
|
self, user_message: str, context: Optional[Dict[str, Any]] = None
|
|
) -> str:
|
|
"""
|
|
构建用于 Claude 的提示
|
|
|
|
Args:
|
|
user_message: 用户消息
|
|
context: 上下文信息
|
|
|
|
Returns:
|
|
提示文本
|
|
"""
|
|
available_commands: str = ", ".join(self.command_keywords.keys())
|
|
|
|
prompt: str = f"""你是一个 Obsidian 日记整理助手的意图识别器。
|
|
|
|
用户消息: "{user_message}"
|
|
|
|
可用的命令有: {available_commands}
|
|
|
|
请分析用户的意图,并返回一个 JSON 对象,包含以下字段:
|
|
|
|
{{
|
|
"command": "识别出的命令名称(必须是可用命令之一)",
|
|
"parameters": {{
|
|
"date": "如果用户指定了日期,转换为 YYYY-MM-DD 格式;否则为 null",
|
|
"category": "如果用户指定了分类,提取分类名称;否则为 null",
|
|
"format": "如果用户指定了导出格式,提取格式;否则为 null",
|
|
"other_params": "其他相关参数"
|
|
}},
|
|
"confidence": 0.0 到 1.0 之间的置信度,
|
|
"clarification_needed": 是否需要澄清(布尔值),
|
|
"clarification_question": "如果需要澄清,提出的问题;否则为 null",
|
|
"reasoning": "简短的推理说明"
|
|
}}
|
|
|
|
请确保返回有效的 JSON 格式。"""
|
|
|
|
if context and "conversation_history" in context:
|
|
prompt += f"\n\n对话历史(最近的消息):\n"
|
|
for msg in context["conversation_history"][-3:]:
|
|
prompt += f"- {msg['role']}: {msg['content']}\n"
|
|
|
|
return prompt
|
|
|
|
def _parse_claude_response(self, response_text: str, user_message: str) -> Intent:
|
|
"""
|
|
解析 Claude 的响应
|
|
|
|
Args:
|
|
response_text: Claude 的响应文本
|
|
user_message: 原始用户消息
|
|
|
|
Returns:
|
|
Intent: 提取的意图
|
|
"""
|
|
try:
|
|
# 尝试从响应中提取 JSON
|
|
json_match: Optional[re.Match[str]] = re.search(
|
|
r"\{.*\}", response_text, re.DOTALL
|
|
)
|
|
if not json_match:
|
|
raise ValueError("未找到 JSON 响应")
|
|
|
|
json_str: str = json_match.group(0)
|
|
data: Dict[str, Any] = json.loads(json_str)
|
|
|
|
# 构建 Intent 对象
|
|
intent: Intent = Intent(
|
|
command=data.get("command", "unknown"),
|
|
parameters={
|
|
k: v for k, v in data.get("parameters", {}).items() if v is not None
|
|
},
|
|
confidence=data.get("confidence", 0.7),
|
|
clarification_needed=data.get("clarification_needed", False),
|
|
clarification_question=data.get("clarification_question"),
|
|
raw_understanding=data.get("reasoning", ""),
|
|
)
|
|
|
|
return intent
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"解析 Claude 响应失败: {str(e)}")
|
|
return Intent(
|
|
command="unknown",
|
|
clarification_needed=True,
|
|
clarification_question="抱歉,我在处理您的请求时遇到了问题。能否请您重新表述?",
|
|
)
|