mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
fix(community): reject booleans in the knowledge_search provider settings (#7042)
This commit is contained in:
parent
f4eaa51a73
commit
cb1d782fb3
@ -7,10 +7,11 @@ from collections.abc import Mapping
|
||||
from typing import Literal
|
||||
|
||||
from langchain_core.tools import StructuredTool
|
||||
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, SecretStr, ValidationError, field_validator
|
||||
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, SecretStr, ValidationError, ValidationInfo, field_validator
|
||||
|
||||
from deerflow.community.provider_credentials import redact_secret, reject_url_userinfo, resolve_api_key
|
||||
from deerflow.config import get_app_config
|
||||
from deerflow.config._boolean_guards import reject_boolean
|
||||
from deerflow.knowledge import format_result
|
||||
from deerflow.knowledge.providers import validate_scope_capabilities
|
||||
from deerflow.knowledge.sources import redact_strings, source_artifact
|
||||
@ -49,6 +50,24 @@ class _LightRAGRetrievalSettings(BaseModel):
|
||||
def _reject_url_userinfo(cls, value: AnyHttpUrl) -> AnyHttpUrl:
|
||||
return reject_url_userinfo(value)
|
||||
|
||||
@field_validator("top_k", "max_chars_per_chunk", "max_total_chars", mode="before")
|
||||
@classmethod
|
||||
def _reject_boolean_integers(cls, value: object, info: ValidationInfo) -> object:
|
||||
"""Reject booleans before Pydantic coerces them to integers."""
|
||||
return reject_boolean(value, info, kind="an integer")
|
||||
|
||||
@field_validator("chunk_top_k", mode="before")
|
||||
@classmethod
|
||||
def _reject_boolean_optional_integer(cls, value: object, info: ValidationInfo) -> object:
|
||||
"""Reject booleans before Pydantic coerces them to an optional integer."""
|
||||
return reject_boolean(value, info, kind="an integer or null")
|
||||
|
||||
@field_validator("timeout", mode="before")
|
||||
@classmethod
|
||||
def _reject_boolean_timeout(cls, value: object, info: ValidationInfo) -> object:
|
||||
"""Reject booleans before Pydantic coerces them to a number."""
|
||||
return reject_boolean(value, info, kind="a number")
|
||||
|
||||
|
||||
def _api_key(settings: _LightRAGRetrievalSettings) -> str | None:
|
||||
# LightRAG may run without authentication, so a missing key stays valid;
|
||||
|
||||
@ -10,10 +10,11 @@ from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from langchain_core.tools import StructuredTool
|
||||
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, SecretStr, ValidationError, field_validator
|
||||
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, SecretStr, ValidationError, ValidationInfo, field_validator
|
||||
|
||||
from deerflow.community.provider_credentials import redact_secret, reject_url_userinfo, resolve_api_key
|
||||
from deerflow.config import get_app_config
|
||||
from deerflow.config._boolean_guards import reject_boolean
|
||||
from deerflow.knowledge import format_result
|
||||
from deerflow.knowledge.sources import redact_strings
|
||||
from deerflow.knowledge_scope import (
|
||||
@ -89,6 +90,18 @@ class _RAGFlowRetrievalSettings(BaseModel):
|
||||
def _reject_url_userinfo(cls, value: AnyHttpUrl) -> AnyHttpUrl:
|
||||
return reject_url_userinfo(value)
|
||||
|
||||
@field_validator("page_size", "top_k", "max_chars_per_chunk", "max_total_chars", mode="before")
|
||||
@classmethod
|
||||
def _reject_boolean_integers(cls, value: object, info: ValidationInfo) -> object:
|
||||
"""Reject booleans before Pydantic coerces them to integers."""
|
||||
return reject_boolean(value, info, kind="an integer")
|
||||
|
||||
@field_validator("timeout", "similarity_threshold", "vector_similarity_weight", mode="before")
|
||||
@classmethod
|
||||
def _reject_boolean_numbers(cls, value: object, info: ValidationInfo) -> object:
|
||||
"""Reject booleans before Pydantic coerces them to numbers."""
|
||||
return reject_boolean(value, info, kind="a number")
|
||||
|
||||
|
||||
def _api_key(settings: _RAGFlowRetrievalSettings) -> str | None:
|
||||
return resolve_api_key(settings.api_key)
|
||||
|
||||
@ -5,6 +5,7 @@ from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
import deerflow.community.lightrag.tools as lightrag_tools
|
||||
from deerflow.community.lightrag.client import LightRAGAPIError, LightRAGConnectionError
|
||||
@ -446,3 +447,35 @@ async def test_runtime_unfiltered_scope_preserves_lightrag_search(monkeypatch, s
|
||||
assert content == "evidence"
|
||||
assert artifact is not None
|
||||
search.assert_awaited_once_with("q", with_sources=True)
|
||||
|
||||
|
||||
LIGHTRAG_BOOLEAN_FIELDS = (
|
||||
("timeout", "a number"),
|
||||
("top_k", "an integer"),
|
||||
("chunk_top_k", "an integer or null"),
|
||||
("max_chars_per_chunk", "an integer"),
|
||||
("max_total_chars", "an integer"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [True, False])
|
||||
@pytest.mark.parametrize(("field", "kind"), LIGHTRAG_BOOLEAN_FIELDS)
|
||||
def test_lightrag_settings_reject_booleans(field: str, kind: str, value: bool) -> None:
|
||||
with pytest.raises(ValidationError, match=f"{field} must be {kind}, not a boolean"):
|
||||
lightrag_tools._LightRAGRetrievalSettings.model_validate({field: value})
|
||||
|
||||
|
||||
def test_lightrag_settings_keep_numeric_values() -> None:
|
||||
settings = lightrag_tools._LightRAGRetrievalSettings.model_validate({"timeout": 45, "top_k": 10, "chunk_top_k": 5, "max_chars_per_chunk": 400, "max_total_chars": 2000})
|
||||
assert (settings.timeout, settings.top_k, settings.chunk_top_k) == (45.0, 10, 5)
|
||||
assert (settings.max_chars_per_chunk, settings.max_total_chars) == (400, 2000)
|
||||
|
||||
|
||||
def test_lightrag_boolean_setting_reports_invalid_configuration(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""A boolean in config.yaml must not silently become one second of timeout."""
|
||||
monkeypatch.setattr(lightrag_tools, "get_app_config", lambda: _config(extra={"timeout": True}))
|
||||
|
||||
settings, error = lightrag_tools._settings_or_error()
|
||||
|
||||
assert settings is None
|
||||
assert error == "Error: Invalid LightRAG settings for knowledge_search; check config.yaml."
|
||||
|
||||
@ -5,6 +5,7 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
import deerflow.community.ragflow.tools as ragflow_tools
|
||||
from deerflow.community.ragflow.client import RAGFlowAPIError, RAGFlowConnectionError
|
||||
@ -1265,3 +1266,56 @@ async def test_large_document_scope_batches_share_global_concurrency_limit() ->
|
||||
assert error is None
|
||||
assert result == {item["dataset_id"]: item["document_ids"] for item in filters}
|
||||
assert peak == 4
|
||||
|
||||
|
||||
RAGFLOW_BOOLEAN_FIELDS = (
|
||||
("timeout", "a number"),
|
||||
("page_size", "an integer"),
|
||||
("similarity_threshold", "a number"),
|
||||
("vector_similarity_weight", "a number"),
|
||||
("top_k", "an integer"),
|
||||
("max_chars_per_chunk", "an integer"),
|
||||
("max_total_chars", "an integer"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [True, False])
|
||||
@pytest.mark.parametrize(("field", "kind"), RAGFLOW_BOOLEAN_FIELDS)
|
||||
def test_ragflow_settings_reject_booleans(field: str, kind: str, value: bool) -> None:
|
||||
with pytest.raises(ValidationError, match=f"{field} must be {kind}, not a boolean"):
|
||||
ragflow_tools._RAGFlowRetrievalSettings.model_validate({field: value})
|
||||
|
||||
|
||||
def test_ragflow_settings_keep_numeric_values() -> None:
|
||||
settings = ragflow_tools._RAGFlowRetrievalSettings.model_validate(
|
||||
{
|
||||
"timeout": 45,
|
||||
"page_size": 20,
|
||||
"similarity_threshold": 0.5,
|
||||
"vector_similarity_weight": 0.6,
|
||||
"top_k": 64,
|
||||
"max_chars_per_chunk": 400,
|
||||
"max_total_chars": 2000,
|
||||
}
|
||||
)
|
||||
assert (settings.timeout, settings.page_size) == (45.0, 20)
|
||||
assert (settings.similarity_threshold, settings.vector_similarity_weight) == (0.5, 0.6)
|
||||
assert (settings.top_k, settings.max_chars_per_chunk, settings.max_total_chars) == (64, 400, 2000)
|
||||
|
||||
|
||||
def test_ragflow_boolean_setting_reports_invalid_configuration() -> None:
|
||||
"""A boolean in config.yaml must not silently become a single result at a one-second timeout."""
|
||||
entry = ToolConfig(
|
||||
name="knowledge_search",
|
||||
group="knowledge",
|
||||
use="deerflow.community.ragflow.tools:knowledge_search_tool",
|
||||
base_url="http://ragflow.test",
|
||||
api_key="ragflow-secret",
|
||||
top_k=True,
|
||||
)
|
||||
app_config = SimpleNamespace(get_tool_config=lambda name: entry if name == "knowledge_search" else None)
|
||||
|
||||
settings, error = ragflow_tools._settings_or_error(app_config)
|
||||
|
||||
assert settings is None
|
||||
assert error == "Error: Invalid RAGFlow settings for knowledge_search; check config.yaml."
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user