mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 04:14:42 +00:00
refactor: read settings directly instead of getattr with a second default
About 85 call sites read a setting as getattr(settings, "NAME", fallback), each carrying its own copy of the default. Every one of those names is a field with a default on the model, so the fallback could never apply to the real settings object; it only masked drift. Two had drifted: - OPENAI_PROMPT_CACHE_KEY defaults to True on the model but the reader fell back to False, and two test stubs relied on that. - SharePoint's MICROSOFT_AUTHORITY fallback to https://login.microsoftonline.com/<tenant> never fired, because the attribute always exists (as None), so MSAL got authority=None. The connector now derives the tenant authority when the setting is unset, as its test always assumed. Four places read EMBEDDINGS_KEY straight from os.environ, skipping the "None"/"" normalisation the model applies; they read the setting now. Test stubs that replaced a module's settings with a SimpleNamespace list every setting the code under test reads.
This commit is contained in:
1 parent
d70644743b
commit
f882ef49a7
46 files changed
+111
-113
No files matched your search
@@ -1080,7 +1080,7 @@ Azure AD tenant id, or 'common' for multi-tenant.
|
||||
|
||||
Type `str`, default unset.
|
||||
|
||||
Authority URL override, e.g. "https://login.microsoftonline.com/\{tenant_id\}".
|
||||
Authority URL override; unset derives https://login.microsoftonline.com/<MICROSOFT_TENANT_ID>.
|
||||
|
||||
### `CONFLUENCE_CLIENT_ID`
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ class BaseAgent(ABC):
|
||||
when the conversation was compressed after that turn was produced —
|
||||
the compressed local history is the context then, not the server's.
|
||||
"""
|
||||
if not getattr(settings, "OPENAI_RESPONSES_CHAIN_ACROSS_TURNS", True):
|
||||
if not settings.OPENAI_RESPONSES_CHAIN_ACROSS_TURNS:
|
||||
return None
|
||||
if not self.chat_history:
|
||||
return None
|
||||
@@ -422,7 +422,7 @@ class BaseAgent(ABC):
|
||||
# No provider-reported usage on the previous turn (older rows,
|
||||
# estimate-only providers): nothing to bound against.
|
||||
return meta["response_id"]
|
||||
budget = getattr(settings, "OPENAI_RESPONSES_CHAIN_BUDGET_TOKENS", None)
|
||||
budget = settings.OPENAI_RESPONSES_CHAIN_BUDGET_TOKENS
|
||||
if not budget:
|
||||
from docsgpt.core.model_utils import get_token_limit
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ def _dedupable_tool_names() -> frozenset:
|
||||
"""
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
return frozenset(BUILTIN_AGENT_TOOLS) | frozenset(getattr(settings, "DEFAULT_CHAT_TOOLS", None) or [])
|
||||
return frozenset(BUILTIN_AGENT_TOOLS) | frozenset(settings.DEFAULT_CHAT_TOOLS or [])
|
||||
|
||||
|
||||
def _requires_approval(tool: Dict, action: Dict) -> bool:
|
||||
|
||||
@@ -829,7 +829,7 @@ class ArtifactGeneratorTool(Tool):
|
||||
spec_path = f"{token_dir}/spec.json"
|
||||
out_path = f"{token_dir}/out.{_KIND_INFO[kind]['ext']}"
|
||||
program = _RENDERERS[kind].format(spec_path=spec_path, out_path=out_path)
|
||||
timeout = float(getattr(settings, "SANDBOX_EXEC_TIMEOUT", 60))
|
||||
timeout = float(settings.SANDBOX_EXEC_TIMEOUT)
|
||||
|
||||
manager = SandboxCreator.get_manager()
|
||||
try:
|
||||
|
||||
@@ -113,7 +113,7 @@ def bridge_attachment(
|
||||
# Reject oversize attachments BEFORE buffering them: the authoritative ``size``
|
||||
# column lets us avoid pulling a multi-hundred-MB file fully into worker memory,
|
||||
# and the bounded read below backstops a missing/lying ``size``.
|
||||
max_bytes = int(getattr(settings, "ARTIFACT_MAX_BYTES", 0) or 0)
|
||||
max_bytes = int(settings.ARTIFACT_MAX_BYTES or 0)
|
||||
declared_size = attachment.get("size")
|
||||
if max_bytes and isinstance(declared_size, (int, float)) and declared_size > max_bytes:
|
||||
raise AttachmentBridgeError(
|
||||
|
||||
@@ -87,9 +87,9 @@ class CodeExecutorTool(Tool):
|
||||
baked in. Keep the package lists in sync with deployment/sandbox/Dockerfile
|
||||
(jupyter) and scripts/build_daytona_snapshot.py (daytona snapshot).
|
||||
"""
|
||||
backend = str(getattr(settings, "SANDBOX_BACKEND", "jupyter") or "jupyter").lower()
|
||||
backend = str(settings.SANDBOX_BACKEND or "jupyter").lower()
|
||||
if backend == "daytona":
|
||||
if getattr(settings, "DAYTONA_SNAPSHOT", None):
|
||||
if settings.DAYTONA_SNAPSHOT:
|
||||
return (
|
||||
"Preinstalled beyond the stdlib: python-pptx, python-docx, openpyxl, "
|
||||
"reportlab, lxml, pillow. pip install anything else from within the code "
|
||||
@@ -330,7 +330,7 @@ class CodeExecutorTool(Tool):
|
||||
# Reject an oversize input BEFORE buffering it: the declared ``size``
|
||||
# avoids pulling a huge file into worker memory, and the bounded read
|
||||
# below backstops a missing/lying size column.
|
||||
max_bytes = int(getattr(settings, "SANDBOX_MAX_INPUT_BYTES", 0) or 0)
|
||||
max_bytes = int(settings.SANDBOX_MAX_INPUT_BYTES or 0)
|
||||
declared_size = version.get("size")
|
||||
if max_bytes and isinstance(declared_size, (int, float)) and declared_size > max_bytes:
|
||||
return {"error": f"input artifact {artifact_id} exceeds the {max_bytes}-byte sandbox input limit."}
|
||||
@@ -484,7 +484,7 @@ class CodeExecutorTool(Tool):
|
||||
@staticmethod
|
||||
def _exec_timeout() -> float:
|
||||
"""Return the fixed per-run wall-clock cap (SANDBOX_EXEC_TIMEOUT; not caller-adjustable)."""
|
||||
return float(getattr(settings, "SANDBOX_EXEC_TIMEOUT", 60))
|
||||
return float(settings.SANDBOX_EXEC_TIMEOUT)
|
||||
|
||||
@staticmethod
|
||||
def _is_timeout(result: ExecResult) -> bool:
|
||||
|
||||
@@ -108,11 +108,11 @@ class MCPTool(Tool):
|
||||
if configured_redirect_uri:
|
||||
return configured_redirect_uri.rstrip("/")
|
||||
|
||||
explicit = getattr(settings, "MCP_OAUTH_REDIRECT_URI", None)
|
||||
explicit = settings.MCP_OAUTH_REDIRECT_URI
|
||||
if explicit:
|
||||
return explicit.rstrip("/")
|
||||
|
||||
connector_base = getattr(settings, "CONNECTOR_REDIRECT_BASE_URI", None)
|
||||
connector_base = settings.CONNECTOR_REDIRECT_BASE_URI
|
||||
if connector_base:
|
||||
parsed = urlparse(connector_base)
|
||||
if parsed.scheme and parsed.netloc:
|
||||
|
||||
@@ -255,7 +255,7 @@ class ReadDocumentTool(Tool):
|
||||
# The task's per-call time limits are raised to match the awaited window: bound to
|
||||
# the base timeout at import, the worker would otherwise self-terminate a large
|
||||
# parse long before this await gives up.
|
||||
queue = getattr(settings, "DOCUMENT_PARSE_QUEUE", "parsing")
|
||||
queue = settings.DOCUMENT_PARSE_QUEUE
|
||||
try:
|
||||
async_result = parse_document.apply_async(
|
||||
args=[artifact_id, parent, self.user_id, options],
|
||||
|
||||
@@ -331,7 +331,7 @@ class WorkflowAgent(BaseAgent):
|
||||
from docsgpt.storage.storage_creator import StorageCreator
|
||||
|
||||
storage = StorageCreator.get_storage()
|
||||
max_bytes = int(getattr(settings, "ARTIFACT_MAX_BYTES", 0) or 0)
|
||||
max_bytes = int(settings.ARTIFACT_MAX_BYTES or 0)
|
||||
dropped: List[str] = []
|
||||
if len(self.attachments) > _MAX_INPUT_DOCUMENTS:
|
||||
over = len(self.attachments) - _MAX_INPUT_DOCUMENTS
|
||||
|
||||
@@ -639,7 +639,7 @@ class WorkflowEngine:
|
||||
raw_ids = self._resolve_input_artifact_ids(inputs)
|
||||
if not raw_ids:
|
||||
return loaded
|
||||
max_bytes = int(getattr(settings, "SANDBOX_MAX_INPUT_BYTES", 0) or 0)
|
||||
max_bytes = int(settings.SANDBOX_MAX_INPUT_BYTES or 0)
|
||||
storage = StorageCreator.get_storage()
|
||||
# Two inputs whose current versions share a filename would clobber each other at the
|
||||
# same ``inputs/{name}`` path; track used paths and disambiguate deterministically.
|
||||
@@ -749,15 +749,15 @@ class WorkflowEngine:
|
||||
|
||||
supported = set(supported_types)
|
||||
supports_images = any(t.startswith("image/") for t in supported)
|
||||
max_files = int(getattr(settings, "WORKFLOW_NODE_NATIVE_MAX_FILES", 5))
|
||||
extract_max = int(getattr(settings, "WORKFLOW_NODE_EXTRACT_MAX_FILES", 5))
|
||||
max_files = int(settings.WORKFLOW_NODE_NATIVE_MAX_FILES)
|
||||
extract_max = int(settings.WORKFLOW_NODE_EXTRACT_MAX_FILES)
|
||||
# One wall clock for every blocking parse this node issues. The cap
|
||||
# above bounds how MANY parses run; this bounds how LONG they take in
|
||||
# total, so N documents cannot serialize N size-scaled windows.
|
||||
parse_deadline = time.monotonic() + float(
|
||||
getattr(settings, "WORKFLOW_NODE_EXTRACT_BUDGET_SECONDS", 900)
|
||||
settings.WORKFLOW_NODE_EXTRACT_BUDGET_SECONDS
|
||||
)
|
||||
max_bytes = int(getattr(settings, "SANDBOX_MAX_INPUT_BYTES", 25 * 1024 * 1024))
|
||||
max_bytes = int(settings.SANDBOX_MAX_INPUT_BYTES)
|
||||
|
||||
# One read-only connection for the whole batch; the resolved-version
|
||||
# rows are collected, then storage reads happen outside the DB context.
|
||||
@@ -976,7 +976,7 @@ class WorkflowEngine:
|
||||
if not user_id:
|
||||
return None
|
||||
options = {"output": "markdown", "include_tables": False, "persist": False}
|
||||
queue = getattr(settings, "DOCUMENT_PARSE_QUEUE", "parsing")
|
||||
queue = settings.DOCUMENT_PARSE_QUEUE
|
||||
# OCR cost scales with pages, so the window grows with the document's size
|
||||
# (floored at DOCUMENT_PARSE_TIMEOUT); the task's per-call time limits are
|
||||
# raised to match, else the worker would self-terminate mid-parse.
|
||||
@@ -1084,7 +1084,7 @@ class WorkflowEngine:
|
||||
"""Return the stricter of the node's requested timeout and the sandbox cap."""
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
cap = float(getattr(settings, "SANDBOX_EXEC_TIMEOUT", 60))
|
||||
cap = float(settings.SANDBOX_EXEC_TIMEOUT)
|
||||
if requested is None:
|
||||
return cap
|
||||
try:
|
||||
|
||||
@@ -367,7 +367,7 @@ class CompressionService:
|
||||
never mutated.
|
||||
"""
|
||||
max_tokens = int(
|
||||
getattr(settings, "COMPRESSION_RECENT_FIELD_MAX_TOKENS", 8000) or 0
|
||||
settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS or 0
|
||||
)
|
||||
if max_tokens <= 0:
|
||||
return queries
|
||||
|
||||
@@ -33,7 +33,6 @@ from docsgpt.streaming.async_event_replay import (
|
||||
)
|
||||
from docsgpt.streaming.async_redis import get_async_redis_instance
|
||||
from docsgpt.streaming.event_replay import (
|
||||
DEFAULT_KEEPALIVE_SECONDS,
|
||||
DEFAULT_POLL_TIMEOUT_SECONDS,
|
||||
)
|
||||
from docsgpt.streaming.sse_leases import StreamCapExceeded, acquire_stream_lease
|
||||
@@ -127,7 +126,7 @@ async def stream_message_events(request: Request) -> Response:
|
||||
)
|
||||
last_event_id = _normalise_last_event_id(raw_cursor)
|
||||
keepalive_seconds = float(
|
||||
getattr(settings, "SSE_KEEPALIVE_SECONDS", DEFAULT_KEEPALIVE_SECONDS)
|
||||
settings.SSE_KEEPALIVE_SECONDS
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -154,7 +154,7 @@ async def download_artifact(request: Request) -> Response:
|
||||
# URL. If the active backend can't mint one, that's a config error:
|
||||
# surface a 500 rather than silently proxying bytes from a backend
|
||||
# the operator expected to be off the hot path.
|
||||
if getattr(settings, "URL_STRATEGY", "backend") == "s3":
|
||||
if settings.URL_STRATEGY == "s3":
|
||||
try:
|
||||
url = await anyio.to_thread.run_sync(
|
||||
partial(storage.generate_presigned_url, storage_path, expires_in=_PRESIGNED_URL_TTL)
|
||||
|
||||
@@ -235,7 +235,7 @@ def get_vector_store(source_id):
|
||||
store = VectorCreator.create_vectorstore(
|
||||
settings.VECTOR_STORE,
|
||||
source_id=source_id,
|
||||
embeddings_key=os.getenv("EMBEDDINGS_KEY"),
|
||||
embeddings_key=settings.EMBEDDINGS_KEY,
|
||||
)
|
||||
return store
|
||||
|
||||
|
||||
@@ -346,9 +346,9 @@ def parse_timeout_for_size(size_bytes: Optional[int]) -> float:
|
||||
"""
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
base = float(getattr(settings, "DOCUMENT_PARSE_TIMEOUT", 120) or 120)
|
||||
per_mib = float(getattr(settings, "DOCUMENT_PARSE_TIMEOUT_PER_MB", 0) or 0)
|
||||
ceiling = float(getattr(settings, "DOCUMENT_PARSE_TIMEOUT_MAX", base) or base)
|
||||
base = float(settings.DOCUMENT_PARSE_TIMEOUT or 120)
|
||||
per_mib = float(settings.DOCUMENT_PARSE_TIMEOUT_PER_MB or 0)
|
||||
ceiling = float(settings.DOCUMENT_PARSE_TIMEOUT_MAX or base)
|
||||
size = float(size_bytes) if isinstance(size_bytes, (int, float)) else 0.0
|
||||
scaled = base + per_mib * max(size, 0.0) / (1024 * 1024)
|
||||
return min(ceiling, max(base, scaled))
|
||||
|
||||
@@ -140,7 +140,7 @@ class ModelRegistry:
|
||||
from docsgpt.llm.providers import ALL_PROVIDERS
|
||||
|
||||
directories = [BUILTIN_MODELS_DIR]
|
||||
operator_dir = getattr(settings, "MODELS_CONFIG_DIR", None)
|
||||
operator_dir = settings.MODELS_CONFIG_DIR
|
||||
if operator_dir:
|
||||
op_path = Path(operator_dir)
|
||||
if not op_path.exists():
|
||||
|
||||
@@ -35,7 +35,8 @@ class ConnectorSettings(SettingsGroup):
|
||||
default="common", description="Azure AD tenant id, or 'common' for multi-tenant."
|
||||
)
|
||||
MICROSOFT_AUTHORITY: Optional[str] = Field(
|
||||
default=None, description='Authority URL override, e.g. "https://login.microsoftonline.com/{tenant_id}".'
|
||||
default=None,
|
||||
description="Authority URL override; unset derives https://login.microsoftonline.com/<MICROSOFT_TENANT_ID>.",
|
||||
)
|
||||
|
||||
# Confluence Cloud integration.
|
||||
|
||||
@@ -631,15 +631,15 @@ class DeviceBroker:
|
||||
|
||||
@staticmethod
|
||||
def _inv_ttl() -> int:
|
||||
return int(getattr(settings, "REMOTE_DEVICE_INVOCATION_TTL_SECONDS", 900))
|
||||
return int(settings.REMOTE_DEVICE_INVOCATION_TTL_SECONDS)
|
||||
|
||||
@staticmethod
|
||||
def _cmd_ttl() -> int:
|
||||
return int(getattr(settings, "REMOTE_DEVICE_CMD_QUEUE_TTL_SECONDS", 900))
|
||||
return int(settings.REMOTE_DEVICE_CMD_QUEUE_TTL_SECONDS)
|
||||
|
||||
@staticmethod
|
||||
def _out_maxlen() -> int:
|
||||
return int(getattr(settings, "REMOTE_DEVICE_OUTPUT_STREAM_MAXLEN", 10_000))
|
||||
return int(settings.REMOTE_DEVICE_OUTPUT_STREAM_MAXLEN)
|
||||
|
||||
|
||||
def _to_int(value: Optional[str]) -> Optional[int]:
|
||||
|
||||
@@ -77,11 +77,9 @@ class GraphStore:
|
||||
"""Stores and queries a per-source knowledge graph in the pgvector DB."""
|
||||
|
||||
def __init__(self, connection_string: Optional[str] = None):
|
||||
self._connection_string = connection_string or getattr(
|
||||
settings, "PGVECTOR_CONNECTION_STRING", None
|
||||
)
|
||||
self._connection_string = connection_string or settings.PGVECTOR_CONNECTION_STRING
|
||||
|
||||
if not self._connection_string and getattr(settings, "POSTGRES_URI", None):
|
||||
if not self._connection_string and settings.POSTGRES_URI:
|
||||
from docsgpt.core.db_uri import normalize_pgvector_connection_string
|
||||
|
||||
self._connection_string = normalize_pgvector_connection_string(
|
||||
|
||||
@@ -52,7 +52,7 @@ class GuardrailCreator:
|
||||
does not require an operator to also edit their env.
|
||||
"""
|
||||
cls._ensure_builtin()
|
||||
allowlist = getattr(settings, "GUARDRAILS_CHECKS_ENABLED", None) or []
|
||||
allowlist = settings.GUARDRAILS_CHECKS_ENABLED or []
|
||||
if not allowlist:
|
||||
return sorted(cls.checks)
|
||||
return sorted(k for k in cls.checks if k in set(allowlist))
|
||||
|
||||
@@ -38,7 +38,7 @@ def _merge_mode(agent_mode: str, floor_mode: str) -> str:
|
||||
|
||||
def instance_floor() -> Optional[GuardrailsConfig]:
|
||||
"""The operator-set minimum, or None when unset/invalid."""
|
||||
raw = getattr(settings, "GUARDRAILS_FLOOR", None)
|
||||
raw = settings.GUARDRAILS_FLOOR
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
@@ -104,7 +104,7 @@ def floor_keys() -> set:
|
||||
|
||||
def resolve_config(raw_agent_config: Optional[dict]) -> GuardrailsConfig:
|
||||
"""Parse ``agents.config`` and apply the instance floor."""
|
||||
if not getattr(settings, "GUARDRAILS_ENABLED", True):
|
||||
if not settings.GUARDRAILS_ENABLED:
|
||||
return GuardrailsConfig()
|
||||
agent = AgentConfig.parse(raw_agent_config).guardrails
|
||||
return merge_floor(agent, instance_floor())
|
||||
@@ -123,7 +123,7 @@ def _judge_factory(agent):
|
||||
decoded_token=agent.decoded_token,
|
||||
model_id=(
|
||||
model_override
|
||||
or getattr(settings, "GUARDRAILS_JUDGE_MODEL", None)
|
||||
or settings.GUARDRAILS_JUDGE_MODEL
|
||||
or agent.upstream_model_id
|
||||
),
|
||||
agent_id=agent.agent_id,
|
||||
@@ -169,7 +169,7 @@ class GuardrailRecorder:
|
||||
self._seen: set = set()
|
||||
|
||||
def __call__(self, decision: StageDecision) -> None:
|
||||
store_text = bool(getattr(settings, "GUARDRAILS_STORE_SCANNED_TEXT", False))
|
||||
store_text = bool(settings.GUARDRAILS_STORE_SCANNED_TEXT)
|
||||
for verdict in decision.verdicts:
|
||||
if not verdict.outcome.triggered and verdict.outcome.evaluated:
|
||||
continue
|
||||
|
||||
@@ -35,7 +35,7 @@ def _bound_tool_response_for_llm(tool_response: Any) -> Any:
|
||||
from docsgpt.core.settings import settings
|
||||
from docsgpt.utils import num_tokens_from_string
|
||||
|
||||
max_tokens = int(getattr(settings, "TOOL_RESULT_MAX_TOKENS", 20000) or 0)
|
||||
max_tokens = int(settings.TOOL_RESULT_MAX_TOKENS or 0)
|
||||
if max_tokens <= 0:
|
||||
return tool_response
|
||||
text = tool_response if isinstance(tool_response, str) else str(tool_response)
|
||||
|
||||
@@ -1307,14 +1307,14 @@ class OpenAILLM(BaseLLM):
|
||||
params["include"] = ["reasoning.encrypted_content"]
|
||||
# Backstop against a chain that outgrows the model's native window:
|
||||
# the provider drops the oldest input items instead of failing.
|
||||
if getattr(settings, "OPENAI_RESPONSES_TRUNCATION_AUTO", False):
|
||||
if settings.OPENAI_RESPONSES_TRUNCATION_AUTO:
|
||||
params["truncation"] = "auto"
|
||||
# Prompt-cache hints. The key pins a conversation to one cache shard;
|
||||
# retention asks for the extended tier where the deployment offers it.
|
||||
cache_key = getattr(self, "_prompt_cache_key", None)
|
||||
if cache_key and getattr(settings, "OPENAI_PROMPT_CACHE_KEY", False):
|
||||
if cache_key and settings.OPENAI_PROMPT_CACHE_KEY:
|
||||
params["prompt_cache_key"] = str(cache_key)
|
||||
retention = getattr(settings, "OPENAI_PROMPT_CACHE_RETENTION", None)
|
||||
retention = settings.OPENAI_PROMPT_CACHE_RETENTION
|
||||
if retention:
|
||||
params["prompt_cache_retention"] = retention
|
||||
return params
|
||||
|
||||
@@ -41,7 +41,7 @@ class SharePointAuth(BaseConnectorAuth):
|
||||
|
||||
self.redirect_uri = settings.CONNECTOR_REDIRECT_BASE_URI
|
||||
self.tenant_id = settings.MICROSOFT_TENANT_ID
|
||||
self.authority = getattr(settings, "MICROSOFT_AUTHORITY", f"https://login.microsoftonline.com/{self.tenant_id}")
|
||||
self.authority = settings.MICROSOFT_AUTHORITY or f"https://login.microsoftonline.com/{self.tenant_id}"
|
||||
|
||||
self.auth_app = ConfidentialClientApplication(
|
||||
client_id=self.client_id,
|
||||
|
||||
@@ -97,10 +97,10 @@ def bound_parse_payload(payload: Dict[str, Any], max_chars: Optional[int] = None
|
||||
|
||||
def _max_input_bytes() -> int:
|
||||
"""Return the size cap for a parsed document (its own setting, else the sandbox cap)."""
|
||||
explicit = int(getattr(settings, "DOCUMENT_PARSE_MAX_BYTES", 0) or 0)
|
||||
explicit = int(settings.DOCUMENT_PARSE_MAX_BYTES or 0)
|
||||
if explicit > 0:
|
||||
return explicit
|
||||
return int(getattr(settings, "SANDBOX_MAX_INPUT_BYTES", 25 * 1024 * 1024))
|
||||
return int(settings.SANDBOX_MAX_INPUT_BYTES)
|
||||
|
||||
|
||||
# Every zip-packaged format a parser map can route: OOXML and its macro/
|
||||
@@ -127,8 +127,8 @@ def _zip_bomb_reason(source: Union[bytes, str, Path], suffix: str) -> Optional[s
|
||||
"""
|
||||
if suffix not in _ZIP_CONTAINER_EXTENSIONS:
|
||||
return None
|
||||
max_entries = int(getattr(settings, "DOCUMENT_MAX_ARCHIVE_ENTRIES", 10000))
|
||||
cap = int(getattr(settings, "DOCUMENT_MAX_DECOMPRESSED_BYTES", 300 * 1024 * 1024))
|
||||
max_entries = int(settings.DOCUMENT_MAX_ARCHIVE_ENTRIES)
|
||||
cap = int(settings.DOCUMENT_MAX_DECOMPRESSED_BYTES)
|
||||
opened = io.BytesIO(source) if isinstance(source, bytes) else source
|
||||
try:
|
||||
with zipfile.ZipFile(opened) as zf:
|
||||
@@ -169,14 +169,14 @@ def _resolve_ocr_enabled(ocr: str) -> bool:
|
||||
return True
|
||||
if ocr == "off":
|
||||
return False
|
||||
return bool(getattr(settings, "OCR_ENABLED", False))
|
||||
return bool(settings.OCR_ENABLED)
|
||||
|
||||
|
||||
def _effective_engine(engine: str) -> str:
|
||||
"""Resolve ``auto`` to the server's ``DOC_PARSER_ENGINE``; other values pass through."""
|
||||
if engine != "auto":
|
||||
return engine
|
||||
configured = getattr(settings, "DOC_PARSER_ENGINE", None) or "anydoc"
|
||||
configured = settings.DOC_PARSER_ENGINE or "anydoc"
|
||||
return str(configured).strip().lower()
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ def _resolve_batch_size() -> int:
|
||||
Returns:
|
||||
Chunks per embed request, always >= 1.
|
||||
"""
|
||||
raw = getattr(settings, "EMBEDDINGS_BATCH_SIZE", None)
|
||||
raw = settings.EMBEDDINGS_BATCH_SIZE
|
||||
# Explicit type check rather than a bare ``int(raw)``: ``int(MagicMock())``
|
||||
# succeeds and yields 1, which would silently drop ingest back to the
|
||||
# per-chunk behaviour this batching replaces.
|
||||
@@ -326,7 +326,7 @@ def embed_and_store_documents(
|
||||
store = VectorCreator.create_vectorstore(
|
||||
settings.VECTOR_STORE,
|
||||
source_id=source_id,
|
||||
embeddings_key=os.getenv("EMBEDDINGS_KEY"),
|
||||
embeddings_key=settings.EMBEDDINGS_KEY,
|
||||
)
|
||||
loop_start = resume_index
|
||||
else:
|
||||
@@ -336,7 +336,7 @@ def embed_and_store_documents(
|
||||
settings.VECTOR_STORE,
|
||||
docs_init=[docs[0]],
|
||||
source_id=source_id,
|
||||
embeddings_key=os.getenv("EMBEDDINGS_KEY"),
|
||||
embeddings_key=settings.EMBEDDINGS_KEY,
|
||||
)
|
||||
# Record the seeded chunk so single-doc ingests don't fail
|
||||
# ``assert_index_complete`` — the loop never runs for
|
||||
@@ -351,7 +351,7 @@ def embed_and_store_documents(
|
||||
store = VectorCreator.create_vectorstore(
|
||||
settings.VECTOR_STORE,
|
||||
source_id=source_id,
|
||||
embeddings_key=os.getenv("EMBEDDINGS_KEY"),
|
||||
embeddings_key=settings.EMBEDDINGS_KEY,
|
||||
)
|
||||
# Only wipe the index on a fresh run — a resume must keep the
|
||||
# chunks that earlier attempts already embedded.
|
||||
|
||||
@@ -344,7 +344,7 @@ def get_default_file_extractor(
|
||||
"""
|
||||
if ocr_enabled is None:
|
||||
ocr_enabled = settings.OCR_ENABLED
|
||||
selected = (engine or getattr(settings, "DOC_PARSER_ENGINE", None) or "anydoc")
|
||||
selected = (engine or settings.DOC_PARSER_ENGINE or "anydoc")
|
||||
selected = str(selected).strip().lower()
|
||||
if selected == "docling":
|
||||
return _docling_file_extractor(ocr_enabled, pdf_text_fast_path)
|
||||
|
||||
@@ -335,11 +335,7 @@ def _ocr_min_chars_per_page() -> int:
|
||||
|
||||
try:
|
||||
return int(
|
||||
getattr(
|
||||
settings,
|
||||
"OCR_MIN_CHARS_PER_PAGE",
|
||||
_DEFAULT_OCR_MIN_CHARS_PER_PAGE,
|
||||
)
|
||||
settings.OCR_MIN_CHARS_PER_PAGE
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return _DEFAULT_OCR_MIN_CHARS_PER_PAGE
|
||||
|
||||
@@ -124,7 +124,7 @@ def resolve_ocr_backend(requested: Optional[str] = None) -> str:
|
||||
"""
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
backend = str(requested or getattr(settings, "OCR_BACKEND", None) or "auto").strip().lower()
|
||||
backend = str(requested or settings.OCR_BACKEND or "auto").strip().lower()
|
||||
if backend not in VALID_OCR_BACKENDS:
|
||||
logger.warning(f"Unknown OCR_BACKEND {backend!r}; using auto")
|
||||
backend = "auto"
|
||||
@@ -154,7 +154,7 @@ def resolve_native_ocr_engine(requested: Optional[str] = None) -> str:
|
||||
"""
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
engine = str(requested or getattr(settings, "OCR_ENGINE", None) or "tesseract").strip().lower()
|
||||
engine = str(requested or settings.OCR_ENGINE or "tesseract").strip().lower()
|
||||
if engine in NATIVE_OCR_ENGINES:
|
||||
return engine
|
||||
if engine in VALID_OCR_ENGINES:
|
||||
@@ -172,7 +172,7 @@ def ocr_min_chars_per_page() -> int:
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
try:
|
||||
return int(getattr(settings, "OCR_MIN_CHARS_PER_PAGE", _DEFAULT_MIN_CHARS_PER_PAGE))
|
||||
return int(settings.OCR_MIN_CHARS_PER_PAGE)
|
||||
except (TypeError, ValueError):
|
||||
return _DEFAULT_MIN_CHARS_PER_PAGE
|
||||
|
||||
@@ -182,7 +182,7 @@ def render_dpi() -> int:
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
try:
|
||||
dpi = int(getattr(settings, "OCR_RENDER_DPI", _DEFAULT_RENDER_DPI))
|
||||
dpi = int(settings.OCR_RENDER_DPI)
|
||||
except (TypeError, ValueError):
|
||||
dpi = _DEFAULT_RENDER_DPI
|
||||
return max(_MIN_RENDER_DPI, min(_MAX_RENDER_DPI, dpi))
|
||||
@@ -299,7 +299,7 @@ class TesseractEngine:
|
||||
else:
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
configured = str(getattr(settings, "OCR_LANGS", "") or "eng")
|
||||
configured = str(settings.OCR_LANGS or "eng")
|
||||
langs = [lang.strip() for lang in configured.split("+") if lang.strip()]
|
||||
return "+".join(langs) or "eng"
|
||||
|
||||
@@ -388,7 +388,7 @@ class DeepseekOcrEngine:
|
||||
|
||||
self.url = url or settings.OCR_DEEPSEEK_URL
|
||||
self.model = model or settings.OCR_DEEPSEEK_MODEL
|
||||
self.timeout = float(timeout if timeout is not None else getattr(settings, "OCR_DEEPSEEK_TIMEOUT", 300))
|
||||
self.timeout = float(timeout if timeout is not None else settings.OCR_DEEPSEEK_TIMEOUT)
|
||||
self.prompt = prompt
|
||||
self.max_tokens = max_tokens
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class GitHubLoader(BaseRemote):
|
||||
|
||||
def _max_file_bytes(self) -> int:
|
||||
"""Resolve the per-blob size cap; ``0`` disables it."""
|
||||
raw = getattr(settings, "GITHUB_INGEST_MAX_FILE_BYTES", None)
|
||||
raw = settings.GITHUB_INGEST_MAX_FILE_BYTES
|
||||
if isinstance(raw, bool) or not isinstance(raw, (int, str)):
|
||||
return 1048576
|
||||
try:
|
||||
@@ -139,7 +139,7 @@ class GitHubLoader(BaseRemote):
|
||||
|
||||
def _max_workers(self) -> int:
|
||||
"""Resolve the parallel-fetch width, clamped to a sane range."""
|
||||
raw = getattr(settings, "GITHUB_INGEST_MAX_WORKERS", None)
|
||||
raw = settings.GITHUB_INGEST_MAX_WORKERS
|
||||
if isinstance(raw, bool) or not isinstance(raw, (int, str)):
|
||||
return 8
|
||||
try:
|
||||
|
||||
@@ -267,7 +267,7 @@ def get_token_counter(embeddings_name: Optional[str] = None) -> TokenCounter:
|
||||
A :class:`HuggingFaceCounter` for a model whose tokenizer could be
|
||||
loaded, else a :class:`TiktokenCounter`.
|
||||
"""
|
||||
name = embeddings_name or getattr(settings, "EMBEDDINGS_NAME", None)
|
||||
name = embeddings_name or settings.EMBEDDINGS_NAME
|
||||
key = name or "__default__"
|
||||
with _cache_lock:
|
||||
if key in _cache:
|
||||
|
||||
@@ -344,6 +344,6 @@ def build_dispatcher(create_classic: Callable[[], BaseRetriever], **kwargs):
|
||||
Returns:
|
||||
A ``Dispatcher`` or the legacy retriever from ``create_classic``.
|
||||
"""
|
||||
if not getattr(settings, "PER_SOURCE_RETRIEVAL_ENABLED", True):
|
||||
if not settings.PER_SOURCE_RETRIEVAL_ENABLED:
|
||||
return create_classic()
|
||||
return Dispatcher(**kwargs)
|
||||
@@ -277,7 +277,7 @@ def _cleanup_orphan(storage: Any, saved_key: Optional[str]) -> None:
|
||||
|
||||
def _check_single_artifact_size(size: int) -> None:
|
||||
"""Reject a single artifact version whose byte size exceeds ``ARTIFACT_MAX_BYTES``."""
|
||||
max_bytes = int(getattr(settings, "ARTIFACT_MAX_BYTES", 0) or 0)
|
||||
max_bytes = int(settings.ARTIFACT_MAX_BYTES or 0)
|
||||
if max_bytes > 0 and size > max_bytes:
|
||||
raise QuotaExceeded(f"artifact is too large: {size} bytes exceeds the {max_bytes}-byte per-file cap")
|
||||
|
||||
@@ -292,10 +292,10 @@ def _enforce_user_quota(repo: ArtifactsRepository, user_id: str, added_bytes: in
|
||||
existing identity.
|
||||
"""
|
||||
_check_single_artifact_size(added_bytes)
|
||||
max_count = int(getattr(settings, "ARTIFACT_MAX_COUNT_PER_USER", 0) or 0)
|
||||
max_count = int(settings.ARTIFACT_MAX_COUNT_PER_USER or 0)
|
||||
if new_artifact and max_count > 0 and repo.count_for_user(user_id) >= max_count:
|
||||
raise QuotaExceeded(f"artifact count quota reached ({max_count}); delete artifacts to free space")
|
||||
max_total = int(getattr(settings, "ARTIFACT_MAX_TOTAL_BYTES_PER_USER", 0) or 0)
|
||||
max_total = int(settings.ARTIFACT_MAX_TOTAL_BYTES_PER_USER or 0)
|
||||
if max_total > 0 and repo.total_bytes_for_user(user_id) + added_bytes > max_total:
|
||||
raise QuotaExceeded(f"artifact storage quota reached ({max_total} bytes); delete artifacts to free space")
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class SandboxCreator:
|
||||
def get_manager(cls) -> SandboxManager:
|
||||
"""Return the process-wide ``SandboxManager``, building it on first use."""
|
||||
if cls._instance is None:
|
||||
backend = cls.create_backend(getattr(settings, "SANDBOX_BACKEND", "jupyter"))
|
||||
backend = cls.create_backend(settings.SANDBOX_BACKEND)
|
||||
cls._instance = SandboxManager(
|
||||
backend=backend,
|
||||
max_ttl=float(settings.SANDBOX_MAX_TTL),
|
||||
|
||||
@@ -309,7 +309,7 @@ def reembed_pgvector(source_id: str, batch_size: int, dry_run: bool) -> Tuple[in
|
||||
# The graph seeds every traversal from its own vectors, so leaving them
|
||||
# in the old model's space is the same silent mismatch this script
|
||||
# exists to remove -- and at equal widths nothing would report it.
|
||||
if getattr(settings, "GRAPHRAG_ENABLED", False):
|
||||
if settings.GRAPHRAG_ENABLED:
|
||||
nodes = reembed_graph_nodes(store, conn, source_id, batch_size, dry_run)
|
||||
if nodes:
|
||||
logger.info(
|
||||
@@ -512,7 +512,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
# latency and a dependency on a worker running. Loading the model here also
|
||||
# means the script reports a real failure for a model it cannot load,
|
||||
# instead of timing out against an empty queue.
|
||||
if getattr(settings, "EMBEDDINGS_DELEGATE_TO_WORKER", False):
|
||||
if settings.EMBEDDINGS_DELEGATE_TO_WORKER:
|
||||
logger.info("Embedding in-process; worker delegation does not apply here.")
|
||||
settings.EMBEDDINGS_DELEGATE_TO_WORKER = False
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ def _release_boot_only_embeddings(log: logging.Logger) -> None:
|
||||
|
||||
if settings.EMBEDDINGS_BASE_URL:
|
||||
return
|
||||
if getattr(settings, "EMBEDDINGS_DELEGATE_TO_WORKER", False) is not True:
|
||||
if settings.EMBEDDINGS_DELEGATE_TO_WORKER is not True:
|
||||
return
|
||||
|
||||
import gc
|
||||
@@ -144,8 +144,8 @@ def ensure_vector_schema(*, logger: Optional[logging.Logger] = None) -> None:
|
||||
)
|
||||
return
|
||||
|
||||
dsn = getattr(settings, "PGVECTOR_CONNECTION_STRING", None)
|
||||
if not dsn and getattr(settings, "POSTGRES_URI", None):
|
||||
dsn = settings.PGVECTOR_CONNECTION_STRING
|
||||
if not dsn and settings.POSTGRES_URI:
|
||||
from docsgpt.core.db_uri import normalize_pgvector_connection_string
|
||||
|
||||
dsn = normalize_pgvector_connection_string(settings.POSTGRES_URI)
|
||||
@@ -172,7 +172,7 @@ def ensure_vector_schema(*, logger: Optional[logging.Logger] = None) -> None:
|
||||
|
||||
dim: Optional[int] = dimension_for(settings.EMBEDDINGS_NAME)
|
||||
|
||||
graph_enabled = bool(getattr(settings, "GRAPHRAG_ENABLED", False))
|
||||
graph_enabled = bool(settings.GRAPHRAG_ENABLED)
|
||||
started = time.monotonic()
|
||||
# A plain connection, never the store's pool: this can run pre-fork under
|
||||
# ``gunicorn --preload``, and an inherited pooled socket is a broken one.
|
||||
|
||||
@@ -18,7 +18,7 @@ class StorageCreator:
|
||||
@classmethod
|
||||
def get_storage(cls) -> BaseStorage:
|
||||
if cls._instance is None:
|
||||
storage_type = getattr(settings, "STORAGE_TYPE", "local")
|
||||
storage_type = settings.STORAGE_TYPE
|
||||
cls._instance = cls.create_storage(storage_type)
|
||||
|
||||
return cls._instance
|
||||
|
||||
+2
-2
@@ -347,7 +347,7 @@ def generate_agent_image_capability(
|
||||
agent_id: object, image_path: object, user_id: object
|
||||
) -> str:
|
||||
"""Create an HMAC capability for one agent's current internal image."""
|
||||
secret = getattr(settings, "JWT_SECRET_KEY", "")
|
||||
secret = settings.JWT_SECRET_KEY
|
||||
if not isinstance(secret, str) or not secret:
|
||||
return ""
|
||||
try:
|
||||
@@ -389,7 +389,7 @@ def generate_image_url(image_path, agent_id=None, user_id=None):
|
||||
if not capability:
|
||||
return ""
|
||||
canonical_agent_id = str(uuid.UUID(str(agent_id)))
|
||||
base_url = getattr(settings, "API_URL", "http://localhost:7091").rstrip("/")
|
||||
base_url = settings.API_URL.rstrip("/")
|
||||
return f"{base_url}/api/images/{canonical_agent_id}/{capability}"
|
||||
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ def _delegation_enabled() -> bool:
|
||||
with a ``MagicMock``, whose every attribute is a truthy object, and
|
||||
``bool()`` on that would silently route them through the broker.
|
||||
"""
|
||||
return getattr(settings, "EMBEDDINGS_DELEGATE_TO_WORKER", False) is True
|
||||
return settings.EMBEDDINGS_DELEGATE_TO_WORKER is True
|
||||
|
||||
|
||||
def get_embeddings(
|
||||
|
||||
@@ -173,8 +173,8 @@ class DelegatedEmbeddings:
|
||||
the full timeout at once -- at the shipped 60s and 96 WSGI threads, an
|
||||
API that serves nothing at all, health checks included.
|
||||
"""
|
||||
queue = getattr(settings, "EMBEDDINGS_QUEUE", "embeddings")
|
||||
timeout = getattr(settings, "EMBEDDINGS_DELEGATE_TIMEOUT", 60)
|
||||
queue = settings.EMBEDDINGS_QUEUE
|
||||
timeout = settings.EMBEDDINGS_DELEGATE_TIMEOUT
|
||||
|
||||
remaining = self._cooldown_remaining()
|
||||
if remaining > 0:
|
||||
|
||||
@@ -188,8 +188,8 @@ def _describe_from_repo(repo: str) -> Optional[EmbeddingModel]:
|
||||
|
||||
def _apply_overrides(spec: EmbeddingModel) -> EmbeddingModel:
|
||||
"""Let ``EMBEDDINGS_POOLING``/``EMBEDDINGS_NORMALIZE`` win over any source."""
|
||||
pooling = getattr(settings, "EMBEDDINGS_POOLING", None)
|
||||
normalize = getattr(settings, "EMBEDDINGS_NORMALIZE", None)
|
||||
pooling = settings.EMBEDDINGS_POOLING
|
||||
normalize = settings.EMBEDDINGS_NORMALIZE
|
||||
changes = {}
|
||||
if isinstance(pooling, str) and pooling.strip().lower() in ("cls", "mean"):
|
||||
changes["pooling"] = pooling.strip().lower()
|
||||
@@ -289,10 +289,10 @@ class EmbeddingsWrapper:
|
||||
try:
|
||||
_register(self.spec)
|
||||
init_kwargs = {"model_name": self.spec.repo}
|
||||
threads = getattr(settings, "EMBEDDINGS_THREADS", None)
|
||||
threads = settings.EMBEDDINGS_THREADS
|
||||
if isinstance(threads, int) and threads > 0:
|
||||
init_kwargs["threads"] = threads
|
||||
cache_dir = getattr(settings, "EMBEDDINGS_CACHE_DIR", None)
|
||||
cache_dir = settings.EMBEDDINGS_CACHE_DIR
|
||||
if cache_dir:
|
||||
init_kwargs["cache_dir"] = cache_dir
|
||||
self.model = TextEmbedding(**init_kwargs)
|
||||
@@ -331,7 +331,7 @@ class EmbeddingsWrapper:
|
||||
if not documents:
|
||||
return []
|
||||
batch_size: Optional[int] = None
|
||||
raw = getattr(settings, "EMBEDDINGS_MODEL_BATCH_SIZE", None)
|
||||
raw = settings.EMBEDDINGS_MODEL_BATCH_SIZE
|
||||
if isinstance(raw, int) and not isinstance(raw, bool) and raw > 0:
|
||||
batch_size = raw
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ def resolve_pool_max_size() -> int:
|
||||
"""
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
value = getattr(settings, "PGVECTOR_POOL_MAX_SIZE", DEFAULT_POOL_MAX_SIZE)
|
||||
value = settings.PGVECTOR_POOL_MAX_SIZE
|
||||
if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
|
||||
return value
|
||||
return DEFAULT_POOL_MAX_SIZE
|
||||
|
||||
@@ -54,9 +54,9 @@ class PGVectorStore(BaseVectorStore):
|
||||
# Use provided connection string or fall back to settings.
|
||||
# If PGVECTOR_CONNECTION_STRING is not set but POSTGRES_URI is,
|
||||
# reuse the same cluster — normalize from SQLAlchemy dialect to libpq form.
|
||||
self._connection_string = connection_string or getattr(settings, 'PGVECTOR_CONNECTION_STRING', None)
|
||||
self._connection_string = connection_string or settings.PGVECTOR_CONNECTION_STRING
|
||||
|
||||
if not self._connection_string and getattr(settings, 'POSTGRES_URI', None):
|
||||
if not self._connection_string and settings.POSTGRES_URI:
|
||||
from docsgpt.core.db_uri import normalize_pgvector_connection_string
|
||||
self._connection_string = normalize_pgvector_connection_string(settings.POSTGRES_URI)
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ def _make_llm(monkeypatch, capabilities=None, store_responses=False):
|
||||
AZURE_DEPLOYMENT_NAME="dep",
|
||||
OPENAI_RESPONSES_STORE=store_responses,
|
||||
OPENAI_REASONING_SUMMARY="auto",
|
||||
OPENAI_RESPONSES_TRUNCATION_AUTO=False,
|
||||
OPENAI_PROMPT_CACHE_KEY=False,
|
||||
OPENAI_PROMPT_CACHE_RETENTION=None,
|
||||
),
|
||||
)
|
||||
from docsgpt.llm.openai import OpenAILLM
|
||||
|
||||
@@ -24,18 +24,19 @@ def _make_llm(monkeypatch, store_responses=True, **extra_settings):
|
||||
"docsgpt.llm.openai.StorageCreator",
|
||||
types.SimpleNamespace(get_storage=lambda: None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"docsgpt.llm.openai.settings",
|
||||
types.SimpleNamespace(
|
||||
OPENAI_API_KEY="k",
|
||||
API_KEY="k",
|
||||
OPENAI_BASE_URL="",
|
||||
AZURE_DEPLOYMENT_NAME="dep",
|
||||
OPENAI_RESPONSES_STORE=store_responses,
|
||||
OPENAI_REASONING_SUMMARY="auto",
|
||||
**extra_settings,
|
||||
),
|
||||
)
|
||||
# Every setting the Responses path reads, with the hints off; tests opt in per case.
|
||||
stub = {
|
||||
"OPENAI_API_KEY": "k",
|
||||
"API_KEY": "k",
|
||||
"OPENAI_BASE_URL": "",
|
||||
"AZURE_DEPLOYMENT_NAME": "dep",
|
||||
"OPENAI_RESPONSES_STORE": store_responses,
|
||||
"OPENAI_REASONING_SUMMARY": "auto",
|
||||
"OPENAI_RESPONSES_TRUNCATION_AUTO": False,
|
||||
"OPENAI_PROMPT_CACHE_KEY": False,
|
||||
"OPENAI_PROMPT_CACHE_RETENTION": None,
|
||||
}
|
||||
monkeypatch.setattr("docsgpt.llm.openai.settings", types.SimpleNamespace(**{**stub, **extra_settings}))
|
||||
from docsgpt.llm.openai import OpenAILLM
|
||||
|
||||
llm = OpenAILLM(api_key="k")
|
||||
@@ -142,7 +143,7 @@ def _params(llm, **kwargs):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_build_responses_params_defaults_omit_truncation_and_cache_hints(monkeypatch):
|
||||
def test_build_responses_params_omits_truncation_and_cache_hints_when_off(monkeypatch):
|
||||
llm = _make_llm(monkeypatch)
|
||||
llm._prompt_cache_key = "conv-123"
|
||||
params = _params(llm)
|
||||
|
||||
@@ -14,8 +14,8 @@ def mock_settings():
|
||||
s.MICROSOFT_TENANT_ID = "tenant-id-123"
|
||||
s.CONNECTOR_REDIRECT_BASE_URI = "https://redirect.example.com/callback"
|
||||
s.MONGO_DB_NAME = "test_db"
|
||||
# Delete MICROSOFT_AUTHORITY so getattr falls back to default
|
||||
del s.MICROSOFT_AUTHORITY
|
||||
# Unset, as in a real Settings object, so the tenant-derived authority is used.
|
||||
s.MICROSOFT_AUTHORITY = None
|
||||
return s
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user