Skip to content

fix: prompt explorer citations always empty (annotations have no type field)#82

Open
bookingseo wants to merge 1 commit into
every-app:mainfrom
bookingseo:fix/llm-citation-extraction
Open

fix: prompt explorer citations always empty (annotations have no type field)#82
bookingseo wants to merge 1 commit into
every-app:mainfrom
bookingseo:fix/llm-citation-extraction

Conversation

@bookingseo

@bookingseo bookingseo commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Problem

extractCitations in promptExplorer.ts filters annotations with:

if (annotation.type !== "citation") continue;

But the DataForSEO LLM Responses payload returns references as untyped { title, url } objects. The SDK model (AnnotationInfo in dataforseo-client) is literally:

interface IAnnotationInfo { title?: string; url?: string; [key: string]: any; }

There is no type discriminator, so annotation.type is always undefined, the guard is always true, and every citation is dropped — the Prompt Explorer shows sources in the model text but an empty citations list even when web_search ran. The type !== "citation" check looks copied from OpenAI's native Responses API (where annotations are typed url_citation), but DataForSEO normalizes them to untyped references.

Fix

Drop the spurious type filter; keep the URL-safety guard (safeHttpUrl), which is the real reason the loop rejects entries (e.g. javascript: payloads rendered as <a href>). Confirmed against the DataForSEO docs (annotations documented as { title, url }) and the generated SDK model.

Adds a promptExplorer.test.ts covering: untyped annotations are kept, URLs dedupe, unsafe schemes drop, and annotations outside message items are ignored.

tsc clean; new test passes.

(Found while auditing AI-visibility citation coverage on a self-hosted deployment — citation rate was 0% across ChatGPT and Perplexity despite web search.)

Greptile Summary

This PR fixes Prompt Explorer citation extraction for DataForSEO LLM responses. The main changes are:

  • Treats response annotations as untyped { title, url } references.
  • Keeps the existing HTTP/HTTPS URL safety guard and URL deduplication.
  • Adds focused tests for untyped citations, unsafe URL filtering, duplicates, and non-message annotations.

Confidence Score: 5/5

Safe to merge with minimal risk.

The production change is narrowly scoped to citation extraction, keeps the existing URL safety boundary, and includes focused tests for the changed behavior.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex executed the Vitest test suite and saved the proof log.
  • T-Rex ran the TypeScript no-emit check and saved the proof log.
  • T-Rex inspected both logs to verify that they captured the exact command, working directory, output, and exit code.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src/server/features/ai-search/services/promptExplorer.ts Exports extractCitations for direct testing and accepts DataForSEO's untyped annotation references while preserving URL safety and dedupe guards.
src/server/features/ai-search/services/promptExplorer.test.ts Adds behavioral tests for untyped citations, deduplication, unsafe URL rejection, and ignoring non-message annotations.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant PE as Prompt Explorer
participant DFS as DataForSEO LLM Response
participant EC as extractCitations
participant UI as Prompt Explorer UI

PE->>DFS: fetch model response
DFS-->>PE: "items[].sections[].annotations [{title, url}]"
PE->>EC: shapeSuccess(response)
loop message item sections
    EC->>EC: validate annotation.url with safeHttpUrl
    EC->>EC: dedupe accepted URLs
end
EC-->>PE: PromptExplorerCitation[]
PE-->>UI: model result with citations
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant PE as Prompt Explorer
participant DFS as DataForSEO LLM Response
participant EC as extractCitations
participant UI as Prompt Explorer UI

PE->>DFS: fetch model response
DFS-->>PE: "items[].sections[].annotations [{title, url}]"
PE->>EC: shapeSuccess(response)
loop message item sections
    EC->>EC: validate annotation.url with safeHttpUrl
    EC->>EC: dedupe accepted URLs
end
EC-->>PE: PromptExplorerCitation[]
PE-->>UI: model result with citations
Loading

Reviews (1): Last reviewed commit: "fix(ai-search): stop dropping every LLM ..." | Re-trigger Greptile

extractCitations filtered annotations on `type === "citation"`, but
DataForSEO's LLM Responses payload returns untyped `{ title, url }`
reference objects — the SDK's AnnotationInfo has no type discriminator,
so the filter matched nothing and the prompt explorer's citations were
always empty even with web search enabled. Guard on URL safety only
(the real reason the loop existed) and add a unit test covering the
untyped-annotation, dedupe, unsafe-scheme, and non-message-item cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant