@@ -5,22 +5,29 @@ import { config } from "../config";
55// trained on the line-like spans `parseMarkdownToSentences` produces, so callers
66// pass those spans as explicit `lines`, the service scores each against the
77// query, and returns the selected spans as `highlights[]` with their `index`
8- // back into the input `lines` array. Callers map those indices through
9- // `assembleAnswer` to rebuild structure (tables/code). We push every page
10- // through one `/batch_highlight` call (resident GPU model). Endpoint + token are
11- // config-gated (HIGHLIGHT_MODEL_URL / HIGHLIGHT_MODEL_TOKEN); callers must
12- // confirm they're set via highlightsEnvReady() before invoking.
8+ // back into the input `lines` array (and a `score`). Callers run those scored
9+ // spans through the neighbor/group budgeter, then `assembleAnswer` to rebuild
10+ // structure (tables/code). We push every page through one `/batch_highlight`
11+ // call (resident GPU model). Endpoint + token are config-gated
12+ // (HIGHLIGHT_MODEL_URL / HIGHLIGHT_MODEL_TOKEN); callers must confirm they're
13+ // set via highlightsEnvReady() before invoking.
1314
1415// Experimental score cutoff for keeping a span, tuned against the line-span
1516// format the model was trained on.
1617const HIGHLIGHT_THRESHOLD = 0.08 ;
1718// Cap on how many spans to keep per page after thresholding (highest-scoring).
19+ // We do char budgeting ourselves (group-aware, in selectHighlightIndices), so we
20+ // don't send max_highlight_chars — the service would budget by raw lines.
1821const HIGHLIGHT_TOP_K = 12 ;
19- // Character budget per page; the service keeps the highest-scoring spans until
20- // the budget is reached (measured on raw span text).
21- const MAX_HIGHLIGHT_CHARS = 800 ;
2222const REQUEST_TIMEOUT_MS = 30000 ;
2323
24+ // One scored candidate span: `index` into the page's `lines`, `score` from the
25+ // model. Consumed by the neighbor/group budgeter (selectHighlightIndices).
26+ export interface ScoredSpan {
27+ index : number ;
28+ score : number ;
29+ }
30+
2431interface HighlightItem {
2532 query : string ;
2633 // Candidate spans for the page, in document order — from
@@ -41,14 +48,18 @@ interface BatchHighlightResponse {
4148 results ?: HighlightResult [ ] ;
4249}
4350
44- // Pull the selected span indices out of one page's result. The service already
45- // applied the threshold / char budget, so we just collect the kept indices .
46- function selectedIndices ( result : HighlightResult | undefined ) : number [ ] {
51+ // Pull the scored spans out of one page's result, keeping only well-formed ones
52+ // (integer index, numeric score). The service already applied threshold + top_k .
53+ function scoredSpans ( result : HighlightResult | undefined ) : ScoredSpan [ ] {
4754 const highlights = result ?. highlights ?? [ ] ;
48- const out : number [ ] = [ ] ;
55+ const out : ScoredSpan [ ] = [ ] ;
4956 for ( const h of highlights ) {
50- if ( typeof h . index === "number" && Number . isInteger ( h . index ) ) {
51- out . push ( h . index ) ;
57+ if (
58+ typeof h . index === "number" &&
59+ Number . isInteger ( h . index ) &&
60+ typeof h . score === "number"
61+ ) {
62+ out . push ( { index : h . index , score : h . score } ) ;
5263 }
5364 }
5465 return out ;
@@ -57,14 +68,14 @@ function selectedIndices(result: HighlightResult | undefined): number[] {
5768/**
5869 * Score many pages' candidate spans in a single batch call to the
5970 * query-highlights model. Returns an array aligned to `items`: for each page,
60- * the indices ( into that page's `lines`) the model selected, or null when the
61- * whole batch fails (so callers can keep the provider snippet). A page with no
62- * spans clearing the threshold yields an empty array, not null .
71+ * the scored spans (index into that page's `lines`, plus score) the model
72+ * selected, or null when the whole batch fails (so callers can keep the provider
73+ * snippet). A page with no spans clearing the threshold yields an empty array.
6374 */
6475export async function generateHighlightsBatch (
6576 items : HighlightItem [ ] ,
6677 opts : { logger : Logger } ,
67- ) : Promise < ( number [ ] | null ) [ ] > {
78+ ) : Promise < ( ScoredSpan [ ] | null ) [ ] > {
6879 if ( items . length === 0 ) return [ ] ;
6980
7081 const controller = new AbortController ( ) ;
@@ -83,7 +94,6 @@ export async function generateHighlightsBatch(
8394 lines : item . lines ,
8495 threshold : HIGHLIGHT_THRESHOLD ,
8596 top_k : HIGHLIGHT_TOP_K ,
86- max_highlight_chars : MAX_HIGHLIGHT_CHARS ,
8797 } ) ) ,
8898 } ) ,
8999 signal : controller . signal ,
@@ -98,15 +108,15 @@ export async function generateHighlightsBatch(
98108
99109 const data = ( await res . json ( ) ) as BatchHighlightResponse ;
100110 const results = data . results ?? [ ] ;
101- const indices = items . map ( ( _ , i ) => selectedIndices ( results [ i ] ) ) ;
111+ const spans = items . map ( ( _ , i ) => scoredSpans ( results [ i ] ) ) ;
102112
103113 opts . logger . info ( "query highlights batch generated" , {
104114 pages : items . length ,
105- withHighlights : indices . filter ( x => x . length > 0 ) . length ,
115+ withHighlights : spans . filter ( x => x . length > 0 ) . length ,
106116 elapsedMs : Date . now ( ) - start ,
107117 } ) ;
108118
109- return indices ;
119+ return spans ;
110120 } catch ( error ) {
111121 opts . logger . warn ( "query highlights batch failed" , {
112122 error : error instanceof Error ? error . message : String ( error ) ,
0 commit comments