mirror of
https://github.com/VectifyAI/PageIndex.git
synced 2026-10-02 07:44:37 +08:00
fix: .env stays unset when the cwd tree has none; a local client with a blank chat_model refuses at the chat door; storage_path is typed PathLike
find_dotenv(usecwd=True) returns '' when nothing is reachable from the cwd, and `or None` turned that into load_dotenv's own upward walk from utils.py — the install-dir leak the cwd search was added to replace. A pip-installed SDK could load another project's .env from above site-packages, silently. _local_chat treats a blank chat_model as "managed chat", which a client without an api_key does not have: chat_completions() then reached for LocalAPI.chat_completions and raised a bare AttributeError. The managed branch now refuses as a PageIndexAPIError naming chat_model. py.typed made the annotations authoritative while storage_path was typed str; _ARG_TYPES accepts os.PathLike, so Path(...) ran fine and failed the user's type check. Both signatures and LocalIndexConfig now say so. Claude-Session: https://claude.ai/code/session_017Fd7jVm366S2Xamzhxv6yb
This commit is contained in:
+7
-2
@@ -346,7 +346,7 @@ class PageIndexClient:
|
||||
model: Optional[str] = None,
|
||||
summary_model: Optional[str] = None,
|
||||
retrieve_model: Optional[str] = None,
|
||||
storage_path: Optional[str] = None,
|
||||
storage_path: Optional[Union[str, os.PathLike[str]]] = None,
|
||||
index_backend: Optional[dict[str, Any]] = None,
|
||||
chat_backend: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
@@ -915,6 +915,11 @@ class PageIndexClient:
|
||||
reasoning_effort=reasoning_effort, extra_body=extra_body,
|
||||
extra_headers=extra_headers, backend=backend,
|
||||
)
|
||||
if not getattr(self, "api_key", None):
|
||||
raise PageIndexAPIError(
|
||||
"chat_model is empty — it configures nothing, and a local "
|
||||
"client has no managed chat to fall back to. Set "
|
||||
"chat_model=... to run the agent with your own model.")
|
||||
if (model is not None or max_turns is not None or top_p is not None
|
||||
or max_tokens is not None or reasoning_effort is not None
|
||||
or extra_body is not None or extra_headers is not None
|
||||
@@ -1649,7 +1654,7 @@ class PageIndexLocalClient(PageIndexClient):
|
||||
model: Optional[str] = None,
|
||||
summary_model: Optional[str] = None,
|
||||
retrieve_model: Optional[str] = None,
|
||||
storage_path: Optional[str] = None,
|
||||
storage_path: Optional[Union[str, os.PathLike[str]]] = None,
|
||||
index_backend: Optional[dict[str, Any]] = None,
|
||||
chat_backend: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
|
||||
+2
-1
@@ -11,6 +11,7 @@ other keys. The ``"pageindex-cloud"`` string is the label spelling for
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Literal, TypedDict, Union
|
||||
|
||||
PAGEINDEX_CLOUD = "pageindex-cloud"
|
||||
@@ -32,7 +33,7 @@ class LocalIndexConfig(TypedDict, total=False):
|
||||
model: str
|
||||
summary_model: str
|
||||
backend: dict
|
||||
storage_path: str
|
||||
storage_path: Union[str, os.PathLike[str]]
|
||||
|
||||
|
||||
class ChatConfig(TypedDict, total=False):
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import copy
|
||||
import asyncio
|
||||
from io import BytesIO
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
load_dotenv(find_dotenv(usecwd=True) or None)
|
||||
load_dotenv(find_dotenv(usecwd=True))
|
||||
import logging
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
@@ -396,6 +396,31 @@ def test_env_key_found_from_cwd(tmp_path):
|
||||
assert out.stdout.strip() == "ok"
|
||||
|
||||
|
||||
def test_env_not_found_from_cwd_stays_unset(tmp_path, tmp_path_factory):
|
||||
"""The cwd search finding nothing must end the search: find_dotenv
|
||||
returns '' then, and `or None` handed load_dotenv its own upward walk
|
||||
from utils.py — the install-dir leak the cwd search replaced. A
|
||||
symlinked package puts utils.py under a tree whose root holds a .env;
|
||||
the cwd tree holds none."""
|
||||
site = tmp_path / "site"
|
||||
site.mkdir()
|
||||
(site / "pageindex").symlink_to(Path(__file__).parent.parent / "pageindex")
|
||||
(tmp_path / ".env").write_text("PAGEINDEX_API_KEY=pi-leaked\n")
|
||||
cwd = tmp_path_factory.mktemp("elsewhere")
|
||||
(cwd / "app.py").write_text(
|
||||
"from pageindex import PageIndexCloudClient, PageIndexAPIError\n"
|
||||
"try:\n"
|
||||
" print(PageIndexCloudClient().api_key)\n"
|
||||
"except PageIndexAPIError:\n"
|
||||
" print('unset')\n")
|
||||
env = {**os.environ, "PYTHONPATH": str(site)}
|
||||
env.pop("PAGEINDEX_API_KEY", None)
|
||||
out = subprocess.run([sys.executable, "app.py"], cwd=cwd, env=env,
|
||||
capture_output=True, text=True)
|
||||
assert out.returncode == 0, out.stderr
|
||||
assert out.stdout.strip() != "pi-leaked", out.stdout
|
||||
|
||||
|
||||
def test_empty_values_refused_never_silent():
|
||||
"""An empty value configures nothing — pre-fix, an empty chat-side
|
||||
value on a cloud client silently selected own-model chat on the
|
||||
@@ -1916,6 +1941,16 @@ def test_blank_chat_model_assignment_stays_managed():
|
||||
assert not client._local_chat
|
||||
|
||||
|
||||
def test_local_client_blank_chat_model_refuses_at_chat_door(local_client):
|
||||
"""A local client has no managed chat to fall back to: with chat_model
|
||||
blanked, chat_completions() must refuse as a PageIndexAPIError, not
|
||||
surface LocalAPI's missing chat_completions as an AttributeError."""
|
||||
for blank in ("", " ", None):
|
||||
local_client.chat_model = blank
|
||||
with pytest.raises(PageIndexAPIError, match="chat_model is empty"):
|
||||
local_client.chat_completions("hi")
|
||||
|
||||
|
||||
def test_blank_chat_model_carries_no_model_into_agent_config():
|
||||
"""Same rule at the config door: a blank chat_model must not become
|
||||
a model literally named " " in the returned config."""
|
||||
|
||||
Reference in New Issue
Block a user