from unittest.mock import AsyncMock, Mock, patch
import pytest
from openjiuwen_deepsearch.algorithm.query_understanding.intent_recognition import (
IntentRecognitionResult,
recognize_report_intent,
)
from openjiuwen_deepsearch.framework.openjiuwen.agent.search_context import ResearchIntent
@pytest.fixture
def sample_tool_response():
return {
"tool_calls": [
{
"name": "emit_report_intent",
"args": {
"research_query": "AI Agent trends",
"section_count": 5,
"audience_role": "研发负责人",
"tone": "analytical",
"report_type": "professional",
"include_url": ["https://example.com/a", "https://example.com/b"],
"exclude_url": [],
"include_domains": [],
"exclude_domains": [],
},
"id": "tc1",
"type": "tool_call",
}
],
"content": "",
}
@pytest.mark.asyncio
async def test_recognize_report_intent_success(sample_tool_response):
mock_llm = Mock()
with patch(
"openjiuwen_deepsearch.algorithm.query_understanding.intent_recognition.llm_context",
return_value={"basic": mock_llm},
), patch(
"openjiuwen_deepsearch.algorithm.query_understanding.intent_recognition.llm_utils.ainvoke_llm_with_stats",
new_callable=AsyncMock,
return_value=sample_tool_response,
):
result = await recognize_report_intent(
{
"original_query": "Write a report: AI Agent\nhttps://example.com/a",
"llm_model_name": "basic",
"messages": [],
}
)
assert isinstance(result, IntentRecognitionResult)
assert result.research_query == "AI Agent trends"
assert result.research_intent.section_count == 5
assert result.research_intent.audience_role == "研发负责人"
assert result.research_intent.tone == "analytical"
assert result.research_intent.report_type == "professional"
assert "https://example.com/a" in result.research_intent.include_url
assert "example.com" in result.research_intent.include_domains
@pytest.mark.asyncio
async def test_recognize_report_intent_no_tool_calls_fallback():
with patch(
"openjiuwen_deepsearch.algorithm.query_understanding.intent_recognition.llm_context",
return_value={"basic": Mock()},
), patch(
"openjiuwen_deepsearch.algorithm.query_understanding.intent_recognition.llm_utils.ainvoke_llm_with_stats",
new_callable=AsyncMock,
return_value={"tool_calls": [], "content": ""},
):
q = "原始问题全文"
result = await recognize_report_intent({"original_query": q, "llm_model_name": "basic"})
assert result.original_query == q
assert result.research_query == q
assert result.research_intent == ResearchIntent()
@pytest.mark.asyncio
async def test_recognize_report_intent_exception_fallback():
with patch(
"openjiuwen_deepsearch.algorithm.query_understanding.intent_recognition.llm_context",
return_value={"basic": Mock()},
), patch(
"openjiuwen_deepsearch.algorithm.query_understanding.intent_recognition.llm_utils.ainvoke_llm_with_stats",
new_callable=AsyncMock,
side_effect=RuntimeError("llm down"),
):
q = "fallback text"
result = await recognize_report_intent({"original_query": q, "llm_model_name": "basic"})
assert result.research_query == q
assert result.research_intent.section_count is None
@pytest.mark.asyncio
async def test_empty_original_query():
result = await recognize_report_intent({"original_query": "", "llm_model_name": "basic"})
assert result.research_query == ""
assert result.research_intent == ResearchIntent()
@pytest.mark.asyncio
async def test_normalize_invalid_report_type_defaults_professional():
legacy_response = {
"tool_calls": [
{
"name": "emit_report_intent",
"args": {
"research_query": "topic",
"report_type": "deep_research",
},
"id": "tc1",
"type": "tool_call",
}
],
"content": "",
}
with patch(
"openjiuwen_deepsearch.algorithm.query_understanding.intent_recognition.llm_context",
return_value={"basic": Mock()},
), patch(
"openjiuwen_deepsearch.algorithm.query_understanding.intent_recognition.llm_utils.ainvoke_llm_with_stats",
new_callable=AsyncMock,
return_value=legacy_response,
):
result = await recognize_report_intent(
{"original_query": "深度研究 topic", "llm_model_name": "basic"}
)
assert result.research_intent.report_type == "professional"
@pytest.mark.asyncio
async def test_normalize_invalid_section_count():
bad_response = {
"tool_calls": [
{
"args": {
"research_query": "topic",
"section_count": -1,
"include_url": ["https://x.y/z"],
},
}
]
}
with patch(
"openjiuwen_deepsearch.algorithm.query_understanding.intent_recognition.llm_context",
return_value={"basic": Mock()},
), patch(
"openjiuwen_deepsearch.algorithm.query_understanding.intent_recognition.llm_utils.ainvoke_llm_with_stats",
new_callable=AsyncMock,
return_value=bad_response,
):
result = await recognize_report_intent(
{"original_query": "topic", "llm_model_name": "basic"}
)
assert result.research_intent.section_count is None
assert "x.y" in result.research_intent.include_domains