From 3ee356f23231b0a847ad569acbe4c6013f38b397 Mon Sep 17 00:00:00 2001 From: Alok Kumar Date: Sun, 30 Aug 2026 12:12:35 +0530 Subject: [PATCH] Improve contextual fallback sentence quality --- CHANGELOG.md | 1 + backlink_intelligence/placement.py | 78 +++++++++++++++++++++++++++--- tests/test_placement.py | 39 +++++++++++++++ 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea9b5b0..7f21fc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to Backlink Intelligence are documented here. - Added conservative singular/plural anchor adaptation when the natural grammatical form already exists in source copy. - Added destination-intent scoring so context specific to the destination topic receives more weight than generic anchor repetition. - Added destination-fit and actual placed-anchor details to placement CLI output. +- Replaced mechanical target-title fallback sentences with concise destination-intent-aware editorial copy. ## 1.0.0 - 2026-08-30 diff --git a/backlink_intelligence/placement.py b/backlink_intelligence/placement.py index 6e85373..27533e1 100644 --- a/backlink_intelligence/placement.py +++ b/backlink_intelligence/placement.py @@ -70,6 +70,73 @@ def _simple_anchor_variants(anchor: str) -> list[str]: return variants +def _fallback_anchor_case(anchor: str) -> str: + """Use conservative editorial casing for generated fallback copy. + + When a requested anchor begins with a short acronym followed by title-cased words, + preserve the acronym but lowercase the descriptive words. This turns ``AI Agent`` + into ``AI agent`` without changing arbitrary brand or mixed-case anchors. + """ + words = anchor.strip().split() + if len(words) < 2: + return anchor.strip() + if not (words[0].isupper() and 1 < len(words[0]) <= 5): + return anchor.strip() + + changed = False + output = [words[0]] + for word in words[1:]: + if word[:1].isupper() and word[1:].islower(): + output.append(word.lower()) + changed = True + else: + output.append(word) + return " ".join(output) if changed else anchor.strip() + + +def _contextual_fallback_sentence( + paragraph: str, + anchor: str, + target_url: str, + target_title: str, +) -> tuple[str, str, list[str]]: + """Create concise deterministic fallback copy without dumping the target title.""" + placed_anchor = _fallback_anchor_case(anchor) + linked = f"[{placed_anchor}]({target_url})" + target_lower = target_title.lower() + paragraph_lower = paragraph.lower() + + cost_intent = any(term in target_lower for term in ("cost", "pricing", "price", "tco", "roi")) + cost_context = any(term in paragraph_lower for term in ("cost", "price", "pricing", "budget", "expense", "roi", "investment", "expensive")) + + notes: list[str] = ["target_title_not_injected_into_source_copy"] + if placed_anchor != anchor: + notes.append("anchor_casing_adapted_for_generated_sentence") + + if cost_intent and cost_context: + sentence = ( + f"These factors are useful when estimating {linked} implementation costs, " + "ongoing operating expenses, and expected ROI." + ) + notes.append("destination_intent_used_for_contextual_sentence") + return sentence, placed_anchor, notes + + if cost_intent: + sentence = ( + f"Businesses evaluating this type of automation should also account for {linked} costs, " + "including implementation, integrations, ongoing operation, and expected ROI." + ) + notes.append("destination_intent_used_for_contextual_sentence") + return sentence, placed_anchor, notes + + if any(term in target_lower for term in ("roadmap", "learning", "course", "guide")): + sentence = f"Readers who want a structured next step can explore this {linked} for more detail." + return sentence, placed_anchor, notes + + sentence = f"Readers who want additional context can review this {linked} resource." + return sentence, placed_anchor, notes + + def _compose_after( paragraph: str, anchor: str, @@ -102,13 +169,10 @@ def _compose_after( ["anchor_adapted_to_source_grammar", "requested_anchor_not_used_verbatim"], ) - linked = f"[{anchor}]({target_url})" - topic = target_title.strip() - if topic and topic.lower() != anchor.lower(): - sentence = f"For a more detailed resource on {topic}, see {linked}." - else: - sentence = f"For a more detailed resource on this topic, see {linked}." - return "contextual_sentence", paragraph.rstrip() + " " + sentence, anchor, [] + sentence, placed_anchor, notes = _contextual_fallback_sentence( + paragraph, anchor, target_url, target_title + ) + return "contextual_sentence", paragraph.rstrip() + " " + sentence, placed_anchor, notes def _stem(term: str) -> str: diff --git a/tests/test_placement.py b/tests/test_placement.py index da0eaab..e5279bf 100644 --- a/tests/test_placement.py +++ b/tests/test_placement.py @@ -106,6 +106,45 @@ def test_destination_intent_prioritizes_cost_context(self, fetch): self.assertEqual(item.destination_fit, "low") + @patch("backlink_intelligence.placement.fetch_page") + def test_contextual_sentence_avoids_target_title_dump(self, fetch): + source = parse_page( + "

AI automation does not have to be expensive, but the cost depends on what you want to build and the systems that need to be connected.

", + requested_url="https://s.com", final_url="https://s.com", status_code=200, + ) + target = parse_page( + "AI Agent Cost in 2026: Pricing, TCO and ROI Guide

AI Agent Cost

Pricing depends on implementation, integrations, operations, and expected ROI.

", + requested_url="https://t.com", final_url="https://t.com", status_code=200, + ) + fetch.side_effect = [source, target] + item = suggest_placements("https://s.com", "https://t.com", "AI Agent", top_n=1)[0] + self.assertEqual(item.strategy, "contextual_sentence") + self.assertNotIn("AI Agent Cost in 2026: Pricing, TCO and ROI Guide", item.after) + self.assertNotIn("see [", item.after) + self.assertIn("[AI agent](https://t.com)", item.after) + self.assertIn("implementation costs", item.after) + self.assertEqual(item.suggested_anchor, "AI agent") + self.assertIn("target_title_not_injected_into_source_copy", item.reasons) + self.assertIn("destination_intent_used_for_contextual_sentence", item.reasons) + + @patch("backlink_intelligence.placement.fetch_page") + def test_general_contextual_sentence_does_not_echo_target_title(self, fetch): + source = parse_page( + "

Teams often introduce these capabilities progressively as systems become more autonomous and reliable across increasingly complex production workflows.

", + requested_url="https://s.com", final_url="https://s.com", status_code=200, + ) + target = parse_page( + "Agentic AI Learning Roadmap

Learn Agentic AI

A structured roadmap for tool calling, retrieval, memory, evaluation, and production reliability.

", + requested_url="https://t.com", final_url="https://t.com", status_code=200, + ) + fetch.side_effect = [source, target] + item = suggest_placements("https://s.com", "https://t.com", "Agentic AI learning roadmap", top_n=1)[0] + self.assertEqual(item.strategy, "contextual_sentence") + self.assertNotIn("For a more detailed resource on", item.after) + self.assertNotIn("see [", item.after) + self.assertIn("[Agentic AI learning roadmap](https://t.com)", item.after) + + if __name__ == "__main__": unittest.main()