Skip to content

Commit 9d7d50e

Browse files
committed
fix(entity-types): address xhigh review — DEFAULT_CONFIG parity, empty-list inherit, silent reads, set-diff
- config: define DEFAULT_ENTITY_TYPES before DEFAULT_CONFIG and add `entity_types` to DEFAULT_CONFIG so the key layers like every other GLOBAL_SCALAR_KEY and always appears in the effective config (#1). - config: resolve_effective_config treats an empty entity_types list the same as null → inherit, so a KB that cleared its override doesn't pin an empty vocabulary (#2). - config: resolve_entity_types(config, *, warn=True); config-read paths pass warn=False so a plain GET doesn't spam coercion warnings (#4). - api_config: both read paths call resolve_entity_types(..., warn=False). - KbSettingsSheet: inherited badge shows the effective list, not `globalValue ?? effective`; drop the now-unused globalValue prop (#3). - Settings: global entity_types diff is order-insensitive — the vocabulary is a set, so re-adding a removed type is not a change (#6). Backend pytest 1240 passed; ruff/format/mypy clean; frontend build green. Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
1 parent 9abd2bb commit 9d7d50e

4 files changed

Lines changed: 48 additions & 36 deletions

File tree

frontend/src/components/KbSettingsSheet.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,6 @@ function KbConfigSection({ kb }: { kb: string }) {
278278
<EntityTypesRow
279279
source={config.sources.entity_types}
280280
effective={config.entity_types}
281-
globalValue={config.global_values.entity_types}
282281
busy={busy}
283282
onSet={setEntityTypesOverride}
284283
onRevert={() => setEntityTypesOverride(null)}
@@ -415,13 +414,12 @@ function OverrideRow({
415414
* chips list (EntityTypesEditor) and every add/remove persists immediately —
416415
* the chips always show the server-cleaned effective list. */
417416
function EntityTypesRow({
418-
source, effective, globalValue, busy, onSet, onRevert,
417+
source, effective, busy, onSet, onRevert,
419418
}: {
420419
source: ConfigSource
421-
/** Cleaned effective list (always includes "other"). */
420+
/** Cleaned effective list (always includes "other") — shown as-is in the
421+
* inherited badge, so it matches the vocabulary the compiler actually uses. */
422422
effective: string[]
423-
/** RAW global-layer list; null when global.yaml is silent. */
424-
globalValue: string[] | null
425423
busy: boolean
426424
onSet: (value: string[]) => void
427425
onRevert: () => void
@@ -432,7 +430,7 @@ function EntityTypesRow({
432430

433431
const inheritedBadge =
434432
source === 'global'
435-
? t('kbSettings:inheritGlobal', { value: (globalValue ?? effective).join(', ') })
433+
? t('kbSettings:inheritGlobal', { value: effective.join(', ') })
436434
: t('kbSettings:inheritDefault', { value: effective.join(', ') })
437435

438436
return (

frontend/src/pages/Settings.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,12 @@ export default function Settings() {
109109
if (threshold.trim() !== '' && Number.isInteger(n) && n >= 1 && n !== config.pageindex_threshold) {
110110
cfg.pageindex_threshold = n
111111
}
112-
// Order-sensitive compare against the last-fetched cleaned list. The
113-
// editor only appends/removes, so equal content ⇒ equal order; a reorder
114-
// never happens client-side.
115-
const baseTypes = config.entity_types
116-
if (entityTypes.length !== baseTypes.length || entityTypes.some((v, i) => v !== baseTypes[i])) {
112+
// Order-INSENSITIVE compare: the vocabulary is a set (the compiler treats
113+
// it as an allowlist), and removing-then-re-adding a type appends it at the
114+
// end — so a same-set reorder must NOT dirty the form or persist a no-op.
115+
const a = [...entityTypes].sort()
116+
const b = [...config.entity_types].sort()
117+
if (a.length !== b.length || a.some((v, i) => v !== b[i])) {
117118
cfg.entity_types = entityTypes
118119
}
119120
if (Object.keys(cfg).length > 0) patch.config = cfg

openkb/api_config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def read_kb_config(kb_dir: Path) -> KbConfigResponse:
153153
pageindex_threshold=effective["pageindex_threshold"],
154154
# Cleaned effective list (what the compiler will use), not the raw stored
155155
# value — resolve_entity_types defaults + dedupes + ensures "other".
156-
entity_types=resolve_entity_types(effective),
156+
entity_types=resolve_entity_types(effective, warn=False),
157157
openai_api_base=bundle.base_url,
158158
has_api_key=bundle.api_key is not None,
159159
sources=sources,
@@ -268,7 +268,7 @@ def read_global_config() -> GlobalConfigResponse:
268268
language=gc.get("language", DEFAULT_CONFIG["language"]),
269269
pageindex_threshold=gc.get("pageindex_threshold", DEFAULT_CONFIG["pageindex_threshold"]),
270270
# Effective global vocabulary (cleaned; defaults to DEFAULT_ENTITY_TYPES).
271-
entity_types=resolve_entity_types(gc),
271+
entity_types=resolve_entity_types(gc, warn=False),
272272
# Report the EFFECTIVE root (env > global.yaml kb_root > default) via the
273273
# module object so a test-monkeypatched GLOBAL_CONFIG_DIR is honored.
274274
kb_root=str(_config_module.kb_root_dir()),

openkb/config.py

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,7 @@
1515

1616
logger = logging.getLogger(__name__)
1717

18-
DEFAULT_CONFIG: dict[str, Any] = {
19-
"model": "gpt-5.4",
20-
"language": "en",
21-
"pageindex_threshold": 20,
22-
}
23-
24-
# Default entity-type vocabulary. Overridable per-KB via the optional
18+
# Default entity-type vocabulary. Overridable globally / per-KB via the optional
2519
# ``entity_types:`` config key (see ``resolve_entity_types``).
2620
DEFAULT_ENTITY_TYPES: tuple[str, ...] = (
2721
"person",
@@ -33,6 +27,17 @@
3327
"other",
3428
)
3529

30+
DEFAULT_CONFIG: dict[str, Any] = {
31+
"model": "gpt-5.4",
32+
"language": "en",
33+
"pageindex_threshold": 20,
34+
# A GLOBAL_SCALAR_KEY like the three above, so the merged `effective` dict
35+
# always carries it (the layering/`sources` logic is type-agnostic). A
36+
# global/KB list overrides it wholesale; resolve_entity_types cleans the
37+
# effective value on read.
38+
"entity_types": list(DEFAULT_ENTITY_TYPES),
39+
}
40+
3641
GLOBAL_CONFIG_DIR = Path.home() / ".config" / "openkb"
3742
GLOBAL_CONFIG_PATH = GLOBAL_CONFIG_DIR / "global.yaml"
3843
GLOBAL_CONFIG_LOCK_PATH = GLOBAL_CONFIG_DIR / "global.lock"
@@ -88,7 +93,7 @@ def _load_global_config_unlocked() -> dict[str, Any]:
8893
return {}
8994

9095

91-
def resolve_entity_types(config: dict) -> list[str]:
96+
def resolve_entity_types(config: dict, *, warn: bool = True) -> list[str]:
9297
"""Resolve the effective entity-type list from a loaded config dict.
9398
9499
If ``config["entity_types"]`` is a non-empty list, each string item is
@@ -98,18 +103,23 @@ def resolve_entity_types(config: dict) -> list[str]:
98103
de-duped (order preserving) and ``"other"`` is always appended when missing
99104
(it is the coercion fallback). Otherwise — key absent, not a list, empty,
100105
or fully malformed — :data:`DEFAULT_ENTITY_TYPES` is returned, so behavior
101-
is byte-identical to the default. A warning is logged only when
102-
``entity_types`` was present-but-malformed.
106+
is byte-identical to the default.
107+
108+
``warn`` logs a warning when ``entity_types`` is present-but-malformed. It is
109+
``True`` for the compile path (a real config problem the author should see)
110+
and passed ``False`` by the config-READ endpoints, which run on every GET and
111+
must not spam the log for a malformed value.
103112
"""
104113
raw = config.get("entity_types")
105114
if raw is None:
106115
return list(DEFAULT_ENTITY_TYPES)
107116
if not isinstance(raw, list):
108-
logger.warning(
109-
"config: 'entity_types' must be a list of strings, got %s — "
110-
"falling back to the default entity types.",
111-
type(raw).__name__,
112-
)
117+
if warn:
118+
logger.warning(
119+
"config: 'entity_types' must be a list of strings, got %s — "
120+
"falling back to the default entity types.",
121+
type(raw).__name__,
122+
)
113123
return list(DEFAULT_ENTITY_TYPES)
114124
cleaned: list[str] = []
115125
for x in raw:
@@ -119,10 +129,11 @@ def resolve_entity_types(config: dict) -> list[str]:
119129
if s and s not in cleaned:
120130
cleaned.append(s)
121131
if not cleaned:
122-
logger.warning(
123-
"config: 'entity_types' was present but yielded no usable values — "
124-
"falling back to the default entity types.",
125-
)
132+
if warn:
133+
logger.warning(
134+
"config: 'entity_types' was present but yielded no usable values — "
135+
"falling back to the default entity types.",
136+
)
126137
return list(DEFAULT_ENTITY_TYPES)
127138
if "other" not in cleaned:
128139
cleaned.append("other")
@@ -533,10 +544,12 @@ def resolve_effective_config(kb_dir: Path) -> tuple[dict[str, Any], dict[str, st
533544
if not isinstance(kb_config, dict):
534545
kb_config = {}
535546
for key, value in kb_config.items():
536-
# A scalar explicitly nulled in config.yaml means "inherit": don't
537-
# let it clobber the global/default layer. The gate is on
538-
# GLOBAL_SCALAR_KEYS so non-scalar nulls (parallel_tool_calls) stay.
539-
if key in GLOBAL_SCALAR_KEYS and value is None:
547+
# A GLOBAL_SCALAR_KEYS value explicitly nulled — or an empty
548+
# entity_types list, since an empty vocabulary is meaningless —
549+
# means "inherit": don't clobber the global/default layer, and keep
550+
# `sources` reporting the layer that actually supplied the value. The
551+
# gate is on GLOBAL_SCALAR_KEYS so non-scalar nulls (parallel_tool_calls) stay.
552+
if key in GLOBAL_SCALAR_KEYS and (value is None or value == []):
540553
continue
541554
effective[key] = value
542555
if key in GLOBAL_SCALAR_KEYS:

0 commit comments

Comments
 (0)