431 lines
18 KiB
Python
431 lines
18 KiB
Python
"""
|
|||
|
|
Integration tests for conversational agent flow.
|
||
|
|
Tests the v2.0 conversational interface with natural language processing.
|
||
|
|
"""
|
||
|
|
import pytest
|
||
|
|
import json
|
||
|
|
from unittest.mock import AsyncMock, patch, Mock
|
||
|
|
from aioresponses import aioresponses
|
||
|
|
|
||
|
|
from agent_core import CommandContext, SkillResult
|
||
|
|
from conversation.conversational_agent import ConversationalAgent
|
||
|
|
from conversation.conversation_state import ConversationState
|
||
|
|
from conversation.intent_understanding import IntentUnderstanding
|
||
|
|
from conversation.response_generator import ResponseGenerator
|
||
|
|
|
||
|
|
|
||
|
|
class TestConversationalAgent:
|
||
|
|
"""Integration tests for ConversationalAgent"""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def agent(self, sample_config):
|
||
|
|
"""Create ConversationalAgent instance"""
|
||
|
|
return ConversationalAgent(sample_config)
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def sample_user_inputs(self):
|
||
|
|
"""Sample user inputs for testing"""
|
||
|
|
return [
|
||
|
|
"整理今天的日记",
|
||
|
|
"organize today's journal",
|
||
|
|
"分析2024年1月15日的日记",
|
||
|
|
"help me organize my notes from yesterday",
|
||
|
|
"今天学到了什么?",
|
||
|
|
"what did I learn today?",
|
||
|
|
"整理昨天的经验和教训"
|
||
|
|
]
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_conversational_agent_basic_flow(self, agent):
|
||
|
|
"""Test basic conversational flow"""
|
||
|
|
user_input = "整理今天的日记"
|
||
|
|
|
||
|
|
# Mock the underlying organize command execution
|
||
|
|
with patch.object(agent.agent, 'execute_command') as mock_execute:
|
||
|
|
mock_result = SkillResult(
|
||
|
|
success=True,
|
||
|
|
data={
|
||
|
|
"summary": "Successfully organized journal",
|
||
|
|
"created_notes": [
|
||
|
|
{"type": "experience", "title": "Test Experience", "path": "Knowledge/Experiences/test.md"}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
message="Journal organized successfully"
|
||
|
|
)
|
||
|
|
mock_execute.return_value = mock_result
|
||
|
|
|
||
|
|
response = await agent.process_message(user_input)
|
||
|
|
|
||
|
|
assert response is not None
|
||
|
|
assert "成功" in response or "successfully" in response.lower()
|
||
|
|
mock_execute.assert_called_once()
|
||
|
|
|
||
|
|
# Verify the command was called with correct parameters
|
||
|
|
call_args = mock_execute.call_args
|
||
|
|
assert call_args[0][0] == "organize" # Command name
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_conversational_agent_with_date_extraction(self, agent):
|
||
|
|
"""Test conversational agent with date parameter extraction"""
|
||
|
|
user_input = "分析2024年1月15日的日记"
|
||
|
|
|
||
|
|
with patch.object(agent.agent, 'execute_command') as mock_execute:
|
||
|
|
mock_result = SkillResult(success=True, data={}, message="Analysis completed")
|
||
|
|
mock_execute.return_value = mock_result
|
||
|
|
|
||
|
|
response = await agent.process_message(user_input)
|
||
|
|
|
||
|
|
assert response is not None
|
||
|
|
mock_execute.assert_called_once()
|
||
|
|
|
||
|
|
# Verify date was extracted and passed
|
||
|
|
call_args = mock_execute.call_args
|
||
|
|
assert "date" in call_args[1]["args"] # Should have extracted date
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_conversational_agent_error_handling(self, agent):
|
||
|
|
"""Test conversational agent error handling"""
|
||
|
|
user_input = "整理今天的日记"
|
||
|
|
|
||
|
|
with patch.object(agent.agent, 'execute_command') as mock_execute:
|
||
|
|
mock_result = SkillResult(
|
||
|
|
success=False,
|
||
|
|
error="Journal file not found",
|
||
|
|
message="Failed to organize journal"
|
||
|
|
)
|
||
|
|
mock_execute.return_value = mock_result
|
||
|
|
|
||
|
|
response = await agent.process_message(user_input)
|
||
|
|
|
||
|
|
assert response is not None
|
||
|
|
assert "错误" in response or "error" in response.lower() or "failed" in response.lower()
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_conversational_agent_unknown_intent(self, agent):
|
||
|
|
"""Test conversational agent with unknown intent"""
|
||
|
|
user_input = "今天天气怎么样?" # Weather question, not related to journal organization
|
||
|
|
|
||
|
|
response = await agent.process_message(user_input)
|
||
|
|
|
||
|
|
assert response is not None
|
||
|
|
assert "不理解" in response or "不明白" in response or "help" in response.lower()
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_conversational_agent_help_request(self, agent):
|
||
|
|
"""Test conversational agent help functionality"""
|
||
|
|
help_inputs = ["help", "帮助", "你能做什么?", "what can you do?"]
|
||
|
|
|
||
|
|
for user_input in help_inputs:
|
||
|
|
response = await agent.process_message(user_input)
|
||
|
|
|
||
|
|
assert response is not None
|
||
|
|
assert "整理" in response or "organize" in response.lower()
|
||
|
|
assert "日记" in response or "journal" in response.lower()
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_conversational_agent_multiple_turns(self, agent):
|
||
|
|
"""Test multi-turn conversation"""
|
||
|
|
conversation_turns = [
|
||
|
|
("你好", "greeting"),
|
||
|
|
("整理今天的日记", "organize"),
|
||
|
|
("谢谢", "thanks")
|
||
|
|
]
|
||
|
|
|
||
|
|
for user_input, expected_intent in conversation_turns:
|
||
|
|
if expected_intent == "organize":
|
||
|
|
with patch.object(agent.agent, 'execute_command') as mock_execute:
|
||
|
|
mock_result = SkillResult(success=True, data={}, message="Success")
|
||
|
|
mock_execute.return_value = mock_result
|
||
|
|
|
||
|
|
response = await agent.process_message(user_input)
|
||
|
|
else:
|
||
|
|
response = await agent.process_message(user_input)
|
||
|
|
|
||
|
|
assert response is not None
|
||
|
|
assert len(response) > 0
|
||
|
|
|
||
|
|
|
||
|
|
class TestIntentUnderstanding:
|
||
|
|
"""Integration tests for IntentUnderstanding"""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def intent_processor(self):
|
||
|
|
"""Create IntentUnderstanding instance"""
|
||
|
|
return IntentUnderstanding()
|
||
|
|
|
||
|
|
def test_organize_intent_detection(self, intent_processor):
|
||
|
|
"""Test detection of organize intents"""
|
||
|
|
organize_inputs = [
|
||
|
|
"整理今天的日记",
|
||
|
|
"organize today's journal",
|
||
|
|
"分析我的日记",
|
||
|
|
"help me organize my notes",
|
||
|
|
"整理昨天的笔记"
|
||
|
|
]
|
||
|
|
|
||
|
|
for user_input in organize_inputs:
|
||
|
|
intent = intent_processor.understand_intent(user_input)
|
||
|
|
|
||
|
|
assert intent["action"] == "organize"
|
||
|
|
assert "command" in intent
|
||
|
|
assert intent["command"] == "organize"
|
||
|
|
|
||
|
|
def test_date_parameter_extraction(self, intent_processor):
|
||
|
|
"""Test extraction of date parameters"""
|
||
|
|
date_inputs = [
|
||
|
|
("整理2024年1月15日的日记", "2024-01-15"),
|
||
|
|
("analyze journal from yesterday", "yesterday"),
|
||
|
|
("organize today's notes", "today"),
|
||
|
|
("分析昨天的日记", "yesterday")
|
||
|
|
]
|
||
|
|
|
||
|
|
for user_input, expected_date in date_inputs:
|
||
|
|
intent = intent_processor.understand_intent(user_input)
|
||
|
|
|
||
|
|
if expected_date in ["today", "yesterday"]:
|
||
|
|
# These should be converted to actual dates
|
||
|
|
assert "date" in intent["parameters"]
|
||
|
|
else:
|
||
|
|
assert intent["parameters"].get("date") == expected_date
|
||
|
|
|
||
|
|
def test_help_intent_detection(self, intent_processor):
|
||
|
|
"""Test detection of help intents"""
|
||
|
|
help_inputs = [
|
||
|
|
"help",
|
||
|
|
"帮助",
|
||
|
|
"你能做什么?",
|
||
|
|
"what can you do?",
|
||
|
|
"how to use this?"
|
||
|
|
]
|
||
|
|
|
||
|
|
for user_input in help_inputs:
|
||
|
|
intent = intent_processor.understand_intent(user_input)
|
||
|
|
|
||
|
|
assert intent["action"] == "help"
|
||
|
|
|
||
|
|
def test_unknown_intent_handling(self, intent_processor):
|
||
|
|
"""Test handling of unknown intents"""
|
||
|
|
unknown_inputs = [
|
||
|
|
"今天天气怎么样?",
|
||
|
|
"what's the weather like?",
|
||
|
|
"计算1+1等于多少",
|
||
|
|
"play music"
|
||
|
|
]
|
||
|
|
|
||
|
|
for user_input in unknown_inputs:
|
||
|
|
intent = intent_processor.understand_intent(user_input)
|
||
|
|
|
||
|
|
assert intent["action"] == "unknown"
|
||
|
|
assert "confidence" in intent
|
||
|
|
assert intent["confidence"] < 0.5 # Low confidence for unknown intents
|
||
|
|
|
||
|
|
|
||
|
|
class TestResponseGenerator:
|
||
|
|
"""Integration tests for ResponseGenerator"""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def response_generator(self):
|
||
|
|
"""Create ResponseGenerator instance"""
|
||
|
|
return ResponseGenerator()
|
||
|
|
|
||
|
|
def test_success_response_generation(self, response_generator):
|
||
|
|
"""Test generation of success responses"""
|
||
|
|
result = SkillResult(
|
||
|
|
success=True,
|
||
|
|
data={
|
||
|
|
"summary": "Successfully organized journal",
|
||
|
|
"created_notes": [
|
||
|
|
{"type": "experience", "title": "Project Milestone", "path": "Knowledge/Experiences/milestone.md"},
|
||
|
|
{"type": "lesson", "title": "Python Tips", "path": "Knowledge/Lessons/python.md"}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
message="Journal organized successfully"
|
||
|
|
)
|
||
|
|
|
||
|
|
response = response_generator.generate_response(result, "organize")
|
||
|
|
|
||
|
|
assert response is not None
|
||
|
|
assert "成功" in response or "successfully" in response.lower()
|
||
|
|
assert "2" in response # Should mention number of notes created
|
||
|
|
assert "经验" in response or "experience" in response.lower()
|
||
|
|
assert "教训" in response or "lesson" in response.lower()
|
||
|
|
|
||
|
|
def test_error_response_generation(self, response_generator):
|
||
|
|
"""Test generation of error responses"""
|
||
|
|
result = SkillResult(
|
||
|
|
success=False,
|
||
|
|
error="Journal file not found for date 2024-01-15",
|
||
|
|
message="Failed to organize journal"
|
||
|
|
)
|
||
|
|
|
||
|
|
response = response_generator.generate_response(result, "organize")
|
||
|
|
|
||
|
|
assert response is not None
|
||
|
|
assert "错误" in response or "error" in response.lower() or "失败" in response
|
||
|
|
assert "2024-01-15" in response # Should include the problematic date
|
||
|
|
|
||
|
|
def test_help_response_generation(self, response_generator):
|
||
|
|
"""Test generation of help responses"""
|
||
|
|
response = response_generator.generate_help_response()
|
||
|
|
|
||
|
|
assert response is not None
|
||
|
|
assert "整理" in response or "organize" in response.lower()
|
||
|
|
assert "日记" in response or "journal" in response.lower()
|
||
|
|
assert "命令" in response or "command" in response.lower()
|
||
|
|
|
||
|
|
def test_unknown_intent_response(self, response_generator):
|
||
|
|
"""Test generation of unknown intent responses"""
|
||
|
|
response = response_generator.generate_unknown_response("今天天气怎么样?")
|
||
|
|
|
||
|
|
assert response is not None
|
||
|
|
assert "不理解" in response or "不明白" in response or "understand" in response.lower()
|
||
|
|
assert "帮助" in response or "help" in response.lower()
|
||
|
|
|
||
|
|
|
||
|
|
class TestConversationState:
|
||
|
|
"""Integration tests for ConversationState"""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def conversation_state(self):
|
||
|
|
"""Create ConversationState instance"""
|
||
|
|
return ConversationState()
|
||
|
|
|
||
|
|
def test_conversation_history_tracking(self, conversation_state):
|
||
|
|
"""Test conversation history tracking"""
|
||
|
|
# Add some conversation turns
|
||
|
|
conversation_state.add_turn("user", "整理今天的日记")
|
||
|
|
conversation_state.add_turn("assistant", "好的,我来帮您整理今天的日记。")
|
||
|
|
conversation_state.add_turn("user", "谢谢")
|
||
|
|
conversation_state.add_turn("assistant", "不客气!还有其他需要帮助的吗?")
|
||
|
|
|
||
|
|
history = conversation_state.get_history()
|
||
|
|
|
||
|
|
assert len(history) == 4
|
||
|
|
assert history[0]["role"] == "user"
|
||
|
|
assert history[0]["content"] == "整理今天的日记"
|
||
|
|
assert history[1]["role"] == "assistant"
|
||
|
|
assert history[-1]["role"] == "assistant"
|
||
|
|
|
||
|
|
def test_context_management(self, conversation_state):
|
||
|
|
"""Test conversation context management"""
|
||
|
|
# Set some context
|
||
|
|
conversation_state.set_context("last_command", "organize")
|
||
|
|
conversation_state.set_context("last_date", "2024-01-15")
|
||
|
|
conversation_state.set_context("user_preference", "detailed_summary")
|
||
|
|
|
||
|
|
# Retrieve context
|
||
|
|
assert conversation_state.get_context("last_command") == "organize"
|
||
|
|
assert conversation_state.get_context("last_date") == "2024-01-15"
|
||
|
|
assert conversation_state.get_context("user_preference") == "detailed_summary"
|
||
|
|
assert conversation_state.get_context("nonexistent") is None
|
||
|
|
|
||
|
|
def test_conversation_reset(self, conversation_state):
|
||
|
|
"""Test conversation reset functionality"""
|
||
|
|
# Add some data
|
||
|
|
conversation_state.add_turn("user", "test message")
|
||
|
|
conversation_state.set_context("test_key", "test_value")
|
||
|
|
|
||
|
|
# Verify data exists
|
||
|
|
assert len(conversation_state.get_history()) == 1
|
||
|
|
assert conversation_state.get_context("test_key") == "test_value"
|
||
|
|
|
||
|
|
# Reset conversation
|
||
|
|
conversation_state.reset()
|
||
|
|
|
||
|
|
# Verify data is cleared
|
||
|
|
assert len(conversation_state.get_history()) == 0
|
||
|
|
assert conversation_state.get_context("test_key") is None
|
||
|
|
|
||
|
|
|
||
|
|
class TestFullConversationalFlow:
|
||
|
|
"""End-to-end integration tests for the complete conversational flow"""
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_complete_organize_conversation(self, sample_config, sample_journal_content):
|
||
|
|
"""Test complete conversation flow for journal organization"""
|
||
|
|
agent = ConversationalAgent(sample_config)
|
||
|
|
|
||
|
|
# Mock all external dependencies
|
||
|
|
with aioresponses() as m:
|
||
|
|
# Mock Obsidian API
|
||
|
|
m.get(
|
||
|
|
"https://localhost:27123/vault/Daily/2024-01-15.md",
|
||
|
|
payload={
|
||
|
|
"content": sample_journal_content,
|
||
|
|
"stat": {"ctime": 1642204800000, "mtime": 1642204800000, "size": len(sample_journal_content)}
|
||
|
|
},
|
||
|
|
status=200
|
||
|
|
)
|
||
|
|
|
||
|
|
# Mock note creation
|
||
|
|
m.put(
|
||
|
|
"https://localhost:27123/vault/Knowledge/Experiences/test.md",
|
||
|
|
payload={"path": "Knowledge/Experiences/test.md", "stat": {}},
|
||
|
|
status=200
|
||
|
|
)
|
||
|
|
|
||
|
|
# Mock Claude API
|
||
|
|
with patch('skills.claude_skill.anthropic') as mock_anthropic:
|
||
|
|
mock_client = Mock()
|
||
|
|
mock_anthropic.Anthropic.return_value = mock_client
|
||
|
|
|
||
|
|
# Mock analysis response
|
||
|
|
analyze_response = {
|
||
|
|
"experiences": [{"title": "Test Experience", "description": "Test", "category": "Test"}],
|
||
|
|
"lessons": [],
|
||
|
|
"problems": [],
|
||
|
|
"achievements": []
|
||
|
|
}
|
||
|
|
|
||
|
|
mock_analyze_message = Mock()
|
||
|
|
mock_analyze_message.content = [Mock(text=json.dumps(analyze_response, ensure_ascii=False))]
|
||
|
|
|
||
|
|
mock_transform_message = Mock()
|
||
|
|
mock_transform_message.content = [Mock(text="# Test Experience\n\nTest content")]
|
||
|
|
|
||
|
|
mock_client.messages.create.side_effect = [mock_analyze_message, mock_transform_message]
|
||
|
|
|
||
|
|
# Simulate conversation
|
||
|
|
conversation_turns = [
|
||
|
|
"你好",
|
||
|
|
"整理今天的日记",
|
||
|
|
"谢谢你的帮助"
|
||
|
|
]
|
||
|
|
|
||
|
|
responses = []
|
||
|
|
for user_input in conversation_turns:
|
||
|
|
response = await agent.process_message(user_input)
|
||
|
|
responses.append(response)
|
||
|
|
assert response is not None
|
||
|
|
assert len(response) > 0
|
||
|
|
|
||
|
|
# Verify conversation flow
|
||
|
|
assert "你好" in responses[0] or "hello" in responses[0].lower() # Greeting response
|
||
|
|
assert "成功" in responses[1] or "successfully" in responses[1].lower() # Success response
|
||
|
|
assert "不客气" in responses[2] or "welcome" in responses[2].lower() # Thanks response
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_error_recovery_conversation(self, sample_config):
|
||
|
|
"""Test conversation flow with error recovery"""
|
||
|
|
agent = ConversationalAgent(sample_config)
|
||
|
|
|
||
|
|
with aioresponses() as m:
|
||
|
|
# Mock journal file not found
|
||
|
|
m.get(
|
||
|
|
"https://localhost:27123/vault/Daily/2024-01-15.md",
|
||
|
|
status=404,
|
||
|
|
payload={"error": "File not found"}
|
||
|
|
)
|
||
|
|
|
||
|
|
# Simulate error scenario
|
||
|
|
user_input = "整理今天的日记"
|
||
|
|
response = await agent.process_message(user_input)
|
||
|
|
|
||
|
|
assert response is not None
|
||
|
|
assert "找不到" in response or "not found" in response.lower() or "错误" in response
|
||
|
|
|
||
|
|
# Follow up with help request
|
||
|
|
help_response = await agent.process_message("我应该怎么办?")
|
||
|
|
|
||
|
|
assert help_response is not None
|
||
|
|
assert "建议" in help_response or "suggest" in help_response.lower() or "帮助" in help_response
|