From 0c5010f481ed98e52eb62d70ab2489a7c60fcdcb Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 16:03:18 +0800 Subject: [PATCH 01/17] perf(construct): trim graph prompt metadata --- bcg/config/config.example.yaml | 4 +-- bcg/config/defaults.yaml | 2 +- bcg/construct/hybrid/edge_generation.py | 2 +- bcg/construct/unified/extract.py | 34 +++++++++++++++++- bcg/construct/unified/prompts.py | 28 +++------------ bcg/construct/unified/stream.py | 18 ++++++---- bcg/model_config.example.json | 3 +- tests/test_belief_graph.py | 46 ++++++++++++++++++++++++- tests/test_env.py | 2 +- 9 files changed, 100 insertions(+), 39 deletions(-) diff --git a/bcg/config/config.example.yaml b/bcg/config/config.example.yaml index 178f54c7..59f14023 100644 --- a/bcg/config/config.example.yaml +++ b/bcg/config/config.example.yaml @@ -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 diff --git a/bcg/config/defaults.yaml b/bcg/config/defaults.yaml index abcc40ca..c93176fe 100644 --- a/bcg/config/defaults.yaml +++ b/bcg/config/defaults.yaml @@ -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 diff --git a/bcg/construct/hybrid/edge_generation.py b/bcg/construct/hybrid/edge_generation.py index 6d7dc5ce..71df50e2 100644 --- a/bcg/construct/hybrid/edge_generation.py +++ b/bcg/construct/hybrid/edge_generation.py @@ -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))), } diff --git a/bcg/construct/unified/extract.py b/bcg/construct/unified/extract.py index 3b29fcea..63d51bda 100644 --- a/bcg/construct/unified/extract.py +++ b/bcg/construct/unified/extract.py @@ -952,10 +952,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)) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index 3a400c50..672507d2 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -242,8 +242,6 @@ "belief": "", "stance": "asserted | recalled | speculated | judged", "entities": ["", "..."], - "tool_name": "", - "query": "", "supporting_excerpts": [""] } ], @@ -271,8 +269,6 @@ "belief": "", "stance": "asserted | recalled | speculated | judged", "entities": ["", "..."], - "tool_name": "", - "query": "", "supporting_sentence_indices": [0, 2] } ], @@ -300,7 +296,6 @@ 4. Each new belief needs a unique "tmp_id": n0, n1, n2, … in output order. 5. Each new decision needs a unique "tmp_id": d0, d1, d2, … in output order. 6. Empty beliefs / decisions / relations lists are OK when the content expresses none. -7. Every query-bearing tool call MUST produce one belief with exact "tool_name" and "query" properties. Code validates both fields against the source call. """ _HARD_CONSTRAINTS_SENTENCES = """\ @@ -313,7 +308,6 @@ 4. Each new belief needs a unique "tmp_id": n0, n1, n2, … in output order. 5. Each new decision needs a unique "tmp_id": d0, d1, d2, … in output order. 6. Empty beliefs / decisions / relations lists are OK when the sentences express none. -7. Every query-bearing tool call MUST produce one belief with exact "tool_name" and "query" properties. Code validates both fields against the source call. """ @@ -352,26 +346,18 @@ 'hedged guesses are "speculated".', ), "assistant": ( - "Extract coherent BELIEFS and DECISIONS from the ASSISTANT turn below. The content may " - "contain reasoning, tool-call syntax, and a final answer all together — read through ALL " - "of it and preserve the reasoning chain without creating tiny redundant nodes.", + "Extract coherent BELIEFS and DECISIONS from the ASSISTANT turn below. Preserve the " + "reasoning chain without creating tiny redundant nodes.", """\ ## Source role: ASSISTANT -The turn may mix internal reasoning, tool invocations, and the final answer. Extract: +Extract: - **Factual claims and intermediate conclusions** the assistant commits to (domain facts, numbers, diagnoses, derived states). - **Recommendations / advice** given to the user. - **Assessments** of the user's situation. - **Final decisions**: when the assistant gives a final answer, especially inside ``\\boxed{...}``, put it in ``decisions`` instead of ``beliefs``. -- **Tool calls**: extract every tool call as a belief — describe the assistant's information-seeking intent in natural language, capturing the tool name, the key parameters or constraints issued, and any hypothesis the call presupposes or commits to. -- **Query-bearing tool calls are mandatory**: for every ```` whose - ``arguments`` contains a string ``query`` (or ``q``), emit exactly one belief - for that call. Add ``tool_name`` and ``query`` properties to that belief and - copy both values exactly, character-for-character, from the tool call. Never - paraphrase, shorten, normalize, or omit either field. Do not add these - properties to beliefs derived from non-query content. - **Key reasoning steps that are falsifiable, reusable, or needed by later turns** — keep enough detail to reconstruct causal/dependency chains between user request, tool result, reasoning, and final answer. -Do NOT extract: pure procedure / planning filler ("Let me search next", "First I need to…") unless it encodes a substantive dependency; self-questions; raw tool-call JSON syntax / key names; or politeness. +Do NOT extract: pure procedure / planning filler ("Let me search next", "First I need to…") unless it encodes a substantive dependency; self-questions; or politeness. Write each belief in the third person ("The assistant…", "The user…") so it is self-contained. Resolve pronouns using the existing graph context when unambiguous.""", @@ -490,8 +476,6 @@ def build_update_prompt( "belief": "", "stance": "asserted | recalled | speculated | judged", "entities": ["", "..."], - "tool_name": "", - "query": "", "supporting_excerpts": [""] } ], @@ -516,8 +500,6 @@ def build_update_prompt( "belief": "", "stance": "asserted | recalled | speculated | judged", "entities": ["", "..."], - "tool_name": "", - "query": "", "supporting_sentence_indices": [0, 2] } ], @@ -542,7 +524,6 @@ def build_update_prompt( 4. Each new belief needs a unique "tmp_id": n0, n1, n2, … in output order. 5. Each new decision needs a unique "tmp_id": d0, d1, d2, … in output order. 6. Empty beliefs / decisions lists are OK when the content expresses none. -7. Every query-bearing tool call MUST produce one belief with exact "tool_name" and "query" properties. Code validates both fields against the source call. """ _HARD_CONSTRAINTS_SENTENCES_NODES = """\ @@ -555,7 +536,6 @@ def build_update_prompt( 4. Each new belief needs a unique "tmp_id": n0, n1, n2, … in output order. 5. Each new decision needs a unique "tmp_id": d0, d1, d2, … in output order. 6. Empty beliefs / decisions lists are OK when the sentences express none. -7. Every query-bearing tool call MUST produce one belief with exact "tool_name" and "query" properties. Code validates both fields against the source call. """ diff --git a/bcg/construct/unified/stream.py b/bcg/construct/unified/stream.py index 155ae240..77a232aa 100644 --- a/bcg/construct/unified/stream.py +++ b/bcg/construct/unified/stream.py @@ -61,6 +61,7 @@ extract_nodes, extract_relations, extract_rule_tool_result_nodes, + format_extraction_nodes, format_graph_edges, format_graph_nodes, format_relation_nodes, @@ -91,11 +92,11 @@ class StreamOptions: context_chars: int = 100000 # existing-nodes context budget # Limit node-extraction history to the latest N non-empty Graph turns before # applying ``context_chars``. Zero preserves the character-budget-only path. - extraction_history_turns: int = 0 + extraction_history_turns: int = 2 # Maximum number of non-empty historical turn windows considered while # looking for a current-to-prior relation. Assistant turns bundle these # windows into one request; other roles inspect them sequentially. - max_previous_windows: int = 4 + max_previous_windows: int = 3 # None selects the model-aware default (GPT-5.6-Luna -> none; otherwise # medium). A configured value is forwarded to every graph-model call. reasoning_effort: str | None = None @@ -309,7 +310,7 @@ def _node_extraction_history(self) -> list[dict[str, Any]]: ] def _formatted_node_extraction_history(self) -> str: - return format_graph_nodes( + return format_extraction_nodes( self._node_extraction_history(), char_budget=self.options.context_chars, ) @@ -1572,7 +1573,6 @@ def _extract_relations_for_layered_edge_window( displayed_layers.append( { "layer": layer_number, - "trajectory_index": trajectory_index, "node_ids": sorted(retained_ids), } ) @@ -1815,9 +1815,13 @@ def _extract_relations_for_edge_window( this is the backward-search stop condition. """ edge_window_ids = surviving_new_ids | previous_node_ids - graph_nodes_post = format_graph_nodes( - [node for node in active_nodes if node.get("id") in edge_window_ids], - char_budget=context_chars, + window_nodes = [ + node for node in active_nodes if node.get("id") in edge_window_ids + ] + graph_nodes_post = ( + format_relation_nodes(window_nodes, char_budget=context_chars) + if normalize_role(role) == "tool" + else format_graph_nodes(window_nodes, char_budget=context_chars) ) graph_edges_post = format_graph_edges( self.graph.relations, keep_ids=edge_window_ids diff --git a/bcg/model_config.example.json b/bcg/model_config.example.json index 2dbb0149..a7a39c05 100644 --- a/bcg/model_config.example.json +++ b/bcg/model_config.example.json @@ -90,11 +90,12 @@ "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, + "extraction_history_turns": 2, "min_content_len": 0 }, "incremental_merge": { diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 267a66d7..2ed6933f 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -40,6 +40,7 @@ extract_compact_tool_result_nodes_batch, extract_nodes, extract_rule_tool_result_nodes, + format_extraction_nodes, format_graph_nodes, format_relation_nodes, ) @@ -633,6 +634,27 @@ def test_unified_relation_nodes_include_only_id_and_content() -> None: ) assert json.loads(rendered) == [{"id": 7, "content": "A compact semantic fact."}] + + +def test_unified_extraction_history_includes_only_content() -> None: + rendered = format_extraction_nodes( + [ + { + "id": 7, + "node_type": "belief", + "belief": "A prior semantic fact.", + "role": "assistant", + "stance": "speculated", + "confidence": 0.6, + "entities": ["fact"], + "event_time": "2026-08-27T00:00:00Z", + "source": {"turn_index": 3}, + } + ], + char_budget=None, + ) + + assert json.loads(rendered) == [{"content": "A prior semantic fact."}] assert "stance" not in rendered assert "confidence" not in rendered assert "entities" not in rendered @@ -719,6 +741,10 @@ def fake_layered(*args: Any, **kwargs: Any) -> dict[str, Any]: 2, 3, ] + assert all( + "trajectory_index" not in layer + for layer in layered_calls[0]["candidate_layers"] + ) assert "Current reasoning." in layered_calls[0]["content"] assert "Visible reasoning." in layered_calls[0]["content"] assert "" not in layered_calls[0]["content"] @@ -845,6 +871,9 @@ def test_unified_node_extraction_prompt_omits_edges() -> None: assert "The latest node." in prompt assert "Existing relations" not in prompt assert '"from": 1' not in prompt + assert '"tool_name"' not in prompt + assert '"query"' not in prompt + assert "query-bearing tool call" not in prompt def test_stream_node_extraction_respects_context_chars( @@ -894,7 +923,7 @@ def fake_extract_nodes(*args: Any, **kwargs: Any) -> dict[str, Any]: ) builder.ingest_turn("user", "oldest " + "a" * 260) builder.ingest_turn("user", "newest " + "b" * 260) - expected = format_graph_nodes(builder.graph.active(), char_budget=450) + expected = format_extraction_nodes(builder.graph.active(), char_budget=450) builder.ingest_turn("user", "current") assert extraction_contexts[-1] == expected @@ -1065,6 +1094,7 @@ def test_grouped_parallel_results_pair_exact_calls_then_model_link_thinking( monkeypatch: pytest.MonkeyPatch, ) -> None: relation_windows: list[str] = [] + relation_contents: list[str] = [] def fake_extract_nodes(*args: Any, **kwargs: Any) -> dict[str, Any]: del args @@ -1127,6 +1157,7 @@ def fake_extract_relations(*args: Any, **kwargs: Any) -> dict[str, Any]: del args graph_nodes = str(kwargs["graph_nodes_str"]) relation_windows.append(graph_nodes) + relation_contents.append(str(kwargs["content"])) if thinking_id is not None and "Alpha result" in graph_nodes: current_ids = sorted(kwargs["new_node_ids"]) return { @@ -1203,6 +1234,19 @@ def fake_extract_relations(*args: Any, **kwargs: Any) -> dict[str, Any]: for relation in builder.graph.relations ) assert len(relation_windows) == 1 + assert json.loads(relation_windows[0]) == [ + {"id": node["id"], "content": node["belief"]} + for node in [ + next( + graph_node + for graph_node in builder.graph.active() + if graph_node["id"] == thinking_id + ), + *results, + ] + ] + assert "" in relation_contents[0] + assert "Alpha result" in relation_contents[0] assert "The assistant is comparing alpha and beta." in relation_windows[0] assert "using web_search" not in relation_windows[0] assert event["edge_attempts"][0]["pairing_strategy"] == "tool_call_id" diff --git a/tests/test_env.py b/tests/test_env.py index 3f35bae4..b406e01c 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -195,7 +195,7 @@ def test_hybrid_configs_resolve_all_credentials_from_environment( assert embedding["api_key"] == "embedding-secret" assert extractor["api_key"] == "local-secret" assert edge["api_key"] == "local-secret" - assert edge["max_previous_windows"] == 4 + assert edge["max_previous_windows"] == 3 def test_hybrid_edge_config_accepts_bounded_historical_window_override( From 912c049ff3c1dcbf211feb0ad19661e3462b57df Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 16:51:19 +0800 Subject: [PATCH 02/17] perf(construct): omit raw content from relation prompts --- bcg/construct/unified/prompts.py | 58 +++++++++++++++++++------------- bcg/construct/unified/stream.py | 15 ++++++--- tests/test_belief_graph.py | 8 ++--- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index 672507d2..a8741f34 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -802,16 +802,21 @@ def build_relation_extraction_prompt( "into the graph. Your job is to identify meaningful semantic relations inside the " "candidate node window below: current-turn surviving new nodes plus at most one " "candidate prior turn, or current-turn nodes only.\n", - f"## Current turn ({role})\n" + CONTENT_PLACEHOLDER + "\n", - "## Candidate node window\n" - "The graph below is deliberately limited to the current turn's surviving new nodes " - "and one candidate prior turn's surviving nodes. It is not the full graph.\n\n" - "### Candidate nodes\n" + GRAPH_NODES_PLACEHOLDER + "\n\n" - "### Existing relations\n" + GRAPH_EDGES_PLACEHOLDER + "\n", - "## Nodes from this turn\n" + NEW_NODE_IDS_PLACEHOLDER + "\n", - _RELATION_EDGE_RULES, - _RELATION_OUTPUT_FORMAT, ] + if content.strip(): + parts.append(f"## Current turn ({role})\n" + CONTENT_PLACEHOLDER + "\n") + parts.extend( + [ + "## Candidate node window\n" + "The graph below is deliberately limited to the current turn's surviving new nodes " + "and one candidate prior turn's surviving nodes. It is not the full graph.\n\n" + "### Candidate nodes\n" + GRAPH_NODES_PLACEHOLDER + "\n\n" + "### Existing relations\n" + GRAPH_EDGES_PLACEHOLDER + "\n", + "## Nodes from this turn\n" + NEW_NODE_IDS_PLACEHOLDER + "\n", + _RELATION_EDGE_RULES, + _RELATION_OUTPUT_FORMAT, + ] + ) prompt = "\n".join(parts) prompt = prompt.replace(CONTENT_PLACEHOLDER, content or "") prompt = prompt.replace(GRAPH_NODES_PLACEHOLDER, graph_nodes or "[]") @@ -880,21 +885,28 @@ def build_layered_relation_extraction_prompt( parts: list[str] = [ "# Task", "Link the current Assistant belief nodes to the most relevant prior layer.", - f"## Current Assistant reasoning ({role})\n" + CONTENT_PLACEHOLDER + "\n", - "## Candidate previous layers\n" - "Layer 1 is the nearest non-empty Graph turn; larger numbers are older. " - "Layer membership is authoritative.\n" + candidate_layers + "\n", - "## Candidate nodes\n" - + GRAPH_NODES_PLACEHOLDER - + "\n\n## Existing relations\n" - + GRAPH_EDGES_PLACEHOLDER - + "\n", - "## Current-turn node ids\n" + NEW_NODE_IDS_PLACEHOLDER + "\n", - _LAYERED_RELATION_EDGE_RULES, - "## Layer selection\n" - "Compare all candidates in one pass. Cross-turn relations may use ZERO OR " - "ONE previous layer, never a mixture. Select null when none is meaningful.\n", ] + if content.strip(): + parts.append( + f"## Current Assistant reasoning ({role})\n" + CONTENT_PLACEHOLDER + "\n" + ) + parts.extend( + [ + "## Candidate previous layers\n" + "Layer 1 is the nearest non-empty Graph turn; larger numbers are older. " + "Layer membership is authoritative.\n" + candidate_layers + "\n", + "## Candidate nodes\n" + + GRAPH_NODES_PLACEHOLDER + + "\n\n## Existing relations\n" + + GRAPH_EDGES_PLACEHOLDER + + "\n", + "## Current-turn node ids\n" + NEW_NODE_IDS_PLACEHOLDER + "\n", + _LAYERED_RELATION_EDGE_RULES, + "## Layer selection\n" + "Compare all candidates in one pass. Cross-turn relations may use ZERO OR " + "ONE previous layer, never a mixture. Select null when none is meaningful.\n", + ] + ) if validation_feedback: parts.append( "## Validation feedback from the previous attempt\n" diff --git a/bcg/construct/unified/stream.py b/bcg/construct/unified/stream.py index 77a232aa..42b12298 100644 --- a/bcg/construct/unified/stream.py +++ b/bcg/construct/unified/stream.py @@ -36,7 +36,7 @@ from typing import Any from .._shared.roles import normalize_role -from .._shared.tool_queries import extract_tool_calls, strip_valid_tool_calls +from .._shared.tool_queries import extract_tool_calls from .._shared.tool_results import extract_tool_results from .._shared.writers import ArtifactWriter, EventRecorder from ..hybrid.named_entities import normalize_entity_config @@ -1632,9 +1632,10 @@ def cross_layer(relation: dict[str, Any]) -> int | None: self.client, self.model, role=role, - # Tool calls already have deterministic belief nodes. Keep only - # Thinking/plain reasoning as semantic evidence for relations. - content=strip_valid_tool_calls(content), + # Current Assistant semantics are already represented by the + # surviving belief nodes. Avoid duplicating the raw reasoning in + # every relation request. + content="", graph_nodes_str=graph_nodes_post, graph_edges_str=graph_edges_post, new_node_ids=displayed_new_ids, @@ -1837,7 +1838,11 @@ def _extract_relations_for_edge_window( self.client, self.model, role=role, - content=content, + # Tool Result semantics are already represented by the extracted + # current-turn nodes. The deterministic provenance pass separately + # pairs each result with its exact Tool Call, so the raw result text + # is redundant in this model-based relation request. + content="" if normalize_role(role) == "tool" else content, graph_nodes_str=graph_nodes_post, graph_edges_str=graph_edges_post, new_node_ids=surviving_new_ids, diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 2ed6933f..57317495 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -745,10 +745,7 @@ def fake_layered(*args: Any, **kwargs: Any) -> dict[str, Any]: "trajectory_index" not in layer for layer in layered_calls[0]["candidate_layers"] ) - assert "Current reasoning." in layered_calls[0]["content"] - assert "Visible reasoning." in layered_calls[0]["content"] - assert "" not in layered_calls[0]["content"] - assert "secret query" not in layered_calls[0]["content"] + assert layered_calls[0]["content"] == "" assert '"stance"' not in layered_calls[0]["graph_nodes_str"] assert '"entities"' not in layered_calls[0]["graph_nodes_str"] assert event["edge_attempts"][0]["validation_passed"] is True @@ -1245,8 +1242,7 @@ def fake_extract_relations(*args: Any, **kwargs: Any) -> dict[str, Any]: *results, ] ] - assert "" in relation_contents[0] - assert "Alpha result" in relation_contents[0] + assert relation_contents[0] == "" assert "The assistant is comparing alpha and beta." in relation_windows[0] assert "using web_search" not in relation_windows[0] assert event["edge_attempts"][0]["pairing_strategy"] == "tool_call_id" From 4e4b3b49f67e8a96d60891b7cf386017ba82e353 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 17:27:50 +0800 Subject: [PATCH 03/17] perf(construct): assign extraction temp ids in code --- bcg/construct/hybrid/prompts.py | 15 +++---- bcg/construct/unified/extract.py | 33 +++++++++++++--- bcg/construct/unified/prompts.py | 39 ++++++++++--------- tests/test_belief_graph.py | 67 +++++++++++++++++++++++++++++++- 4 files changed, 122 insertions(+), 32 deletions(-) diff --git a/bcg/construct/hybrid/prompts.py b/bcg/construct/hybrid/prompts.py index 06cf3502..9cd46a1d 100644 --- a/bcg/construct/hybrid/prompts.py +++ b/bcg/construct/hybrid/prompts.py @@ -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() diff --git a/bcg/construct/unified/extract.py b/bcg/construct/unified/extract.py index 63d51bda..f925b7e6 100644 --- a/bcg/construct/unified/extract.py +++ b/bcg/construct/unified/extract.py @@ -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): @@ -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] = { @@ -1156,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: @@ -1179,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: diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index a8741f34..c4bf5bd0 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -167,6 +167,13 @@ {GRAPH_NODES_PLACEHOLDER} """ + +def _has_existing_nodes(graph_nodes: str | None) -> bool: + """Return whether a rendered graph-node block contains any nodes.""" + + return (graph_nodes or "").strip() not in {"", "[]", "null", "None"} + + _FORWARD_EDGE_RULES = """\ ## Relations between nodes After creating the NEW beliefs/decisions for this turn, emit relations that connect: @@ -433,9 +440,10 @@ def build_update_prompt( _BELIEF_DEFINITION, _STANCE_DEFINITION, guidance + "\n", - _GRAPH_CONTEXT_BLOCK, - _FORWARD_EDGE_RULES, ] + if _has_existing_nodes(graph_nodes): + parts.append(_GRAPH_CONTEXT_BLOCK) + parts.append(_FORWARD_EDGE_RULES) if mode == "excerpt": parts.append(_HARD_CONSTRAINTS_EXCERPT) parts.append(_OUTPUT_FORMAT_EXCERPT) @@ -472,7 +480,6 @@ def build_update_prompt( { "beliefs": [ { - "tmp_id": "n0", "belief": "", "stance": "asserted | recalled | speculated | judged", "entities": ["", "..."], @@ -481,7 +488,6 @@ def build_update_prompt( ], "decisions": [ { - "tmp_id": "d0", "decision": "", "stance": "asserted | recalled | speculated | judged", "entities": ["", "..."], @@ -496,7 +502,6 @@ def build_update_prompt( { "beliefs": [ { - "tmp_id": "n0", "belief": "", "stance": "asserted | recalled | speculated | judged", "entities": ["", "..."], @@ -505,7 +510,6 @@ def build_update_prompt( ], "decisions": [ { - "tmp_id": "d0", "decision": "", "stance": "asserted | recalled | speculated | judged", "entities": ["", "..."], @@ -521,9 +525,7 @@ def build_update_prompt( 2. Do NOT add information not present in the content. No outside knowledge. 3. Every belief and decision MUST have at least one supporting excerpt — a VERBATIM, CONTIGUOUS substring copied character-for-character from the content. No excerpt → drop that node. -4. Each new belief needs a unique "tmp_id": n0, n1, n2, … in output order. -5. Each new decision needs a unique "tmp_id": d0, d1, d2, … in output order. -6. Empty beliefs / decisions lists are OK when the content expresses none. +4. Empty beliefs / decisions lists are OK when the content expresses none. """ _HARD_CONSTRAINTS_SENTENCES_NODES = """\ @@ -533,9 +535,7 @@ def build_update_prompt( 3. Every belief and decision MUST list the indices of the COMPLETE sentence(s) that support it in "supporting_sentence_indices" (use the [k] indices shown). Evidence is always a whole sentence. If the whole group supports it, list all its indices. -4. Each new belief needs a unique "tmp_id": n0, n1, n2, … in output order. -5. Each new decision needs a unique "tmp_id": d0, d1, d2, … in output order. -6. Empty beliefs / decisions lists are OK when the sentences express none. +4. Empty beliefs / decisions lists are OK when the sentences express none. """ @@ -565,8 +565,9 @@ def build_node_extraction_prompt( _BELIEF_DEFINITION, _STANCE_DEFINITION, guidance + "\n", - _NODE_GRAPH_CONTEXT_BLOCK, ] + if _has_existing_nodes(graph_nodes): + parts.append(_NODE_GRAPH_CONTEXT_BLOCK) if mode == "excerpt": parts.append(_HARD_CONSTRAINTS_EXCERPT_NODES) parts.append(_OUTPUT_FORMAT_EXCERPT_NODES) @@ -626,6 +627,12 @@ def build_assistant_tool_result_extraction_prompt( f"{assistant_sentences_block or ''}" ) ) + graph_context = "" + if _has_existing_nodes(graph_nodes): + graph_context = f"""## Existing belief nodes (read only) +{graph_nodes} + +""" return f"""# Task Extract belief/decision nodes from TWO ORDERED SOURCE LAYERS in one response: 1. the Assistant turn; @@ -644,9 +651,7 @@ def build_assistant_tool_result_extraction_prompt( Assistant's substantive reasoning, hypotheses, conclusions, and decisions, but do not copy raw tool-call syntax as semantic beliefs. -## Existing belief nodes (read only) -{graph_nodes or "[]"} - +{graph_context} {assistant_input} ## Tool Result items @@ -662,7 +667,6 @@ def build_assistant_tool_result_extraction_prompt( "assistant": {{ "beliefs": [ {{ - "tmp_id": "n0", "belief": "", "stance": "asserted | recalled | speculated | judged", "entities": ["", "..."], @@ -671,7 +675,6 @@ def build_assistant_tool_result_extraction_prompt( ], "decisions": [ {{ - "tmp_id": "d0", "decision": "", "stance": "asserted | recalled | speculated | judged", "entities": ["", "..."], diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 57317495..03d9df72 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -873,6 +873,70 @@ def test_unified_node_extraction_prompt_omits_edges() -> None: assert "query-bearing tool call" not in prompt +def test_unified_node_extraction_prompt_omits_empty_context_and_tmp_ids() -> None: + prompt = build_node_extraction_prompt( + "user", + mode="sentences", + sentences_block="[0] The user asks a question.", + graph_nodes="[]", + ) + + assert prompt is not None + assert "Existing belief nodes" not in prompt + assert '"tmp_id"' not in prompt + assert '"stance"' in prompt + assert '"entities"' in prompt + + +def test_unified_node_extraction_assigns_code_owned_tmp_ids( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def fake_call(*args: Any, **kwargs: Any) -> str: + del args, kwargs + return json.dumps( + { + "beliefs": [ + { + "tmp_id": "model-chosen-id", + "belief": "First belief.", + "stance": "asserted", + "entities": ["First"], + "supporting_sentence_indices": [0], + }, + { + "tmp_id": "model-chosen-id", + "belief": "Second belief.", + "stance": "judged", + "entities": ["Second"], + "supporting_sentence_indices": [1], + }, + ], + "decisions": [ + { + "tmp_id": "another-model-id", + "decision": "Final decision.", + "stance": "judged", + "entities": ["Final"], + "supporting_sentence_indices": [1], + } + ], + } + ) + + monkeypatch.setattr("bcg.construct.unified.extract.llm.call_model", fake_call) + result = extract_nodes( + object(), + "graph-model", + role="assistant", + mode="sentences", + sentences=["First belief.", "Second belief and final decision."], + ) + + assert [node["tmp_id"] for node in result["nodes"]] == ["n0", "n1", "d2"] + assert result["nodes"][1]["stance"] == "judged" + assert result["nodes"][1]["entities"] == ["Second"] + + def test_stream_node_extraction_respects_context_chars( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1654,7 +1718,8 @@ def no_relations(*args: Any, **kwargs: Any) -> dict[str, Any]: assert len(extraction_prompts) == 2 assistant_prompt, tool_prompt = extraction_prompts - assert "## Existing belief nodes" in assistant_prompt + assert "## Existing belief nodes" not in assistant_prompt + assert '"tmp_id"' not in assistant_prompt assert "Alpha may be relevant." in assistant_prompt assert "alpha query" not in assistant_prompt assert "Items:" in tool_prompt From f33b5609b9ff9dc46ae47c930535c64db4a977c7 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 17:39:49 +0800 Subject: [PATCH 04/17] perf(construct): trim initial user extraction prompt --- bcg/construct/unified/prompts.py | 114 ++++++++++++++++++++++++++----- tests/test_belief_graph.py | 6 ++ 2 files changed, 102 insertions(+), 18 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index c4bf5bd0..ea14b0c4 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -92,7 +92,7 @@ - generic filler: "the content", "this turn", "the answer", "the issue", "the thing", "the result"; - bare generic nouns: car, file, code, graph, node, edge, model, prompt, time, data, message, unless they are qualified enough to be identifiable; - abstract feelings or vague concepts unless the belief is specifically about that concept; -- temporal expressions as entities; preserve temporal information in the belief or decision text instead (event metadata is assigned by the graph builder); +- temporal expressions as entities; preserve temporal information in the belief or decision text instead; - duplicate surface forms referring to the same entity in one belief. Use the most specific form supported by the CURRENT turn and existing graph context. @@ -123,8 +123,6 @@ - **judged** — evaluative conclusion, recommendation, ranking, diagnosis, or selected option ("most likely", "best answer is", "I recommend X"). -NOTE: do NOT output a confidence number — confidence is assigned downstream by code -rules based on (role, stance). """ _GRAPH_CONTEXT_BLOCK = f"""\ @@ -519,6 +517,34 @@ def build_update_prompt( } """ +_OUTPUT_FORMAT_EXCERPT_USER_NODES = """\ +## Output (JSON only — no markdown fences, no commentary) +{ + "beliefs": [ + { + "belief": "", + "stance": "asserted | recalled | speculated | judged", + "entities": ["", "..."], + "supporting_excerpts": [""] + } + ] +} +""" + +_OUTPUT_FORMAT_SENTENCES_USER_NODES = """\ +## Output (JSON only — no markdown fences, no commentary) +{ + "beliefs": [ + { + "belief": "", + "stance": "asserted | recalled | speculated | judged", + "entities": ["", "..."], + "supporting_sentence_indices": [0, 2] + } + ] +} +""" + _HARD_CONSTRAINTS_EXCERPT_NODES = """\ ## Hard constraints 1. Preserve named entities, numbers, dates, quantities EXACTLY as written (incl. unusual punctuation like "!Kung"). @@ -538,6 +564,25 @@ def build_update_prompt( 4. Empty beliefs / decisions lists are OK when the sentences express none. """ +_HARD_CONSTRAINTS_EXCERPT_USER_NODES = """\ +## Hard constraints +1. Preserve named entities, numbers, dates, quantities EXACTLY as written (incl. unusual punctuation like "!Kung"). +2. Do NOT add information not present in the content. No outside knowledge. +3. Every belief MUST have at least one supporting excerpt — a VERBATIM, CONTIGUOUS substring copied + character-for-character from the content. No excerpt → drop that belief. +4. An empty beliefs list is OK when the content expresses none. +""" + +_HARD_CONSTRAINTS_SENTENCES_USER_NODES = """\ +## Hard constraints +1. Preserve named entities, numbers, dates, quantities EXACTLY as written (incl. unusual punctuation like "!Kung"). +2. Do NOT add information not present in the sentences. No outside knowledge. +3. Every belief MUST list the indices of the COMPLETE sentence(s) that support it in + "supporting_sentence_indices" (use the [k] indices shown). Evidence is always a whole sentence. + If the whole group supports it, list all its indices. +4. An empty beliefs list is OK when the sentences express none. +""" + def build_node_extraction_prompt( role: str, @@ -556,21 +601,46 @@ def build_node_extraction_prompt( return None task_line, guidance, _stance_hint = _GUIDANCE[key] - parts: list[str] = [ - "# Task", - task_line, - "\nYou maintain a belief graph INCREMENTALLY. From the CURRENT turn, output only the NEW " - "belief/decision nodes. Relations will be extracted in a separate step. " - "Existing nodes (shown below) must not be repeated.\n", - _BELIEF_DEFINITION, - _STANCE_DEFINITION, - guidance + "\n", - ] - if _has_existing_nodes(graph_nodes): + has_existing_nodes = _has_existing_nodes(graph_nodes) + if key == "user" and not has_existing_nodes: + task_intro = ( + "Extract coherent, self-contained beliefs from the current USER turn only. " + "Preserve distinct constraints without fragmenting one coherent request." + ) + else: + task_intro = task_line + + parts: list[str] = ["# Task", task_intro] + if key != "user" or has_existing_nodes: + node_kinds = "belief" if key == "user" else "belief/decision" + parts.append( + "\nMaintain the belief graph incrementally. Output only NEW " + f"{node_kinds} nodes from the CURRENT turn; relation extraction is separate. " + "Do not repeat existing nodes.\n" + ) + belief_definition = _BELIEF_DEFINITION + stance_definition = _STANCE_DEFINITION + if key == "user": + belief_definition = belief_definition.replace( + "belief or decision text", "belief text" + ) + stance_definition = stance_definition.replace( + "per belief or decision", "per belief" + ) + parts.extend([belief_definition, stance_definition, guidance + "\n"]) + if has_existing_nodes: parts.append(_NODE_GRAPH_CONTEXT_BLOCK) if mode == "excerpt": - parts.append(_HARD_CONSTRAINTS_EXCERPT_NODES) - parts.append(_OUTPUT_FORMAT_EXCERPT_NODES) + parts.append( + _HARD_CONSTRAINTS_EXCERPT_USER_NODES + if key == "user" + else _HARD_CONSTRAINTS_EXCERPT_NODES + ) + parts.append( + _OUTPUT_FORMAT_EXCERPT_USER_NODES + if key == "user" + else _OUTPUT_FORMAT_EXCERPT_NODES + ) parts.append(f"## Current turn content\n{CONTENT_PLACEHOLDER}\n") else: parts.append( @@ -578,8 +648,16 @@ def build_node_extraction_prompt( "The current turn's content was split into COMPLETE sentences with stable indices [k]. " "Reference them in supporting_sentence_indices; evidence is always a whole sentence.\n" ) - parts.append(_HARD_CONSTRAINTS_SENTENCES_NODES) - parts.append(_OUTPUT_FORMAT_SENTENCES_NODES) + parts.append( + _HARD_CONSTRAINTS_SENTENCES_USER_NODES + if key == "user" + else _HARD_CONSTRAINTS_SENTENCES_NODES + ) + parts.append( + _OUTPUT_FORMAT_SENTENCES_USER_NODES + if key == "user" + else _OUTPUT_FORMAT_SENTENCES_NODES + ) parts.append(f"## Current turn sentences\n{SENTENCES_PLACEHOLDER}\n") prompt = "\n".join(parts) diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 03d9df72..24c51770 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -884,8 +884,14 @@ def test_unified_node_extraction_prompt_omits_empty_context_and_tmp_ids() -> Non assert prompt is not None assert "Existing belief nodes" not in prompt assert '"tmp_id"' not in prompt + assert '"decisions"' not in prompt + assert "decision" not in prompt.lower() assert '"stance"' in prompt assert '"entities"' in prompt + assert '"supporting_sentence_indices"' in prompt + assert "confidence is assigned downstream" not in prompt + assert "event metadata is assigned" not in prompt + assert "You maintain a belief graph INCREMENTALLY" not in prompt def test_unified_node_extraction_assigns_code_owned_tmp_ids( From 6eee905dcd71b7fd47fc262d0311f974a0260366 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 17:47:45 +0800 Subject: [PATCH 05/17] perf(construct): compact user extraction instructions --- bcg/construct/unified/prompts.py | 80 +++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index ea14b0c4..4a081113 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -125,6 +125,42 @@ """ +_USER_BELIEF_DEFINITION = """\ +## What is a belief +A belief is a self-contained memory or reasoning unit, usually shaped like: + + +Preserve the most specific supported wording. Each belief must express one +reusable semantic unit and remain understandable outside the original turn. + +## Granularity +Merge clauses that jointly define one setup, condition, event, or causal step. +Split only propositions that can be independently confirmed, contradicted, or +reused. Keep claims with different epistemic status separate. + +## Entities +For each belief, list specific named or uniquely qualified people, +organizations, places, products, files, tools, models, APIs, datasets, and +concepts explicitly present in it. Exclude pronouns, temporal expressions, +generic nouns, vague concepts, and duplicates. Use [] when none exists. +""" + +_USER_STANCE_DEFINITION = """\ +## Stance +Choose one per belief: asserted = direct statement; recalled = explicit memory; +speculated = uncertain possibility; judged = assessment, recommendation, or +conclusion. +""" + +_USER_GUIDANCE = """\ +## Source role: USER +Extract the user's substantive request, facts, events, preferences, plans, +constraints, questions, corrections, and updates. Rewrite questions as +self-contained statements about what the user wants to know. Preserve related +constraints together. Write in the third person about "The user" or the named +subject. Skip greetings and purely cosmetic instructions. +""" + _GRAPH_CONTEXT_BLOCK = f"""\ ## Existing belief graph (context — READ ONLY) These NODES and EDGES were already extracted from EARLIER turns. Use them only to: @@ -566,21 +602,20 @@ def build_update_prompt( _HARD_CONSTRAINTS_EXCERPT_USER_NODES = """\ ## Hard constraints -1. Preserve named entities, numbers, dates, quantities EXACTLY as written (incl. unusual punctuation like "!Kung"). -2. Do NOT add information not present in the content. No outside knowledge. -3. Every belief MUST have at least one supporting excerpt — a VERBATIM, CONTIGUOUS substring copied - character-for-character from the content. No excerpt → drop that belief. -4. An empty beliefs list is OK when the content expresses none. +Preserve names, numbers, dates, quantities, and unusual punctuation exactly. +Use only the current content; do not add outside knowledge. Every belief must +include at least one VERBATIM, CONTIGUOUS "supporting_excerpts" substring copied +from the content. Drop beliefs without an excerpt. An empty beliefs list is valid +when the input expresses none. """ _HARD_CONSTRAINTS_SENTENCES_USER_NODES = """\ ## Hard constraints -1. Preserve named entities, numbers, dates, quantities EXACTLY as written (incl. unusual punctuation like "!Kung"). -2. Do NOT add information not present in the sentences. No outside knowledge. -3. Every belief MUST list the indices of the COMPLETE sentence(s) that support it in - "supporting_sentence_indices" (use the [k] indices shown). Evidence is always a whole sentence. - If the whole group supports it, list all its indices. -4. An empty beliefs list is OK when the sentences express none. +Preserve names, numbers, dates, quantities, and unusual punctuation exactly. +Use only the current indexed sentences; do not add outside knowledge. For every +belief, return all COMPLETE sentence indices that directly support it in +"supporting_sentence_indices". Drop beliefs without supporting sentences. An +empty beliefs list is valid when the input expresses none. """ @@ -620,14 +655,12 @@ def build_node_extraction_prompt( ) belief_definition = _BELIEF_DEFINITION stance_definition = _STANCE_DEFINITION + role_guidance = guidance + "\n" if key == "user": - belief_definition = belief_definition.replace( - "belief or decision text", "belief text" - ) - stance_definition = stance_definition.replace( - "per belief or decision", "per belief" - ) - parts.extend([belief_definition, stance_definition, guidance + "\n"]) + belief_definition = _USER_BELIEF_DEFINITION + stance_definition = _USER_STANCE_DEFINITION + role_guidance = _USER_GUIDANCE + parts.extend([belief_definition, stance_definition, role_guidance]) if has_existing_nodes: parts.append(_NODE_GRAPH_CONTEXT_BLOCK) if mode == "excerpt": @@ -643,11 +676,12 @@ def build_node_extraction_prompt( ) parts.append(f"## Current turn content\n{CONTENT_PLACEHOLDER}\n") else: - parts.append( - "## Sentence input\n" - "The current turn's content was split into COMPLETE sentences with stable indices [k]. " - "Reference them in supporting_sentence_indices; evidence is always a whole sentence.\n" - ) + if key != "user": + parts.append( + "## Sentence input\n" + "The current turn's content was split into COMPLETE sentences with stable indices [k]. " + "Reference them in supporting_sentence_indices; evidence is always a whole sentence.\n" + ) parts.append( _HARD_CONSTRAINTS_SENTENCES_USER_NODES if key == "user" From 37fb31a62239b464a27f0a086bf7160eef0f68b7 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 17:55:42 +0800 Subject: [PATCH 06/17] fix(construct): preserve reusable user constraints --- bcg/construct/unified/prompts.py | 8 ++++++-- tests/test_belief_graph.py | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index 4a081113..e5ccdb29 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -136,13 +136,17 @@ ## Granularity Merge clauses that jointly define one setup, condition, event, or causal step. Split only propositions that can be independently confirmed, contradicted, or -reused. Keep claims with different epistemic status separate. +reused. Do not merge independently searchable numbered clues solely because +they describe the same target. Keep claims with different epistemic status +separate. ## Entities For each belief, list specific named or uniquely qualified people, organizations, places, products, files, tools, models, APIs, datasets, and concepts explicitly present in it. Exclude pronouns, temporal expressions, -generic nouns, vague concepts, and duplicates. Use [] when none exists. +bare generic nouns, vague concepts, and duplicates. Include task-defining +qualified roles when they distinguish reusable constraints, such as "winning +team", "first poet", or "target ODI match". Use [] when none exists. """ _USER_STANCE_DEFINITION = """\ diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 24c51770..34d7015c 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -892,6 +892,9 @@ def test_unified_node_extraction_prompt_omits_empty_context_and_tmp_ids() -> Non assert "confidence is assigned downstream" not in prompt assert "event metadata is assigned" not in prompt assert "You maintain a belief graph INCREMENTALLY" not in prompt + assert "independently searchable numbered clues" in prompt + assert "task-defining" in prompt + assert "qualified roles" in prompt def test_unified_node_extraction_assigns_code_owned_tmp_ids( From 78f74af78fb80d8f2ad6339f31c3afd4da1032c8 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 20:48:16 +0800 Subject: [PATCH 07/17] perf(construct): compact assistant extraction prompt --- bcg/construct/unified/prompts.py | 107 +++++++++++++++++++++++++++---- tests/test_belief_graph.py | 27 ++++++++ 2 files changed, 122 insertions(+), 12 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index e5ccdb29..34b3ca32 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -165,6 +165,80 @@ subject. Skip greetings and purely cosmetic instructions. """ +_ASSISTANT_TASK_LINE = ( + "Extract coherent, self-contained BELIEFS and final DECISIONS from the current " + "ASSISTANT turn only. Preserve reusable reasoning without recording procedural " + "filler. Output only new nodes; relations are extracted separately." +) + +_ASSISTANT_BELIEF_DEFINITION = """\ +## What is a belief +- Factual claims and intermediate conclusions the assistant commits to. +- Hypotheses or reasoning steps that are falsifiable, reusable, or needed by later turns. +- Recommendations, assessments, diagnoses, rankings, and derived states. + +## What is a decision +- The assistant's final selected answer, option, result, or explicit final conclusion, especially content inside `\\boxed{...}`. +- A decision must be self-contained; never emit only a bare label, option, verdict, or value. +- Do not emit the same final answer as both a belief and a decision. + +## Granularity +Each belief must express one reusable central claim and remain understandable outside the turn. Keep tightly coupled qualifiers, premises, conditions, and results together when splitting would lose their dependency. Split independent propositions that can be confirmed, contradicted, or reused separately. Keep claims with different epistemic status separate. +""" + +_ASSISTANT_STANCE_DEFINITION = """\ +## Stance +Choose one per belief or decision: asserted = committed claim or final answer; recalled = explicit memory; speculated = uncertain hypothesis; judged = assessment, recommendation, diagnosis, ranking, or selected option. +""" + +_ASSISTANT_GUIDANCE = """\ +## Entities +List only specific named or uniquely qualified entities explicitly present in each belief or decision: people, organizations, places, products, datasets, files, tools, models, APIs, variables, and distinguishable concepts. Exclude pronouns, temporal expressions, bare generic nouns, vague concepts, and duplicates. Use [] when none exists. + +## Source role: ASSISTANT +- Write beliefs in the third person about "The assistant", "The user", or the named subject. +- Skip politeness, self-questions, and pure procedure or planning such as "Let me search" unless it states a substantive hypothesis or dependency. +- Tool-call JSON is extracted deterministically by code. Do not reproduce raw tool-call syntax or create semantic beliefs from its field names. +""" + +_ASSISTANT_HARD_CONSTRAINTS_SENTENCES_NODES = """\ +## Hard constraints +- Preserve names, numbers, dates, quantities, versions, and unusual punctuation exactly. +- Do not add outside knowledge. +- Every belief and decision must list all complete current-turn sentence indices that directly support it in `supporting_sentence_indices`; drop nodes without supporting sentences. +- Empty beliefs and decisions lists are valid. +""" + +_ASSISTANT_OUTPUT_FORMAT_SENTENCES_NODES = """\ +## Output (JSON only — no markdown fences, no commentary) +{ + "beliefs": [ + { + "belief": "", + "stance": "asserted | recalled | speculated | judged", + "entities": ["", "..."], + "supporting_sentence_indices": [0, 2] + } + ], + "decisions": [ + { + "decision": "", + "stance": "asserted | recalled | speculated | judged", + "entities": ["", "..."], + "supporting_sentence_indices": [3] + } + ] +} +""" + +_ASSISTANT_NODE_GRAPH_CONTEXT_BLOCK = f"""\ +## Existing belief nodes (context — READ ONLY) +Use historical nodes only to resolve references and keep entity wording consistent. Extract only from the CURRENT turn; never copy an old node merely because it appears here. If the current turn explicitly restates, confirms, corrects, or updates an old belief, emit a new evidence-bearing node. + +### Existing nodes +{GRAPH_NODES_PLACEHOLDER} +""" + _GRAPH_CONTEXT_BLOCK = f"""\ ## Existing belief graph (context — READ ONLY) These NODES and EDGES were already extracted from EARLIER turns. Use them only to: @@ -644,8 +718,10 @@ def build_node_extraction_prompt( if key == "user" and not has_existing_nodes: task_intro = ( "Extract coherent, self-contained beliefs from the current USER turn only. " - "Preserve distinct constraints without fragmenting one coherent request." + "Preserve distinct constraints without fragmenting one coherent request.\n" ) + elif key == "assistant": + task_intro = _ASSISTANT_TASK_LINE else: task_intro = task_line @@ -664,9 +740,17 @@ def build_node_extraction_prompt( belief_definition = _USER_BELIEF_DEFINITION stance_definition = _USER_STANCE_DEFINITION role_guidance = _USER_GUIDANCE + elif key == "assistant": + belief_definition = _ASSISTANT_BELIEF_DEFINITION + stance_definition = _ASSISTANT_STANCE_DEFINITION + role_guidance = _ASSISTANT_GUIDANCE parts.extend([belief_definition, stance_definition, role_guidance]) if has_existing_nodes: - parts.append(_NODE_GRAPH_CONTEXT_BLOCK) + parts.append( + _ASSISTANT_NODE_GRAPH_CONTEXT_BLOCK + if key == "assistant" + else _NODE_GRAPH_CONTEXT_BLOCK + ) if mode == "excerpt": parts.append( _HARD_CONSTRAINTS_EXCERPT_USER_NODES @@ -686,16 +770,15 @@ def build_node_extraction_prompt( "The current turn's content was split into COMPLETE sentences with stable indices [k]. " "Reference them in supporting_sentence_indices; evidence is always a whole sentence.\n" ) - parts.append( - _HARD_CONSTRAINTS_SENTENCES_USER_NODES - if key == "user" - else _HARD_CONSTRAINTS_SENTENCES_NODES - ) - parts.append( - _OUTPUT_FORMAT_SENTENCES_USER_NODES - if key == "user" - else _OUTPUT_FORMAT_SENTENCES_NODES - ) + if key == "user": + parts.append(_HARD_CONSTRAINTS_SENTENCES_USER_NODES) + parts.append(_OUTPUT_FORMAT_SENTENCES_USER_NODES) + elif key == "assistant": + parts.append(_ASSISTANT_HARD_CONSTRAINTS_SENTENCES_NODES) + parts.append(_ASSISTANT_OUTPUT_FORMAT_SENTENCES_NODES) + else: + parts.append(_HARD_CONSTRAINTS_SENTENCES_NODES) + parts.append(_OUTPUT_FORMAT_SENTENCES_NODES) parts.append(f"## Current turn sentences\n{SENTENCES_PLACEHOLDER}\n") prompt = "\n".join(parts) diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 34d7015c..ff9d634e 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -897,6 +897,33 @@ def test_unified_node_extraction_prompt_omits_empty_context_and_tmp_ids() -> Non assert "qualified roles" in prompt +def test_unified_assistant_node_prompt_uses_compact_role_specific_sections() -> None: + prompt = build_node_extraction_prompt( + "assistant", + mode="sentences", + sentences_block="[0] The assistant identifies the answer.", + graph_nodes='[{"id": 1, "belief": "Earlier evidence."}]', + ) + + assert prompt is not None + assert "Maintain the belief graph incrementally" in prompt + assert "## What is a belief" in prompt + assert "## What is a decision" in prompt + assert "## Source role: ASSISTANT" in prompt + assert "Tool-call JSON is extracted deterministically by code" in prompt + assert "## Existing belief nodes" in prompt + assert "Earlier evidence." in prompt + assert "## Sentence input" in prompt + assert "## Hard constraints" in prompt + assert '"decision": ""' in prompt + assert "## Current turn sentences" in prompt + assert "Too fine-grained" not in prompt + assert prompt.index("## Existing belief nodes") < prompt.index( + "## Hard constraints" + ) + assert prompt.index("## Output") < prompt.index("## Current turn sentences") + + def test_unified_node_extraction_assigns_code_owned_tmp_ids( monkeypatch: pytest.MonkeyPatch, ) -> None: From d1f2f78dd1ce7bfeecb95b83fbe7ce50adf4e349 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 21:38:32 +0800 Subject: [PATCH 08/17] perf(construct): guard compact assistant extraction quality --- bcg/construct/unified/prompts.py | 19 +++++++++++++++---- tests/test_belief_graph.py | 5 +++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index 34b3ca32..5c201168 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -176,24 +176,29 @@ - Factual claims and intermediate conclusions the assistant commits to. - Hypotheses or reasoning steps that are falsifiable, reusable, or needed by later turns. - Recommendations, assessments, diagnoses, rankings, and derived states. +- Task-defining criteria, named candidates or alternatives, comparisons, and + evidence gaps or verification dependencies that guide later reasoning. ## What is a decision - The assistant's final selected answer, option, result, or explicit final conclusion, especially content inside `\\boxed{...}`. - A decision must be self-contained; never emit only a bare label, option, verdict, or value. - Do not emit the same final answer as both a belief and a decision. +- Do not infer a decision from a question, heading, quoted title, incomplete + phrase, or candidate being explored. If there is no explicit final selection, + return an empty decisions list. ## Granularity -Each belief must express one reusable central claim and remain understandable outside the turn. Keep tightly coupled qualifiers, premises, conditions, and results together when splitting would lose their dependency. Split independent propositions that can be confirmed, contradicted, or reused separately. Keep claims with different epistemic status separate. +Each belief must express one reusable central claim and remain understandable outside the turn. Keep tightly coupled qualifiers, premises, conditions, and results together when splitting would lose their dependency. Split independent propositions that can be confirmed, contradicted, or reused separately. Keep claims with different epistemic status separate. Do not merge a task objective or clue set into one candidate hypothesis when its criteria may be needed to evaluate other candidates later. """ _ASSISTANT_STANCE_DEFINITION = """\ ## Stance -Choose one per belief or decision: asserted = committed claim or final answer; recalled = explicit memory; speculated = uncertain hypothesis; judged = assessment, recommendation, diagnosis, ranking, or selected option. +Choose one per belief or decision: asserted = direct committed claim or explicit final answer; recalled = explicit memory; speculated = source wording is uncertain or hedged; judged = assessment, recommendation, diagnosis, ranking, or selected option. Follow the source's epistemic wording: do not mark a direct clue or investigation state as speculated merely because the assistant is still solving the task. """ _ASSISTANT_GUIDANCE = """\ ## Entities -List only specific named or uniquely qualified entities explicitly present in each belief or decision: people, organizations, places, products, datasets, files, tools, models, APIs, variables, and distinguishable concepts. Exclude pronouns, temporal expressions, bare generic nouns, vague concepts, and duplicates. Use [] when none exists. +List every specific named or uniquely qualified entity explicitly present in each belief or decision: people, organizations, places, products, datasets, files, tools, models, APIs, variables, task-defining qualified roles, and distinguishable concepts. Exclude pronouns, temporal expressions, bare generic nouns, vague concepts, and duplicates. Use [] only when no such entity exists. ## Source role: ASSISTANT - Write beliefs in the third person about "The assistant", "The user", or the named subject. @@ -205,8 +210,14 @@ ## Hard constraints - Preserve names, numbers, dates, quantities, versions, and unusual punctuation exactly. - Do not add outside knowledge. -- Every belief and decision must list all complete current-turn sentence indices that directly support it in `supporting_sentence_indices`; drop nodes without supporting sentences. +- Inspect every current-turn sentence. Preserve every substantive reusable claim, + criterion, alternative, comparison, reason, and evidence gap; omit only pure filler. +- Every belief and decision must contain its text, stance, entities, and a NON-EMPTY + `supporting_sentence_indices` list containing all complete current-turn sentences + that directly support it. Drop nodes without supporting sentences. - Empty beliefs and decisions lists are valid. +- Before returning, verify every output object has all required fields and every + substantive source claim is represented exactly once unless intentionally excluded as filler. """ _ASSISTANT_OUTPUT_FORMAT_SENTENCES_NODES = """\ diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index ff9d634e..ebf5d1ab 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -916,6 +916,11 @@ def test_unified_assistant_node_prompt_uses_compact_role_specific_sections() -> assert "## Sentence input" in prompt assert "## Hard constraints" in prompt assert '"decision": ""' in prompt + assert "Do not infer a decision from a question" in prompt + assert "Task-defining criteria" in prompt + assert "Inspect every current-turn sentence" in prompt + assert "NON-EMPTY" in prompt + assert "substantive source claim is represented exactly once" in prompt assert "## Current turn sentences" in prompt assert "Too fine-grained" not in prompt assert prompt.index("## Existing belief nodes") < prompt.index( From c0e15c31e2290ff00719836c3f07a1e91e1931de Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 21:53:33 +0800 Subject: [PATCH 09/17] perf(construct): compact assistant relation prompt --- bcg/construct/unified/prompts.py | 55 +++++++++++++------------------- tests/test_belief_graph.py | 4 ++- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index 5c201168..5b1a9a06 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -1046,36 +1046,26 @@ def build_relation_extraction_prompt( { "from": , "to": , "type": "depends_on | supplements | contradicts", "note": "" } ] } - -Hard output constraints: -- You may connect current-turn nodes to nodes from ZERO OR ONE previous layer. -- If any current-to-previous relation is emitted, every such relation MUST use the - same previous layer and ``selected_previous_layer`` MUST equal that layer number. -- If no current-to-previous relation is emitted, set ``selected_previous_layer`` to null. -- Current-turn to current-turn relations are allowed and do not select a previous layer. -- Never connect nodes from two different previous layers in the same response. -- Every endpoint must be an integer node id shown in the candidate graph. -- At least one endpoint of each relation must be from the current-turn node list. -- Empty relations are valid: {"selected_previous_layer": null, "relations": []}. """ _LAYERED_RELATION_EDGE_RULES = """\ -## Relation rules -Judge meaningful semantic links using node ``content`` alone: -- ``depends_on``: A requires B as a premise, input, evidence, constraint, or context. -- ``supplements``: A adds useful detail or evidence to B without changing it. -- ``contradicts``: A conflicts with, corrects, negates, or replaces B. +## Relation contract +Judge only the complete node ``content``. Shared entities alone do not justify a relation. +Direction is literal: ``A -> B`` means A has the stated relation to B. -Direction is literal: ``A -> B`` means A depends on, supplements, or contradicts B. -Read each complete content field; a request to verify a claim does not assert it. -Shared entities alone do not justify a relation. +- ``depends_on``: A requires B as evidence, premise, constraint, input, or context. +- ``supplements``: A adds useful compatible detail or evidence to B. +- ``contradicts``: A conflicts with, corrects, negates, or replaces B. -Constraints: -- Every relation must contain at least one current-turn node. -- Current-to-current relations are allowed; previous-to-previous relations are not. -- Use only shown integer ids; no self-links or invented ids. -- Prefer 0-4 high-value relations per current node. An empty result is valid. +Hard constraints: +- Every relation touches a current-turn node. Current-to-current is allowed; + previous-to-previous is forbidden. +- Cross-turn relations may use ZERO OR ONE previous layer. Never mix layers. + ``selected_previous_layer`` must be that layer, or null when none is used. +- Use only shown integer ids; no self-links, invented ids, or duplicate existing relations. +- A request to verify a claim does not assert it. Prefer 0-4 high-value relations + per current node; an empty result is valid. """ @@ -1097,7 +1087,8 @@ def build_layered_relation_extraction_prompt( """ parts: list[str] = [ "# Task", - "Link the current Assistant belief nodes to the most relevant prior layer.", + "Link current Assistant belief nodes to each other and, when justified, " + "to the single most relevant prior layer.", ] if content.strip(): parts.append( @@ -1105,19 +1096,17 @@ def build_layered_relation_extraction_prompt( ) parts.extend( [ + "## Current-turn node ids\n" + NEW_NODE_IDS_PLACEHOLDER + "\n", "## Candidate previous layers\n" - "Layer 1 is the nearest non-empty Graph turn; larger numbers are older. " - "Layer membership is authoritative.\n" + candidate_layers + "\n", - "## Candidate nodes\n" + "Layer 1 is the nearest non-empty Graph turn; larger numbers are older.\n" + + candidate_layers + + "\n", + "## Candidate node content\n" + GRAPH_NODES_PLACEHOLDER - + "\n\n## Existing relations\n" + + "\n\n## Existing relations (do not duplicate)\n" + GRAPH_EDGES_PLACEHOLDER + "\n", - "## Current-turn node ids\n" + NEW_NODE_IDS_PLACEHOLDER + "\n", _LAYERED_RELATION_EDGE_RULES, - "## Layer selection\n" - "Compare all candidates in one pass. Cross-turn relations may use ZERO OR " - "ONE previous layer, never a mixture. Select null when none is meaningful.\n", ] ) if validation_feedback: diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index ebf5d1ab..c0a9b315 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -611,8 +611,10 @@ def test_unified_layered_relation_prompt_requires_one_previous_layer() -> None: assert '"selected_previous_layer"' in prompt assert "ZERO OR ONE previous layer" in prompt assert "Layer 1 is the nearest" in prompt - assert "Never connect nodes from two different previous layers" in prompt + assert "Never mix layers" in prompt assert "Shared entities alone do not justify a relation" in prompt + assert "duplicate existing relations" in prompt + assert "previous-to-previous is forbidden" in prompt assert "The user can be charged a late fee" not in prompt From 63546f962f2c57cafb8d62a1992f70b2484a0d22 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 22:06:45 +0800 Subject: [PATCH 10/17] perf(construct): compact tool result relation prompt --- bcg/construct/unified/prompts.py | 44 ++++++++++++++++++++++++++++++++ tests/test_belief_graph.py | 18 +++++++++++++ 2 files changed, 62 insertions(+) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index 5b1a9a06..ab9c77aa 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -998,6 +998,44 @@ def build_assistant_tool_result_extraction_prompt( """ +_TOOL_RELATION_EDGE_RULES = """\ +## Relation contract +Link a Tool Result fact only when complete node ``content`` establishes a useful +semantic connection to prior Assistant reasoning. Shared entities or keywords +alone are insufficient. + +- ``depends_on``: A requires B as evidence, premise, constraint, input, or context. +- ``supplements``: A adds compatible evidence or detail to B. +- ``contradicts``: A conflicts with, corrects, negates, or replaces B. + +Direction is literal: ``A -> B`` means A has the stated relation to B. +Every relation must have exactly one current Tool Result endpoint and one prior +Assistant endpoint; current-to-current and prior-to-prior links are forbidden. +Use only shown integer ids; no self-links, invented ids, or duplicate existing +relations. Prefer 0-4 high-value relations per current node; an empty list is valid. +""" + + +def _build_tool_relation_extraction_prompt( + *, + graph_nodes: str, + graph_edges: str, + new_node_ids: str, +) -> str: + return "\n".join( + [ + "# Task", + "Link current Tool Result belief nodes to directly relevant prior " + "Assistant reasoning nodes.", + "## Candidate node content\n" + graph_nodes + "\n", + "## Existing relations (do not duplicate)\n" + graph_edges + "\n", + "## Current Tool Result node ids\n" + new_node_ids + "\n", + _TOOL_RELATION_EDGE_RULES, + _RELATION_OUTPUT_FORMAT, + ] + ) + + def build_relation_extraction_prompt( *, role: str, @@ -1008,6 +1046,12 @@ def build_relation_extraction_prompt( current_date: str | None = None, ) -> str: """Phase 2 prompt: extract relations only (on post-merge graph).""" + if _resolve_role(role) == "tool": + return _build_tool_relation_extraction_prompt( + graph_nodes=graph_nodes or "[]", + graph_edges=graph_edges or "[]", + new_node_ids=new_node_ids or "[]", + ) parts: list[str] = [ "# Task", "Extract typed relations for the belief graph based on the current turn.", diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index c0a9b315..3464ca3d 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -50,6 +50,7 @@ from bcg.construct.unified.prompts import ( build_layered_relation_extraction_prompt, build_node_extraction_prompt, + build_relation_extraction_prompt, ) from bcg.construct.unified.stream import ( StreamingBeliefBuilder as UnifiedStreamingBeliefBuilder, @@ -638,6 +639,23 @@ def test_unified_relation_nodes_include_only_id_and_content() -> None: assert json.loads(rendered) == [{"id": 7, "content": "A compact semantic fact."}] +def test_unified_tool_relation_prompt_uses_compact_role_contract() -> None: + prompt = build_relation_extraction_prompt( + role="tool", + content="Raw Tool Result content should not be repeated.", + graph_nodes='[{"id": 1, "content": "A prior hypothesis."}, {"id": 2, "content": "A result fact."}]', + graph_edges="[]", + new_node_ids="[2]", + ) + + assert "directly relevant prior Assistant reasoning nodes" in prompt + assert "exactly one current Tool Result endpoint" in prompt + assert "current-to-current and prior-to-prior links are forbidden" in prompt + assert "Shared entities or keywords" in prompt + assert "Raw Tool Result content should not be repeated" not in prompt + assert "The user can be charged a late fee" not in prompt + + def test_unified_extraction_history_includes_only_content() -> None: rendered = format_extraction_nodes( [ From 4f4518b7ab43815bed03256354ee27f45f4c8607 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 22:25:38 +0800 Subject: [PATCH 11/17] revert(construct): restore baseline graph prompts --- bcg/construct/unified/prompts.py | 217 +++++++------------------------ tests/test_belief_graph.py | 54 +------- 2 files changed, 46 insertions(+), 225 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index ab9c77aa..e5ccdb29 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -165,91 +165,6 @@ subject. Skip greetings and purely cosmetic instructions. """ -_ASSISTANT_TASK_LINE = ( - "Extract coherent, self-contained BELIEFS and final DECISIONS from the current " - "ASSISTANT turn only. Preserve reusable reasoning without recording procedural " - "filler. Output only new nodes; relations are extracted separately." -) - -_ASSISTANT_BELIEF_DEFINITION = """\ -## What is a belief -- Factual claims and intermediate conclusions the assistant commits to. -- Hypotheses or reasoning steps that are falsifiable, reusable, or needed by later turns. -- Recommendations, assessments, diagnoses, rankings, and derived states. -- Task-defining criteria, named candidates or alternatives, comparisons, and - evidence gaps or verification dependencies that guide later reasoning. - -## What is a decision -- The assistant's final selected answer, option, result, or explicit final conclusion, especially content inside `\\boxed{...}`. -- A decision must be self-contained; never emit only a bare label, option, verdict, or value. -- Do not emit the same final answer as both a belief and a decision. -- Do not infer a decision from a question, heading, quoted title, incomplete - phrase, or candidate being explored. If there is no explicit final selection, - return an empty decisions list. - -## Granularity -Each belief must express one reusable central claim and remain understandable outside the turn. Keep tightly coupled qualifiers, premises, conditions, and results together when splitting would lose their dependency. Split independent propositions that can be confirmed, contradicted, or reused separately. Keep claims with different epistemic status separate. Do not merge a task objective or clue set into one candidate hypothesis when its criteria may be needed to evaluate other candidates later. -""" - -_ASSISTANT_STANCE_DEFINITION = """\ -## Stance -Choose one per belief or decision: asserted = direct committed claim or explicit final answer; recalled = explicit memory; speculated = source wording is uncertain or hedged; judged = assessment, recommendation, diagnosis, ranking, or selected option. Follow the source's epistemic wording: do not mark a direct clue or investigation state as speculated merely because the assistant is still solving the task. -""" - -_ASSISTANT_GUIDANCE = """\ -## Entities -List every specific named or uniquely qualified entity explicitly present in each belief or decision: people, organizations, places, products, datasets, files, tools, models, APIs, variables, task-defining qualified roles, and distinguishable concepts. Exclude pronouns, temporal expressions, bare generic nouns, vague concepts, and duplicates. Use [] only when no such entity exists. - -## Source role: ASSISTANT -- Write beliefs in the third person about "The assistant", "The user", or the named subject. -- Skip politeness, self-questions, and pure procedure or planning such as "Let me search" unless it states a substantive hypothesis or dependency. -- Tool-call JSON is extracted deterministically by code. Do not reproduce raw tool-call syntax or create semantic beliefs from its field names. -""" - -_ASSISTANT_HARD_CONSTRAINTS_SENTENCES_NODES = """\ -## Hard constraints -- Preserve names, numbers, dates, quantities, versions, and unusual punctuation exactly. -- Do not add outside knowledge. -- Inspect every current-turn sentence. Preserve every substantive reusable claim, - criterion, alternative, comparison, reason, and evidence gap; omit only pure filler. -- Every belief and decision must contain its text, stance, entities, and a NON-EMPTY - `supporting_sentence_indices` list containing all complete current-turn sentences - that directly support it. Drop nodes without supporting sentences. -- Empty beliefs and decisions lists are valid. -- Before returning, verify every output object has all required fields and every - substantive source claim is represented exactly once unless intentionally excluded as filler. -""" - -_ASSISTANT_OUTPUT_FORMAT_SENTENCES_NODES = """\ -## Output (JSON only — no markdown fences, no commentary) -{ - "beliefs": [ - { - "belief": "", - "stance": "asserted | recalled | speculated | judged", - "entities": ["", "..."], - "supporting_sentence_indices": [0, 2] - } - ], - "decisions": [ - { - "decision": "", - "stance": "asserted | recalled | speculated | judged", - "entities": ["", "..."], - "supporting_sentence_indices": [3] - } - ] -} -""" - -_ASSISTANT_NODE_GRAPH_CONTEXT_BLOCK = f"""\ -## Existing belief nodes (context — READ ONLY) -Use historical nodes only to resolve references and keep entity wording consistent. Extract only from the CURRENT turn; never copy an old node merely because it appears here. If the current turn explicitly restates, confirms, corrects, or updates an old belief, emit a new evidence-bearing node. - -### Existing nodes -{GRAPH_NODES_PLACEHOLDER} -""" - _GRAPH_CONTEXT_BLOCK = f"""\ ## Existing belief graph (context — READ ONLY) These NODES and EDGES were already extracted from EARLIER turns. Use them only to: @@ -729,10 +644,8 @@ def build_node_extraction_prompt( if key == "user" and not has_existing_nodes: task_intro = ( "Extract coherent, self-contained beliefs from the current USER turn only. " - "Preserve distinct constraints without fragmenting one coherent request.\n" + "Preserve distinct constraints without fragmenting one coherent request." ) - elif key == "assistant": - task_intro = _ASSISTANT_TASK_LINE else: task_intro = task_line @@ -751,17 +664,9 @@ def build_node_extraction_prompt( belief_definition = _USER_BELIEF_DEFINITION stance_definition = _USER_STANCE_DEFINITION role_guidance = _USER_GUIDANCE - elif key == "assistant": - belief_definition = _ASSISTANT_BELIEF_DEFINITION - stance_definition = _ASSISTANT_STANCE_DEFINITION - role_guidance = _ASSISTANT_GUIDANCE parts.extend([belief_definition, stance_definition, role_guidance]) if has_existing_nodes: - parts.append( - _ASSISTANT_NODE_GRAPH_CONTEXT_BLOCK - if key == "assistant" - else _NODE_GRAPH_CONTEXT_BLOCK - ) + parts.append(_NODE_GRAPH_CONTEXT_BLOCK) if mode == "excerpt": parts.append( _HARD_CONSTRAINTS_EXCERPT_USER_NODES @@ -781,15 +686,16 @@ def build_node_extraction_prompt( "The current turn's content was split into COMPLETE sentences with stable indices [k]. " "Reference them in supporting_sentence_indices; evidence is always a whole sentence.\n" ) - if key == "user": - parts.append(_HARD_CONSTRAINTS_SENTENCES_USER_NODES) - parts.append(_OUTPUT_FORMAT_SENTENCES_USER_NODES) - elif key == "assistant": - parts.append(_ASSISTANT_HARD_CONSTRAINTS_SENTENCES_NODES) - parts.append(_ASSISTANT_OUTPUT_FORMAT_SENTENCES_NODES) - else: - parts.append(_HARD_CONSTRAINTS_SENTENCES_NODES) - parts.append(_OUTPUT_FORMAT_SENTENCES_NODES) + parts.append( + _HARD_CONSTRAINTS_SENTENCES_USER_NODES + if key == "user" + else _HARD_CONSTRAINTS_SENTENCES_NODES + ) + parts.append( + _OUTPUT_FORMAT_SENTENCES_USER_NODES + if key == "user" + else _OUTPUT_FORMAT_SENTENCES_NODES + ) parts.append(f"## Current turn sentences\n{SENTENCES_PLACEHOLDER}\n") prompt = "\n".join(parts) @@ -998,44 +904,6 @@ def build_assistant_tool_result_extraction_prompt( """ -_TOOL_RELATION_EDGE_RULES = """\ -## Relation contract -Link a Tool Result fact only when complete node ``content`` establishes a useful -semantic connection to prior Assistant reasoning. Shared entities or keywords -alone are insufficient. - -- ``depends_on``: A requires B as evidence, premise, constraint, input, or context. -- ``supplements``: A adds compatible evidence or detail to B. -- ``contradicts``: A conflicts with, corrects, negates, or replaces B. - -Direction is literal: ``A -> B`` means A has the stated relation to B. -Every relation must have exactly one current Tool Result endpoint and one prior -Assistant endpoint; current-to-current and prior-to-prior links are forbidden. -Use only shown integer ids; no self-links, invented ids, or duplicate existing -relations. Prefer 0-4 high-value relations per current node; an empty list is valid. -""" - - -def _build_tool_relation_extraction_prompt( - *, - graph_nodes: str, - graph_edges: str, - new_node_ids: str, -) -> str: - return "\n".join( - [ - "# Task", - "Link current Tool Result belief nodes to directly relevant prior " - "Assistant reasoning nodes.", - "## Candidate node content\n" + graph_nodes + "\n", - "## Existing relations (do not duplicate)\n" + graph_edges + "\n", - "## Current Tool Result node ids\n" + new_node_ids + "\n", - _TOOL_RELATION_EDGE_RULES, - _RELATION_OUTPUT_FORMAT, - ] - ) - - def build_relation_extraction_prompt( *, role: str, @@ -1046,12 +914,6 @@ def build_relation_extraction_prompt( current_date: str | None = None, ) -> str: """Phase 2 prompt: extract relations only (on post-merge graph).""" - if _resolve_role(role) == "tool": - return _build_tool_relation_extraction_prompt( - graph_nodes=graph_nodes or "[]", - graph_edges=graph_edges or "[]", - new_node_ids=new_node_ids or "[]", - ) parts: list[str] = [ "# Task", "Extract typed relations for the belief graph based on the current turn.", @@ -1090,26 +952,36 @@ def build_relation_extraction_prompt( { "from": , "to": , "type": "depends_on | supplements | contradicts", "note": "" } ] } + +Hard output constraints: +- You may connect current-turn nodes to nodes from ZERO OR ONE previous layer. +- If any current-to-previous relation is emitted, every such relation MUST use the + same previous layer and ``selected_previous_layer`` MUST equal that layer number. +- If no current-to-previous relation is emitted, set ``selected_previous_layer`` to null. +- Current-turn to current-turn relations are allowed and do not select a previous layer. +- Never connect nodes from two different previous layers in the same response. +- Every endpoint must be an integer node id shown in the candidate graph. +- At least one endpoint of each relation must be from the current-turn node list. +- Empty relations are valid: {"selected_previous_layer": null, "relations": []}. """ _LAYERED_RELATION_EDGE_RULES = """\ -## Relation contract -Judge only the complete node ``content``. Shared entities alone do not justify a relation. -Direction is literal: ``A -> B`` means A has the stated relation to B. - -- ``depends_on``: A requires B as evidence, premise, constraint, input, or context. -- ``supplements``: A adds useful compatible detail or evidence to B. +## Relation rules +Judge meaningful semantic links using node ``content`` alone: +- ``depends_on``: A requires B as a premise, input, evidence, constraint, or context. +- ``supplements``: A adds useful detail or evidence to B without changing it. - ``contradicts``: A conflicts with, corrects, negates, or replaces B. -Hard constraints: -- Every relation touches a current-turn node. Current-to-current is allowed; - previous-to-previous is forbidden. -- Cross-turn relations may use ZERO OR ONE previous layer. Never mix layers. - ``selected_previous_layer`` must be that layer, or null when none is used. -- Use only shown integer ids; no self-links, invented ids, or duplicate existing relations. -- A request to verify a claim does not assert it. Prefer 0-4 high-value relations - per current node; an empty result is valid. +Direction is literal: ``A -> B`` means A depends on, supplements, or contradicts B. +Read each complete content field; a request to verify a claim does not assert it. +Shared entities alone do not justify a relation. + +Constraints: +- Every relation must contain at least one current-turn node. +- Current-to-current relations are allowed; previous-to-previous relations are not. +- Use only shown integer ids; no self-links or invented ids. +- Prefer 0-4 high-value relations per current node. An empty result is valid. """ @@ -1131,8 +1003,7 @@ def build_layered_relation_extraction_prompt( """ parts: list[str] = [ "# Task", - "Link current Assistant belief nodes to each other and, when justified, " - "to the single most relevant prior layer.", + "Link the current Assistant belief nodes to the most relevant prior layer.", ] if content.strip(): parts.append( @@ -1140,17 +1011,19 @@ def build_layered_relation_extraction_prompt( ) parts.extend( [ - "## Current-turn node ids\n" + NEW_NODE_IDS_PLACEHOLDER + "\n", "## Candidate previous layers\n" - "Layer 1 is the nearest non-empty Graph turn; larger numbers are older.\n" - + candidate_layers - + "\n", - "## Candidate node content\n" + "Layer 1 is the nearest non-empty Graph turn; larger numbers are older. " + "Layer membership is authoritative.\n" + candidate_layers + "\n", + "## Candidate nodes\n" + GRAPH_NODES_PLACEHOLDER - + "\n\n## Existing relations (do not duplicate)\n" + + "\n\n## Existing relations\n" + GRAPH_EDGES_PLACEHOLDER + "\n", + "## Current-turn node ids\n" + NEW_NODE_IDS_PLACEHOLDER + "\n", _LAYERED_RELATION_EDGE_RULES, + "## Layer selection\n" + "Compare all candidates in one pass. Cross-turn relations may use ZERO OR " + "ONE previous layer, never a mixture. Select null when none is meaningful.\n", ] ) if validation_feedback: diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 3464ca3d..34d7015c 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -50,7 +50,6 @@ from bcg.construct.unified.prompts import ( build_layered_relation_extraction_prompt, build_node_extraction_prompt, - build_relation_extraction_prompt, ) from bcg.construct.unified.stream import ( StreamingBeliefBuilder as UnifiedStreamingBeliefBuilder, @@ -612,10 +611,8 @@ def test_unified_layered_relation_prompt_requires_one_previous_layer() -> None: assert '"selected_previous_layer"' in prompt assert "ZERO OR ONE previous layer" in prompt assert "Layer 1 is the nearest" in prompt - assert "Never mix layers" in prompt + assert "Never connect nodes from two different previous layers" in prompt assert "Shared entities alone do not justify a relation" in prompt - assert "duplicate existing relations" in prompt - assert "previous-to-previous is forbidden" in prompt assert "The user can be charged a late fee" not in prompt @@ -639,23 +636,6 @@ def test_unified_relation_nodes_include_only_id_and_content() -> None: assert json.loads(rendered) == [{"id": 7, "content": "A compact semantic fact."}] -def test_unified_tool_relation_prompt_uses_compact_role_contract() -> None: - prompt = build_relation_extraction_prompt( - role="tool", - content="Raw Tool Result content should not be repeated.", - graph_nodes='[{"id": 1, "content": "A prior hypothesis."}, {"id": 2, "content": "A result fact."}]', - graph_edges="[]", - new_node_ids="[2]", - ) - - assert "directly relevant prior Assistant reasoning nodes" in prompt - assert "exactly one current Tool Result endpoint" in prompt - assert "current-to-current and prior-to-prior links are forbidden" in prompt - assert "Shared entities or keywords" in prompt - assert "Raw Tool Result content should not be repeated" not in prompt - assert "The user can be charged a late fee" not in prompt - - def test_unified_extraction_history_includes_only_content() -> None: rendered = format_extraction_nodes( [ @@ -917,38 +897,6 @@ def test_unified_node_extraction_prompt_omits_empty_context_and_tmp_ids() -> Non assert "qualified roles" in prompt -def test_unified_assistant_node_prompt_uses_compact_role_specific_sections() -> None: - prompt = build_node_extraction_prompt( - "assistant", - mode="sentences", - sentences_block="[0] The assistant identifies the answer.", - graph_nodes='[{"id": 1, "belief": "Earlier evidence."}]', - ) - - assert prompt is not None - assert "Maintain the belief graph incrementally" in prompt - assert "## What is a belief" in prompt - assert "## What is a decision" in prompt - assert "## Source role: ASSISTANT" in prompt - assert "Tool-call JSON is extracted deterministically by code" in prompt - assert "## Existing belief nodes" in prompt - assert "Earlier evidence." in prompt - assert "## Sentence input" in prompt - assert "## Hard constraints" in prompt - assert '"decision": ""' in prompt - assert "Do not infer a decision from a question" in prompt - assert "Task-defining criteria" in prompt - assert "Inspect every current-turn sentence" in prompt - assert "NON-EMPTY" in prompt - assert "substantive source claim is represented exactly once" in prompt - assert "## Current turn sentences" in prompt - assert "Too fine-grained" not in prompt - assert prompt.index("## Existing belief nodes") < prompt.index( - "## Hard constraints" - ) - assert prompt.index("## Output") < prompt.index("## Current turn sentences") - - def test_unified_node_extraction_assigns_code_owned_tmp_ids( monkeypatch: pytest.MonkeyPatch, ) -> None: From 214c948ee6d37d452e14915a62c14d74ff5e606f Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 22:57:41 +0800 Subject: [PATCH 12/17] perf(construct): align assistant extraction prompt with runtime input --- bcg/construct/unified/prompts.py | 156 +++++++++++++++++++++++++++++++ tests/test_belief_graph.py | 28 ++++++ 2 files changed, 184 insertions(+) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index e5ccdb29..2a4afeb4 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -165,6 +165,126 @@ subject. Skip greetings and purely cosmetic instructions. """ +_ASSISTANT_NODE_TASK = """\ +Extract only NEW, self-contained BELIEFS and explicit final DECISIONS from the +CURRENT ASSISTANT source. Relations and Tool Call nodes are handled separately. +""" + +_ASSISTANT_NODE_RULES = """\ +## Extraction contract +### Beliefs +Extract substantive content that later reasoning may need: +- factual claims and recalled facts; +- task-defining criteria, constraints, and evidence gaps; +- named candidates, alternatives, and falsifiable hypotheses; +- intermediate conclusions, comparisons, assessments, and recommendations. + +Skip politeness, self-questions, section headings that only label the reasoning, +and pure procedure such as "Let me search". Preserve a plan only when it states +a substantive hypothesis, criterion, or dependency. Valid Tool Call JSON has +already been removed and is extracted deterministically; do not reconstruct it. + +Write each belief in the third person about "The assistant", "The user", or the +named subject. It must remain understandable outside this turn. + +### Decisions +A decision is an explicitly selected final answer, option, result, or conclusion, +especially content inside ``\\boxed{...}``. Do not infer a decision from a question, +heading, incomplete phrase, or candidate under investigation. Do not emit the same +final answer as both a belief and a decision. + +### Granularity +Keep one reusable central claim per node. Keep tightly coupled premises, +conditions, qualifiers, and results together when splitting would lose their +dependency. Split independent claims that can be verified, contradicted, or +reused separately, and keep claims with different epistemic status separate. +Do not merge task criteria into one candidate hypothesis when those criteria may +be needed to evaluate other candidates later. + +### Stance +Choose one per node: ``asserted`` for a committed claim or explicit final answer; +``recalled`` for explicit memory; ``speculated`` for a hedged hypothesis; and +``judged`` for an assessment, recommendation, ranking, diagnosis, or selected +option. Follow the source wording rather than the overall uncertainty of the task. + +### Entities +List specific named or uniquely qualified entities explicitly present in the +node, including task-defining roles when they distinguish a reusable constraint. +Exclude pronouns, temporal expressions, bare generic nouns, vague concepts, and +duplicates. Use ``[]`` when none exists. +""" + +_ASSISTANT_NODE_CONTEXT_BLOCK = f"""\ +## Earlier belief nodes (read only) +Use these nodes only to resolve references and keep entity wording consistent. +Never copy an earlier node merely because it appears here. If the current source +explicitly restates, confirms, corrects, or updates it, emit a new supported node. + +{GRAPH_NODES_PLACEHOLDER} +""" + +_ASSISTANT_HARD_CONSTRAINTS_SENTENCES = """\ +## Grounding requirements +- Preserve names, numbers, dates, quantities, versions, and unusual punctuation exactly. +- Use only the current indexed sentences; do not add outside knowledge. +- Every belief and decision must list every complete current sentence that directly + supports it in ``supporting_sentence_indices``. Drop unsupported nodes. +- Empty ``beliefs`` and ``decisions`` lists are valid. +""" + +_ASSISTANT_HARD_CONSTRAINTS_EXCERPT = """\ +## Grounding requirements +- Preserve names, numbers, dates, quantities, versions, and unusual punctuation exactly. +- Use only the current content; do not add outside knowledge. +- Every belief and decision must include at least one verbatim, contiguous + ``supporting_excerpts`` substring from the current content. Drop unsupported nodes. +- Empty ``beliefs`` and ``decisions`` lists are valid. +""" + +_ASSISTANT_OUTPUT_FORMAT_SENTENCES = """\ +## Output (JSON only — no markdown fences, no commentary) +{ + "beliefs": [ + { + "belief": "", + "stance": "asserted | recalled | speculated | judged", + "entities": ["", "..."], + "supporting_sentence_indices": [0, 2] + } + ], + "decisions": [ + { + "decision": "", + "stance": "asserted | recalled | speculated | judged", + "entities": ["", "..."], + "supporting_sentence_indices": [3] + } + ] +} +""" + +_ASSISTANT_OUTPUT_FORMAT_EXCERPT = """\ +## Output (JSON only — no markdown fences, no commentary) +{ + "beliefs": [ + { + "belief": "", + "stance": "asserted | recalled | speculated | judged", + "entities": ["", "..."], + "supporting_excerpts": [""] + } + ], + "decisions": [ + { + "decision": "", + "stance": "asserted | recalled | speculated | judged", + "entities": ["", "..."], + "supporting_excerpts": [""] + } + ] +} +""" + _GRAPH_CONTEXT_BLOCK = f"""\ ## Existing belief graph (context — READ ONLY) These NODES and EDGES were already extracted from EARLIER turns. Use them only to: @@ -641,6 +761,42 @@ def build_node_extraction_prompt( task_line, guidance, _stance_hint = _GUIDANCE[key] has_existing_nodes = _has_existing_nodes(graph_nodes) + if key == "assistant": + current_input = ( + f"## Current turn content\n{CONTENT_PLACEHOLDER}\n" + if mode == "excerpt" + else ( + "## Current turn sentences\n" + "Indices are stable within this source.\n" + f"{SENTENCES_PLACEHOLDER}\n" + ) + ) + parts = ["# Task", _ASSISTANT_NODE_TASK] + if has_existing_nodes: + parts.append(_ASSISTANT_NODE_CONTEXT_BLOCK) + parts.extend([current_input, _ASSISTANT_NODE_RULES]) + if mode == "excerpt": + parts.extend( + [ + _ASSISTANT_HARD_CONSTRAINTS_EXCERPT, + _ASSISTANT_OUTPUT_FORMAT_EXCERPT, + ] + ) + else: + parts.extend( + [ + _ASSISTANT_HARD_CONSTRAINTS_SENTENCES, + _ASSISTANT_OUTPUT_FORMAT_SENTENCES, + ] + ) + prompt = "\n".join(parts).replace( + GRAPH_NODES_PLACEHOLDER, graph_nodes or "[]" + ) + _ = graph_edges, current_date + if mode == "excerpt": + return prompt.replace(CONTENT_PLACEHOLDER, content or "") + return prompt.replace(SENTENCES_PLACEHOLDER, sentences_block or "") + if key == "user" and not has_existing_nodes: task_intro = ( "Extract coherent, self-contained beliefs from the current USER turn only. " diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 34d7015c..68db8589 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -897,6 +897,34 @@ def test_unified_node_extraction_prompt_omits_empty_context_and_tmp_ids() -> Non assert "qualified roles" in prompt +def test_assistant_node_extraction_prompt_matches_runtime_input_order() -> None: + prompt = build_node_extraction_prompt( + "assistant", + mode="sentences", + sentences_block="[0] The assistant considers Candidate A.", + graph_nodes='[{"content": "An earlier clue."}]', + graph_edges='[{"from": 1, "to": 2, "type": "depends_on"}]', + ) + + assert prompt is not None + current = prompt.index("## Current turn sentences") + context = prompt.index("## Earlier belief nodes") + contract = prompt.index("## Extraction contract") + output = prompt.index("## Output (JSON only") + assert context < current < contract < output + assert "An earlier clue." in prompt + assert "Existing relations" not in prompt + assert '"from": 1' not in prompt + assert '"tmp_id"' not in prompt + assert "Tool Call JSON has\nalready been removed" in prompt + assert "COCO" not in prompt + assert "Mask R-CNN" not in prompt + assert "silver Honda Civic" not in prompt + assert "task-defining criteria" in prompt + assert "evidence gaps" in prompt + assert "Do not infer a decision" in prompt + + def test_unified_node_extraction_assigns_code_owned_tmp_ids( monkeypatch: pytest.MonkeyPatch, ) -> None: From b108f4f499a2da03ac49db061d0fae83dd3e97e3 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 23:08:01 +0800 Subject: [PATCH 13/17] perf(construct): preserve assistant extraction coverage --- bcg/construct/unified/prompts.py | 48 +++++++++++++++++++++----------- tests/test_belief_graph.py | 7 +++-- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index 2a4afeb4..10221c24 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -177,12 +177,15 @@ - factual claims and recalled facts; - task-defining criteria, constraints, and evidence gaps; - named candidates, alternatives, and falsifiable hypotheses; -- intermediate conclusions, comparisons, assessments, and recommendations. +- intermediate conclusions, comparisons, assessments, and recommendations; +- a search or verification need when it names the evidence, clue, criterion, or + candidate that later reasoning still needs to check. Skip politeness, self-questions, section headings that only label the reasoning, -and pure procedure such as "Let me search". Preserve a plan only when it states -a substantive hypothesis, criterion, or dependency. Valid Tool Call JSON has -already been removed and is extracted deterministically; do not reconstruct it. +and empty procedure such as "Let me search" with no stated object or purpose. +Do not discard an explicit investigation state merely because it is phrased as a +next step. Valid Tool Call JSON has already been removed and is extracted +deterministically; do not reconstruct it. Write each belief in the third person about "The assistant", "The user", or the named subject. It must remain understandable outside this turn. @@ -194,12 +197,13 @@ final answer as both a belief and a decision. ### Granularity -Keep one reusable central claim per node. Keep tightly coupled premises, -conditions, qualifiers, and results together when splitting would lose their -dependency. Split independent claims that can be verified, contradicted, or -reused separately, and keep claims with different epistemic status separate. -Do not merge task criteria into one candidate hypothesis when those criteria may -be needed to evaluate other candidates later. +Keep one reusable central claim per node. A single source sentence may support +multiple nodes. Keep tightly coupled premises, conditions, qualifiers, and +results together only when splitting would lose their dependency. Split +independent facts, candidates, alternatives, comparisons, and verification needs +that can be confirmed, contradicted, or reused separately. Keep different +epistemic states separate. Do not merge task criteria into one candidate +hypothesis when those criteria may be needed to evaluate other candidates later. ### Stance Choose one per node: ``asserted`` for a committed claim or explicit final answer; @@ -208,7 +212,7 @@ option. Follow the source wording rather than the overall uncertainty of the task. ### Entities -List specific named or uniquely qualified entities explicitly present in the +List every specific named or uniquely qualified entity explicitly present in the node, including task-defining roles when they distinguish a reusable constraint. Exclude pronouns, temporal expressions, bare generic nouns, vague concepts, and duplicates. Use ``[]`` when none exists. @@ -227,18 +231,28 @@ ## Grounding requirements - Preserve names, numbers, dates, quantities, versions, and unusual punctuation exactly. - Use only the current indexed sentences; do not add outside knowledge. -- Every belief and decision must list every complete current sentence that directly - supports it in ``supporting_sentence_indices``. Drop unsupported nodes. +- Inspect every current sentence. Preserve each substantive reusable claim, + criterion, candidate, alternative, comparison, reason, evidence gap, and + object-specific verification need exactly once. +- Every output object must contain its text, ``stance``, ``entities``, and a + non-empty ``supporting_sentence_indices`` list containing every complete + current sentence that directly supports it. Drop unsupported nodes. - Empty ``beliefs`` and ``decisions`` lists are valid. +- Before returning, verify that every output object has all required fields and + that omitted sentences contain only headings, empty procedure, or other filler. """ _ASSISTANT_HARD_CONSTRAINTS_EXCERPT = """\ ## Grounding requirements - Preserve names, numbers, dates, quantities, versions, and unusual punctuation exactly. - Use only the current content; do not add outside knowledge. -- Every belief and decision must include at least one verbatim, contiguous - ``supporting_excerpts`` substring from the current content. Drop unsupported nodes. +- Preserve each substantive reusable claim, criterion, candidate, alternative, + comparison, reason, evidence gap, and object-specific verification need exactly once. +- Every output object must contain its text, ``stance``, ``entities``, and at + least one verbatim, contiguous ``supporting_excerpts`` substring from the + current content. Drop unsupported nodes. - Empty ``beliefs`` and ``decisions`` lists are valid. +- Before returning, verify that every output object has all required fields. """ _ASSISTANT_OUTPUT_FORMAT_SENTENCES = """\ @@ -771,10 +785,10 @@ def build_node_extraction_prompt( f"{SENTENCES_PLACEHOLDER}\n" ) ) - parts = ["# Task", _ASSISTANT_NODE_TASK] + parts = ["# Task", _ASSISTANT_NODE_TASK, _ASSISTANT_NODE_RULES] if has_existing_nodes: parts.append(_ASSISTANT_NODE_CONTEXT_BLOCK) - parts.extend([current_input, _ASSISTANT_NODE_RULES]) + parts.append(current_input) if mode == "excerpt": parts.extend( [ diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 68db8589..5cf09663 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -911,17 +911,20 @@ def test_assistant_node_extraction_prompt_matches_runtime_input_order() -> None: context = prompt.index("## Earlier belief nodes") contract = prompt.index("## Extraction contract") output = prompt.index("## Output (JSON only") - assert context < current < contract < output + assert contract < context < current < output assert "An earlier clue." in prompt assert "Existing relations" not in prompt assert '"from": 1' not in prompt assert '"tmp_id"' not in prompt - assert "Tool Call JSON has\nalready been removed" in prompt + assert "Valid Tool Call JSON has" in prompt assert "COCO" not in prompt assert "Mask R-CNN" not in prompt assert "silver Honda Civic" not in prompt assert "task-defining criteria" in prompt assert "evidence gaps" in prompt + assert "A single source sentence may support\nmultiple nodes" in prompt + assert "object-specific verification need" in prompt + assert "Every output object must contain" in prompt assert "Do not infer a decision" in prompt From 380f55d76f731431a2ca73236e441537c2e3b123 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 23:13:10 +0800 Subject: [PATCH 14/17] fix(construct): enforce grounded assistant extraction --- bcg/construct/unified/prompts.py | 59 +++++++++++++++++--------------- tests/test_belief_graph.py | 5 +-- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index 10221c24..6950db5b 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -178,13 +178,14 @@ - task-defining criteria, constraints, and evidence gaps; - named candidates, alternatives, and falsifiable hypotheses; - intermediate conclusions, comparisons, assessments, and recommendations; -- a search or verification need when it names the evidence, clue, criterion, or - candidate that later reasoning still needs to check. +- an unresolved evidence gap when resolving it would confirm, reject, or rank a + named candidate or task criterion. Skip politeness, self-questions, section headings that only label the reasoning, -and empty procedure such as "Let me search" with no stated object or purpose. -Do not discard an explicit investigation state merely because it is phrased as a -next step. Valid Tool Call JSON has already been removed and is extracted +and search procedure such as "Let me search", "I should find evidence", or a +query plan. A check is reusable only when the source states the unresolved fact +and why it changes a candidate or criterion; do not create a node merely for the +act of searching. Valid Tool Call JSON has already been removed and is extracted deterministically; do not reconstruct it. Write each belief in the third person about "The assistant", "The user", or the @@ -209,13 +210,15 @@ Choose one per node: ``asserted`` for a committed claim or explicit final answer; ``recalled`` for explicit memory; ``speculated`` for a hedged hypothesis; and ``judged`` for an assessment, recommendation, ranking, diagnosis, or selected -option. Follow the source wording rather than the overall uncertainty of the task. +option. An explicitly stated investigation state is ``asserted``, not ``judged``. +Follow the source wording rather than the overall uncertainty of the task. ### Entities List every specific named or uniquely qualified entity explicitly present in the node, including task-defining roles when they distinguish a reusable constraint. -Exclude pronouns, temporal expressions, bare generic nouns, vague concepts, and -duplicates. Use ``[]`` when none exists. +Exclude pronouns, dates and other temporal expressions, bare generic nouns, vague +concepts, and duplicates. Preserve dates in node text, never as entities. Use +``[]`` when no supported entity exists. """ _ASSISTANT_NODE_CONTEXT_BLOCK = f"""\ @@ -229,30 +232,30 @@ _ASSISTANT_HARD_CONSTRAINTS_SENTENCES = """\ ## Grounding requirements -- Preserve names, numbers, dates, quantities, versions, and unusual punctuation exactly. -- Use only the current indexed sentences; do not add outside knowledge. -- Inspect every current sentence. Preserve each substantive reusable claim, - criterion, candidate, alternative, comparison, reason, evidence gap, and - object-specific verification need exactly once. -- Every output object must contain its text, ``stance``, ``entities``, and a - non-empty ``supporting_sentence_indices`` list containing every complete - current sentence that directly supports it. Drop unsupported nodes. -- Empty ``beliefs`` and ``decisions`` lists are valid. -- Before returning, verify that every output object has all required fields and - that omitted sentences contain only headings, empty procedure, or other filler. +1. Preserve names, numbers, dates, quantities, versions, and unusual punctuation EXACTLY. +2. Use ONLY the current indexed sentences; do not add outside knowledge. +3. Inspect every current sentence. Preserve each substantive reusable claim, + criterion, candidate, alternative, comparison, reason, and evidence gap once. +4. EVERY belief and decision MUST contain its text, ``stance``, ``entities``, and + a NON-EMPTY ``supporting_sentence_indices`` list. List ALL complete current + sentences that directly support the node. A missing evidence list makes the + entire response invalid; drop an unsupported node instead. +5. Empty ``beliefs`` and ``decisions`` lists are valid. +6. Before returning, verify every output object has every required field and that + omitted sentences contain only headings, search procedure, or other filler. """ _ASSISTANT_HARD_CONSTRAINTS_EXCERPT = """\ ## Grounding requirements -- Preserve names, numbers, dates, quantities, versions, and unusual punctuation exactly. -- Use only the current content; do not add outside knowledge. -- Preserve each substantive reusable claim, criterion, candidate, alternative, - comparison, reason, evidence gap, and object-specific verification need exactly once. -- Every output object must contain its text, ``stance``, ``entities``, and at - least one verbatim, contiguous ``supporting_excerpts`` substring from the - current content. Drop unsupported nodes. -- Empty ``beliefs`` and ``decisions`` lists are valid. -- Before returning, verify that every output object has all required fields. +1. Preserve names, numbers, dates, quantities, versions, and unusual punctuation EXACTLY. +2. Use ONLY the current content; do not add outside knowledge. +3. Preserve each substantive reusable claim, criterion, candidate, alternative, + comparison, reason, and evidence gap once. +4. EVERY belief and decision MUST contain its text, ``stance``, ``entities``, and + at least one VERBATIM, CONTIGUOUS ``supporting_excerpts`` substring. A missing + evidence list makes the entire response invalid; drop an unsupported node instead. +5. Empty ``beliefs`` and ``decisions`` lists are valid. +6. Before returning, verify every output object has every required field. """ _ASSISTANT_OUTPUT_FORMAT_SENTENCES = """\ diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 5cf09663..8c192939 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -923,8 +923,9 @@ def test_assistant_node_extraction_prompt_matches_runtime_input_order() -> None: assert "task-defining criteria" in prompt assert "evidence gaps" in prompt assert "A single source sentence may support\nmultiple nodes" in prompt - assert "object-specific verification need" in prompt - assert "Every output object must contain" in prompt + assert "do not create a node merely for the\nact of searching" in prompt + assert "EVERY belief and decision MUST contain" in prompt + assert "missing evidence list makes the\n entire response invalid" in prompt assert "Do not infer a decision" in prompt From 3d94940488cc86ab530ea40e99c30772bc3b0426 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 23:16:14 +0800 Subject: [PATCH 15/17] perf(construct): place assistant source at prompt tail --- bcg/construct/unified/prompts.py | 3 ++- tests/test_belief_graph.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index 6950db5b..ebab4063 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -791,12 +791,12 @@ def build_node_extraction_prompt( parts = ["# Task", _ASSISTANT_NODE_TASK, _ASSISTANT_NODE_RULES] if has_existing_nodes: parts.append(_ASSISTANT_NODE_CONTEXT_BLOCK) - parts.append(current_input) if mode == "excerpt": parts.extend( [ _ASSISTANT_HARD_CONSTRAINTS_EXCERPT, _ASSISTANT_OUTPUT_FORMAT_EXCERPT, + current_input, ] ) else: @@ -804,6 +804,7 @@ def build_node_extraction_prompt( [ _ASSISTANT_HARD_CONSTRAINTS_SENTENCES, _ASSISTANT_OUTPUT_FORMAT_SENTENCES, + current_input, ] ) prompt = "\n".join(parts).replace( diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 8c192939..74fa91d6 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -911,7 +911,7 @@ def test_assistant_node_extraction_prompt_matches_runtime_input_order() -> None: context = prompt.index("## Earlier belief nodes") contract = prompt.index("## Extraction contract") output = prompt.index("## Output (JSON only") - assert contract < context < current < output + assert contract < context < output < current assert "An earlier clue." in prompt assert "Existing relations" not in prompt assert '"from": 1' not in prompt From 3dd1e503abbfcc5b2ba33bdc03c83de211c51f24 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 23:21:45 +0800 Subject: [PATCH 16/17] perf(construct): trim irrelevant assistant extraction examples --- bcg/construct/unified/prompts.py | 187 +++---------------------------- tests/test_belief_graph.py | 22 ++-- 2 files changed, 27 insertions(+), 182 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index ebab4063..7b50f4dc 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -165,141 +165,24 @@ subject. Skip greetings and purely cosmetic instructions. """ -_ASSISTANT_NODE_TASK = """\ -Extract only NEW, self-contained BELIEFS and explicit final DECISIONS from the -CURRENT ASSISTANT source. Relations and Tool Call nodes are handled separately. -""" - -_ASSISTANT_NODE_RULES = """\ -## Extraction contract -### Beliefs -Extract substantive content that later reasoning may need: -- factual claims and recalled facts; -- task-defining criteria, constraints, and evidence gaps; -- named candidates, alternatives, and falsifiable hypotheses; -- intermediate conclusions, comparisons, assessments, and recommendations; -- an unresolved evidence gap when resolving it would confirm, reject, or rank a - named candidate or task criterion. - -Skip politeness, self-questions, section headings that only label the reasoning, -and search procedure such as "Let me search", "I should find evidence", or a -query plan. A check is reusable only when the source states the unresolved fact -and why it changes a candidate or criterion; do not create a node merely for the -act of searching. Valid Tool Call JSON has already been removed and is extracted -deterministically; do not reconstruct it. - -Write each belief in the third person about "The assistant", "The user", or the -named subject. It must remain understandable outside this turn. - -### Decisions -A decision is an explicitly selected final answer, option, result, or conclusion, -especially content inside ``\\boxed{...}``. Do not infer a decision from a question, -heading, incomplete phrase, or candidate under investigation. Do not emit the same -final answer as both a belief and a decision. - -### Granularity -Keep one reusable central claim per node. A single source sentence may support -multiple nodes. Keep tightly coupled premises, conditions, qualifiers, and -results together only when splitting would lose their dependency. Split -independent facts, candidates, alternatives, comparisons, and verification needs -that can be confirmed, contradicted, or reused separately. Keep different -epistemic states separate. Do not merge task criteria into one candidate -hypothesis when those criteria may be needed to evaluate other candidates later. - -### Stance -Choose one per node: ``asserted`` for a committed claim or explicit final answer; -``recalled`` for explicit memory; ``speculated`` for a hedged hypothesis; and -``judged`` for an assessment, recommendation, ranking, diagnosis, or selected -option. An explicitly stated investigation state is ``asserted``, not ``judged``. -Follow the source wording rather than the overall uncertainty of the task. - -### Entities -List every specific named or uniquely qualified entity explicitly present in the -node, including task-defining roles when they distinguish a reusable constraint. -Exclude pronouns, dates and other temporal expressions, bare generic nouns, vague -concepts, and duplicates. Preserve dates in node text, never as entities. Use -``[]`` when no supported entity exists. -""" - -_ASSISTANT_NODE_CONTEXT_BLOCK = f"""\ -## Earlier belief nodes (read only) -Use these nodes only to resolve references and keep entity wording consistent. -Never copy an earlier node merely because it appears here. If the current source -explicitly restates, confirms, corrects, or updates it, emit a new supported node. - -{GRAPH_NODES_PLACEHOLDER} -""" - -_ASSISTANT_HARD_CONSTRAINTS_SENTENCES = """\ -## Grounding requirements -1. Preserve names, numbers, dates, quantities, versions, and unusual punctuation EXACTLY. -2. Use ONLY the current indexed sentences; do not add outside knowledge. -3. Inspect every current sentence. Preserve each substantive reusable claim, - criterion, candidate, alternative, comparison, reason, and evidence gap once. -4. EVERY belief and decision MUST contain its text, ``stance``, ``entities``, and - a NON-EMPTY ``supporting_sentence_indices`` list. List ALL complete current - sentences that directly support the node. A missing evidence list makes the - entire response invalid; drop an unsupported node instead. -5. Empty ``beliefs`` and ``decisions`` lists are valid. -6. Before returning, verify every output object has every required field and that - omitted sentences contain only headings, search procedure, or other filler. -""" - -_ASSISTANT_HARD_CONSTRAINTS_EXCERPT = """\ -## Grounding requirements -1. Preserve names, numbers, dates, quantities, versions, and unusual punctuation EXACTLY. -2. Use ONLY the current content; do not add outside knowledge. -3. Preserve each substantive reusable claim, criterion, candidate, alternative, - comparison, reason, and evidence gap once. -4. EVERY belief and decision MUST contain its text, ``stance``, ``entities``, and - at least one VERBATIM, CONTIGUOUS ``supporting_excerpts`` substring. A missing - evidence list makes the entire response invalid; drop an unsupported node instead. -5. Empty ``beliefs`` and ``decisions`` lists are valid. -6. Before returning, verify every output object has every required field. -""" +_ASSISTANT_BELIEF_DEFINITION = """\ +## What is a belief +A belief is a self-contained, reusable memory or reasoning unit. Preserve the +most specific supported wording and enough subject, scope, and qualification for +the claim to remain understandable outside its original turn. -_ASSISTANT_OUTPUT_FORMAT_SENTENCES = """\ -## Output (JSON only — no markdown fences, no commentary) -{ - "beliefs": [ - { - "belief": "", - "stance": "asserted | recalled | speculated | judged", - "entities": ["", "..."], - "supporting_sentence_indices": [0, 2] - } - ], - "decisions": [ - { - "decision": "", - "stance": "asserted | recalled | speculated | judged", - "entities": ["", "..."], - "supporting_sentence_indices": [3] - } - ] -} -""" +## Granularity — coherent units, not tiny shards +Keep tightly coupled conditions, reasons, qualifiers, and results together when +splitting would destroy their dependency. Split independent propositions that +can be confirmed, contradicted, or reused separately, and keep claims with +different epistemic status separate. Each node must have one central meaning. -_ASSISTANT_OUTPUT_FORMAT_EXCERPT = """\ -## Output (JSON only — no markdown fences, no commentary) -{ - "beliefs": [ - { - "belief": "", - "stance": "asserted | recalled | speculated | judged", - "entities": ["", "..."], - "supporting_excerpts": [""] - } - ], - "decisions": [ - { - "decision": "", - "stance": "asserted | recalled | speculated | judged", - "entities": ["", "..."], - "supporting_excerpts": [""] - } - ] -} +## Entities +For every node, list specific named or uniquely qualified entities explicitly +involved in it: people, organizations, places, products, datasets, files, tools, +models, APIs, variables, and distinguishable concepts. Exclude pronouns, temporal +expressions, bare generic nouns, vague concepts, and duplicates. Use ``[]`` when +none exists; never invent an entity. """ _GRAPH_CONTEXT_BLOCK = f"""\ @@ -778,42 +661,6 @@ def build_node_extraction_prompt( task_line, guidance, _stance_hint = _GUIDANCE[key] has_existing_nodes = _has_existing_nodes(graph_nodes) - if key == "assistant": - current_input = ( - f"## Current turn content\n{CONTENT_PLACEHOLDER}\n" - if mode == "excerpt" - else ( - "## Current turn sentences\n" - "Indices are stable within this source.\n" - f"{SENTENCES_PLACEHOLDER}\n" - ) - ) - parts = ["# Task", _ASSISTANT_NODE_TASK, _ASSISTANT_NODE_RULES] - if has_existing_nodes: - parts.append(_ASSISTANT_NODE_CONTEXT_BLOCK) - if mode == "excerpt": - parts.extend( - [ - _ASSISTANT_HARD_CONSTRAINTS_EXCERPT, - _ASSISTANT_OUTPUT_FORMAT_EXCERPT, - current_input, - ] - ) - else: - parts.extend( - [ - _ASSISTANT_HARD_CONSTRAINTS_SENTENCES, - _ASSISTANT_OUTPUT_FORMAT_SENTENCES, - current_input, - ] - ) - prompt = "\n".join(parts).replace( - GRAPH_NODES_PLACEHOLDER, graph_nodes or "[]" - ) - _ = graph_edges, current_date - if mode == "excerpt": - return prompt.replace(CONTENT_PLACEHOLDER, content or "") - return prompt.replace(SENTENCES_PLACEHOLDER, sentences_block or "") if key == "user" and not has_existing_nodes: task_intro = ( @@ -838,6 +685,8 @@ def build_node_extraction_prompt( belief_definition = _USER_BELIEF_DEFINITION stance_definition = _USER_STANCE_DEFINITION role_guidance = _USER_GUIDANCE + elif key == "assistant": + belief_definition = _ASSISTANT_BELIEF_DEFINITION parts.extend([belief_definition, stance_definition, role_guidance]) if has_existing_nodes: parts.append(_NODE_GRAPH_CONTEXT_BLOCK) diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 74fa91d6..5779b101 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -897,7 +897,7 @@ def test_unified_node_extraction_prompt_omits_empty_context_and_tmp_ids() -> Non assert "qualified roles" in prompt -def test_assistant_node_extraction_prompt_matches_runtime_input_order() -> None: +def test_assistant_node_extraction_prompt_uses_compact_definition_and_baseline_order() -> None: prompt = build_node_extraction_prompt( "assistant", mode="sentences", @@ -907,26 +907,22 @@ def test_assistant_node_extraction_prompt_matches_runtime_input_order() -> None: ) assert prompt is not None - current = prompt.index("## Current turn sentences") - context = prompt.index("## Earlier belief nodes") - contract = prompt.index("## Extraction contract") + definition = prompt.index("## What is a belief") + context = prompt.index("## Existing belief nodes") + hard = prompt.index("## Hard constraints") output = prompt.index("## Output (JSON only") - assert contract < context < output < current + current = prompt.index("## Current turn sentences") + assert definition < context < hard < output < current assert "An earlier clue." in prompt assert "Existing relations" not in prompt assert '"from": 1' not in prompt assert '"tmp_id"' not in prompt - assert "Valid Tool Call JSON has" in prompt + assert "self-contained, reusable memory or reasoning unit" in prompt + assert "Factual claims and intermediate conclusions" in prompt + assert "supporting_sentence_indices" in prompt assert "COCO" not in prompt assert "Mask R-CNN" not in prompt assert "silver Honda Civic" not in prompt - assert "task-defining criteria" in prompt - assert "evidence gaps" in prompt - assert "A single source sentence may support\nmultiple nodes" in prompt - assert "do not create a node merely for the\nact of searching" in prompt - assert "EVERY belief and decision MUST contain" in prompt - assert "missing evidence list makes the\n entire response invalid" in prompt - assert "Do not infer a decision" in prompt def test_unified_node_extraction_assigns_code_owned_tmp_ids( From 625f34defb5b1951f25318d71ab71ff73edb3a61 Mon Sep 17 00:00:00 2001 From: Yofuria Date: Thu, 27 Aug 2026 23:24:28 +0800 Subject: [PATCH 17/17] revert(construct): restore assistant extraction baseline --- bcg/construct/unified/prompts.py | 23 ----------------------- tests/test_belief_graph.py | 28 ---------------------------- 2 files changed, 51 deletions(-) diff --git a/bcg/construct/unified/prompts.py b/bcg/construct/unified/prompts.py index 7b50f4dc..e5ccdb29 100644 --- a/bcg/construct/unified/prompts.py +++ b/bcg/construct/unified/prompts.py @@ -165,26 +165,6 @@ subject. Skip greetings and purely cosmetic instructions. """ -_ASSISTANT_BELIEF_DEFINITION = """\ -## What is a belief -A belief is a self-contained, reusable memory or reasoning unit. Preserve the -most specific supported wording and enough subject, scope, and qualification for -the claim to remain understandable outside its original turn. - -## Granularity — coherent units, not tiny shards -Keep tightly coupled conditions, reasons, qualifiers, and results together when -splitting would destroy their dependency. Split independent propositions that -can be confirmed, contradicted, or reused separately, and keep claims with -different epistemic status separate. Each node must have one central meaning. - -## Entities -For every node, list specific named or uniquely qualified entities explicitly -involved in it: people, organizations, places, products, datasets, files, tools, -models, APIs, variables, and distinguishable concepts. Exclude pronouns, temporal -expressions, bare generic nouns, vague concepts, and duplicates. Use ``[]`` when -none exists; never invent an entity. -""" - _GRAPH_CONTEXT_BLOCK = f"""\ ## Existing belief graph (context — READ ONLY) These NODES and EDGES were already extracted from EARLIER turns. Use them only to: @@ -661,7 +641,6 @@ def build_node_extraction_prompt( task_line, guidance, _stance_hint = _GUIDANCE[key] has_existing_nodes = _has_existing_nodes(graph_nodes) - if key == "user" and not has_existing_nodes: task_intro = ( "Extract coherent, self-contained beliefs from the current USER turn only. " @@ -685,8 +664,6 @@ def build_node_extraction_prompt( belief_definition = _USER_BELIEF_DEFINITION stance_definition = _USER_STANCE_DEFINITION role_guidance = _USER_GUIDANCE - elif key == "assistant": - belief_definition = _ASSISTANT_BELIEF_DEFINITION parts.extend([belief_definition, stance_definition, role_guidance]) if has_existing_nodes: parts.append(_NODE_GRAPH_CONTEXT_BLOCK) diff --git a/tests/test_belief_graph.py b/tests/test_belief_graph.py index 5779b101..34d7015c 100644 --- a/tests/test_belief_graph.py +++ b/tests/test_belief_graph.py @@ -897,34 +897,6 @@ def test_unified_node_extraction_prompt_omits_empty_context_and_tmp_ids() -> Non assert "qualified roles" in prompt -def test_assistant_node_extraction_prompt_uses_compact_definition_and_baseline_order() -> None: - prompt = build_node_extraction_prompt( - "assistant", - mode="sentences", - sentences_block="[0] The assistant considers Candidate A.", - graph_nodes='[{"content": "An earlier clue."}]', - graph_edges='[{"from": 1, "to": 2, "type": "depends_on"}]', - ) - - assert prompt is not None - definition = prompt.index("## What is a belief") - context = prompt.index("## Existing belief nodes") - hard = prompt.index("## Hard constraints") - output = prompt.index("## Output (JSON only") - current = prompt.index("## Current turn sentences") - assert definition < context < hard < output < current - assert "An earlier clue." in prompt - assert "Existing relations" not in prompt - assert '"from": 1' not in prompt - assert '"tmp_id"' not in prompt - assert "self-contained, reusable memory or reasoning unit" in prompt - assert "Factual claims and intermediate conclusions" in prompt - assert "supporting_sentence_indices" in prompt - assert "COCO" not in prompt - assert "Mask R-CNN" not in prompt - assert "silver Honda Civic" not in prompt - - def test_unified_node_extraction_assigns_code_owned_tmp_ids( monkeypatch: pytest.MonkeyPatch, ) -> None: