Skip to content

Commit ea450de

Browse files
Noor Aclaude
authored andcommitted
fix(chat): tighten citation rules and switch to Markdown link format
External sources were sometimes named (title/journal) without being retrieved, since the prompt explicitly allowed unlinked plain-text mentions of non-retrieved sources — now forbidden outright. Combined citation markers like "[Docs 3 et 5]" are unlinkable (no single URL to resolve to), so the prompt now requires one document per marker, and the model is told to verify a claim is actually in the document it cites. Also switch citation format from HTML anchors to Markdown links ([Doc N](URL)), matching the rest of a Markdown reply and reducing formatting mistakes. linkify_missing_citations follows suit, but note it only patches the non-streaming endpoint and the saved chat record — it never reaches the live streamed view, so the prompt rules are the only real fix for what streams to the user. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 008235b commit ea450de

3 files changed

Lines changed: 42 additions & 28 deletions

File tree

‎src/app/services/helpers.py‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,19 @@ def stringify_docs_content(docs: List[Any]) -> str:
141141
return documents.strip()
142142

143143

144-
_CITATION_RE = re.compile(r'(?<!target="_blank">)\[Doc\s*(\d+)\]')
144+
_CITATION_RE = re.compile(r'(?<!target="_blank">)\[Doc\s*(\d+)\](?!\()', re.IGNORECASE)
145145

146146

147147
def linkify_missing_citations(text: str, docs: List[Any]) -> str:
148148
"""
149-
Wraps any bare `[Doc N]` marker in `text` with the `<a href=... target="_blank">`
150-
tag for document N, using the URL from `docs[N-1]`. Markers already wrapped in an
151-
<a> tag are left untouched. Safety net for when the LLM forgets to format a
152-
citation as a link itself.
149+
Wraps any bare `[Doc N]` marker in `text` into a Markdown link `[Doc N](URL)`
150+
for document N, using the URL from `docs[N-1]`. Markers already followed by a
151+
parenthesized URL, or already wrapped in an HTML `<a>` tag, are left untouched.
152+
Safety net for when the LLM forgets to format a citation as a link itself.
153+
154+
Only matches a single document number per marker — a combined citation like
155+
"[Docs 3 et 5]" is intentionally left as-is, since there's no single URL a
156+
two-document marker could safely resolve to.
153157
154158
Args:
155159
text: The assembled answer text.
@@ -173,7 +177,7 @@ def _replace(match: "re.Match[str]") -> str:
173177
url = _url_for(n)
174178
if not url:
175179
return match.group(0)
176-
return f'<a href="{url}" target="_blank">[Doc {n}]</a>'
180+
return f"[Doc {n}]({url})"
177181

178182
return _CITATION_RE.sub(_replace, text)
179183

‎src/app/services/prompts.py‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,28 @@
3131
- Make sure to use as many of the retrieved documents as relevant to answer the user's question, and cite them explicitly in your response next to the information that you used from their content. When citing a retrieved document, make sure to stay within the context of the document and not to make up information.
3232
- If the retrieved documents are insufficient to answer, say so in your response — do not make a second tool call.
3333
34-
**No links beyond what was retrieved**
35-
- Never produce a link, URL, or `<a>` tag for anything other than a document returned by your most recent `get_resources_about_sustainability` call — either from this turn, or from an earlier turn if you are reusing its results because the topic hasn't shifted. This includes links you might otherwise produce from general/parametric knowledge (a well-known Wikipedia page, a UN SDG page, a journal homepage, etc.). If you want to reference something you did not retrieve, name it in plain text with no link and no fabricated URL.
34+
**No sources beyond what was retrieved**
35+
- Never name, describe, or link any source — an article, video, journal, dataset, or creator — other than a document returned by your most recent `get_resources_about_sustainability` call, either from this turn or from an earlier turn if you are reusing its results because the topic hasn't shifted. This applies even with no link attached: do not mention a title, journal name, or video you did not retrieve, not even in plain text.
36+
- If you don't have a retrieved document to support a point, make the point in your own words with no source attribution at all. Never produce a link, URL, or fabricated citation from general/parametric knowledge (a well-known Wikipedia page, a UN SDG page, a journal homepage, etc.).
3637
3738
**Citing sources**
3839
- Every document's URL is on a dedicated line formatted as `url:<URL>`. Copy that URL character-for-character. Never substitute, construct, guess, or modify a URL in any way — not even a Wikipedia URL you believe is close enough.
39-
- Format every inline citation as: <a href="URL" target="_blank">[Doc N]</a>, where URL is the verbatim value from that document's url line and N is its document number. Never write a bare `[Doc N]` without the surrounding `<a>` tag — the tag is what makes the citation clickable.
40+
- Format every inline citation as Markdown: [Doc N](URL), where URL is the verbatim value from that document's url line and N is its document number. Never write a bare `[Doc N]` with no parenthesized URL — that's what makes the citation clickable.
41+
- One document per citation marker. Never combine document numbers in a single bracket (never write "[Doc 3 and 5]" or "[Docs 3 et 5]" — a link can only point to one URL, so a combined marker is always broken). If a claim draws on two documents, place two separate markers next to each other: [Doc 3](url3) [Doc 5](url5).
42+
- Before citing a document for a specific claim, confirm that exact claim is actually stated in that document's content — never attribute a fact, quote, or statistic to a document that doesn't contain it, even if a different retrieved document does.
4043
- Only cite a document from your most recent `get_resources_about_sustainability` call — never a document number from before that call. Each call produces its own fresh Doc 1, Doc 2, etc.; once a newer call happens, the previous numbering is no longer valid, even if you cited it in an earlier response.
4144
- Do not invent examples, quotes, statistics, or facts not explicitly stated in the retrieved documents. If a document does not contain enough to support a claim, omit the claim.
4245
- Do not cite any source besides what was returned by your most recent `get_resources_about_sustainability` call.
4346
4447
"""
4548

4649
AGENT_REMINDER_PROMPT = """Reminder of your standing instructions — re-checking every turn, especially in a long conversation:
47-
- 3–4 sentences max, unless the user explicitly asked for more detail/a list/a full plan. 1–2 sentences for a bare introduction or topic-naming message.
48-
- No sycophantic openers. Same language as the user; vouvoiement (vous) if that language is French.
49-
- New topic + thin context (discipline, level, kind of help wanted) → ask up to 3 clarifying questions instead of answering; don't call the retrieval tool on that turn.
50-
- Call `get_resources_about_sustainability` at most once per response, and only for factual/sourced questions — skip it for greetings, meta-turns, or if the current topic is already covered by your most recent call.
51-
- Cite only documents from your most recent `get_resources_about_sustainability` call, as <a href="URL" target="_blank">[Doc N]</a> with the verbatim url — never a URL you weren't given, never numbering from a superseded call, never an invented fact."""
50+
- 3–4 sentences max unless the user asked for more; same language as the user, vouvoiement (vous) if French; no sycophantic openers.
51+
- New topic + thin context → ask up to 3 clarifying questions instead of answering; no tool call on that turn.
52+
- Call `get_resources_about_sustainability` at most once per response, only for factual/sourced questions — skip it if the current topic is already covered by your most recent call.
53+
- Never name, describe, or link a source you did not retrieve — not even without a link, not even just a title. Zero exceptions.
54+
- Cite only documents from your most recent `get_resources_about_sustainability` call, one document per marker as [Doc N](URL) with the verbatim url — never combine numbers in one marker, never a URL you weren't given, never numbering from a superseded call.
55+
- Every citation's claim must actually be stated in that specific document — never attribute it to the wrong document or invent it."""
5256

5357
## TAKEN OUT OF THE ABOVE PROMPT TO DEACTIVATE SUGGESTING A NEXT STEP AFTER AN ANSWER
5458
# **Suggesting a next step**

‎src/app/tests/services/test_helpers.py‎

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,15 @@ def _make_doc(self, url: str) -> Document:
115115
def test_linkify_missing_citations_wraps_bare_marker(self):
116116
docs = [self._make_doc("https://example.org/1")]
117117
text = "Sustainability matters [Doc 1]."
118-
expected = (
119-
'Sustainability matters <a href="https://example.org/1" '
120-
'target="_blank">[Doc 1]</a>.'
121-
)
118+
expected = "Sustainability matters [Doc 1](https://example.org/1)."
122119
self.assertEqual(linkify_missing_citations(text, docs), expected)
123120

124-
def test_linkify_missing_citations_leaves_existing_link_untouched(self):
121+
def test_linkify_missing_citations_leaves_existing_markdown_link_untouched(self):
122+
docs = [self._make_doc("https://example.org/1")]
123+
text = "Already linked [Doc 1](https://example.org/1)."
124+
self.assertEqual(linkify_missing_citations(text, docs), text)
125+
126+
def test_linkify_missing_citations_leaves_existing_html_link_untouched(self):
125127
docs = [self._make_doc("https://example.org/1")]
126128
text = 'Already linked <a href="https://example.org/1" target="_blank">[Doc 1]</a>.'
127129
self.assertEqual(linkify_missing_citations(text, docs), text)
@@ -131,13 +133,10 @@ def test_linkify_missing_citations_mixed_bare_and_linked(self):
131133
self._make_doc("https://example.org/1"),
132134
self._make_doc("https://example.org/2"),
133135
]
134-
text = (
135-
'See <a href="https://example.org/1" target="_blank">[Doc 1]</a> '
136-
"and also [Doc 2]."
137-
)
136+
text = "See [Doc 1](https://example.org/1) and also [Doc 2]."
138137
expected = (
139-
'See <a href="https://example.org/1" target="_blank">[Doc 1]</a> '
140-
'and also <a href="https://example.org/2" target="_blank">[Doc 2]</a>.'
138+
"See [Doc 1](https://example.org/1) "
139+
"and also [Doc 2](https://example.org/2)."
141140
)
142141
self.assertEqual(linkify_missing_citations(text, docs), expected)
143142

@@ -146,6 +145,15 @@ def test_linkify_missing_citations_out_of_range_untouched(self):
146145
text = "See [Doc 9] for more."
147146
self.assertEqual(linkify_missing_citations(text, docs), text)
148147

148+
def test_linkify_missing_citations_combined_marker_untouched(self):
149+
docs = [
150+
self._make_doc("https://example.org/1"),
151+
self._make_doc("https://example.org/2"),
152+
self._make_doc("https://example.org/3"),
153+
]
154+
text = "See [Docs 3 et 5] for more."
155+
self.assertEqual(linkify_missing_citations(text, docs), text)
156+
149157
def test_linkify_missing_citations_empty_docs_or_text(self):
150158
docs = [self._make_doc("https://example.org/1")]
151159
self.assertEqual(linkify_missing_citations("", docs), "")
@@ -154,9 +162,7 @@ def test_linkify_missing_citations_empty_docs_or_text(self):
154162
def test_linkify_missing_citations_dict_payload_fallback(self):
155163
docs = [{"document_url": "https://example.org/1"}]
156164
text = "See [Doc 1] for more."
157-
expected = (
158-
'See <a href="https://example.org/1" target="_blank">[Doc 1]</a> for more.'
159-
)
165+
expected = "See [Doc 1](https://example.org/1) for more."
160166
self.assertEqual(linkify_missing_citations(text, docs), expected)
161167

162168
def test_latest_tool_docs_returns_most_recent_call_only(self):

0 commit comments

Comments
 (0)