Keep Leo follow-up retrieval on the active subject (#184)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
This commit is contained in:
parent
6a4209475b
commit
f475bc0d5d
4 changed files with 507 additions and 31 deletions
|
|
@ -253,6 +253,16 @@ def parse_args() -> argparse.Namespace:
|
||||||
|
|
||||||
context = sub.add_parser("context", help="Build an agent-ready KB context bundle for a question.")
|
context = sub.add_parser("context", help="Build an agent-ready KB context bundle for a question.")
|
||||||
context.add_argument("query", nargs="+")
|
context.add_argument("query", nargs="+")
|
||||||
|
context.add_argument(
|
||||||
|
"--retrieval-query",
|
||||||
|
help="Optional read-only semantic query; operational contracts remain bound to the exact operator query.",
|
||||||
|
)
|
||||||
|
context.add_argument(
|
||||||
|
"--retrieval-anchor",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Bounded topic anchor used only to rank read-only claim retrieval; may be repeated.",
|
||||||
|
)
|
||||||
context.add_argument("--limit", type=int, default=4)
|
context.add_argument("--limit", type=int, default=4)
|
||||||
context.add_argument("--context-limit", type=int, default=6)
|
context.add_argument("--context-limit", type=int, default=6)
|
||||||
add_output_flag(context)
|
add_output_flag(context)
|
||||||
|
|
@ -1951,13 +1961,24 @@ def markdown_claim_link(claim_id: str | None, label: str | None = None) -> str:
|
||||||
return f"[`{text}`]({claim_url(claim_id)})"
|
return f"[`{text}`]({claim_url(claim_id)})"
|
||||||
|
|
||||||
|
|
||||||
def find_claims(args: argparse.Namespace, query: str, limit: int) -> list[dict[str, Any]]:
|
def find_claims(
|
||||||
|
args: argparse.Namespace,
|
||||||
|
query: str,
|
||||||
|
limit: int,
|
||||||
|
retrieval_anchors: list[str] | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
patterns = [f"%{term}%" for term in query_terms(query)]
|
patterns = [f"%{term}%" for term in query_terms(query)]
|
||||||
|
anchor_terms = query_terms(" ".join(retrieval_anchors or [])) if retrieval_anchors else []
|
||||||
|
anchor_patterns = [f"%{term}%" for term in anchor_terms]
|
||||||
min_score = 2 if len(patterns) >= 3 else 1
|
min_score = 2 if len(patterns) >= 3 else 1
|
||||||
|
anchor_limit = max(1, min(2, limit))
|
||||||
sql = f"""
|
sql = f"""
|
||||||
with params(patterns) as (
|
with params(patterns) as (
|
||||||
values ({sql_array(patterns)})
|
values ({sql_array(patterns)})
|
||||||
),
|
),
|
||||||
|
anchors(patterns) as (
|
||||||
|
values ({sql_array(anchor_patterns)})
|
||||||
|
),
|
||||||
ranked as (
|
ranked as (
|
||||||
select c.id,
|
select c.id,
|
||||||
c.type,
|
c.type,
|
||||||
|
|
@ -1974,6 +1995,15 @@ def find_claims(args: argparse.Namespace, query: str, limit: int) -> list[dict[s
|
||||||
where lower(tag) like pattern
|
where lower(tag) like pattern
|
||||||
)
|
)
|
||||||
) as score,
|
) as score,
|
||||||
|
(
|
||||||
|
select count(*)
|
||||||
|
from anchors a, unnest(a.patterns) pattern
|
||||||
|
where lower(c.text) like pattern
|
||||||
|
or exists (
|
||||||
|
select 1 from unnest(coalesce(c.tags, '{{}}'::text[])) tag
|
||||||
|
where lower(tag) like pattern
|
||||||
|
)
|
||||||
|
) as anchor_score,
|
||||||
(select count(*) from claim_evidence ce where ce.claim_id = c.id) as evidence_count,
|
(select count(*) from claim_evidence ce where ce.claim_id = c.id) as evidence_count,
|
||||||
(select count(*) from claim_edges e where e.from_claim = c.id or e.to_claim = c.id) as edge_count
|
(select count(*) from claim_edges e where e.from_claim = c.id or e.to_claim = c.id) as edge_count
|
||||||
from claims c
|
from claims c
|
||||||
|
|
@ -1983,6 +2013,29 @@ def find_claims(args: argparse.Namespace, query: str, limit: int) -> list[dict[s
|
||||||
select ranked.*,
|
select ranked.*,
|
||||||
max(score) over () as max_score
|
max(score) over () as max_score
|
||||||
from ranked
|
from ranked
|
||||||
|
),
|
||||||
|
raw_selected as (
|
||||||
|
select eligible.*
|
||||||
|
from eligible
|
||||||
|
where score >= case when max_score >= {min_score} then {min_score} else 1 end
|
||||||
|
order by score desc, evidence_count desc, edge_count desc,
|
||||||
|
coalesce(confidence, 0) desc, length(text), text, id
|
||||||
|
limit {limit}
|
||||||
|
),
|
||||||
|
anchor_selected as (
|
||||||
|
select eligible.*
|
||||||
|
from eligible
|
||||||
|
where anchor_score >= 1
|
||||||
|
order by anchor_score desc, score desc, evidence_count desc, edge_count desc,
|
||||||
|
coalesce(confidence, 0) desc, length(text), text, id
|
||||||
|
limit {anchor_limit}
|
||||||
|
),
|
||||||
|
lanes as (
|
||||||
|
select raw_selected.*, 'raw'::text as selection_lane
|
||||||
|
from raw_selected
|
||||||
|
union all
|
||||||
|
select anchor_selected.*, 'anchor'::text as selection_lane
|
||||||
|
from anchor_selected
|
||||||
)
|
)
|
||||||
select jsonb_build_object(
|
select jsonb_build_object(
|
||||||
'id', id::text,
|
'id', id::text,
|
||||||
|
|
@ -1991,16 +2044,31 @@ def find_claims(args: argparse.Namespace, query: str, limit: int) -> list[dict[s
|
||||||
'confidence', confidence,
|
'confidence', confidence,
|
||||||
'tags', coalesce(to_jsonb(tags), '[]'::jsonb),
|
'tags', coalesce(to_jsonb(tags), '[]'::jsonb),
|
||||||
'score', score,
|
'score', score,
|
||||||
|
'anchor_score', anchor_score,
|
||||||
'evidence_count', evidence_count,
|
'evidence_count', evidence_count,
|
||||||
'edge_count', edge_count
|
'edge_count', edge_count,
|
||||||
|
'_selection_lane', selection_lane
|
||||||
)::text
|
)::text
|
||||||
from eligible
|
from lanes
|
||||||
where score >= 1
|
order by case selection_lane when 'raw' then 0 else 1 end,
|
||||||
and score >= case when max_score >= {min_score} then {min_score} else 1 end
|
score desc, anchor_score desc, evidence_count desc, edge_count desc,
|
||||||
order by score desc, evidence_count desc, edge_count desc, coalesce(confidence, 0) desc, length(text), text, id
|
coalesce(confidence, 0) desc, length(text), text, id;
|
||||||
limit {limit};
|
|
||||||
"""
|
"""
|
||||||
return psql_json(args, sql)
|
rows = psql_json(args, sql)
|
||||||
|
raw_rows: list[dict[str, Any]] = []
|
||||||
|
anchor_rows: list[dict[str, Any]] = []
|
||||||
|
for row in rows:
|
||||||
|
lane = row.pop("_selection_lane", None)
|
||||||
|
if lane == "raw":
|
||||||
|
raw_rows.append(row)
|
||||||
|
elif lane == "anchor":
|
||||||
|
anchor_rows.append(row)
|
||||||
|
if not anchor_patterns or any(int(row.get("anchor_score") or 0) > 0 for row in raw_rows):
|
||||||
|
return raw_rows[:limit]
|
||||||
|
|
||||||
|
raw_ids = {str(row.get("id")) for row in raw_rows}
|
||||||
|
backfill = [row for row in anchor_rows if str(row.get("id")) not in raw_ids][:anchor_limit]
|
||||||
|
return backfill + raw_rows[: max(0, limit - len(backfill))]
|
||||||
|
|
||||||
|
|
||||||
def find_context_rows(args: argparse.Namespace, query: str, limit: int) -> list[dict[str, Any]]:
|
def find_context_rows(args: argparse.Namespace, query: str, limit: int) -> list[dict[str, Any]]:
|
||||||
|
|
@ -3339,6 +3407,8 @@ def build_retrieval_receipt(
|
||||||
return {
|
return {
|
||||||
"schema": RETRIEVAL_RECEIPT_SCHEMA,
|
"schema": RETRIEVAL_RECEIPT_SCHEMA,
|
||||||
"query_sha256": hashlib.sha256(str(data.get("query") or "").encode("utf-8")).hexdigest(),
|
"query_sha256": hashlib.sha256(str(data.get("query") or "").encode("utf-8")).hexdigest(),
|
||||||
|
"retrieval_query_sha256": data.get("retrieval_query_sha256"),
|
||||||
|
"retrieval_anchors_sha256": data.get("retrieval_anchors_sha256"),
|
||||||
"semantic_context_sha256": canonical_json_sha256(semantic_context),
|
"semantic_context_sha256": canonical_json_sha256(semantic_context),
|
||||||
"artifact_state_sha256": canonical_json_sha256(artifact_states),
|
"artifact_state_sha256": canonical_json_sha256(artifact_states),
|
||||||
"claim_ids": [str(claim.get("id")) for claim in data.get("claims", []) if claim.get("id")],
|
"claim_ids": [str(claim.get("id")) for claim in data.get("claims", []) if claim.get("id")],
|
||||||
|
|
@ -3362,16 +3432,26 @@ def build_retrieval_receipt(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _bundle_once(args: argparse.Namespace, query: str, claim_limit: int, context_limit: int) -> dict[str, Any]:
|
def _bundle_once(
|
||||||
|
args: argparse.Namespace,
|
||||||
|
query: str,
|
||||||
|
claim_limit: int,
|
||||||
|
context_limit: int,
|
||||||
|
retrieval_query: str | None = None,
|
||||||
|
retrieval_anchors: list[str] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
semantic_query = retrieval_query or query
|
||||||
contracts = context_operational_contracts(args, query)
|
contracts = context_operational_contracts(args, query)
|
||||||
claims = find_claims(args, query, claim_limit)
|
claims = find_claims(args, semantic_query, claim_limit, retrieval_anchors)
|
||||||
claim_ids = [claim["id"] for claim in claims]
|
claim_ids = [claim["id"] for claim in claims]
|
||||||
evidence = load_evidence(args, claim_ids, 4)
|
evidence = load_evidence(args, claim_ids, 4)
|
||||||
edges = load_edges(args, claim_ids, 6)
|
edges = load_edges(args, claim_ids, 6)
|
||||||
bind_claim_evidence_challenge(contracts, claims, evidence)
|
bind_claim_evidence_challenge(contracts, claims, evidence)
|
||||||
return {
|
return {
|
||||||
"query": query,
|
"query": query,
|
||||||
"terms": query_terms(query),
|
"retrieval_query_sha256": hashlib.sha256(semantic_query.encode("utf-8")).hexdigest(),
|
||||||
|
"retrieval_anchors_sha256": canonical_json_sha256(retrieval_anchors or []),
|
||||||
|
"terms": query_terms(semantic_query),
|
||||||
"operational_contracts": contracts,
|
"operational_contracts": contracts,
|
||||||
"compiled_response": compile_operational_response(contracts),
|
"compiled_response": compile_operational_response(contracts),
|
||||||
"claims": [
|
"claims": [
|
||||||
|
|
@ -3383,11 +3463,18 @@ def _bundle_once(args: argparse.Namespace, query: str, claim_limit: int, context
|
||||||
}
|
}
|
||||||
for claim in claims
|
for claim in claims
|
||||||
],
|
],
|
||||||
"context_rows": find_context_rows(args, query, context_limit),
|
"context_rows": find_context_rows(args, semantic_query, context_limit),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def bundle(args: argparse.Namespace, query: str, claim_limit: int, context_limit: int) -> dict[str, Any]:
|
def bundle(
|
||||||
|
args: argparse.Namespace,
|
||||||
|
query: str,
|
||||||
|
claim_limit: int,
|
||||||
|
context_limit: int,
|
||||||
|
retrieval_query: str | None = None,
|
||||||
|
retrieval_anchors: list[str] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
max_attempts = max(1, min(int(os.environ.get("TELEO_KB_READ_ATTEMPTS", "3")), 5))
|
max_attempts = max(1, min(int(os.environ.get("TELEO_KB_READ_ATTEMPTS", "3")), 5))
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
|
|
@ -3396,7 +3483,7 @@ def bundle(args: argparse.Namespace, query: str, claim_limit: int, context_limit
|
||||||
last_markers: tuple[dict[str, Any], dict[str, Any]] | None = None
|
last_markers: tuple[dict[str, Any], dict[str, Any]] | None = None
|
||||||
for attempt in range(1, max_attempts + 1):
|
for attempt in range(1, max_attempts + 1):
|
||||||
before = database_read_marker(args)
|
before = database_read_marker(args)
|
||||||
data = _bundle_once(args, query, claim_limit, context_limit)
|
data = _bundle_once(args, query, claim_limit, context_limit, retrieval_query, retrieval_anchors)
|
||||||
after = database_read_marker(args)
|
after = database_read_marker(args)
|
||||||
last_markers = (before, after)
|
last_markers = (before, after)
|
||||||
consistency_status = "stable_wal_marker" if _read_marker_stable(before, after) else "wal_changed_during_read"
|
consistency_status = "stable_wal_marker" if _read_marker_stable(before, after) else "wal_changed_during_read"
|
||||||
|
|
@ -3607,7 +3694,14 @@ def main() -> int:
|
||||||
if args.command == "status":
|
if args.command == "status":
|
||||||
data = status(args)
|
data = status(args)
|
||||||
elif args.command in {"search", "context"}:
|
elif args.command in {"search", "context"}:
|
||||||
data = bundle(args, args.query, args.limit, args.context_limit)
|
data = bundle(
|
||||||
|
args,
|
||||||
|
args.query,
|
||||||
|
args.limit,
|
||||||
|
args.context_limit,
|
||||||
|
getattr(args, "retrieval_query", None),
|
||||||
|
getattr(args, "retrieval_anchor", None),
|
||||||
|
)
|
||||||
elif args.command == "show":
|
elif args.command == "show":
|
||||||
claim = get_claim(args, args.claim_id)
|
claim = get_claim(args, args.claim_id)
|
||||||
if not claim:
|
if not claim:
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from collections import OrderedDict
|
from collections import Counter, OrderedDict
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -23,6 +23,10 @@ CONTEXT_ROW_LIMIT = 4
|
||||||
MAX_CLAIM_TEXT_CHARS = 1_000
|
MAX_CLAIM_TEXT_CHARS = 1_000
|
||||||
MAX_CONTEXT_BODY_CHARS = 800
|
MAX_CONTEXT_BODY_CHARS = 800
|
||||||
MAX_EVIDENCE_EXCERPT_CHARS = 600
|
MAX_EVIDENCE_EXCERPT_CHARS = 600
|
||||||
|
MAX_RETRIEVAL_ANCHORS = 8
|
||||||
|
MAX_RETRIEVAL_SUBJECT_ANCHORS = 2
|
||||||
|
MAX_RETRIEVAL_FALLBACK_ANCHORS = 3
|
||||||
|
RETRIEVAL_HISTORY_USER_TURNS = 6
|
||||||
DATABASE_REASONING_PROTOCOL_VERSION = "leo-db-reasoning-v2"
|
DATABASE_REASONING_PROTOCOL_VERSION = "leo-db-reasoning-v2"
|
||||||
WORD_RE = re.compile(r"\b\w+(?:[-']\w+)*\b")
|
WORD_RE = re.compile(r"\b\w+(?:[-']\w+)*\b")
|
||||||
LIST_PREFIX_RE = re.compile(r"^(\s*(?:[-*]|\d+[.)])\s+)(.*)$")
|
LIST_PREFIX_RE = re.compile(r"^(\s*(?:[-*]|\d+[.)])\s+)(.*)$")
|
||||||
|
|
@ -31,6 +35,120 @@ REQUESTED_SUBJECT_RE = re.compile(
|
||||||
r"name the subject exactly once as\s+`(?P<subject>[^`\r\n]{1,120})`\s+in your answer",
|
r"name the subject exactly once as\s+`(?P<subject>[^`\r\n]{1,120})`\s+in your answer",
|
||||||
re.I,
|
re.I,
|
||||||
)
|
)
|
||||||
|
RETRIEVAL_TOKEN_RE = re.compile(r"[A-Za-z][A-Za-z0-9.+#'-]{2,}")
|
||||||
|
RETRIEVAL_ANCHOR_STOPWORDS = frozenset(
|
||||||
|
{
|
||||||
|
"about",
|
||||||
|
"across",
|
||||||
|
"after",
|
||||||
|
"all",
|
||||||
|
"also",
|
||||||
|
"and",
|
||||||
|
"answer",
|
||||||
|
"actually",
|
||||||
|
"any",
|
||||||
|
"anything",
|
||||||
|
"are",
|
||||||
|
"argue",
|
||||||
|
"around",
|
||||||
|
"before",
|
||||||
|
"being",
|
||||||
|
"been",
|
||||||
|
"better",
|
||||||
|
"both",
|
||||||
|
"but",
|
||||||
|
"can",
|
||||||
|
"could",
|
||||||
|
"did",
|
||||||
|
"does",
|
||||||
|
"each",
|
||||||
|
"for",
|
||||||
|
"from",
|
||||||
|
"had",
|
||||||
|
"has",
|
||||||
|
"have",
|
||||||
|
"how",
|
||||||
|
"into",
|
||||||
|
"less",
|
||||||
|
"more",
|
||||||
|
"not",
|
||||||
|
"now",
|
||||||
|
"only",
|
||||||
|
"our",
|
||||||
|
"over",
|
||||||
|
"same",
|
||||||
|
"say",
|
||||||
|
"should",
|
||||||
|
"still",
|
||||||
|
"than",
|
||||||
|
"that",
|
||||||
|
"the",
|
||||||
|
"their",
|
||||||
|
"them",
|
||||||
|
"then",
|
||||||
|
"they",
|
||||||
|
"this",
|
||||||
|
"through",
|
||||||
|
"use",
|
||||||
|
"was",
|
||||||
|
"were",
|
||||||
|
"what",
|
||||||
|
"when",
|
||||||
|
"where",
|
||||||
|
"which",
|
||||||
|
"who",
|
||||||
|
"why",
|
||||||
|
"will",
|
||||||
|
"with",
|
||||||
|
"without",
|
||||||
|
"would",
|
||||||
|
"you",
|
||||||
|
"your",
|
||||||
|
"asserting",
|
||||||
|
"based",
|
||||||
|
"body",
|
||||||
|
"candidate",
|
||||||
|
"canonical",
|
||||||
|
"challenge",
|
||||||
|
"claim",
|
||||||
|
"concrete",
|
||||||
|
"confidence",
|
||||||
|
"context",
|
||||||
|
"database",
|
||||||
|
"direct",
|
||||||
|
"distinguish",
|
||||||
|
"discussion",
|
||||||
|
"evidence",
|
||||||
|
"exact",
|
||||||
|
"explain",
|
||||||
|
"explicit",
|
||||||
|
"existing",
|
||||||
|
"falsifier",
|
||||||
|
"give",
|
||||||
|
"identify",
|
||||||
|
"initial",
|
||||||
|
"inference",
|
||||||
|
"minimum",
|
||||||
|
"missing",
|
||||||
|
"most",
|
||||||
|
"nothing",
|
||||||
|
"opposite",
|
||||||
|
"outcome",
|
||||||
|
"requirements",
|
||||||
|
"return",
|
||||||
|
"revised",
|
||||||
|
"reviewable",
|
||||||
|
"source",
|
||||||
|
"stage",
|
||||||
|
"state",
|
||||||
|
"strongest",
|
||||||
|
"support",
|
||||||
|
"test",
|
||||||
|
"tool",
|
||||||
|
"while",
|
||||||
|
"write",
|
||||||
|
}
|
||||||
|
)
|
||||||
COMPILED_CONTRACT_IDS = frozenset(
|
COMPILED_CONTRACT_IDS = frozenset(
|
||||||
{
|
{
|
||||||
"mixed_packet_composition",
|
"mixed_packet_composition",
|
||||||
|
|
@ -96,6 +214,8 @@ def _retrieval_receipt_trace(
|
||||||
safe = {
|
safe = {
|
||||||
"schema": value.get("schema"),
|
"schema": value.get("schema"),
|
||||||
"query_sha256": value.get("query_sha256"),
|
"query_sha256": value.get("query_sha256"),
|
||||||
|
"retrieval_query_sha256": value.get("retrieval_query_sha256"),
|
||||||
|
"retrieval_anchors_sha256": value.get("retrieval_anchors_sha256"),
|
||||||
"semantic_context_sha256": value.get("semantic_context_sha256"),
|
"semantic_context_sha256": value.get("semantic_context_sha256"),
|
||||||
"artifact_state_sha256": value.get("artifact_state_sha256"),
|
"artifact_state_sha256": value.get("artifact_state_sha256"),
|
||||||
"claim_ids": sorted(str(item) for item in value.get("claim_ids") or []),
|
"claim_ids": sorted(str(item) for item in value.get("claim_ids") or []),
|
||||||
|
|
@ -134,6 +254,100 @@ def _failure_context(reason: str) -> str:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _history_message_text(message: Any) -> str:
|
||||||
|
if not isinstance(message, dict) or message.get("role") != "user":
|
||||||
|
return ""
|
||||||
|
content = message.get("content")
|
||||||
|
if isinstance(content, str):
|
||||||
|
return content
|
||||||
|
if not isinstance(content, list):
|
||||||
|
return ""
|
||||||
|
parts: list[str] = []
|
||||||
|
for item in content:
|
||||||
|
if isinstance(item, str):
|
||||||
|
parts.append(item)
|
||||||
|
elif isinstance(item, dict) and isinstance(item.get("text"), str):
|
||||||
|
parts.append(str(item["text"]))
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_subject_token(token: str) -> bool:
|
||||||
|
return token.isupper() or any(char.isupper() for char in token[1:])
|
||||||
|
|
||||||
|
|
||||||
|
def _contextual_retrieval_query(user_message: str, conversation_history: Any) -> tuple[str, list[str]]:
|
||||||
|
"""Return the exact query plus bounded user-only topic anchors for separate ranking."""
|
||||||
|
|
||||||
|
query = str(user_message or "")[:MAX_QUERY_CHARS]
|
||||||
|
if not isinstance(conversation_history, list):
|
||||||
|
return query, []
|
||||||
|
|
||||||
|
prior_user_turns: list[str] = []
|
||||||
|
skipped_current = False
|
||||||
|
for message in reversed(conversation_history):
|
||||||
|
text = _history_message_text(message).strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
if not skipped_current and text == query.strip():
|
||||||
|
skipped_current = True
|
||||||
|
continue
|
||||||
|
prior_user_turns.append(text)
|
||||||
|
if len(prior_user_turns) >= RETRIEVAL_HISTORY_USER_TURNS:
|
||||||
|
break
|
||||||
|
|
||||||
|
counts: Counter[str] = Counter()
|
||||||
|
recency: dict[str, int] = {}
|
||||||
|
surfaces: dict[str, str] = {}
|
||||||
|
subject_tokens: set[str] = set()
|
||||||
|
for turn_index, text in enumerate(prior_user_turns):
|
||||||
|
seen_in_turn: set[str] = set()
|
||||||
|
for token in RETRIEVAL_TOKEN_RE.findall(text):
|
||||||
|
surface = token.strip("-'.+")
|
||||||
|
if surface.casefold().endswith("'s"):
|
||||||
|
surface = surface[:-2]
|
||||||
|
normalized = surface.casefold()
|
||||||
|
if len(normalized) < 3 or normalized in RETRIEVAL_ANCHOR_STOPWORDS or normalized in seen_in_turn:
|
||||||
|
continue
|
||||||
|
seen_in_turn.add(normalized)
|
||||||
|
counts[normalized] += 1
|
||||||
|
recency.setdefault(normalized, RETRIEVAL_HISTORY_USER_TURNS - turn_index)
|
||||||
|
surfaces.setdefault(normalized, surface)
|
||||||
|
if _is_subject_token(surface):
|
||||||
|
subject_tokens.add(normalized)
|
||||||
|
|
||||||
|
current_terms: set[str] = set()
|
||||||
|
for token in RETRIEVAL_TOKEN_RE.findall(query):
|
||||||
|
surface = token.strip("-'.+")
|
||||||
|
if surface.casefold().endswith("'s"):
|
||||||
|
surface = surface[:-2]
|
||||||
|
normalized = surface.casefold()
|
||||||
|
if len(normalized) < 3 or normalized in RETRIEVAL_ANCHOR_STOPWORDS or normalized in current_terms:
|
||||||
|
continue
|
||||||
|
current_terms.add(normalized)
|
||||||
|
counts[normalized] += 1
|
||||||
|
recency[normalized] = RETRIEVAL_HISTORY_USER_TURNS + 1
|
||||||
|
surfaces[normalized] = surface
|
||||||
|
if _is_subject_token(surface):
|
||||||
|
subject_tokens.add(normalized)
|
||||||
|
|
||||||
|
subject_anchors = sorted(
|
||||||
|
subject_tokens,
|
||||||
|
key=lambda term: (
|
||||||
|
-counts[term],
|
||||||
|
-recency[term],
|
||||||
|
term,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if subject_anchors:
|
||||||
|
selected = subject_anchors[:MAX_RETRIEVAL_SUBJECT_ANCHORS]
|
||||||
|
else:
|
||||||
|
repeated = [term for term, count in counts.items() if count >= 2]
|
||||||
|
repeated.sort(key=lambda term: (-counts[term], -recency[term], term))
|
||||||
|
selected = repeated[:MAX_RETRIEVAL_FALLBACK_ANCHORS]
|
||||||
|
anchors = [surfaces[term] for term in selected[:MAX_RETRIEVAL_ANCHORS]]
|
||||||
|
return query, anchors
|
||||||
|
|
||||||
|
|
||||||
def _requires_database_truth(query: str) -> bool:
|
def _requires_database_truth(query: str) -> bool:
|
||||||
lowered = query.lower()
|
lowered = query.lower()
|
||||||
named_packet_question = "helmer" in lowered and any(
|
named_packet_question = "helmer" in lowered and any(
|
||||||
|
|
@ -345,12 +559,14 @@ def _take_snapshot(session_id: str, query_sha256: str) -> dict[str, Any] | None:
|
||||||
def _load_database_snapshot(
|
def _load_database_snapshot(
|
||||||
user_message: str,
|
user_message: str,
|
||||||
*,
|
*,
|
||||||
|
conversation_history: Any = None,
|
||||||
hermes_home: Path | None = None,
|
hermes_home: Path | None = None,
|
||||||
runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
|
runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Load one immutable DB-contract snapshot for generation and validation."""
|
"""Load one immutable DB-contract snapshot for generation and validation."""
|
||||||
|
|
||||||
query = str(user_message or "")[:MAX_QUERY_CHARS]
|
query = str(user_message or "")[:MAX_QUERY_CHARS]
|
||||||
|
retrieval_query, retrieval_anchors = _contextual_retrieval_query(query, conversation_history)
|
||||||
home = hermes_home or Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes")))
|
home = hermes_home or Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes")))
|
||||||
kb_tool = home / "bin" / "kb_tool.py"
|
kb_tool = home / "bin" / "kb_tool.py"
|
||||||
query_sha256 = hashlib.sha256(query.encode("utf-8")).hexdigest()
|
query_sha256 = hashlib.sha256(query.encode("utf-8")).hexdigest()
|
||||||
|
|
@ -358,6 +574,11 @@ def _load_database_snapshot(
|
||||||
"generated_at_utc": datetime.now(timezone.utc).isoformat(),
|
"generated_at_utc": datetime.now(timezone.utc).isoformat(),
|
||||||
"event": "pre_llm_call",
|
"event": "pre_llm_call",
|
||||||
"query_sha256": query_sha256,
|
"query_sha256": query_sha256,
|
||||||
|
"retrieval_query_sha256": hashlib.sha256(retrieval_query.encode("utf-8")).hexdigest(),
|
||||||
|
"retrieval_anchor_count": len(retrieval_anchors),
|
||||||
|
"retrieval_anchors_sha256": hashlib.sha256(
|
||||||
|
json.dumps(retrieval_anchors, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||||
|
).hexdigest(),
|
||||||
"source": "kb_tool.py --local context",
|
"source": "kb_tool.py --local context",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -373,13 +594,21 @@ def _load_database_snapshot(
|
||||||
"--local",
|
"--local",
|
||||||
"context",
|
"context",
|
||||||
query,
|
query,
|
||||||
|
"--retrieval-query",
|
||||||
|
retrieval_query,
|
||||||
|
]
|
||||||
|
for anchor in retrieval_anchors:
|
||||||
|
command.extend(("--retrieval-anchor", anchor))
|
||||||
|
command.extend(
|
||||||
|
(
|
||||||
"--limit",
|
"--limit",
|
||||||
str(CONTEXT_CLAIM_LIMIT),
|
str(CONTEXT_CLAIM_LIMIT),
|
||||||
"--context-limit",
|
"--context-limit",
|
||||||
str(CONTEXT_ROW_LIMIT),
|
str(CONTEXT_ROW_LIMIT),
|
||||||
"--format",
|
"--format",
|
||||||
"json",
|
"json",
|
||||||
]
|
)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
completed = runner(command, capture_output=True, text=True, timeout=timeout, check=False)
|
completed = runner(command, capture_output=True, text=True, timeout=timeout, check=False)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
|
|
@ -454,12 +683,20 @@ def _load_database_snapshot(
|
||||||
def build_database_context(
|
def build_database_context(
|
||||||
user_message: str,
|
user_message: str,
|
||||||
*,
|
*,
|
||||||
|
conversation_history: Any = None,
|
||||||
hermes_home: Path | None = None,
|
hermes_home: Path | None = None,
|
||||||
runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
|
runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Return current operational contracts for one exact operator question."""
|
"""Return current operational contracts for one exact operator question."""
|
||||||
|
|
||||||
return str(_load_database_snapshot(user_message, hermes_home=hermes_home, runner=runner)["context"])
|
return str(
|
||||||
|
_load_database_snapshot(
|
||||||
|
user_message,
|
||||||
|
conversation_history=conversation_history,
|
||||||
|
hermes_home=hermes_home,
|
||||||
|
runner=runner,
|
||||||
|
)["context"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _word_count(value: str) -> int:
|
def _word_count(value: str) -> int:
|
||||||
|
|
@ -1082,7 +1319,7 @@ def _requires_compiled_fallback(issues: list[str]) -> bool:
|
||||||
|
|
||||||
def _pre_llm_call(**kwargs: Any) -> dict[str, str]:
|
def _pre_llm_call(**kwargs: Any) -> dict[str, str]:
|
||||||
user_message = str(kwargs.get("user_message") or "")
|
user_message = str(kwargs.get("user_message") or "")
|
||||||
snapshot = _load_database_snapshot(user_message)
|
snapshot = _load_database_snapshot(user_message, conversation_history=kwargs.get("conversation_history"))
|
||||||
_store_snapshot(str(kwargs.get("session_id") or ""), snapshot)
|
_store_snapshot(str(kwargs.get("session_id") or ""), snapshot)
|
||||||
return {"context": str(snapshot["context"])}
|
return {"context": str(snapshot["context"])}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,8 @@ def test_plugin_injects_bounded_retrieval_rows_and_writes_body_redacted_trace(tm
|
||||||
"retrieval_receipt": {
|
"retrieval_receipt": {
|
||||||
"schema": "livingip.teleoKbRetrievalReceipt.v1",
|
"schema": "livingip.teleoKbRetrievalReceipt.v1",
|
||||||
"query_sha256": "1" * 64,
|
"query_sha256": "1" * 64,
|
||||||
|
"retrieval_query_sha256": "4" * 64,
|
||||||
|
"retrieval_anchors_sha256": "5" * 64,
|
||||||
"semantic_context_sha256": "2" * 64,
|
"semantic_context_sha256": "2" * 64,
|
||||||
"artifact_state_sha256": "3" * 64,
|
"artifact_state_sha256": "3" * 64,
|
||||||
"claim_ids": ["claim-1"],
|
"claim_ids": ["claim-1"],
|
||||||
|
|
@ -402,12 +404,15 @@ def test_plugin_injects_bounded_retrieval_rows_and_writes_body_redacted_trace(tm
|
||||||
assert record["contract_ids"] == ["reply_budget", "source_intake"]
|
assert record["contract_ids"] == ["reply_budget", "source_intake"]
|
||||||
assert record["database_reasoning_protocol_version"] == "leo-db-reasoning-v2"
|
assert record["database_reasoning_protocol_version"] == "leo-db-reasoning-v2"
|
||||||
assert record["retrieval_receipt"]["semantic_context_sha256"] == "2" * 64
|
assert record["retrieval_receipt"]["semantic_context_sha256"] == "2" * 64
|
||||||
|
assert record["retrieval_receipt"]["retrieval_query_sha256"] == "4" * 64
|
||||||
|
assert record["retrieval_receipt"]["retrieval_anchors_sha256"] == "5" * 64
|
||||||
assert record["retrieval_receipt"]["read_consistency"]["system_identifier"] == "system-1"
|
assert record["retrieval_receipt"]["read_consistency"]["system_identifier"] == "system-1"
|
||||||
assert record["retrieval_receipt"]["counts"] == {"claims": 1, "context_rows": 1, "evidence_rows": 1}
|
assert record["retrieval_receipt"]["counts"] == {"claims": 1, "context_rows": 1, "evidence_rows": 1}
|
||||||
assert record["retrieval_receipt"]["injected_rows_sha256"] == injected_rows_sha256
|
assert record["retrieval_receipt"]["injected_rows_sha256"] == injected_rows_sha256
|
||||||
assert len(record["retrieval_receipt"]["receipt_sha256"]) == 64
|
assert len(record["retrieval_receipt"]["receipt_sha256"]) == 64
|
||||||
command, kwargs = calls[0]
|
command, kwargs = calls[0]
|
||||||
assert command[2:5] == ["--local", "context", "operator secret-shaped but nonsecret message"]
|
assert command[2:5] == ["--local", "context", "operator secret-shaped but nonsecret message"]
|
||||||
|
assert command[command.index("--retrieval-query") + 1] == "operator secret-shaped but nonsecret message"
|
||||||
claim_limit = int(command[command.index("--limit") + 1])
|
claim_limit = int(command[command.index("--limit") + 1])
|
||||||
context_limit = int(command[command.index("--context-limit") + 1])
|
context_limit = int(command[command.index("--context-limit") + 1])
|
||||||
assert 0 < claim_limit <= 8
|
assert 0 < claim_limit <= 8
|
||||||
|
|
@ -417,6 +422,85 @@ def test_plugin_injects_bounded_retrieval_rows_and_writes_body_redacted_trace(tm
|
||||||
assert kwargs["timeout"] == module.DEFAULT_TIMEOUT_SECONDS
|
assert kwargs["timeout"] == module.DEFAULT_TIMEOUT_SECONDS
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugin_anchors_followup_retrieval_to_recent_user_topics_without_assistant_text() -> None:
|
||||||
|
module = load_plugin()
|
||||||
|
query = "Narrow that candidate and return the revised falsifier."
|
||||||
|
history = [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Does LivingIP token ownership create governance over narrative and licensing?",
|
||||||
|
},
|
||||||
|
{"role": "assistant", "content": "UNRELATED_ASSISTANT_SECRET about GLP-1."},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Compare LivingIP ownership with governance rights and treasury control.",
|
||||||
|
},
|
||||||
|
{"role": "user", "content": query},
|
||||||
|
]
|
||||||
|
|
||||||
|
retrieval_query, anchors = module._contextual_retrieval_query(query, history)
|
||||||
|
|
||||||
|
assert retrieval_query == query
|
||||||
|
assert anchors == ["LivingIP"]
|
||||||
|
assert "UNRELATED_ASSISTANT_SECRET" not in retrieval_query
|
||||||
|
assert len(anchors) <= module.MAX_RETRIEVAL_ANCHORS
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugin_keeps_operational_query_separate_from_contextual_anchors(tmp_path: Path) -> None:
|
||||||
|
module = load_plugin()
|
||||||
|
home = tmp_path / "profile"
|
||||||
|
kb_tool = home / "bin" / "kb_tool.py"
|
||||||
|
kb_tool.parent.mkdir(parents=True)
|
||||||
|
kb_tool.write_text("# fixture\n", encoding="utf-8")
|
||||||
|
calls = []
|
||||||
|
payload = {
|
||||||
|
"claims": [],
|
||||||
|
"context_rows": [],
|
||||||
|
"operational_contracts": [{"id": "reply_budget", "hard_max_words": 220}],
|
||||||
|
"compiled_response": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def fake_runner(command, **kwargs):
|
||||||
|
calls.append(command)
|
||||||
|
return subprocess.CompletedProcess(command, 0, stdout=json.dumps(payload), stderr="")
|
||||||
|
|
||||||
|
query = "Revise that candidate."
|
||||||
|
module.build_database_context(
|
||||||
|
query,
|
||||||
|
conversation_history=[
|
||||||
|
{"role": "user", "content": "LivingIP ownership needs governance and treasury rights."},
|
||||||
|
{"role": "user", "content": "LivingIP token ownership is not narrative control."},
|
||||||
|
{"role": "user", "content": query},
|
||||||
|
],
|
||||||
|
hermes_home=home,
|
||||||
|
runner=fake_runner,
|
||||||
|
)
|
||||||
|
|
||||||
|
command = calls[0]
|
||||||
|
assert command[4] == query
|
||||||
|
retrieval_query = command[command.index("--retrieval-query") + 1]
|
||||||
|
assert retrieval_query == query
|
||||||
|
anchors = [command[index + 1] for index, item in enumerate(command) if item == "--retrieval-anchor"]
|
||||||
|
assert anchors == ["LivingIP"]
|
||||||
|
assert all("treasury rights" not in item for item in anchors)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugin_uses_repeated_user_topics_only_when_no_explicit_subject_anchor() -> None:
|
||||||
|
module = load_plugin()
|
||||||
|
query = "Narrow that position."
|
||||||
|
history = [
|
||||||
|
{"role": "user", "content": "Compare ownership governance with licensing constraints."},
|
||||||
|
{"role": "assistant", "content": "UNRELATED_ASSISTANT_SECRET governance ownership."},
|
||||||
|
{"role": "user", "content": "Ownership governance may not imply narrative control."},
|
||||||
|
{"role": "user", "content": query},
|
||||||
|
]
|
||||||
|
|
||||||
|
retrieval_query, anchors = module._contextual_retrieval_query(query, history)
|
||||||
|
|
||||||
|
assert retrieval_query == query
|
||||||
|
assert {item.casefold() for item in anchors} == {"ownership", "governance"}
|
||||||
|
|
||||||
|
|
||||||
def test_plugin_replaces_contract_violating_draft_with_compiled_db_response(tmp_path: Path, monkeypatch) -> None:
|
def test_plugin_replaces_contract_violating_draft_with_compiled_db_response(tmp_path: Path, monkeypatch) -> None:
|
||||||
module = load_plugin()
|
module = load_plugin()
|
||||||
home = tmp_path / "profile"
|
home = tmp_path / "profile"
|
||||||
|
|
|
||||||
|
|
@ -1411,7 +1411,7 @@ def test_vps_bridge_context_receipt_is_deterministic_and_retries_moving_wal(monk
|
||||||
monkeypatch.setattr(module, "database_read_marker", lambda _args: next(markers))
|
monkeypatch.setattr(module, "database_read_marker", lambda _args: next(markers))
|
||||||
monkeypatch.setattr(module, "context_operational_contracts", contracts)
|
monkeypatch.setattr(module, "context_operational_contracts", contracts)
|
||||||
monkeypatch.setattr(module, "compile_operational_response", lambda _contracts: None)
|
monkeypatch.setattr(module, "compile_operational_response", lambda _contracts: None)
|
||||||
monkeypatch.setattr(module, "find_claims", lambda _args, _query, _limit: [])
|
monkeypatch.setattr(module, "find_claims", lambda _args, _query, _limit, _anchors=None: [])
|
||||||
monkeypatch.setattr(module, "load_evidence", lambda _args, _ids, _limit: {})
|
monkeypatch.setattr(module, "load_evidence", lambda _args, _ids, _limit: {})
|
||||||
monkeypatch.setattr(module, "load_edges", lambda _args, _ids, _limit: {})
|
monkeypatch.setattr(module, "load_edges", lambda _args, _ids, _limit: {})
|
||||||
monkeypatch.setattr(module, "find_context_rows", lambda _args, _query, _limit: [])
|
monkeypatch.setattr(module, "find_context_rows", lambda _args, _query, _limit: [])
|
||||||
|
|
@ -2211,7 +2211,7 @@ def test_vps_bridge_context_corpus_does_not_expose_persona_source_ref(monkeypatc
|
||||||
assert "p.voice, p.role" in captured["sql"]
|
assert "p.voice, p.role" in captured["sql"]
|
||||||
|
|
||||||
|
|
||||||
def test_vps_bridge_ranked_retrieval_keeps_one_match_recall_floor(monkeypatch) -> None:
|
def test_vps_bridge_ranked_retrieval_boosts_bounded_anchors_and_keeps_one_match_recall_floor(monkeypatch) -> None:
|
||||||
module = _load_module(BRIDGE_DIR / "kb_tool.py")
|
module = _load_module(BRIDGE_DIR / "kb_tool.py")
|
||||||
sql_statements = []
|
sql_statements = []
|
||||||
|
|
||||||
|
|
@ -2220,14 +2220,75 @@ def test_vps_bridge_ranked_retrieval_keeps_one_match_recall_floor(monkeypatch) -
|
||||||
return []
|
return []
|
||||||
|
|
||||||
monkeypatch.setattr(module, "psql_json", capture_sql)
|
monkeypatch.setattr(module, "psql_json", capture_sql)
|
||||||
module.find_claims(SimpleNamespace(), "singular ceramic topic with several wrapper terms", 4)
|
module.find_claims(
|
||||||
|
SimpleNamespace(),
|
||||||
|
"singular ceramic topic with several wrapper terms",
|
||||||
|
4,
|
||||||
|
["LivingIP", "ownership"],
|
||||||
|
)
|
||||||
module.find_context_rows(SimpleNamespace(), "singular ceramic topic with several wrapper terms", 4)
|
module.find_context_rows(SimpleNamespace(), "singular ceramic topic with several wrapper terms", 4)
|
||||||
|
|
||||||
assert len(sql_statements) == 2
|
assert len(sql_statements) == 2
|
||||||
for sql in sql_statements:
|
claim_sql, context_sql = sql_statements
|
||||||
assert "max(score) over () as max_score" in sql
|
assert "anchors(patterns) as" in claim_sql
|
||||||
assert "score >= 1" in sql
|
assert "%livingip%" in claim_sql
|
||||||
assert "case when max_score >= 2 then 2 else 1 end" in sql
|
assert "%ownership%" in claim_sql
|
||||||
|
assert "anchor_score" in claim_sql
|
||||||
|
assert "raw_selected as" in claim_sql
|
||||||
|
assert "anchor_selected as" in claim_sql
|
||||||
|
assert "'_selection_lane', selection_lane" in claim_sql
|
||||||
|
assert "where anchor_score >= 1" in claim_sql
|
||||||
|
assert "max(score) over () as max_score" in claim_sql
|
||||||
|
assert "max(score) over () as max_score" in context_sql
|
||||||
|
assert "case when max_score >= 2 then 2 else 1 end" in context_sql
|
||||||
|
|
||||||
|
|
||||||
|
def test_vps_bridge_ranked_retrieval_has_no_match_all_anchor_when_anchors_absent(monkeypatch) -> None:
|
||||||
|
module = _load_module(BRIDGE_DIR / "kb_tool.py")
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def capture_sql(_args, sql):
|
||||||
|
captured["sql"] = sql
|
||||||
|
return []
|
||||||
|
|
||||||
|
monkeypatch.setattr(module, "psql_json", capture_sql)
|
||||||
|
module.find_claims(SimpleNamespace(), "specific topic", 4)
|
||||||
|
|
||||||
|
assert "array[]::text[]" in captured["sql"]
|
||||||
|
assert "'%%'" not in captured["sql"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_vps_bridge_ranked_retrieval_preserves_raw_results_when_subject_already_present(monkeypatch) -> None:
|
||||||
|
module = _load_module(BRIDGE_DIR / "kb_tool.py")
|
||||||
|
rows = [
|
||||||
|
{"id": "raw-subject", "score": 4, "anchor_score": 1, "_selection_lane": "raw"},
|
||||||
|
{"id": "raw-cross-domain", "score": 3, "anchor_score": 0, "_selection_lane": "raw"},
|
||||||
|
{"id": "anchor-duplicate", "score": 4, "anchor_score": 1, "_selection_lane": "anchor"},
|
||||||
|
]
|
||||||
|
monkeypatch.setattr(module, "psql_json", lambda _args, _sql: rows)
|
||||||
|
|
||||||
|
result = module.find_claims(SimpleNamespace(), "LivingIP ownership", 4, ["LivingIP"])
|
||||||
|
|
||||||
|
assert [row["id"] for row in result] == ["raw-subject", "raw-cross-domain"]
|
||||||
|
assert all("_selection_lane" not in row for row in result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vps_bridge_ranked_retrieval_backfills_subject_without_discarding_top_raw_rows(monkeypatch) -> None:
|
||||||
|
module = _load_module(BRIDGE_DIR / "kb_tool.py")
|
||||||
|
rows = [
|
||||||
|
*(
|
||||||
|
{"id": f"raw-{index}", "score": 5 - index, "anchor_score": 0, "_selection_lane": "raw"}
|
||||||
|
for index in range(4)
|
||||||
|
),
|
||||||
|
{"id": "subject-1", "score": 1, "anchor_score": 1, "_selection_lane": "anchor"},
|
||||||
|
{"id": "subject-2", "score": 0, "anchor_score": 1, "_selection_lane": "anchor"},
|
||||||
|
]
|
||||||
|
monkeypatch.setattr(module, "psql_json", lambda _args, _sql: rows)
|
||||||
|
|
||||||
|
result = module.find_claims(SimpleNamespace(), "revise that conclusion", 4, ["LivingIP"])
|
||||||
|
|
||||||
|
assert [row["id"] for row in result] == ["subject-1", "subject-2", "raw-0", "raw-1"]
|
||||||
|
assert all("_selection_lane" not in row for row in result)
|
||||||
|
|
||||||
|
|
||||||
def test_vps_bridge_combines_identity_and_restart_contracts_for_independent_paraphrase() -> None:
|
def test_vps_bridge_combines_identity_and_restart_contracts_for_independent_paraphrase() -> None:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue