diff --git a/graphify/llm.py b/graphify/llm.py index c7b1040..dfc3ff5 100644 --- a/graphify/llm.py +++ b/graphify/llm.py @@ -1303,7 +1303,12 @@ def extract_files_direct( Returns dict with nodes, edges, hyperedges, input_tokens, output_tokens. Raises ValueError for unknown backends or when no API key is configured. Raises ImportError if SDK missing. + + Accepts ``str`` paths as well as ``Path``; string entries are coerced up + front so downstream helpers (``_partition_semantic_files``, ``_read_files``, + ``_build_image_refs``) can rely on ``Path`` semantics (#1386). """ + files = [Path(f) for f in files] if backend is None: backend = detect_backend() if backend is None: @@ -1728,7 +1733,11 @@ def extract_corpus_parallel( Returns merged dict with nodes, edges, hyperedges, input_tokens, output_tokens. Failed chunks are logged to stderr and skipped — one bad chunk does not abort the run. + + Accepts ``str`` paths as well as ``Path``; string entries are coerced up + front so packing/slicing helpers can rely on ``Path`` semantics (#1386). """ + files = [Path(f) for f in files] # Split oversized splittable documents into slices that cover the whole file # before packing, so content past _FILE_CHAR_CAP is extracted instead of # silently dropped (#1369). Files at/under the cap pass through unchanged. diff --git a/tests/test_llm_backends.py b/tests/test_llm_backends.py index 888545e..63f1b5e 100644 --- a/tests/test_llm_backends.py +++ b/tests/test_llm_backends.py @@ -132,6 +132,58 @@ def test_missing_gemini_key_names_both_supported_env_vars(monkeypatch): assert "GEMINI_API_KEY or GOOGLE_API_KEY" in str(exc.value) +# --------------------------------------------------------------------------- +# #1386: public entry points accept str paths, not just pathlib.Path +# --------------------------------------------------------------------------- + + +def test_extract_files_direct_accepts_str_paths(tmp_path, monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.setenv("GOOGLE_API_KEY", "google-key") + source = tmp_path / "note.md" + source.write_text("# Architecture\n\nThe runner emits a snapshot.\n") + result = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 1, "output_tokens": 1} + + # str path must not raise AttributeError: 'str' object has no attribute 'suffix' + with patch("graphify.llm._call_openai_compat", return_value=result): + assert llm.extract_files_direct([str(source)], backend="gemini", root=tmp_path) is result + + +def test_extract_corpus_parallel_accepts_str_and_mixed_paths(tmp_path, monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.setenv("GOOGLE_API_KEY", "google-key") + f1 = tmp_path / "a.md" + f1.write_text("# A\n\nNode one.\n") + f2 = tmp_path / "b.md" + f2.write_text("# B\n\nNode two.\n") + result = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 1, "output_tokens": 1} + + with patch("graphify.llm._call_openai_compat", return_value=result): + # all-str, all-Path, and mixed must each pack + run without AttributeError + for files in ([str(f1), str(f2)], [f1, f2], [str(f1), f2]): + merged = llm.extract_corpus_parallel( + files, backend="gemini", root=tmp_path, max_concurrency=1 + ) + assert merged["failed_chunks"] == 0 + + +def test_str_path_entry_points_handle_edge_cases(tmp_path, monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.setenv("GOOGLE_API_KEY", "google-key") + result = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 1, "output_tokens": 1} + + with patch("graphify.llm._call_openai_compat", return_value=result): + # empty list: no chunks, nothing to extract, no crash + empty = llm.extract_corpus_parallel([], backend="gemini", root=tmp_path) + assert empty["nodes"] == [] and empty["failed_chunks"] == 0 + # a Path subclass is still a Path and must pass through unchanged + class _SubPath(type(Path())): # concrete OS-specific Path subclass + pass + sub = _SubPath(tmp_path / "c.md") + sub.write_text("# C\n\nNode.\n") + assert llm.extract_files_direct([sub], backend="gemini", root=tmp_path) is result + + # --------------------------------------------------------------------------- # Adaptive retry: context-window overflow recovery # ---------------------------------------------------------------------------