""" 意图理解模块 使用 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="抱歉,我在处理您的请求时遇到了问题。能否请您重新表述?", )