Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions backend/agents/create_agent_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from nexent.core.agents.agent_context import ContextManagerConfig
from nexent.memory.memory_service import search_memory_in_levels

from services.file_management_service import get_llm_model, validate_urls_access
from services.file_management_service import get_llm_model, build_llm_model, validate_urls_access
from services.vectordatabase_service import (
ElasticSearchService,
get_vector_db_core,
Expand Down Expand Up @@ -412,6 +412,7 @@
user_id,
version_no=version_no,
tool_params=normalized_tool_params,
override_model_id=override_model_id,
)

# Build system prompt: prioritize segmented fields, fallback to original prompt field if not available
Expand Down Expand Up @@ -644,6 +645,7 @@
user_id,
version_no: int = 0,
tool_params: Optional[ToolParamsRequest | Dict[str, Any]] = None,
override_model_id: int | None = None,
):
tool_config_list = []
langchain_tools = await discover_langchain_tools()
Expand All @@ -657,6 +659,14 @@
# but we include it in error messages so callers can identify which agent/tool caused a failure.
agent_info = search_agent_info_by_agent_id(agent_id=agent_id, tenant_id=tenant_id, version_no=version_no)
agent_name = agent_info.get("name") if agent_info else None

# Resolve the model the agent itself runs with, so tools that rely on an LLM
# (e.g. AnalyzeTextFileTool) use the agent-configured model rather than the
# tenant-wide default from the model management page. Mirrors the main model
# resolution in create_agent_config (override takes precedence over the
# persisted agent model_id).
agent_model_id = override_model_id if override_model_id else (
agent_info.get("model_id") if agent_info else None)

Check warning on line 669 in backend/agents/create_agent_info.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested conditional expression into an independent statement.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ8d1XAEyDUB3x3rQIAQ&open=AZ8d1XAEyDUB3x3rQIAQ&pullRequest=3351
Comment on lines +1011 to +1012
agent_tool_overrides = _get_agent_tool_overrides(normalized_tool_params, agent_name)

tool_keys_seen = set()
Expand Down Expand Up @@ -759,8 +769,18 @@
"rerank_model": rerank_model,
}
elif tool_config.class_name == "AnalyzeTextFileTool":
# Prefer the agent-configured LLM; fall back to the tenant default
# when the agent has no model configured.
agent_model_config = (
get_model_by_model_id(agent_model_id, tenant_id=tenant_id)
if agent_model_id else None
)
llm_model = (
build_llm_model(agent_model_config)
if agent_model_config else get_llm_model(tenant_id=tenant_id)
)
tool_config.metadata = {
"llm_model": get_llm_model(tenant_id=tenant_id),
"llm_model": llm_model,
"storage_client": minio_client,
"data_process_service_url": DATA_PROCESS_SERVICE,
"validate_url_access": lambda urls: validate_urls_access(urls, user_id)
Expand Down
27 changes: 16 additions & 11 deletions backend/services/file_management_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,24 +448,29 @@ async def list_files_impl(prefix: str, limit: Optional[int] = None):
return files


def get_llm_model(tenant_id: str):
# Get the tenant config
main_model_config = tenant_config_manager.get_model_config(
key=MODEL_CONFIG_MAPPING["llm"], tenant_id=tenant_id)
timeout_seconds = main_model_config.get(
"timeout_seconds") if main_model_config else None
def build_llm_model(model_config):
"""Build an OpenAILongContextModel from a model configuration dict."""
timeout_seconds = model_config.get(
"timeout_seconds") if model_config else None
long_text_to_text_model = OpenAILongContextModel(
observer=MessageObserver(),
model_id=get_model_name_from_config(main_model_config),
api_base=main_model_config.get("base_url"),
api_key=main_model_config.get("api_key"),
max_context_tokens=main_model_config.get("max_tokens"),
ssl_verify=main_model_config.get("ssl_verify", True),
model_id=get_model_name_from_config(model_config),
api_base=model_config.get("base_url"),
api_key=model_config.get("api_key"),
max_context_tokens=model_config.get("max_tokens"),
ssl_verify=model_config.get("ssl_verify", True),
timeout_seconds=timeout_seconds,
)
return long_text_to_text_model


def get_llm_model(tenant_id: str):
# Get the tenant config
main_model_config = tenant_config_manager.get_model_config(
key=MODEL_CONFIG_MAPPING["llm"], tenant_id=tenant_id)
return build_llm_model(main_model_config)


async def resolve_preview_file(object_name: str) -> Tuple[str, str, int]:
"""
Resolve the actual object name, content type, and total size for preview.
Expand Down
138 changes: 129 additions & 9 deletions test/backend/agents/test_create_agent_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
sys.modules['services.file_management_service'] = _create_stub_module(
"services.file_management_service",
get_llm_model=MagicMock(return_value="stub_llm_model"),
build_llm_model=MagicMock(return_value="stub_llm_model_from_config"),
validate_urls_access=MagicMock(),
Comment on lines 253 to 256
)
sys.modules['services.tool_configuration_service'] = _create_stub_module(
Expand Down Expand Up @@ -887,16 +888,19 @@
assert callable(mock_tool_instance.metadata["validate_url_access"])

@pytest.mark.asyncio
async def test_create_tool_config_list_with_analyze_text_file_tool(self):
"""Ensure AnalyzeTextFileTool receives text-specific metadata."""
async def test_create_tool_config_list_analyze_text_file_uses_agent_model(self):
"""AnalyzeTextFileTool should use the agent-configured LLM, not the tenant default."""
mock_tool_instance = MagicMock()
mock_tool_instance.class_name = "AnalyzeTextFileTool"
mock_tool_config.return_value = mock_tool_instance

with patch('backend.agents.create_agent_info.discover_langchain_tools', return_value=[]), \
patch('backend.agents.create_agent_info.search_tools_for_sub_agent') as mock_search_tools, \
patch('backend.agents.create_agent_info.search_agent_info_by_agent_id') as mock_search_agent, \
patch('backend.agents.create_agent_info.get_model_by_model_id') as mock_get_model_by_id, \
patch('backend.agents.create_agent_info.build_llm_model') as mock_build_llm_model, \
patch('backend.agents.create_agent_info.get_llm_model') as mock_get_llm_model, \
patch('backend.agents.create_agent_info.minio_client', new_callable=MagicMock) as mock_minio_client:
patch('backend.agents.create_agent_info.minio_client', new_callable=MagicMock):

mock_search_tools.return_value = [
{
Expand All @@ -910,20 +914,120 @@
"usage": None
}
]
mock_get_llm_model.return_value = "mock_llm_model"
mock_search_agent.return_value = {"name": "agent", "model_id": 42}
agent_model_config = {
"model_repo": "openai",
"model_name": "agent-llm",
"base_url": "https://agent.example.com/v1",
"api_key": "agent-key",
"max_tokens": 8192,
"ssl_verify": True,
"timeout_seconds": 30,
}
mock_get_model_by_id.return_value = agent_model_config
mock_build_llm_model.return_value = "agent_llm_model"

result = await create_tool_config_list("agent_1", "tenant_1", "user_1")

assert len(result) == 1
assert result[0] is mock_tool_instance
mock_get_llm_model.assert_called_once_with(tenant_id="tenant_1")
# Verify metadata includes validate_url_access lambda
assert "llm_model" in mock_tool_instance.metadata
# Agent model is resolved and built; tenant default is not used.
mock_get_model_by_id.assert_called_once_with(42, tenant_id="tenant_1")
mock_build_llm_model.assert_called_once_with(agent_model_config)
mock_get_llm_model.assert_not_called()
assert mock_tool_instance.metadata["llm_model"] == "agent_llm_model"
assert "storage_client" in mock_tool_instance.metadata
assert "data_process_service_url" in mock_tool_instance.metadata
assert "validate_url_access" in mock_tool_instance.metadata
assert callable(mock_tool_instance.metadata["validate_url_access"])

@pytest.mark.asyncio
async def test_create_tool_config_list_analyze_text_file_falls_back_to_tenant_model(self):
"""When the agent has no configured model, fall back to the tenant default LLM."""
mock_tool_instance = MagicMock()
mock_tool_instance.class_name = "AnalyzeTextFileTool"
mock_tool_config.return_value = mock_tool_instance

with patch('backend.agents.create_agent_info.discover_langchain_tools', return_value=[]), \
patch('backend.agents.create_agent_info.search_tools_for_sub_agent') as mock_search_tools, \
patch('backend.agents.create_agent_info.search_agent_info_by_agent_id') as mock_search_agent, \
patch('backend.agents.create_agent_info.get_model_by_model_id') as mock_get_model_by_id, \
patch('backend.agents.create_agent_info.build_llm_model') as mock_build_llm_model, \
patch('backend.agents.create_agent_info.get_llm_model') as mock_get_llm_model, \
patch('backend.agents.create_agent_info.minio_client', new_callable=MagicMock):

mock_search_tools.return_value = [
{
"class_name": "AnalyzeTextFileTool",
"name": "analyze_text_file",
"description": "Analyze text file tool",
"inputs": "string",
"output_type": "array",
"params": [{"name": "prompt", "default": "describe"}],
"source": "local",
"usage": None
}
]
mock_search_agent.return_value = {"name": "agent", "model_id": None}
mock_get_llm_model.return_value = "tenant_llm_model"

result = await create_tool_config_list("agent_1", "tenant_1", "user_1")

assert len(result) == 1
mock_get_model_by_id.assert_not_called()
mock_build_llm_model.assert_not_called()
mock_get_llm_model.assert_called_once_with(tenant_id="tenant_1")
assert mock_tool_instance.metadata["llm_model"] == "tenant_llm_model"

@pytest.mark.asyncio
async def test_create_tool_config_list_analyze_text_file_override_model(self):
"""An explicit override_model_id should win over the persisted agent model."""
mock_tool_instance = MagicMock()
mock_tool_instance.class_name = "AnalyzeTextFileTool"
mock_tool_config.return_value = mock_tool_instance

with patch('backend.agents.create_agent_info.discover_langchain_tools', return_value=[]), \
patch('backend.agents.create_agent_info.search_tools_for_sub_agent') as mock_search_tools, \
patch('backend.agents.create_agent_info.search_agent_info_by_agent_id') as mock_search_agent, \
patch('backend.agents.create_agent_info.get_model_by_model_id') as mock_get_model_by_id, \
patch('backend.agents.create_agent_info.build_llm_model') as mock_build_llm_model, \
patch('backend.agents.create_agent_info.get_llm_model') as mock_get_llm_model, \
patch('backend.agents.create_agent_info.minio_client', new_callable=MagicMock):

mock_search_tools.return_value = [
{
"class_name": "AnalyzeTextFileTool",
"name": "analyze_text_file",
"description": "Analyze text file tool",
"inputs": "string",
"output_type": "array",
"params": [{"name": "prompt", "default": "describe"}],
"source": "local",
"usage": None
}
]
mock_search_agent.return_value = {"name": "agent", "model_id": 42}
override_model_config = {
"model_repo": "openai",
"model_name": "override-llm",
"base_url": "https://override.example.com/v1",
"api_key": "override-key",
"max_tokens": 16384,
"ssl_verify": False,
"timeout_seconds": 45,
}
mock_get_model_by_id.return_value = override_model_config
mock_build_llm_model.return_value = "override_llm_model"

result = await create_tool_config_list(
"agent_1", "tenant_1", "user_1", override_model_id=99)

assert len(result) == 1
# Override id (99) takes precedence over the persisted agent model_id (42).
mock_get_model_by_id.assert_called_once_with(99, tenant_id="tenant_1")
mock_build_llm_model.assert_called_once_with(override_model_config)
mock_get_llm_model.assert_not_called()
assert mock_tool_instance.metadata["llm_model"] == "override_llm_model"

@pytest.mark.asyncio
async def test_create_tool_config_list_with_knowledge_base_tool_metadata(self):
"""
Expand Down Expand Up @@ -1667,7 +1771,23 @@
"system_prompt": "populated_system_prompt"}
mock_get_model_by_id.return_value = {"display_name": "test_model"}

result = await create_agent_config("agent_1", "tenant_1", "user_1", "zh", "test query")
result = await create_agent_config(

Check warning on line 1774 in test/backend/agents/test_create_agent_info.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused local variable "result".

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ8d1W-pyDUB3x3rQIAP&open=AZ8d1W-pyDUB3x3rQIAP&pullRequest=3351
"agent_1",
"tenant_1",
"user_1",
"zh",
"test query",
override_model_id=456,
)

mock_create_tools.assert_called_once_with(
"agent_1",
"tenant_1",
"user_1",
version_no=0,
tool_params=ANY,
override_model_id=456,
)

# Verify that AgentConfig was called correctly
mock_agent_config.assert_called_once_with(
Expand Down
38 changes: 38 additions & 0 deletions test/backend/services/test_file_management_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,44 @@ def test_get_llm_model_with_different_tenant_ids(self, mock_tenant_config, mock_
assert mock_tenant_config.get_model_config.call_args_list[1][1]["tenant_id"] == "tenant2"


class TestBuildLlmModel:
"""Test cases for build_llm_model function"""

@patch('backend.services.file_management_service.MessageObserver')
@patch('backend.services.file_management_service.OpenAILongContextModel')
@patch('backend.services.file_management_service.get_model_name_from_config')
def test_build_llm_model_from_config(self, mock_get_model_name, mock_openai_model, mock_message_observer):
"""build_llm_model builds an OpenAILongContextModel from an explicit config dict."""
from backend.services.file_management_service import build_llm_model

mock_config = {
"base_url": "http://agent.example.com",
"api_key": "agent_api_key",
"max_tokens": 8192,
"ssl_verify": False,
"timeout_seconds": 30,
}
mock_get_model_name.return_value = "agent-model"
mock_observer_instance = Mock()
mock_message_observer.return_value = mock_observer_instance
mock_model_instance = Mock()
mock_openai_model.return_value = mock_model_instance

result = build_llm_model(mock_config)

assert result == mock_model_instance
mock_get_model_name.assert_called_once_with(mock_config)
mock_openai_model.assert_called_once_with(
observer=mock_observer_instance,
model_id="agent-model",
api_base="http://agent.example.com",
api_key="agent_api_key",
max_context_tokens=8192,
ssl_verify=False,
timeout_seconds=30,
)


class TestResolvePreviewFile:
"""Test cases for resolve_preview_file function"""

Expand Down