Skip to content

Commit e12e74e

Browse files
committed
feat(config): make compile concurrency configurable (closes #173)
Concept/entity generation ran at a hardcoded concurrency of 5 (DEFAULT_COMPILE_CONCURRENCY) with no way to lower it when the LLM provider rate-limits. Add a `compile_concurrency` config key (default 5) and thread it through every add / recompile / cloud-import compile call via a shared `_compile_concurrency` resolver (null / non-positive → the compiler default). Pairs with `pageindex_max_concurrency` (indexing side) from this PR — both concurrency knobs are now KB-configurable.
1 parent b29b6d9 commit e12e74e

4 files changed

Lines changed: 88 additions & 4 deletions

File tree

openkb/cli.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def filter(self, record: logging.LogRecord) -> bool:
4747
litellm.suppress_debug_info = True
4848
from dotenv import load_dotenv
4949

50-
from openkb.agent.compiler import compile_long_doc
50+
from openkb.agent.compiler import DEFAULT_COMPILE_CONCURRENCY, compile_long_doc
5151
from openkb.config import (
5252
DEFAULT_CONFIG,
5353
load_config,
@@ -439,6 +439,19 @@ def _run_compile_with_retry(coro_factory, label: str) -> None:
439439
raise
440440

441441

442+
def _compile_concurrency(config: dict) -> int:
443+
"""Concurrency cap for the compile step (concept/entity generation).
444+
445+
Configurable per KB via ``compile_concurrency`` in config.yaml; a missing,
446+
null, or non-positive value falls back to the compiler's built-in default.
447+
Lower it when the LLM provider rate-limits.
448+
"""
449+
value = config.get("compile_concurrency")
450+
if isinstance(value, int) and value > 0:
451+
return value
452+
return DEFAULT_COMPILE_CONCURRENCY
453+
454+
442455
def add_single_file(
443456
file_path: Path, kb_dir: Path, *, stage: bool = True
444457
) -> Literal["added", "skipped", "failed"]:
@@ -564,6 +577,7 @@ def _add_single_file_locked(
564577
kb_dir,
565578
model,
566579
doc_description=index_result.description,
580+
max_concurrency=_compile_concurrency(config),
567581
),
568582
label=f"Compiling long doc (doc_id={index_result.doc_id})",
569583
)
@@ -572,7 +586,13 @@ def _add_single_file_locked(
572586
raise RuntimeError(f"Converted document has no source artifact: {file_path.name}")
573587
source_path = result.source_path
574588
_run_compile_with_retry(
575-
lambda: compile_short_doc(doc_name, source_path, kb_dir, model),
589+
lambda: compile_short_doc(
590+
doc_name,
591+
source_path,
592+
kb_dir,
593+
model,
594+
max_concurrency=_compile_concurrency(config),
595+
),
576596
label="Compiling short doc",
577597
)
578598

@@ -699,6 +719,7 @@ def import_from_pageindex_cloud(doc_id: str, kb_dir: Path) -> Literal["added", "
699719
kb_dir,
700720
model,
701721
doc_description=cloud.description,
722+
max_concurrency=_compile_concurrency(config),
702723
),
703724
label=f"Compiling imported doc (doc_id={doc_id})",
704725
)
@@ -1679,7 +1700,16 @@ def _classify(meta: dict) -> str:
16791700
click.echo(f"[{i}/{total}] Recompiling long doc {name}...")
16801701
start = time.time()
16811702
try:
1682-
asyncio.run(compiler.compile_long_doc(name, summary_path, doc_id, kb_dir, model))
1703+
asyncio.run(
1704+
compiler.compile_long_doc(
1705+
name,
1706+
summary_path,
1707+
doc_id,
1708+
kb_dir,
1709+
model,
1710+
max_concurrency=_compile_concurrency(config),
1711+
)
1712+
)
16831713
except Exception as exc:
16841714
click.echo(f" [ERROR] Compilation failed: {exc}")
16851715
logging.getLogger(__name__).debug("Recompile traceback:", exc_info=True)
@@ -1699,7 +1729,15 @@ def _classify(meta: dict) -> str:
16991729
click.echo(f"[{i}/{total}] Recompiling short doc {name}...")
17001730
start = time.time()
17011731
try:
1702-
asyncio.run(compiler.compile_short_doc(name, source_path, kb_dir, model))
1732+
asyncio.run(
1733+
compiler.compile_short_doc(
1734+
name,
1735+
source_path,
1736+
kb_dir,
1737+
model,
1738+
max_concurrency=_compile_concurrency(config),
1739+
)
1740+
)
17031741
except Exception as exc:
17041742
click.echo(f" [ERROR] Compilation failed: {exc}")
17051743
logging.getLogger(__name__).debug("Recompile traceback:", exc_info=True)

openkb/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
# document. None = let PageIndex apply its own default. Raise it to index
2222
# faster, lower it if you hit provider rate limits or "too many open files".
2323
"pageindex_max_concurrency": None,
24+
# Cap on concurrent compile LLM calls OpenKB makes when generating concept
25+
# and entity pages. Lower it if your LLM provider rate-limits.
26+
"compile_concurrency": 5,
2427
}
2528

2629
# Default entity-type vocabulary. Overridable per-KB via the optional

tests/test_add_command.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,39 @@ def test_add_single_file_compile_failure_rolls_back_converted_artifacts(self, tm
9797
assert not (kb_dir / "wiki" / "sources" / "notes.md").exists()
9898
assert HashRegistry(kb_dir / ".openkb" / "hashes.json").all_entries() == {}
9999

100+
def test_compile_concurrency_resolution(self):
101+
from openkb.agent.compiler import DEFAULT_COMPILE_CONCURRENCY
102+
from openkb.cli import _compile_concurrency
103+
104+
assert _compile_concurrency({}) == DEFAULT_COMPILE_CONCURRENCY
105+
assert _compile_concurrency({"compile_concurrency": 3}) == 3
106+
# None / non-positive / non-int fall back to the compiler default.
107+
assert _compile_concurrency({"compile_concurrency": None}) == DEFAULT_COMPILE_CONCURRENCY
108+
assert _compile_concurrency({"compile_concurrency": 0}) == DEFAULT_COMPILE_CONCURRENCY
109+
110+
def test_add_forwards_compile_concurrency_from_config(self, tmp_path):
111+
from unittest.mock import AsyncMock
112+
113+
from openkb.cli import add_single_file
114+
115+
kb_dir = self._setup_kb(tmp_path)
116+
(kb_dir / ".openkb" / "config.yaml").write_text(
117+
"model: gpt-4o-mini\ncompile_concurrency: 3\n", encoding="utf-8"
118+
)
119+
doc = tmp_path / "notes.md"
120+
doc.write_text("# Notes\n\nBody", encoding="utf-8")
121+
122+
with (
123+
patch(
124+
"openkb.agent.compiler.compile_short_doc", new_callable=AsyncMock
125+
) as mock_compile,
126+
patch("openkb.cli._setup_llm_key"),
127+
):
128+
outcome = add_single_file(doc, kb_dir)
129+
130+
assert outcome == "added"
131+
assert mock_compile.call_args.kwargs["max_concurrency"] == 3
132+
100133
def _long_doc_conv(self, kb_dir, name, file_hash):
101134
from openkb.converter import ConvertResult
102135

tests/test_config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ def test_load_pageindex_max_concurrency_override(tmp_path):
3636
assert load_config(config_path)["pageindex_max_concurrency"] == 12
3737

3838

39+
def test_compile_concurrency_defaults_to_5():
40+
assert DEFAULT_CONFIG["compile_concurrency"] == 5
41+
42+
43+
def test_load_compile_concurrency_override(tmp_path):
44+
config_path = tmp_path / "config.yaml"
45+
config_path.write_text("compile_concurrency: 3\n", encoding="utf-8")
46+
assert load_config(config_path)["compile_concurrency"] == 3
47+
48+
3949
def test_load_missing_file_returns_defaults(tmp_path):
4050
missing = tmp_path / "nonexistent" / "config.yaml"
4151
config = load_config(missing)

0 commit comments

Comments
 (0)