Skip to content

Commit f749896

Browse files
authored
Load local personas from .jupyter/personas instead of .jupyter/ (#1443)
* export local personas from .jupyter/personas/ subdirectory, warn if any are in .jupyter/ * remove unused PersonaDefaults import * adjust system message formatting * extract and reuse persona finding logic, don't instantiate personas from .jupyter * Add tests for find_persona_files function / persona files searching logic
1 parent c28a8f6 commit f749896

File tree

2 files changed

+72
-27
lines changed

2 files changed

+72
-27
lines changed

packages/jupyter-ai/jupyter_ai/personas/persona_manager.py

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,21 @@ def _init_local_persona_classes(self) -> None:
209209
dotjupyter_dir = self.get_dotjupyter_dir()
210210
if dotjupyter_dir is None:
211211
self.log.info("No .jupyter directory found for loading local personas.")
212-
else:
213-
self._local_persona_classes = load_from_dir(dotjupyter_dir, self.log)
212+
return
213+
214+
if find_persona_files(dotjupyter_dir):
215+
self.send_system_message(
216+
"Found persona files in `.jupyter` directory. Please move them to `.jupyter/personas/` subdirectory."
217+
)
218+
219+
personas_subdir = os.path.join(dotjupyter_dir, "personas")
220+
if not os.path.exists(personas_subdir):
221+
self.log.info(
222+
"No `personas` subdirectory found in `.jupyter` directory for loading local personas."
223+
)
224+
return
225+
226+
self._local_persona_classes = load_from_dir(personas_subdir, self.log)
214227

215228
def _init_personas(self) -> dict[str, BasePersona]:
216229
"""
@@ -562,6 +575,25 @@ def is_persona(username: str):
562575
return username.startswith("jupyter-ai-personas")
563576

564577

578+
def find_persona_files(dir: str) -> list[str]:
579+
"""Find persona Python files in a directory without loading them."""
580+
if not os.path.exists(dir):
581+
return []
582+
583+
try:
584+
all_py_files = glob(os.path.join(dir, "*.py"))
585+
py_files = []
586+
for f in all_py_files:
587+
fname_lower = Path(f).stem.lower()
588+
if "persona" in fname_lower and not (
589+
fname_lower.startswith("_") or fname_lower.startswith(".")
590+
):
591+
py_files.append(f)
592+
return py_files
593+
except Exception:
594+
return []
595+
596+
565597
def load_from_dir(dir: str, log: Logger) -> list[dict]:
566598
"""
567599
Load _persona class declarations_ from Python files in the local filesystem.
@@ -583,31 +615,11 @@ def load_from_dir(dir: str, log: Logger) -> list[dict]:
583615
"""
584616
persona_classes: list[dict] = []
585617

586-
log.info(f"Searching for persona files in {dir}")
587-
# Check if root directory exists
588-
if not os.path.exists(dir):
589-
return persona_classes
590-
591-
# Find all .py files in the root directory that contain "persona" in the name
592-
try:
593-
all_py_files = glob(os.path.join(dir, "*.py"))
594-
py_files = []
595-
for f in all_py_files:
596-
fname_lower = Path(f).stem.lower()
597-
if "persona" in fname_lower and not (
598-
fname_lower.startswith("_") or fname_lower.startswith(".")
599-
):
600-
py_files.append(f)
601-
602-
except Exception as e:
603-
# On exception with glob operation, return empty list
604-
log.error(
605-
f"{type(e).__name__} occurred while searching for Python files in {dir}"
606-
)
618+
py_files = find_persona_files(dir)
619+
if not py_files:
607620
return persona_classes
608621

609-
if py_files:
610-
log.info(f"Found files from {dir}: {[Path(f).name for f in py_files]}")
622+
log.info(f"Loading persona files from {dir}: {[Path(f).name for f in py_files]}")
611623

612624
# Temporarily add root_dir to sys.path for imports
613625
dir_in_path = dir in sys.path

packages/jupyter-ai/jupyter_ai/tests/test_personas.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from unittest.mock import Mock
88

99
import pytest
10-
from jupyter_ai.personas.base_persona import BasePersona, PersonaDefaults
11-
from jupyter_ai.personas.persona_manager import load_from_dir
10+
from jupyter_ai.personas.base_persona import BasePersona
11+
from jupyter_ai.personas.persona_manager import find_persona_files, load_from_dir
1212

1313

1414
@pytest.fixture
@@ -24,6 +24,39 @@ def mock_logger():
2424
return Mock()
2525

2626

27+
class TestFindPersonaFiles:
28+
"""Test cases for find_persona_files function."""
29+
30+
def test_nonexistent_directory_returns_empty_list(self):
31+
"""Test that a non-existent directory returns an empty list."""
32+
result = find_persona_files("/nonexistent/directory/path")
33+
assert result == []
34+
35+
def test_empty_directory_returns_empty_list(self, tmp_persona_dir):
36+
"""Test that an empty directory returns an empty list."""
37+
result = find_persona_files(str(tmp_persona_dir))
38+
assert result == []
39+
40+
def test_finds_valid_ignores_invalid_persona_files(self, tmp_persona_dir):
41+
"""Test that persona files with valid filenames are found while private, hidden, and non-valid files are ignored."""
42+
(tmp_persona_dir / "my_persona.py").write_text("pass")
43+
(tmp_persona_dir / "PersonalAssistant.py").write_text("pass")
44+
45+
(tmp_persona_dir / "my_other_code.py").write_text("pass")
46+
(tmp_persona_dir / "_private_persona.py").write_text("pass")
47+
(tmp_persona_dir / ".hidden_persona.py").write_text("pass")
48+
49+
result = find_persona_files(str(tmp_persona_dir))
50+
result_names = [Path(f).name for f in result]
51+
52+
assert "my_persona.py" in result_names
53+
assert "PersonalAssistant.py" in result_names
54+
55+
assert "my_other_code.py" not in result_names
56+
assert "_private_persona.py" not in result_names
57+
assert ".hidden_persona.py" not in result_names
58+
59+
2760
class TestLoadPersonaClassesFromDirectory:
2861
"""Test cases for load_from_dir function."""
2962

0 commit comments

Comments
 (0)