Render KB claim links in leoclean replies (#68)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
Render KB claim links in leoclean replies. Local evidence: - 13 focused tests passed. - py_compile passed for Telegram bot, both leoclean KB bridges, and KB claim routes. - ruff F,E9 passed for touched files. GitHub checks were queued at merge time, not failing.
This commit is contained in:
parent
4a035d8727
commit
5e1d7a5b8c
8 changed files with 128 additions and 10 deletions
|
|
@ -24,6 +24,8 @@ STOPWORDS = {
|
||||||
"using", "what", "when", "where", "which", "who", "why", "with", "you", "your",
|
"using", "what", "when", "where", "which", "who", "why", "with", "you", "your",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEFAULT_CLAIM_BASE_URL = "https://dash.livingip.xyz"
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
|
@ -130,6 +132,23 @@ def terms_for(query: str) -> list[str]:
|
||||||
return terms[:14] or [query.lower()]
|
return terms[:14] or [query.lower()]
|
||||||
|
|
||||||
|
|
||||||
|
def claim_base_url() -> str:
|
||||||
|
return os.environ.get("TELEO_KB_CLAIM_BASE_URL", DEFAULT_CLAIM_BASE_URL).rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def claim_url(claim_id: str | None) -> str | None:
|
||||||
|
if not claim_id:
|
||||||
|
return None
|
||||||
|
return f"{claim_base_url()}/kb/claims/{claim_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def markdown_claim_link(claim_id: str | None, label: str | None = None) -> str:
|
||||||
|
if not claim_id:
|
||||||
|
return "`unknown`"
|
||||||
|
text = label or claim_id
|
||||||
|
return f"[`{text}`]({claim_url(claim_id)})"
|
||||||
|
|
||||||
|
|
||||||
def sql_literal(value: str) -> str:
|
def sql_literal(value: str) -> str:
|
||||||
return "'" + value.replace("'", "''") + "'"
|
return "'" + value.replace("'", "''") + "'"
|
||||||
|
|
||||||
|
|
@ -431,8 +450,11 @@ select jsonb_build_object(
|
||||||
edges.setdefault(row["claim_id"], []).append(row)
|
edges.setdefault(row["claim_id"], []).append(row)
|
||||||
|
|
||||||
for claim in claims:
|
for claim in claims:
|
||||||
|
claim["claim_url"] = claim_url(claim["id"])
|
||||||
claim["evidence"] = evidence.get(claim["id"], [])
|
claim["evidence"] = evidence.get(claim["id"], [])
|
||||||
claim["edges"] = edges.get(claim["id"], [])
|
claim["edges"] = edges.get(claim["id"], [])
|
||||||
|
for edge in claim["edges"]:
|
||||||
|
edge["connected_url"] = claim_url(edge.get("connected_id"))
|
||||||
|
|
||||||
context_rows = psql_json_lines(args, context_sql, db=args.canonical_db)
|
context_rows = psql_json_lines(args, context_sql, db=args.canonical_db)
|
||||||
result: dict[str, Any] = {
|
result: dict[str, Any] = {
|
||||||
|
|
@ -700,12 +722,16 @@ def show_canonical_claim(args: argparse.Namespace) -> dict[str, Any]:
|
||||||
raise SystemExit(f"claim not found: {args.claim_id}")
|
raise SystemExit(f"claim not found: {args.claim_id}")
|
||||||
if not include_excerpts(args):
|
if not include_excerpts(args):
|
||||||
claim.pop("text", None)
|
claim.pop("text", None)
|
||||||
|
claim["claim_url"] = claim_url(args.claim_id)
|
||||||
|
edges = canonical_edges(args, args.claim_id, 12)
|
||||||
|
for edge in edges:
|
||||||
|
edge["connected_url"] = claim_url(edge.get("connected_id"))
|
||||||
return {
|
return {
|
||||||
"artifact": "teleo_cloudsql_canonical_kb_claim",
|
"artifact": "teleo_cloudsql_canonical_kb_claim",
|
||||||
"backend": canonical_backend(args),
|
"backend": canonical_backend(args),
|
||||||
"claim": claim,
|
"claim": claim,
|
||||||
"evidence": canonical_evidence(args, args.claim_id, 8),
|
"evidence": canonical_evidence(args, args.claim_id, 8),
|
||||||
"edges": canonical_edges(args, args.claim_id, 12),
|
"edges": edges,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -719,11 +745,15 @@ def evidence_canonical_claim(args: argparse.Namespace) -> dict[str, Any]:
|
||||||
|
|
||||||
|
|
||||||
def edges_canonical_claim(args: argparse.Namespace) -> dict[str, Any]:
|
def edges_canonical_claim(args: argparse.Namespace) -> dict[str, Any]:
|
||||||
|
edges = canonical_edges(args, args.claim_id, args.limit)
|
||||||
|
for edge in edges:
|
||||||
|
edge["connected_url"] = claim_url(edge.get("connected_id"))
|
||||||
return {
|
return {
|
||||||
"artifact": "teleo_cloudsql_canonical_kb_edges",
|
"artifact": "teleo_cloudsql_canonical_kb_edges",
|
||||||
"backend": canonical_backend(args),
|
"backend": canonical_backend(args),
|
||||||
"claim_id": args.claim_id,
|
"claim_id": args.claim_id,
|
||||||
"edges": canonical_edges(args, args.claim_id, args.limit),
|
"claim_url": claim_url(args.claim_id),
|
||||||
|
"edges": edges,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -964,6 +994,7 @@ def emit_markdown(value: dict[str, Any]) -> None:
|
||||||
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")
|
print(f"### {i}. {claim.get('text', '[redacted claim text]')}\n")
|
||||||
print(f"- id: `{claim['id']}`")
|
print(f"- id: `{claim['id']}`")
|
||||||
|
print(f"- claim page: {claim_url(claim['id'])}")
|
||||||
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', [])))}`")
|
||||||
|
|
@ -978,7 +1009,11 @@ def emit_markdown(value: dict[str, Any]) -> None:
|
||||||
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 ""
|
text = f": {edge['connected_text']}" if edge.get("connected_text") else ""
|
||||||
print(f"- `{edge.get('direction')}` `{edge.get('edge_type')}` ({edge.get('connected_id')}){text}")
|
connected = edge.get("connected_id")
|
||||||
|
print(
|
||||||
|
f"- `{edge.get('direction')}` `{edge.get('edge_type')}` "
|
||||||
|
f"({markdown_claim_link(connected, str(connected or '')[:8])}){text}"
|
||||||
|
)
|
||||||
print()
|
print()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -987,6 +1022,7 @@ def emit_markdown(value: dict[str, Any]) -> None:
|
||||||
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"- id: `{claim['id']}`")
|
||||||
|
print(f"- claim page: {claim_url(claim['id'])}")
|
||||||
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"):
|
||||||
|
|
@ -999,13 +1035,18 @@ def emit_markdown(value: dict[str, Any]) -> None:
|
||||||
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 ""
|
text = f": {edge['connected_text']}" if edge.get("connected_text") else ""
|
||||||
print(f"- `{edge.get('direction')}` `{edge.get('edge_type')}` ({edge.get('connected_id')}){text}")
|
connected = edge.get("connected_id")
|
||||||
|
print(
|
||||||
|
f"- `{edge.get('direction')}` `{edge.get('edge_type')}` "
|
||||||
|
f"({markdown_claim_link(connected, str(connected or '')[:8])}){text}"
|
||||||
|
)
|
||||||
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: `{value['claim_id']}`\n")
|
||||||
|
print(f"- Claim page: {claim_url(value['claim_id'])}\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)}`")
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,14 @@ from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
DEFAULT_CLAIM_BASE_URL = "https://dash.livingip.xyz"
|
||||||
|
|
||||||
STOPWORDS = {
|
STOPWORDS = {
|
||||||
"a",
|
"a",
|
||||||
"about",
|
"about",
|
||||||
|
|
@ -217,6 +220,23 @@ def truncate(value: str | None, length: int = 220) -> str:
|
||||||
return squashed[: length - 1] + "..."
|
return squashed[: length - 1] + "..."
|
||||||
|
|
||||||
|
|
||||||
|
def claim_base_url() -> str:
|
||||||
|
return os.environ.get("TELEO_KB_CLAIM_BASE_URL", DEFAULT_CLAIM_BASE_URL).rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def claim_url(claim_id: str | None) -> str | None:
|
||||||
|
if not claim_id:
|
||||||
|
return None
|
||||||
|
return f"{claim_base_url()}/kb/claims/{claim_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def markdown_claim_link(claim_id: str | None, label: str | None = None) -> str:
|
||||||
|
if not claim_id:
|
||||||
|
return "`unknown`"
|
||||||
|
text = label or 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) -> list[dict[str, Any]]:
|
||||||
patterns = [f"%{term}%" for term in query_terms(query)]
|
patterns = [f"%{term}%" for term in query_terms(query)]
|
||||||
min_score = 2 if len(patterns) >= 3 else 1
|
min_score = 2 if len(patterns) >= 3 else 1
|
||||||
|
|
@ -755,6 +775,7 @@ def bundle(args: argparse.Namespace, query: str, claim_limit: int, context_limit
|
||||||
"claims": [
|
"claims": [
|
||||||
{
|
{
|
||||||
**claim,
|
**claim,
|
||||||
|
"claim_url": claim_url(claim["id"]),
|
||||||
"evidence": evidence.get(claim["id"], []),
|
"evidence": evidence.get(claim["id"], []),
|
||||||
"edges": edges.get(claim["id"], []),
|
"edges": edges.get(claim["id"], []),
|
||||||
}
|
}
|
||||||
|
|
@ -785,6 +806,7 @@ def print_claim_bundle(data: dict[str, Any]) -> None:
|
||||||
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}. {claim['text']}\n")
|
||||||
print(f"- id: `{claim['id']}`")
|
print(f"- id: `{claim['id']}`")
|
||||||
|
print(f"- claim page: {claim_url(claim['id'])}")
|
||||||
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', [])))}`")
|
||||||
|
|
@ -798,9 +820,11 @@ def print_claim_bundle(data: 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"]:
|
||||||
|
connected = edge["connected_id"]
|
||||||
print(
|
print(
|
||||||
f"- `{edge['direction']}` `{edge['edge_type']}` "
|
f"- `{edge['direction']}` `{edge['edge_type']}` "
|
||||||
f"({edge['connected_id']}): {truncate(edge['connected_text'], 260)}"
|
f"({markdown_claim_link(connected, connected[:8])}): "
|
||||||
|
f"{truncate(edge['connected_text'], 260)}"
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,9 @@ Make KB answers easy to scan in Telegram:
|
||||||
command names in backticks;
|
command names in backticks;
|
||||||
- when citing a specific claim, include both the claim headline and the claim
|
- when citing a specific claim, include both the claim headline and the claim
|
||||||
ID, for example: `claim text` (`<claim_id>`);
|
ID, for example: `claim text` (`<claim_id>`);
|
||||||
|
- when the bridge output includes `claim page: https://dash.livingip.xyz/kb/claims/<claim_id>`,
|
||||||
|
copy that URL into the answer so Telegram users can open the claim, body,
|
||||||
|
evidence, and edges directly;
|
||||||
- when a dashboard URL is available, include the canonical claim page as
|
- when a dashboard URL is available, include the canonical claim page as
|
||||||
`https://<argus-host>/kb/claims/<claim_id>`; otherwise name the dashboard
|
`https://<argus-host>/kb/claims/<claim_id>`; otherwise name the dashboard
|
||||||
path `/kb/claims/<claim_id>` so the operator can open the claim, body,
|
path `/kb/claims/<claim_id>` so the operator can open the claim, body,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ Make KB answers easy to scan in Telegram:
|
||||||
command names in backticks;
|
command names in backticks;
|
||||||
- when citing a specific claim, include both the claim headline and the claim
|
- when citing a specific claim, include both the claim headline and the claim
|
||||||
ID, for example: `claim text` (`<claim_id>`);
|
ID, for example: `claim text` (`<claim_id>`);
|
||||||
|
- when the bridge output includes `claim page: https://dash.livingip.xyz/kb/claims/<claim_id>`,
|
||||||
|
copy that URL into the answer so Telegram users can open the claim, body,
|
||||||
|
evidence, and edges directly;
|
||||||
- when a dashboard URL is available, include the canonical claim page as
|
- when a dashboard URL is available, include the canonical claim page as
|
||||||
`https://<argus-host>/kb/claims/<claim_id>`; otherwise name the dashboard
|
`https://<argus-host>/kb/claims/<claim_id>`; otherwise name the dashboard
|
||||||
path `/kb/claims/<claim_id>` so the operator can open the claim, body,
|
path `/kb/claims/<claim_id>` so the operator can open the claim, body,
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,7 @@ import json as _json
|
||||||
from kb_retrieval import KBIndex, retrieve_context, retrieve_vector_context
|
from kb_retrieval import KBIndex, retrieve_context, retrieve_vector_context
|
||||||
from retrieval import orchestrate_retrieval
|
from retrieval import orchestrate_retrieval
|
||||||
from market_data import get_token_price, format_price_context, extract_market_data_tokens
|
from market_data import get_token_price, format_price_context, extract_market_data_tokens
|
||||||
from worktree_lock import main_worktree_lock
|
from x_client import search_tweets, check_research_rate_limit, record_research_usage, get_research_remaining
|
||||||
from x_client import search_tweets, fetch_from_url, check_research_rate_limit, record_research_usage, get_research_remaining
|
|
||||||
from http_chat_proxy import (
|
from http_chat_proxy import (
|
||||||
DEFAULT_SMART_RESEARCH_COMMAND_PREFIXES,
|
DEFAULT_SMART_RESEARCH_COMMAND_PREFIXES,
|
||||||
build_chat_proxy_payload,
|
build_chat_proxy_payload,
|
||||||
|
|
@ -137,11 +136,21 @@ user_response_times: dict[int, list[float]] = defaultdict(list)
|
||||||
allowed_groups: set[int] = set()
|
allowed_groups: set[int] = set()
|
||||||
|
|
||||||
TELEGRAM_REPLY_CHUNK_LIMIT = 3500
|
TELEGRAM_REPLY_CHUNK_LIMIT = 3500
|
||||||
|
MARKDOWN_LINK_RE = re.compile(r"\[([^\]\n]{1,180})\]\((https?://[^\s<>)\"]{1,600})\)")
|
||||||
|
|
||||||
|
|
||||||
|
def _markdown_link_to_telegram_html(match: re.Match[str]) -> str:
|
||||||
|
label = match.group(1)
|
||||||
|
url = match.group(2)
|
||||||
|
if label.startswith("`") and label.endswith("`") and len(label) > 1:
|
||||||
|
label = label[1:-1]
|
||||||
|
return f'<a href="{url}">{label}</a>'
|
||||||
|
|
||||||
|
|
||||||
def _telegram_native_html(text: str) -> str:
|
def _telegram_native_html(text: str) -> str:
|
||||||
"""Render common LLM Markdown as Telegram HTML without trusting raw HTML."""
|
"""Render common LLM Markdown as Telegram HTML without trusting raw HTML."""
|
||||||
rendered = escape(text, quote=False)
|
rendered = escape(text, quote=False)
|
||||||
|
rendered = MARKDOWN_LINK_RE.sub(_markdown_link_to_telegram_html, rendered)
|
||||||
rendered = re.sub(r"(?m)^#{1,6}\s+(.+)$", r"<b>\1</b>", rendered)
|
rendered = re.sub(r"(?m)^#{1,6}\s+(.+)$", r"<b>\1</b>", rendered)
|
||||||
rendered = re.sub(r"\*\*([^*\n]{1,240})\*\*", r"<b>\1</b>", rendered)
|
rendered = re.sub(r"\*\*([^*\n]{1,240})\*\*", r"<b>\1</b>", rendered)
|
||||||
rendered = re.sub(r"`([^`\n]{1,240})`", r"<code>\1</code>", rendered)
|
rendered = re.sub(r"`([^`\n]{1,240})`", r"<code>\1</code>", rendered)
|
||||||
|
|
@ -149,6 +158,7 @@ def _telegram_native_html(text: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def _plain_telegram_fallback(text: str) -> str:
|
def _plain_telegram_fallback(text: str) -> str:
|
||||||
|
text = MARKDOWN_LINK_RE.sub(r"\1 (\2)", text)
|
||||||
text = re.sub(r"(?m)^#{1,6}\s+", "", text)
|
text = re.sub(r"(?m)^#{1,6}\s+", "", text)
|
||||||
text = re.sub(r"\*\*([^*\n]{1,240})\*\*", r"\1", text)
|
text = re.sub(r"\*\*([^*\n]{1,240})\*\*", r"\1", text)
|
||||||
text = re.sub(r"`([^`\n]{1,240})`", r"\1", text)
|
text = re.sub(r"`([^`\n]{1,240})`", r"\1", text)
|
||||||
|
|
@ -536,7 +546,7 @@ def get_db_stats() -> dict:
|
||||||
|
|
||||||
from eval_checks import (
|
from eval_checks import (
|
||||||
_LLMResponse, estimate_cost, check_url_fabrication, apply_confidence_floor,
|
_LLMResponse, estimate_cost, check_url_fabrication, apply_confidence_floor,
|
||||||
CONFIDENCE_FLOOR, COST_ALERT_THRESHOLD,
|
COST_ALERT_THRESHOLD,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1734,8 +1744,6 @@ async def handle_tagged(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
retrieval_layers = retrieval["retrieval_layers"]
|
retrieval_layers = retrieval["retrieval_layers"]
|
||||||
tool_calls.extend(retrieval["tool_calls"])
|
tool_calls.extend(retrieval["tool_calls"])
|
||||||
|
|
||||||
stats = get_db_stats()
|
|
||||||
|
|
||||||
# Fetch live market data for any tokens mentioned (Rhea: market-data API)
|
# Fetch live market data for any tokens mentioned (Rhea: market-data API)
|
||||||
entity_terms = [tag for ent in kb_ctx.entities for tag in ent.tags]
|
entity_terms = [tag for ent in kb_ctx.entities for tag in ent.tags]
|
||||||
market_context, market_data_audit, market_duration, token_mentions = await _market_context_for_message(
|
market_context, market_data_audit, market_duration, token_mentions = await _market_context_for_message(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
|
import importlib.util
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -61,3 +62,23 @@ def test_bridge_source_does_not_commit_raw_secret_values() -> None:
|
||||||
for pattern in forbidden_patterns:
|
for pattern in forbidden_patterns:
|
||||||
assert not re.search(pattern, combined), pattern
|
assert not re.search(pattern, combined), pattern
|
||||||
|
|
||||||
|
|
||||||
|
def _load_module(path: Path):
|
||||||
|
spec = importlib.util.spec_from_file_location(path.stem, path)
|
||||||
|
assert spec and spec.loader
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def test_kb_bridges_emit_public_claim_links_for_telegram_rendering() -> None:
|
||||||
|
claim_id = "d3fb892b-3c5a-4700-9512-55e5c680eec1"
|
||||||
|
expected = f"https://dash.livingip.xyz/kb/claims/{claim_id}"
|
||||||
|
|
||||||
|
for filename in ("kb_tool.py", "cloudsql_memory_tool.py"):
|
||||||
|
module = _load_module(BRIDGE_DIR / filename)
|
||||||
|
|
||||||
|
assert module.claim_url(claim_id) == expected
|
||||||
|
assert module.markdown_claim_link(claim_id, "d3fb892b") == (
|
||||||
|
f"[`d3fb892b`]({expected})"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ def test_vps_kb_skill_keeps_vps_scope_explicit() -> None:
|
||||||
assert "draft or refresh the admin review packet" in squashed
|
assert "draft or refresh the admin review packet" in squashed
|
||||||
assert "/kb/claims/<claim_id>" in text
|
assert "/kb/claims/<claim_id>" in text
|
||||||
assert "wrap claim IDs, proposal IDs" in text
|
assert "wrap claim IDs, proposal IDs" in text
|
||||||
|
assert "claim page: https://dash.livingip.xyz/kb/claims/<claim_id>" in text
|
||||||
|
|
||||||
|
|
||||||
def test_gcp_kb_skill_keeps_claim_links_and_backtick_rendering() -> None:
|
def test_gcp_kb_skill_keeps_claim_links_and_backtick_rendering() -> None:
|
||||||
|
|
@ -41,6 +42,7 @@ def test_gcp_kb_skill_keeps_claim_links_and_backtick_rendering() -> None:
|
||||||
|
|
||||||
assert "/kb/claims/<claim_id>" in text
|
assert "/kb/claims/<claim_id>" in text
|
||||||
assert "wrap claim IDs, proposal IDs" in text
|
assert "wrap claim IDs, proposal IDs" in text
|
||||||
|
assert "claim page: https://dash.livingip.xyz/kb/claims/<claim_id>" in text
|
||||||
|
|
||||||
|
|
||||||
def test_leoclean_kb_skills_anchor_external_doctrine_in_target_project_language() -> None:
|
def test_leoclean_kb_skills_anchor_external_doctrine_in_target_project_language() -> None:
|
||||||
|
|
|
||||||
16
tests/test_telegram_native_rendering_source.py
Normal file
16
tests/test_telegram_native_rendering_source.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""Source-level checks for Telegram-native reply rendering."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_telegram_renderer_supports_safe_markdown_links() -> None:
|
||||||
|
text = (REPO_ROOT / "telegram" / "bot.py").read_text()
|
||||||
|
|
||||||
|
assert "MARKDOWN_LINK_RE" in text
|
||||||
|
assert "https?://" in text
|
||||||
|
assert "_markdown_link_to_telegram_html" in text
|
||||||
|
assert '<a href="{url}">{label}</a>' in text
|
||||||
|
assert "MARKDOWN_LINK_RE.sub(r\"\\1 (\\2)\", text)" in text
|
||||||
Loading…
Reference in a new issue