Skip to content

Commit 188badd

Browse files
committed
feat(cli): orchestrate raw-less PageIndex Cloud import
1 parent fc8db5e commit 188badd

2 files changed

Lines changed: 154 additions & 0 deletions

File tree

openkb/cli.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ def filter(self, record: logging.LogRecord) -> bool:
4141
litellm.suppress_debug_info = True
4242
from dotenv import load_dotenv
4343

44+
from openkb.agent.compiler import compile_long_doc
4445
from openkb.config import DEFAULT_CONFIG, load_config, save_config, load_global_config, register_kb
4546
from openkb.converter import _registry_path, convert_document
47+
from openkb.indexer import import_cloud_document
4648
from openkb.locks import atomic_write_json, atomic_write_text, kb_ingest_lock, kb_read_lock
4749
from openkb.log import append_log
4850
from openkb.schema import AGENTS_MD, INDEX_SEED, PAGE_CONTENT_DIRS
@@ -386,6 +388,83 @@ def _add_single_file_locked(file_path: Path, kb_dir: Path) -> Literal["added", "
386388
return "added"
387389

388390

391+
def import_from_pageindex_cloud(
392+
doc_id: str, kb_dir: Path
393+
) -> Literal["added", "skipped", "failed"]:
394+
"""Import an existing PageIndex Cloud document into the KB by ``doc_id``.
395+
396+
Fetches structure + page content from the cloud (no local PDF), compiles
397+
concepts, and registers a raw-less ``pageindex_cloud`` entry. Idempotent:
398+
re-importing the same ``doc_id`` is skipped. The user's cloud corpus is
399+
never modified.
400+
"""
401+
import hashlib
402+
from openkb.state import HashRegistry
403+
404+
logger = logging.getLogger(__name__)
405+
openkb_dir = kb_dir / ".openkb"
406+
config = load_config(openkb_dir / "config.yaml")
407+
_setup_llm_key(kb_dir)
408+
model: str = config.get("model", DEFAULT_CONFIG["model"])
409+
410+
path_key = f"pageindex-cloud:{doc_id}"
411+
synthetic_hash = hashlib.sha256(path_key.encode("utf-8")).hexdigest()
412+
413+
registry = HashRegistry(openkb_dir / "hashes.json")
414+
if registry.is_known(synthetic_hash):
415+
click.echo(f" [SKIP] Already imported from PageIndex Cloud: {doc_id}")
416+
return "skipped"
417+
418+
click.echo(f"Importing from PageIndex Cloud: {doc_id}")
419+
try:
420+
import_result = import_cloud_document(doc_id, kb_dir, path_key)
421+
except Exception as exc:
422+
click.echo(f" [ERROR] Import failed: {exc}")
423+
logger.debug("Cloud import traceback:", exc_info=True)
424+
return "failed"
425+
426+
doc_name = import_result.doc_name
427+
summary_path = kb_dir / "wiki" / "summaries" / f"{doc_name}.md"
428+
click.echo(f" Compiling imported doc (doc_id={doc_id})...")
429+
for attempt in range(2):
430+
try:
431+
asyncio.run(
432+
compile_long_doc(
433+
doc_name, summary_path, doc_id, kb_dir, model,
434+
doc_description=import_result.description,
435+
)
436+
)
437+
break
438+
except Exception as exc:
439+
if attempt == 0:
440+
click.echo(" Retrying compilation in 2s...")
441+
time.sleep(2)
442+
else:
443+
click.echo(f" [ERROR] Compilation failed: {exc}")
444+
logger.debug("Compilation traceback:", exc_info=True)
445+
return "failed"
446+
447+
# Register the raw-less cloud entry only after successful compilation.
448+
registry = HashRegistry(openkb_dir / "hashes.json")
449+
meta = {
450+
"name": import_result.name,
451+
"doc_name": doc_name,
452+
"type": "pageindex_cloud",
453+
"origin": "cloud",
454+
"path": path_key,
455+
"source_path": _registry_path(
456+
kb_dir / "wiki" / "sources" / f"{doc_name}.json", kb_dir
457+
),
458+
"doc_id": doc_id,
459+
}
460+
registry.remove_by_doc_name(doc_name)
461+
registry.add(synthetic_hash, meta)
462+
463+
append_log(kb_dir / "wiki", "ingest", doc_name)
464+
click.echo(f" [OK] {doc_name} imported from PageIndex Cloud.")
465+
return "added"
466+
467+
389468
# ---------------------------------------------------------------------------
390469
# CLI
391470
# ---------------------------------------------------------------------------

tests/test_add_command.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,78 @@ def test_add_oldest_legacy_entry_converges_to_single_entry(self, tmp_path):
203203
new_entries = [m for m in hashes.values() if m.get("doc_name") == "notes"]
204204
assert len(new_entries) == 1 # …exactly one entry survives
205205
assert new_entries[0]["path"] # with path identity persisted
206+
207+
208+
class TestImportFromPageindexCloud:
209+
def _setup_kb(self, tmp_path):
210+
(tmp_path / "raw").mkdir()
211+
(tmp_path / "wiki" / "sources" / "images").mkdir(parents=True)
212+
(tmp_path / "wiki" / "summaries").mkdir(parents=True)
213+
(tmp_path / "wiki" / "concepts").mkdir(parents=True)
214+
(tmp_path / "wiki" / "reports").mkdir(parents=True)
215+
openkb_dir = tmp_path / ".openkb"
216+
openkb_dir.mkdir()
217+
(openkb_dir / "config.yaml").write_text("model: gpt-4o-mini\n")
218+
(openkb_dir / "hashes.json").write_text(json.dumps({}))
219+
return tmp_path
220+
221+
def test_registers_rawless_cloud_entry(self, tmp_path):
222+
import hashlib
223+
from openkb.cli import import_from_pageindex_cloud
224+
from openkb.indexer import CloudImportResult
225+
from openkb.state import HashRegistry
226+
227+
kb_dir = self._setup_kb(tmp_path)
228+
result = CloudImportResult(
229+
doc_id="cloud-1", doc_name="Cloud-Paper", name="Cloud Paper.pdf",
230+
description="desc",
231+
)
232+
233+
with patch("openkb.cli.import_cloud_document", return_value=result), \
234+
patch("openkb.cli.compile_long_doc", return_value=None) as mock_compile, \
235+
patch("openkb.cli._setup_llm_key"):
236+
outcome = import_from_pageindex_cloud("cloud-1", kb_dir)
237+
238+
assert outcome == "added"
239+
mock_compile.assert_called_once()
240+
registry = HashRegistry(kb_dir / ".openkb" / "hashes.json")
241+
synthetic = hashlib.sha256(b"pageindex-cloud:cloud-1").hexdigest()
242+
meta = registry.get(synthetic)
243+
assert meta is not None
244+
assert meta["type"] == "pageindex_cloud"
245+
assert meta["origin"] == "cloud"
246+
assert meta["doc_id"] == "cloud-1"
247+
assert meta["path"] == "pageindex-cloud:cloud-1"
248+
assert "raw_path" not in meta
249+
250+
def test_second_import_is_skipped(self, tmp_path):
251+
from openkb.cli import import_from_pageindex_cloud
252+
from openkb.indexer import CloudImportResult
253+
254+
kb_dir = self._setup_kb(tmp_path)
255+
result = CloudImportResult(
256+
doc_id="cloud-1", doc_name="Cloud-Paper", name="Cloud Paper.pdf",
257+
description="desc",
258+
)
259+
260+
with patch("openkb.cli.import_cloud_document", return_value=result) as mock_import, \
261+
patch("openkb.cli.compile_long_doc", return_value=None), \
262+
patch("openkb.cli._setup_llm_key"):
263+
import_from_pageindex_cloud("cloud-1", kb_dir)
264+
second = import_from_pageindex_cloud("cloud-1", kb_dir)
265+
266+
assert second == "skipped"
267+
assert mock_import.call_count == 1 # not fetched again
268+
269+
def test_import_failure_returns_failed_and_registers_nothing(self, tmp_path):
270+
from openkb.cli import import_from_pageindex_cloud
271+
from openkb.state import HashRegistry
272+
273+
kb_dir = self._setup_kb(tmp_path)
274+
with patch("openkb.cli.import_cloud_document", side_effect=RuntimeError("boom")), \
275+
patch("openkb.cli._setup_llm_key"):
276+
outcome = import_from_pageindex_cloud("cloud-9", kb_dir)
277+
278+
assert outcome == "failed"
279+
registry = HashRegistry(kb_dir / ".openkb" / "hashes.json")
280+
assert registry.all_entries() == {}

0 commit comments

Comments
 (0)