Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bcg/config/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,12 @@ pipeline:
enable_thinking: false
fail_on_error: true
search_previous_turns: true
max_previous_windows: 4
max_previous_windows: 3
runtime:
evidence_mode: chunk
context_chars: 12000
# 0 = all recent nodes within context_chars; 2 = latest two Graph turns.
extraction_history_turns: 0
extraction_history_turns: 2
min_content_len: 0
tool_results:
max_search_results: 10
Expand Down
2 changes: 1 addition & 1 deletion bcg/config/defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pipeline:
# Node-extraction history window. 0 keeps every recent node that fits
# context_chars; positive values keep only nodes from the latest N
# non-empty Graph turns before applying the character budget.
extraction_history_turns: 0
extraction_history_turns: 2
min_content_len: 0
tool_results:
max_search_results: 10
Expand Down
2 changes: 1 addition & 1 deletion bcg/construct/hybrid/edge_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def normalize_edge_config(config: Mapping[str, Any] | None) -> dict[str, Any]:
"enable_thinking": bool(raw["enable_thinking"]),
"fail_on_error": bool(raw["fail_on_error"]),
"search_previous_turns": bool(raw["search_previous_turns"]),
"max_previous_windows": max(1, int(raw.get("max_previous_windows", 4))),
"max_previous_windows": max(1, int(raw.get("max_previous_windows", 3))),
}


Expand Down
15 changes: 8 additions & 7 deletions bcg/construct/hybrid/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,13 +336,14 @@ def build_chunk_extraction_prompt(
if is_assistant:
parts.append(_DECISION_DEFINITION)

parts.append(
"## Existing belief nodes (context — READ ONLY, no relations)\n"
"These nodes were extracted from EARLIER turns. Use them only to resolve "
"pronouns/vague references and to keep entity names and wording consistent. "
"Do NOT copy them as output.\n"
f"{GRAPH_NODES_PLACEHOLDER}\n"
)
if (graph_nodes or "").strip() not in {"", "[]", "null", "None"}:
parts.append(
"## Existing belief nodes (context — READ ONLY, no relations)\n"
"These nodes were extracted from EARLIER turns. Use them only to resolve "
"pronouns/vague references and to keep entity names and wording consistent. "
"Do NOT copy them as output.\n"
f"{GRAPH_NODES_PLACEHOLDER}\n"
)
if (
turn_content
and turn_content.strip()
Expand Down
67 changes: 60 additions & 7 deletions bcg/construct/unified/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def _clean_node(
ordinal: int,
*,
node_type: str,
preserve_model_tmp_id: bool = False,
) -> dict[str, Any] | None:
"""Validate / coerce one belief or decision object from the model."""
if not isinstance(raw, dict):
Expand All @@ -95,10 +96,16 @@ def _clean_node(
else:
return None

tmp = raw.get("tmp_id")
if not isinstance(tmp, str) or not tmp.strip():
tmp = f"n{ordinal}"
tmp = tmp.strip()
# Two-phase extraction does not need the model to invent temporary ids:
# relations are generated later against code-owned global ids. Keep the
# model-provided value only for the legacy single-response node+edge API,
# where relations in that same response must reference temporary ids.
prefix = "d" if node_type == "decision" else "n"
tmp = f"{prefix}{ordinal}"
if preserve_model_tmp_id:
model_tmp = raw.get("tmp_id")
if isinstance(model_tmp, str) and model_tmp.strip():
tmp = model_tmp.strip()

primary_text_key = "decision" if node_type == "decision" else "belief"
out: dict[str, Any] = {
Expand Down Expand Up @@ -952,10 +959,42 @@ def format_graph_nodes(
return "[\n" + ",\n".join(items) + "\n]"


def format_extraction_nodes(
nodes: list[dict[str, Any]], char_budget: int | None = 9000
) -> str:
"""Render only prior semantic content needed during node extraction."""
if not nodes:
return "[]"
ordered = sorted(nodes, key=lambda b: b.get("id", 0))
lines: list[str] = []
for node in ordered:
content = (
node.get("decision")
if node.get("node_type") == "decision"
else node.get("belief")
)
content = content or node.get("belief") or node.get("decision") or ""
if len(content) > 240:
content = content[:220] + " …"
lines.append(json.dumps({"content": content}, ensure_ascii=False))

total = sum(len(line) + 4 for line in lines)
omitted = 0
while char_budget is not None and lines and total > char_budget:
total -= len(lines[0]) + 4
lines.pop(0)
omitted += 1
items = (
[f" (... {omitted} earlier node(s) omitted for length ...)"] if omitted else []
)
items += [" " + line for line in lines]
return "[\n" + ",\n".join(items) + "\n]"


def format_relation_nodes(
nodes: list[dict[str, Any]], char_budget: int | None = 9000
) -> str:
"""Render only the semantic fields needed for Assistant relation judgment."""
"""Render only the semantic fields needed for relation judgment."""
if not nodes:
return "[]"
ordered = sorted(nodes, key=lambda b: b.get("id", 0))
Expand Down Expand Up @@ -1124,7 +1163,14 @@ def update_graph(
ordinal = 0

for b in parsed.get("beliefs", []) or []:
cb = _clean_node(b, mode, n_sentences, ordinal, node_type="belief")
cb = _clean_node(
b,
mode,
n_sentences,
ordinal,
node_type="belief",
preserve_model_tmp_id=True,
)
if cb is None:
continue
if cb["tmp_id"] in seen_tmp:
Expand All @@ -1147,7 +1193,14 @@ def update_graph(
out_nodes.extend(out_beliefs[parsed_belief_count:])

for d in parsed.get("decisions", []) or []:
cd = _clean_node(d, mode, n_sentences, ordinal, node_type="decision")
cd = _clean_node(
d,
mode,
n_sentences,
ordinal,
node_type="decision",
preserve_model_tmp_id=True,
)
if cd is None:
continue
if cd["tmp_id"] in seen_tmp:
Expand Down
Loading
Loading