Files
journal_organizer/conversation/intent_understanding.py
T
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

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