Stage linked KB edge proposals from leoclean (#71)
Some checks are pending
CI / lint-and-test (push) Waiting to run

This commit is contained in:
twentyOne2x 2026-07-09 09:52:47 +02:00 committed by GitHub
parent a1814671e1
commit 2e2dcff4d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 223 additions and 30 deletions

View file

@ -109,6 +109,16 @@ def parse_args() -> argparse.Namespace:
propose.add_argument("--rationale", required=True) propose.add_argument("--rationale", required=True)
propose.add_argument("--format", choices=["markdown", "json"], default=argparse.SUPPRESS) propose.add_argument("--format", choices=["markdown", "json"], default=argparse.SUPPRESS)
propose_edge = sub.add_parser("propose-edge", help="Stage a strict add_edge proposal for reviewer approval.")
propose_edge.add_argument("from_claim")
propose_edge.add_argument("edge_type")
propose_edge.add_argument("to_claim")
propose_edge.add_argument("--proposed-by", default="leo")
propose_edge.add_argument("--channel", default="telegram")
propose_edge.add_argument("--source-ref", required=True)
propose_edge.add_argument("--rationale", required=True)
propose_edge.add_argument("--format", choices=["markdown", "json"], default=argparse.SUPPRESS)
list_proposals = sub.add_parser("list-proposals", help="List staged KB proposals from Cloud SQL.") list_proposals = sub.add_parser("list-proposals", help="List staged KB proposals from Cloud SQL.")
list_proposals.add_argument("--status", default="pending_review") list_proposals.add_argument("--status", default="pending_review")
list_proposals.add_argument("--limit", type=int, default=10) list_proposals.add_argument("--limit", type=int, default=10)
@ -145,7 +155,7 @@ def claim_url(claim_id: str | None) -> str | None:
def markdown_claim_link(claim_id: str | None, label: str | None = None) -> str: def markdown_claim_link(claim_id: str | None, label: str | None = None) -> str:
if not claim_id: if not claim_id:
return "`unknown`" return "`unknown`"
text = label or claim_id text = " ".join(str(label or claim_id).replace("`", "'").split())
return f"[`{text}`]({claim_url(claim_id)})" return f"[`{text}`]({claim_url(claim_id)})"
@ -832,6 +842,99 @@ from inserted;
return rows[0] return rows[0]
def propose_edge(args: argparse.Namespace) -> dict[str, Any]:
sql = f"""
with params as (
select
{sql_literal(args.from_claim)}::uuid as from_claim,
{sql_literal(args.to_claim)}::uuid as to_claim,
{sql_literal(args.edge_type)}::edge_type as edge_type,
nullif({sql_literal(args.proposed_by)}, '') as proposed_by_handle,
nullif({sql_literal(args.channel)}, '') as channel,
nullif({sql_literal(args.source_ref)}, '') as source_ref,
{sql_literal(args.rationale)} as rationale
), from_row as (
select c.id, c.text
from public.claims c, params p
where c.id = p.from_claim
), to_row as (
select c.id, c.text
from public.claims c, params p
where c.id = p.to_claim
), proposer as (
select a.id, a.handle
from public.agents a, params p
where a.handle = p.proposed_by_handle
limit 1
), existing_edge as (
select e.id
from public.claim_edges e, params p
where e.from_claim = p.from_claim
and e.to_claim = p.to_claim
and e.edge_type = p.edge_type
limit 1
), inserted as (
insert into kb_stage.kb_proposals (
proposal_type,
status,
proposed_by_handle,
proposed_by_agent_id,
channel,
source_ref,
rationale,
payload
)
select
'add_edge',
'pending_review',
p.proposed_by_handle,
proposer.id,
coalesce(p.channel, 'telegram'),
p.source_ref,
p.rationale,
jsonb_build_object(
'from_claim', from_row.id::text,
'from_text', from_row.text,
'edge_type', p.edge_type::text,
'to_claim', to_row.id::text,
'to_text', to_row.text,
'existing_edge_id', (select id::text from existing_edge),
'apply_payload', jsonb_build_object(
'from_claim', from_row.id::text,
'to_claim', to_row.id::text,
'edge_type', p.edge_type::text
)
)
from params p
join from_row on true
join to_row on true
left join proposer on true
returning *
)
select jsonb_build_object(
'artifact', 'teleo_cloudsql_kb_edge_proposal',
'backend', {sql_literal(canonical_backend(args))},
'id', id::text,
'proposal_type', proposal_type,
'status', status,
'proposed_by_handle', proposed_by_handle,
'proposed_by_agent_id', proposed_by_agent_id::text,
'channel', channel,
'source_ref', source_ref,
'rationale', rationale,
'payload', payload,
'created_at', created_at::text,
'canonical_apply_done', false,
'runtime_memory_write_done', false
)::text
from inserted;
"""
rows = psql_json_lines(args, sql, db=args.canonical_db)
if not rows:
raise SystemExit("No edge proposal inserted. Check that both claim ids exist and edge_type is valid.")
return rows[0]
def list_proposals(args: argparse.Namespace) -> dict[str, Any]: def list_proposals(args: argparse.Namespace) -> dict[str, Any]:
where = "" where = ""
if args.status.lower() != "all": if args.status.lower() != "all":
@ -992,9 +1095,10 @@ def emit_markdown(value: dict[str, Any]) -> None:
print("No matching canonical claims found.") print("No matching canonical claims found.")
return return
for i, claim in enumerate(value["claims"], 1): for i, claim in enumerate(value["claims"], 1):
print(f"### {i}. {claim.get('text', '[redacted claim text]')}\n") claim_label = claim.get("text") or "[redacted claim text]"
print(f"- id: `{claim['id']}`") print(f"### {i}. {markdown_claim_link(claim['id'], claim_label[:140])}\n")
print(f"- claim page: {claim_url(claim['id'])}") print(f"- claim id: {markdown_claim_link(claim['id'], claim['id'])}")
print(f"- open full claim/body/edges: {markdown_claim_link(claim['id'], 'claim page')}")
print(f"- type: `{claim.get('type')}`; confidence: `{claim.get('confidence')}`; score: `{claim.get('score')}`") print(f"- type: `{claim.get('type')}`; confidence: `{claim.get('confidence')}`; score: `{claim.get('score')}`")
print(f"- tags: `{', '.join(claim.get('tags') or [])}`") print(f"- tags: `{', '.join(claim.get('tags') or [])}`")
print(f"- evidence rows: `{claim.get('evidence_count', len(claim.get('evidence', [])))}`") print(f"- evidence rows: `{claim.get('evidence_count', len(claim.get('evidence', [])))}`")
@ -1008,11 +1112,11 @@ def emit_markdown(value: dict[str, Any]) -> None:
if claim.get("edges"): if claim.get("edges"):
print("\nEdges:") print("\nEdges:")
for edge in claim["edges"]: for edge in claim["edges"]:
text = f": {edge['connected_text']}" if edge.get("connected_text") else ""
connected = edge.get("connected_id") connected = edge.get("connected_id")
connected_label = (edge.get("connected_text") or str(connected or ""))[:120]
print( print(
f"- `{edge.get('direction')}` `{edge.get('edge_type')}` " f"- `{edge.get('direction')}` `{edge.get('edge_type')}` "
f"({markdown_claim_link(connected, str(connected or '')[:8])}){text}" f"{markdown_claim_link(connected, connected_label)}"
) )
print() print()
return return
@ -1021,12 +1125,12 @@ def emit_markdown(value: dict[str, Any]) -> None:
claim = value["claim"] claim = value["claim"]
print("# Teleo KB Claim\n") print("# Teleo KB Claim\n")
print(f"- Backend: `{value['backend']}`") print(f"- Backend: `{value['backend']}`")
print(f"- id: `{claim['id']}`") print(f"- claim id: {markdown_claim_link(claim['id'], claim['id'])}")
print(f"- claim page: {claim_url(claim['id'])}") print(f"- open full claim/body/edges: {markdown_claim_link(claim['id'], 'claim page')}")
print(f"- type: `{claim.get('type')}`; status: `{claim.get('status')}`; confidence: `{claim.get('confidence')}`") print(f"- type: `{claim.get('type')}`; status: `{claim.get('status')}`; confidence: `{claim.get('confidence')}`")
print(f"- tags: `{', '.join(claim.get('tags') or [])}`") print(f"- tags: `{', '.join(claim.get('tags') or [])}`")
if claim.get("text"): if claim.get("text"):
print(f"\n{claim['text']}\n") print(f"\n{markdown_claim_link(claim['id'], claim['text'][:180])}\n")
print("## Evidence\n") print("## Evidence\n")
for ev in value.get("evidence", []): for ev in value.get("evidence", []):
source = ev.get("storage_path") or ev.get("url") or "(no source pointer)" source = ev.get("storage_path") or ev.get("url") or "(no source pointer)"
@ -1034,26 +1138,27 @@ def emit_markdown(value: dict[str, Any]) -> None:
print(f"- `{ev.get('role')}` / `{ev.get('source_type')}` / `{source}`{excerpt}") print(f"- `{ev.get('role')}` / `{ev.get('source_type')}` / `{source}`{excerpt}")
print("\n## Edges\n") print("\n## Edges\n")
for edge in value.get("edges", []): for edge in value.get("edges", []):
text = f": {edge['connected_text']}" if edge.get("connected_text") else ""
connected = edge.get("connected_id") connected = edge.get("connected_id")
connected_label = (edge.get("connected_text") or str(connected or ""))[:120]
print( print(
f"- `{edge.get('direction')}` `{edge.get('edge_type')}` " f"- `{edge.get('direction')}` `{edge.get('edge_type')}` "
f"({markdown_claim_link(connected, str(connected or '')[:8])}){text}" f"{markdown_claim_link(connected, connected_label)}"
) )
return return
if value["artifact"] in {"teleo_cloudsql_canonical_kb_evidence", "teleo_cloudsql_canonical_kb_edges"}: if value["artifact"] in {"teleo_cloudsql_canonical_kb_evidence", "teleo_cloudsql_canonical_kb_edges"}:
print(f"# {value['artifact']}\n") print(f"# {value['artifact']}\n")
print(f"- Backend: `{value['backend']}`") print(f"- Backend: `{value['backend']}`")
print(f"- Claim: `{value['claim_id']}`\n") print(f"- Claim: {markdown_claim_link(value['claim_id'], value['claim_id'])}\n")
print(f"- Claim page: {claim_url(value['claim_id'])}\n") print(f"- Open full claim/body/edges: {markdown_claim_link(value['claim_id'], 'claim page')}\n")
rows = value.get("evidence") or value.get("edges") or [] rows = value.get("evidence") or value.get("edges") or []
for row in rows: for row in rows:
print(f"- `{json.dumps(row, sort_keys=True)}`") print(f"- `{json.dumps(row, sort_keys=True)}`")
return return
if value["artifact"] == "teleo_cloudsql_kb_core_change_proposal": if value["artifact"] in {"teleo_cloudsql_kb_core_change_proposal", "teleo_cloudsql_kb_edge_proposal"}:
print("# Staged KB Core-Change Proposal\n") title = "Staged KB Edge Proposal" if value["artifact"] == "teleo_cloudsql_kb_edge_proposal" else "Staged KB Core-Change Proposal"
print(f"# {title}\n")
print(f"- Proposal id: `{value['id']}`") print(f"- Proposal id: `{value['id']}`")
print(f"- Type: `{value['proposal_type']}`") print(f"- Type: `{value['proposal_type']}`")
print(f"- Status: `{value['status']}`") print(f"- Status: `{value['status']}`")
@ -1125,6 +1230,8 @@ def main() -> None:
result = edges_canonical_claim(args) result = edges_canonical_claim(args)
elif args.command == "propose-core-change": elif args.command == "propose-core-change":
result = propose_core_change(args) result = propose_core_change(args)
elif args.command == "propose-edge":
result = propose_edge(args)
elif args.command == "list-proposals": elif args.command == "list-proposals":
result = list_proposals(args) result = list_proposals(args)
elif args.command == "show-proposal": elif args.command == "show-proposal":

View file

@ -233,7 +233,7 @@ def claim_url(claim_id: str | None) -> str | None:
def markdown_claim_link(claim_id: str | None, label: str | None = None) -> str: def markdown_claim_link(claim_id: str | None, label: str | None = None) -> str:
if not claim_id: if not claim_id:
return "`unknown`" return "`unknown`"
text = label or claim_id text = " ".join(str(label or claim_id).replace("`", "'").split())
return f"[`{text}`]({claim_url(claim_id)})" return f"[`{text}`]({claim_url(claim_id)})"
@ -470,7 +470,12 @@ def propose_edge(args: argparse.Namespace) -> dict[str, Any]:
'edge_type', p.edge_type::text, 'edge_type', p.edge_type::text,
'to_claim', to_row.id::text, 'to_claim', to_row.id::text,
'to_text', to_row.text, 'to_text', to_row.text,
'existing_edge_id', (select id::text from existing_edge) 'existing_edge_id', (select id::text from existing_edge),
'apply_payload', jsonb_build_object(
'from_claim', from_row.id::text,
'to_claim', to_row.id::text,
'edge_type', p.edge_type::text
)
) )
from params p from params p
join from_row on true join from_row on true
@ -804,9 +809,9 @@ def print_claim_bundle(data: dict[str, Any]) -> None:
return return
print("## Claims\n") print("## Claims\n")
for idx, claim in enumerate(data["claims"], start=1): for idx, claim in enumerate(data["claims"], start=1):
print(f"### {idx}. {claim['text']}\n") print(f"### {idx}. {markdown_claim_link(claim['id'], truncate(claim.get('text'), 140))}\n")
print(f"- id: `{claim['id']}`") print(f"- claim id: {markdown_claim_link(claim['id'], claim['id'])}")
print(f"- claim page: {claim_url(claim['id'])}") print(f"- open full claim/body/edges: {markdown_claim_link(claim['id'], 'claim page')}")
print(f"- type: `{claim['type']}`; confidence: `{claim['confidence']}`; score: `{claim.get('score', '-')}`") print(f"- type: `{claim['type']}`; confidence: `{claim['confidence']}`; score: `{claim.get('score', '-')}`")
print(f"- tags: `{', '.join(claim.get('tags') or [])}`") print(f"- tags: `{', '.join(claim.get('tags') or [])}`")
print(f"- evidence rows: `{claim.get('evidence_count', len(claim.get('evidence', [])))}`") print(f"- evidence rows: `{claim.get('evidence_count', len(claim.get('evidence', [])))}`")
@ -821,10 +826,10 @@ def print_claim_bundle(data: dict[str, Any]) -> None:
print("\nEdges:") print("\nEdges:")
for edge in claim["edges"]: for edge in claim["edges"]:
connected = edge["connected_id"] connected = edge["connected_id"]
connected_label = truncate(edge.get("connected_text") or connected, 120)
print( print(
f"- `{edge['direction']}` `{edge['edge_type']}` " f"- `{edge['direction']}` `{edge['edge_type']}` "
f"({markdown_claim_link(connected, connected[:8])}): " f"{markdown_claim_link(connected, connected_label)}"
f"{truncate(edge['connected_text'], 260)}"
) )
print() print()
@ -843,9 +848,11 @@ def print_proposal(proposal: dict[str, Any]) -> None:
payload = proposal.get("payload") or {} payload = proposal.get("payload") or {}
if proposal["proposal_type"] == "add_edge": if proposal["proposal_type"] == "add_edge":
print("\n## Proposed Edge\n") print("\n## Proposed Edge\n")
print(f"- from: `{payload.get('from_claim')}` - {truncate(payload.get('from_text'), 260)}") print(
f"- from: {markdown_claim_link(payload.get('from_claim'), truncate(payload.get('from_text'), 120))}"
)
print(f"- edge_type: `{payload.get('edge_type')}`") print(f"- edge_type: `{payload.get('edge_type')}`")
print(f"- to: `{payload.get('to_claim')}` - {truncate(payload.get('to_text'), 260)}") print(f"- to: {markdown_claim_link(payload.get('to_claim'), truncate(payload.get('to_text'), 120))}")
if payload.get("existing_edge_id"): if payload.get("existing_edge_id"):
print(f"- existing_edge_id: `{payload['existing_edge_id']}`") print(f"- existing_edge_id: `{payload['existing_edge_id']}`")
else: else:
@ -868,8 +875,8 @@ def print_proposal_list(rows: list[dict[str, Any]]) -> None:
print(f"- created_at: `{row.get('created_at')}`") print(f"- created_at: `{row.get('created_at')}`")
if row["proposal_type"] == "add_edge": if row["proposal_type"] == "add_edge":
print( print(
f"- edge: `{payload.get('from_claim')}` " f"- edge: {markdown_claim_link(payload.get('from_claim'), truncate(payload.get('from_text'), 80))} "
f"`{payload.get('edge_type')}` `{payload.get('to_claim')}`" f"`{payload.get('edge_type')}` {markdown_claim_link(payload.get('to_claim'), truncate(payload.get('to_text'), 80))}"
) )
print(f"- rationale: {truncate(row.get('rationale'), 320)}") print(f"- rationale: {truncate(row.get('rationale'), 320)}")
print() print()

View file

@ -10,7 +10,7 @@ CLOUDSQL_STATUS_TIMEOUT=${TELEO_CLOUDSQL_STATUS_TIMEOUT_SECONDS:-30}
cloudsql_supported() { cloudsql_supported() {
case "$COMMAND" in case "$COMMAND" in
status|search|context|show|evidence|edges|propose-core-change|list-proposals|show-proposal) return 0 ;; status|search|context|show|evidence|edges|propose-core-change|propose-edge|list-proposals|show-proposal) return 0 ;;
*) return 1 ;; *) return 1 ;;
esac esac
} }

View file

@ -6,8 +6,8 @@ standalone (`python3 tests/test_apply_worker.py`).
""" """
import argparse import argparse
from pathlib import Path
import sys import sys
from pathlib import Path
try: try:
import pytest import pytest
@ -33,6 +33,19 @@ sys.path.insert(0, str(REPO_ROOT / "scripts"))
import apply_worker as w # noqa: E402 import apply_worker as w # noqa: E402
@pytest.fixture(autouse=True)
def restore_apply_worker_dependencies():
original_load_password = w.ap.load_password
original_fetch_candidates = w.fetch_candidates
original_apply_one = w.apply_one
original_render_one = w.render_one
yield
w.ap.load_password = original_load_password
w.fetch_candidates = original_fetch_candidates
w.apply_one = original_apply_one
w.render_one = original_render_one
# --- candidate query ------------------------------------------------------ # # --- candidate query ------------------------------------------------------ #
def test_candidate_query_filters_on_approved(): def test_candidate_query_filters_on_approved():
sql = w.build_candidate_query() sql = w.build_candidate_query()
@ -192,7 +205,7 @@ if __name__ == "__main__":
try: try:
fn() fn()
print(f"PASS {name}") print(f"PASS {name}")
except Exception: # noqa: BLE001 except Exception:
failures += 1 failures += 1
print(f"FAIL {name}") print(f"FAIL {name}")
traceback.print_exc() traceback.print_exc()

View file

@ -3,7 +3,9 @@
from __future__ import annotations from __future__ import annotations
import ast import ast
import contextlib
import importlib.util import importlib.util
import io
import re import re
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@ -38,17 +40,29 @@ def test_cloudsql_wrapper_supports_expected_review_gated_commands() -> None:
"evidence", "evidence",
"edges", "edges",
"propose-core-change", "propose-core-change",
"propose-edge",
"list-proposals", "list-proposals",
"show-proposal", "show-proposal",
]: ]:
assert command in wrapper_text assert command in wrapper_text
assert command in cloudsql_text assert command in cloudsql_text
assert "propose-edge" not in wrapper_text
assert "kb_stage.kb_proposals" in cloudsql_text assert "kb_stage.kb_proposals" in cloudsql_text
assert "canonical apply" in cloudsql_text.lower() assert "canonical apply" in cloudsql_text.lower()
def test_bridge_edge_proposals_stage_strict_apply_payloads() -> None:
cloudsql_text = (BRIDGE_DIR / "cloudsql_memory_tool.py").read_text()
vps_text = (BRIDGE_DIR / "kb_tool.py").read_text()
for text in (cloudsql_text, vps_text):
assert "'proposal_type', proposal_type" in text or "select 'add_edge'" in text
assert "'apply_payload', jsonb_build_object(" in text
assert "'from_claim', from_row.id::text" in text
assert "'to_claim', to_row.id::text" in text
assert "'edge_type', p.edge_type::text" in text
def test_bridge_source_does_not_commit_raw_secret_values() -> None: def test_bridge_source_does_not_commit_raw_secret_values() -> None:
combined = "\n".join(path.read_text(errors="replace") for path in BRIDGE_DIR.iterdir() if path.is_file()) combined = "\n".join(path.read_text(errors="replace") for path in BRIDGE_DIR.iterdir() if path.is_file())
forbidden_patterns = [ forbidden_patterns = [
@ -82,3 +96,55 @@ def test_kb_bridges_emit_public_claim_links_for_telegram_rendering() -> None:
assert module.markdown_claim_link(claim_id, "d3fb892b") == ( assert module.markdown_claim_link(claim_id, "d3fb892b") == (
f"[`d3fb892b`]({expected})" f"[`d3fb892b`]({expected})"
) )
assert module.markdown_claim_link(claim_id, "claim `with ticks`") == (
f"[`claim 'with ticks'`]({expected})"
)
def test_vps_bridge_markdown_links_claim_text_and_edges() -> None:
module = _load_module(BRIDGE_DIR / "kb_tool.py")
claim_id = "d3fb892b-3c5a-4700-9512-55e5c680eec1"
connected_id = "9d5281fe-7ee4-4fe1-b1bf-c54c4d7fb6a5"
data = {
"query": "strategy kernel",
"context_rows": [],
"claims": [
{
"id": claim_id,
"text": "Claims should be easy to scan in Telegram.",
"type": "belief",
"confidence": 0.8,
"tags": ["telegram"],
"score": 4,
"evidence_count": 2,
"edge_count": 1,
"evidence": [],
"edges": [
{
"direction": "outgoing",
"edge_type": "supports",
"connected_id": connected_id,
"connected_text": "Full claim pages expose body, evidence, and graph edges.",
}
],
}
],
}
buffer = io.StringIO()
with contextlib.redirect_stdout(buffer):
module.print_claim_bundle(data)
markdown = buffer.getvalue()
assert (
f"[`Claims should be easy to scan in Telegram.`](https://leo.livingip.xyz/kb/claims/{claim_id})"
in markdown
)
assert (
f"- open full claim/body/edges: [`claim page`](https://leo.livingip.xyz/kb/claims/{claim_id})"
in markdown
)
assert (
f"[`Full claim pages expose body, evidence, and graph edges.`](https://leo.livingip.xyz/kb/claims/{connected_id})"
in markdown
)