fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
376 lines
12 KiB
Python
376 lines
12 KiB
Python
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from application.agents.classic_agent import ClassicAgent
|
|
from application.agents.tools.internal_search import INTERNAL_TOOL_ID
|
|
|
|
|
|
@pytest.fixture
|
|
def _no_tools(monkeypatch):
|
|
"""Stub ToolExecutor.get_tools to avoid DB hits for most tests."""
|
|
|
|
def _fake_get_tools(self):
|
|
return {}
|
|
|
|
monkeypatch.setattr(
|
|
"application.agents.tool_executor.ToolExecutor.get_tools", _fake_get_tools
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def _no_dir_structure(monkeypatch):
|
|
"""Stub the DB-backed directory-structure lookup used by the search tool."""
|
|
monkeypatch.setattr(
|
|
"application.agents.tools.internal_search.sources_have_directory_structure",
|
|
lambda source: False,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestClassicAgent:
|
|
|
|
def test_classic_agent_initialization(
|
|
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
|
):
|
|
agent = ClassicAgent(**agent_base_params)
|
|
|
|
assert isinstance(agent, ClassicAgent)
|
|
assert agent.endpoint == agent_base_params["endpoint"]
|
|
assert agent.llm_name == agent_base_params["llm_name"]
|
|
|
|
def test_gen_inner_basic_flow(
|
|
self,
|
|
agent_base_params,
|
|
mock_llm,
|
|
mock_llm_handler,
|
|
mock_llm_creator,
|
|
mock_llm_handler_creator,
|
|
_no_tools,
|
|
log_context,
|
|
):
|
|
def mock_gen_stream(*args, **kwargs):
|
|
yield "Answer chunk 1"
|
|
yield "Answer chunk 2"
|
|
|
|
mock_llm.gen_stream = Mock(return_value=mock_gen_stream())
|
|
|
|
def mock_handler(*args, **kwargs):
|
|
yield "Processed answer"
|
|
|
|
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
|
|
|
agent = ClassicAgent(**agent_base_params)
|
|
|
|
results = list(agent._gen_inner("Test query", log_context))
|
|
|
|
assert len(results) >= 2
|
|
sources = [r for r in results if "sources" in r]
|
|
tool_calls = [r for r in results if "tool_calls" in r]
|
|
|
|
assert len(sources) == 1
|
|
assert len(tool_calls) == 1
|
|
|
|
def test_gen_inner_retrieves_documents(
|
|
self,
|
|
agent_base_params,
|
|
mock_llm,
|
|
mock_llm_handler,
|
|
mock_llm_creator,
|
|
mock_llm_handler_creator,
|
|
_no_tools,
|
|
log_context,
|
|
):
|
|
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
|
|
|
def mock_handler(*args, **kwargs):
|
|
yield "Processed"
|
|
|
|
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
|
|
|
agent = ClassicAgent(**agent_base_params)
|
|
list(agent._gen_inner("Test query", log_context))
|
|
|
|
def test_gen_inner_uses_user_tools(
|
|
self,
|
|
agent_base_params,
|
|
mock_llm,
|
|
mock_llm_handler,
|
|
mock_llm_creator,
|
|
mock_llm_handler_creator,
|
|
monkeypatch,
|
|
log_context,
|
|
):
|
|
# Inject a fake user tool dict via get_tools rather than touching DB.
|
|
fake_tools = {
|
|
"t1": {
|
|
"id": "t1",
|
|
"name": "test_tool",
|
|
"config": {},
|
|
"actions": [
|
|
{
|
|
"name": "do_thing",
|
|
"description": "",
|
|
"active": True,
|
|
"parameters": {"properties": {}},
|
|
}
|
|
],
|
|
}
|
|
}
|
|
monkeypatch.setattr(
|
|
"application.agents.tool_executor.ToolExecutor.get_tools",
|
|
lambda self: fake_tools,
|
|
)
|
|
|
|
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
|
|
|
def mock_handler(*args, **kwargs):
|
|
yield "Processed"
|
|
|
|
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
|
|
|
agent = ClassicAgent(**agent_base_params)
|
|
list(agent._gen_inner("Test query", log_context))
|
|
|
|
# After _prepare_tools, the fake user tool should have become an llm tool schema.
|
|
assert any(
|
|
t["function"]["name"] == "do_thing" for t in agent.tools
|
|
)
|
|
|
|
def test_gen_inner_builds_correct_messages(
|
|
self,
|
|
agent_base_params,
|
|
mock_llm,
|
|
mock_llm_handler,
|
|
mock_llm_creator,
|
|
mock_llm_handler_creator,
|
|
_no_tools,
|
|
log_context,
|
|
):
|
|
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
|
|
|
def mock_handler(*args, **kwargs):
|
|
yield "Processed"
|
|
|
|
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
|
|
|
agent = ClassicAgent(**agent_base_params)
|
|
list(agent._gen_inner("Test query", log_context))
|
|
|
|
call_kwargs = mock_llm.gen_stream.call_args[1]
|
|
messages = call_kwargs["messages"]
|
|
|
|
assert len(messages) >= 2
|
|
assert messages[0]["role"] == "system"
|
|
assert messages[-1]["role"] == "user"
|
|
assert messages[-1]["content"] == "Test query"
|
|
|
|
def test_gen_inner_logs_tool_calls(
|
|
self,
|
|
agent_base_params,
|
|
mock_llm,
|
|
mock_llm_handler,
|
|
mock_llm_creator,
|
|
mock_llm_handler_creator,
|
|
_no_tools,
|
|
log_context,
|
|
):
|
|
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
|
|
|
def mock_handler(*args, **kwargs):
|
|
yield "Processed"
|
|
|
|
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
|
|
|
agent = ClassicAgent(**agent_base_params)
|
|
agent.tool_calls = [{"tool": "test", "result": "success"}]
|
|
|
|
list(agent._gen_inner("Test query", log_context))
|
|
|
|
agent_logs = [s for s in log_context.stacks if s["component"] == "agent"]
|
|
assert len(agent_logs) == 1
|
|
assert "tool_calls" in agent_logs[0]["data"]
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestClassicAgentSearchExposure:
|
|
"""ClassicAgent honors per-source exposure via an internal_search tool."""
|
|
|
|
def test_default_registers_no_internal_search(
|
|
self,
|
|
agent_base_params,
|
|
mock_llm,
|
|
mock_llm_handler,
|
|
mock_llm_creator,
|
|
mock_llm_handler_creator,
|
|
_no_tools,
|
|
log_context,
|
|
):
|
|
# No retriever_config (every source at default prefetch) → no search
|
|
# tool is added and pre-fetched sources are preserved (unchanged).
|
|
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
|
|
|
def mock_handler(*args, **kwargs):
|
|
yield "Processed"
|
|
|
|
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
|
|
|
agent = ClassicAgent(**agent_base_params)
|
|
assert agent.retriever_config == {}
|
|
|
|
captured = {}
|
|
original_prepare = agent._prepare_tools
|
|
|
|
def _spy(tools_dict):
|
|
captured["tools_dict"] = tools_dict
|
|
return original_prepare(tools_dict)
|
|
|
|
agent._prepare_tools = _spy
|
|
prefetched = [{"text": "pre", "title": "Pre", "source": "s"}]
|
|
agent.retrieved_docs = list(prefetched)
|
|
|
|
list(agent._gen_inner("q", log_context))
|
|
|
|
assert INTERNAL_TOOL_ID not in captured["tools_dict"]
|
|
# Pre-fetched sources untouched (no _collect_internal_sources overwrite).
|
|
assert agent.retrieved_docs == prefetched
|
|
|
|
def test_with_agentic_tool_source_registers_internal_search(
|
|
self,
|
|
agent_base_params,
|
|
mock_llm,
|
|
mock_llm_handler,
|
|
mock_llm_creator,
|
|
mock_llm_handler_creator,
|
|
_no_tools,
|
|
_no_dir_structure,
|
|
log_context,
|
|
):
|
|
# A retriever_config (agentic_tool subset) → the internal_search tool is
|
|
# registered so the LLM can search that subset on demand.
|
|
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
|
|
|
def mock_handler(*args, **kwargs):
|
|
yield "Processed"
|
|
|
|
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
|
|
|
retriever_config = {
|
|
"source": {"active_docs": ["b"]},
|
|
"retriever_name": "classic",
|
|
"chunks": 2,
|
|
"sources": [{"id": "b", "retrieval": None}],
|
|
}
|
|
agent = ClassicAgent(retriever_config=retriever_config, **agent_base_params)
|
|
|
|
captured = {}
|
|
original_prepare = agent._prepare_tools
|
|
|
|
def _spy(tools_dict):
|
|
captured["tools_dict"] = tools_dict
|
|
return original_prepare(tools_dict)
|
|
|
|
agent._prepare_tools = _spy
|
|
|
|
list(agent._gen_inner("q", log_context))
|
|
|
|
assert INTERNAL_TOOL_ID in captured["tools_dict"]
|
|
assert captured["tools_dict"][INTERNAL_TOOL_ID]["name"] == "internal_search"
|
|
|
|
def test_collect_internal_sources_surfaces_tool_docs(
|
|
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
|
):
|
|
retriever_config = {"source": {"active_docs": ["b"]}}
|
|
agent = ClassicAgent(retriever_config=retriever_config, **agent_base_params)
|
|
|
|
mock_tool = Mock()
|
|
mock_tool.retrieved_docs = [{"text": "Found", "title": "Doc", "source": "t"}]
|
|
cache_key = f"internal_search:{INTERNAL_TOOL_ID}:{agent.user or ''}"
|
|
agent.tool_executor._loaded_tools[cache_key] = mock_tool
|
|
|
|
agent._collect_internal_sources()
|
|
assert [d["title"] for d in agent.retrieved_docs] == ["Doc"]
|
|
|
|
def test_collect_internal_sources_merges_with_prefetched(
|
|
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
|
):
|
|
# Mixed exposure: a pre-fetched source's docs must survive alongside the
|
|
# tool-retrieved docs (not be overwritten), so both are cited.
|
|
retriever_config = {"source": {"active_docs": ["b"]}}
|
|
agent = ClassicAgent(retriever_config=retriever_config, **agent_base_params)
|
|
agent.retrieved_docs = [
|
|
{"text": "Prefetched", "title": "Prefetched Doc", "source": "a"}
|
|
]
|
|
mock_tool = Mock()
|
|
mock_tool.retrieved_docs = [
|
|
{"text": "Found", "title": "Tool Doc", "source": "b"}
|
|
]
|
|
cache_key = f"internal_search:{INTERNAL_TOOL_ID}:{agent.user or ''}"
|
|
agent.tool_executor._loaded_tools[cache_key] = mock_tool
|
|
|
|
agent._collect_internal_sources()
|
|
assert [d["title"] for d in agent.retrieved_docs] == [
|
|
"Prefetched Doc",
|
|
"Tool Doc",
|
|
]
|
|
|
|
def test_collect_internal_sources_dedupes(
|
|
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
|
):
|
|
retriever_config = {"source": {"active_docs": ["b"]}}
|
|
agent = ClassicAgent(retriever_config=retriever_config, **agent_base_params)
|
|
dup = {"text": "X", "title": "Dup", "source": "b"}
|
|
agent.retrieved_docs = [dup]
|
|
mock_tool = Mock()
|
|
mock_tool.retrieved_docs = [dict(dup)]
|
|
cache_key = f"internal_search:{INTERNAL_TOOL_ID}:{agent.user or ''}"
|
|
agent.tool_executor._loaded_tools[cache_key] = mock_tool
|
|
|
|
agent._collect_internal_sources()
|
|
assert [d["title"] for d in agent.retrieved_docs] == ["Dup"]
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestClassicAgentIntegration:
|
|
|
|
def test_gen_method_with_logging(
|
|
self,
|
|
agent_base_params,
|
|
mock_llm,
|
|
mock_llm_handler,
|
|
mock_llm_creator,
|
|
mock_llm_handler_creator,
|
|
_no_tools,
|
|
):
|
|
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
|
|
|
def mock_handler(*args, **kwargs):
|
|
yield "Processed"
|
|
|
|
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
|
|
|
agent = ClassicAgent(**agent_base_params)
|
|
|
|
results = list(agent.gen("Test query"))
|
|
|
|
assert len(results) >= 1
|
|
|
|
def test_gen_method_decorator_applied(
|
|
self,
|
|
agent_base_params,
|
|
mock_llm,
|
|
mock_llm_handler,
|
|
mock_llm_creator,
|
|
mock_llm_handler_creator,
|
|
_no_tools,
|
|
):
|
|
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
|
|
|
def mock_handler(*args, **kwargs):
|
|
yield "Processed"
|
|
|
|
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
|
|
|
agent = ClassicAgent(**agent_base_params)
|
|
|
|
assert hasattr(agent.gen, "__wrapped__")
|