Skip to content

Commit 0b93eae

Browse files
committed
Merge origin/main into feat/import-pageindex-cloud
Resolve conflicts: - cli.py: keep top-level compile_long_doc import (PR) + main's expanded openkb.config import (extra_headers/timeout/litellm resolvers). - indexer.py: keep the PR's _write_long_doc_artifacts helper but thread main's new description= through it (used by both local + cloud paths). - pyproject.toml / uv.lock: take main's pinned deps (incl. pageindex==0.3.0.dev1, portalocker) over the PR's loose/git@dev pins.
2 parents 127e75e + b204ddf commit 0b93eae

42 files changed

Lines changed: 3793 additions & 386 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 185 additions & 62 deletions
Large diffs are not rendered by default.

config.yaml.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,13 @@ pageindex_threshold: 20 # PDF pages threshold for PageIndex
1010
# - organization
1111
# - dataset
1212
# - model
13+
14+
# Optional: LLM / LiteLLM tuning. Keys are forwarded to LiteLLM; `timeout` and
15+
# `extra_headers` apply per request, the rest are set as litellm.<key>.
16+
# litellm:
17+
# timeout: 1200 # per-request timeout (s); raise for slow local backends (Ollama)
18+
# drop_params: true # let LiteLLM drop params a provider rejects (e.g. Ollama)
19+
# num_retries: 3
20+
# extra_headers: # extra HTTP headers some providers need (e.g. GitHub Copilot)
21+
# Editor-Version: vscode/1.95.0
22+
# Copilot-Integration-Id: vscode-chat

openkb/agent/chat.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,8 @@ async def _handle_slash_deck(arg: str, kb_dir: Path, style: Style) -> None:
659659
model = config.get("model", DEFAULT_CONFIG["model"])
660660

661661
from openkb.skill.generator import Generator
662-
skill_label = skill_name if skill_name else "openkb-deck-editorial (default)"
662+
from openkb.deck.creator import DEFAULT_DECK_SKILL
663+
skill_label = skill_name if skill_name else f"{DEFAULT_DECK_SKILL} (default)"
663664
_fmt(
664665
style,
665666
("class:slash.help", f"Generating deck '{name}' via skill {skill_label}...\n"),

openkb/agent/compiler.py

Lines changed: 191 additions & 189 deletions
Large diffs are not rendered by default.

openkb/agent/linter.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from pathlib import Path
55

66
from agents import Agent, Runner, function_tool
7+
from agents.model_settings import ModelSettings
78

89
from openkb.agent.tools import list_wiki_files, read_wiki_file
10+
from openkb.config import get_extra_headers, get_timeout_extra_args
911

1012
MAX_TURNS = 50
1113
from openkb.schema import get_agents_md
@@ -79,6 +81,10 @@ def read_file(path: str) -> str:
7981
instructions=instructions,
8082
tools=[list_files, read_file],
8183
model=f"litellm/{model}",
84+
model_settings=ModelSettings(
85+
extra_headers=get_extra_headers() or None,
86+
extra_args=get_timeout_extra_args(),
87+
),
8288
)
8389

8490

openkb/agent/query.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from agents import Agent, Runner, function_tool
77

88
from agents import ToolOutputImage, ToolOutputText
9+
from openkb.config import get_extra_headers, get_timeout_extra_args
910
from openkb.agent.tools import (
1011
get_wiki_page_content,
1112
read_wiki_file,
@@ -94,7 +95,11 @@ def get_image(image_path: str) -> ToolOutputImage | ToolOutputText:
9495
instructions=instructions,
9596
tools=[read_file, get_page_content, get_image],
9697
model=f"litellm/{model}",
97-
model_settings=ModelSettings(parallel_tool_calls=False),
98+
model_settings=ModelSettings(
99+
parallel_tool_calls=False,
100+
extra_headers=get_extra_headers() or None,
101+
extra_args=get_timeout_extra_args(),
102+
),
98103
)
99104

100105

openkb/cli.py

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ def filter(self, record: logging.LogRecord) -> bool:
4242
from dotenv import load_dotenv
4343

4444
from openkb.agent.compiler import compile_long_doc
45-
from openkb.config import DEFAULT_CONFIG, load_config, save_config, load_global_config, register_kb
45+
from openkb.config import (
46+
DEFAULT_CONFIG, load_config, save_config, load_global_config, register_kb,
47+
resolve_extra_headers, set_extra_headers, resolve_timeout, set_timeout,
48+
resolve_litellm_settings,
49+
)
4650
from openkb.converter import _registry_path, convert_document
4751
from openkb.indexer import import_cloud_document
4852
from openkb.locks import atomic_write_json, atomic_write_text, kb_ingest_lock, kb_read_lock
@@ -55,13 +59,20 @@ def filter(self, record: logging.LogRecord) -> bool:
5559

5660
load_dotenv() # load from cwd (covers running inside the KB dir)
5761

62+
logger = logging.getLogger(__name__)
63+
5864

5965
_KNOWN_PROVIDER_KEYS = (
6066
"OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY",
6167
"DEEPSEEK_API_KEY", "MISTRAL_API_KEY", "MOONSHOT_API_KEY",
6268
"ZHIPUAI_API_KEY", "DASHSCOPE_API_KEY",
6369
)
6470

71+
# Providers that authenticate via OAuth device flow (subscription login
72+
# handled by LiteLLM itself) — no API key env var is needed, so the
73+
# missing-key warning would be a false alarm for them.
74+
_OAUTH_PROVIDERS = {"chatgpt", "github_copilot"}
75+
6576

6677
def _extract_provider(model: str) -> str | None:
6778
"""Extract the LiteLLM provider name from a model string.
@@ -77,6 +88,31 @@ def _extract_provider(model: str) -> str | None:
7788
return "openai"
7889

7990

91+
def _apply_litellm_settings(settings: dict) -> None:
92+
"""Set each ``litellm:`` key verbatim onto the litellm module (process-wide
93+
globals, so they reach every LiteLLM call). Skips with a warning a key the
94+
installed litellm doesn't define, or one that is a litellm function (e.g.
95+
``completion``) since overwriting it would break later calls. Applied, never
96+
reset — the values persist for the life of the process.
97+
"""
98+
for key, value in settings.items():
99+
if not hasattr(litellm, key):
100+
logger.warning(
101+
"config: LiteLLM has no setting %r — ignoring it "
102+
"(check the spelling or your installed litellm version).",
103+
key,
104+
)
105+
continue
106+
if callable(getattr(litellm, key)):
107+
logger.warning(
108+
"config: 'litellm.%s' is a LiteLLM function, not a setting — "
109+
"refusing to overwrite it from the litellm: config block.",
110+
key,
111+
)
112+
continue
113+
setattr(litellm, key, value)
114+
115+
80116
def _setup_llm_key(kb_dir: Path | None = None) -> None:
81117
"""Set LiteLLM API key from LLM_API_KEY env var if present.
82118
@@ -102,23 +138,45 @@ def _setup_llm_key(kb_dir: Path | None = None) -> None:
102138

103139
api_key = os.environ.get("LLM_API_KEY", "")
104140

105-
# Try to resolve the active provider from the KB config
141+
# Try to resolve the active provider, extra headers, and request timeout
142+
# from the KB config
106143
provider: str | None = None
144+
extra_headers: dict[str, str] = {}
145+
timeout: float | None = None
146+
litellm_settings: dict = {}
107147
if kb_dir is not None:
108148
config_path = kb_dir / ".openkb" / "config.yaml"
109149
if config_path.exists():
110150
config = load_config(config_path)
111151
model = config.get("model", "")
112152
provider = _extract_provider(str(model))
153+
extra_headers = resolve_extra_headers(config)
154+
timeout = resolve_timeout(config)
155+
litellm_settings = resolve_litellm_settings(config)
156+
# `timeout` / `extra_headers` in the block route to the per-call
157+
# stashes (replacing the legacy top-level keys); the rest are globals.
158+
if "extra_headers" in litellm_settings:
159+
extra_headers = resolve_extra_headers(
160+
{"extra_headers": litellm_settings.pop("extra_headers")}
161+
)
162+
if "timeout" in litellm_settings:
163+
timeout = resolve_timeout(
164+
{"timeout": litellm_settings.pop("timeout")}
165+
)
166+
set_extra_headers(extra_headers)
167+
set_timeout(timeout)
168+
_apply_litellm_settings(litellm_settings)
113169

114170
if not api_key:
115-
# Check if any provider key is already set
171+
# Check if any provider key is already set. OAuth-based providers
172+
# (ChatGPT subscription, GitHub Copilot) don't use API keys at all,
173+
# so the warning is skipped for them.
116174
check_keys = (
117175
(f"{provider.upper()}_API_KEY",) if provider
118176
else _KNOWN_PROVIDER_KEYS
119177
)
120178
has_key = any(os.environ.get(k) for k in check_keys)
121-
if not has_key:
179+
if not has_key and provider not in _OAUTH_PROVIDERS:
122180
click.echo(
123181
"Warning: No LLM API key found. Set one of:\n"
124182
f" 1. {kb_dir / '.env' if kb_dir else '<kb_dir>/.env'} — LLM_API_KEY=sk-...\n"
@@ -289,7 +347,6 @@ def _add_single_file_locked(file_path: Path, kb_dir: Path) -> Literal["added", "
289347
from openkb.agent.compiler import compile_long_doc, compile_short_doc
290348
from openkb.state import HashRegistry
291349

292-
logger = logging.getLogger(__name__)
293350
openkb_dir = kb_dir / ".openkb"
294351
config = load_config(openkb_dir / "config.yaml")
295352
_setup_llm_key(kb_dir)
@@ -1581,6 +1638,36 @@ def lint(ctx, fix):
15811638
asyncio.run(run_lint(kb_dir))
15821639

15831640

1641+
@cli.command()
1642+
@click.option("--open/--no-open", "open_browser", default=True,
1643+
help="Open the graph in your browser after generating (default: on; --no-open for headless).")
1644+
@click.pass_context
1645+
@_with_kb_lock(exclusive=False)
1646+
def visualize(ctx, open_browser):
1647+
"""Render the wiki's [[wikilink]] graph as a self-contained interactive HTML page."""
1648+
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override"))
1649+
if kb_dir is None:
1650+
click.echo("No knowledge base found. Run `openkb init` first.")
1651+
return
1652+
from openkb import visualize as viz
1653+
graph = viz.build_graph(kb_dir / "wiki")
1654+
if not graph["nodes"]:
1655+
click.echo("No wiki pages to visualize yet. Run `openkb add` first.")
1656+
return
1657+
out = kb_dir / "output" / "visualize" / "graph.html"
1658+
out.parent.mkdir(parents=True, exist_ok=True)
1659+
out.write_text(viz.render_html(graph), encoding="utf-8")
1660+
click.echo(f"Graph written to {out} ({len(graph['nodes'])} nodes, {len(graph['edges'])} edges)")
1661+
if open_browser:
1662+
import webbrowser
1663+
try:
1664+
opened = webbrowser.open(out.resolve().as_uri()) # resolve() so a relative --kb-dir still yields a valid file URI
1665+
except Exception:
1666+
opened = False
1667+
if not opened:
1668+
click.echo("(couldn't launch a browser — open the file above manually, or use --no-open)")
1669+
1670+
15841671
def print_list(kb_dir: Path) -> None:
15851672
"""Print all documents in the knowledge base. Usable from CLI and chat REPL."""
15861673
openkb_dir = kb_dir / ".openkb"
@@ -2348,8 +2435,10 @@ def deck():
23482435
"--skill", "skill_name",
23492436
metavar="SKILL_NAME",
23502437
default=None,
2438+
# NOTE: 'openkb-deck-neon' below must stay in sync with
2439+
# DEFAULT_DECK_SKILL in openkb/deck/creator.py.
23512440
help=(
2352-
"Which deck skill to use. Defaults to 'openkb-deck-editorial' "
2441+
"Which deck skill to use. Defaults to 'openkb-deck-neon' "
23532442
"(the built-in). Pass e.g. 'deck-guizang-editorial' to route to "
23542443
"a third-party skill installed under ~/.openkb/skills/."
23552444
),
@@ -2428,7 +2517,8 @@ def deck_new(ctx, name, intent, yes_flag, critique_flag, skill_name):
24282517

24292518
# Run the generator.
24302519
from openkb.skill.generator import Generator
2431-
skill_label = skill_name if skill_name else "openkb-deck-editorial (default)"
2520+
from openkb.deck.creator import DEFAULT_DECK_SKILL
2521+
skill_label = skill_name if skill_name else f"{DEFAULT_DECK_SKILL} (default)"
24322522
click.echo(f"Generating deck '{name}' via skill {skill_label}...")
24332523
gen = Generator(
24342524
target_type="deck",

0 commit comments

Comments
 (0)