74 lines
3.3 KiB
TypeScript
74 lines
3.3 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||
// The test double of the AI gateway for the documents module: a deterministic reader, standing in for a
|
||
// model behind `platform.ai.respond` of the fake platform.
|
||
//
|
||
// It answers the extraction request exactly as a well-behaved model would: it finds the page's text
|
||
// between the tags the prompt names, takes every `label: value` line as a field, and quotes the line
|
||
// itself. It never sees an image, never invents a word, and returns the same lines for the same text,
|
||
// so a test (or the offline eval) that runs it measures the module's own code -- the request it builds,
|
||
// the schema it parses, the verification, the cache, the proposals -- and not a model.
|
||
|
||
import { PAGE_TEXT_CLOSE, PAGE_TEXT_OPEN } from "@app/modules/documents"
|
||
import type { FakeChatReply } from "@conusai-kit/testing/fake-platform"
|
||
|
||
const FIELD = /^\s*([^::]+?)\s*[::]\s*(\S.*?)\s*$/u
|
||
const MAX_LABEL_CHARS = 60
|
||
const MAX_LABEL_WORDS = 6
|
||
const MAX_LINE_CHARS = 2_000
|
||
|
||
export interface ReadLineOfModel {
|
||
label: string
|
||
value: string
|
||
quote: string
|
||
}
|
||
|
||
/**
|
||
* The fields a reader of two line shapes finds in a page's text: `label: value`, and a table row
|
||
* `first cell | other cells`, whose label is the first cell and whose value is the rest, as the prompt
|
||
* asks a model to read it. The first row of a block of rows is the table's header, which names the
|
||
* columns and is not a field.
|
||
*/
|
||
export function keyValueLines(text: string): ReadLineOfModel[] {
|
||
const found: ReadLineOfModel[] = []
|
||
let inTable = false
|
||
for (const line of text.split("\n")) {
|
||
if (line.includes(" | ")) {
|
||
const header = !inTable
|
||
inTable = true
|
||
const [label, ...rest] = line.split(" | ").map((cell) => cell.trim())
|
||
if (!header && label && rest.length > 0 && rest.every((cell) => cell !== "") && line.length <= MAX_LINE_CHARS) {
|
||
found.push({ label, value: rest.join(" | "), quote: line.trim() })
|
||
}
|
||
continue
|
||
}
|
||
inTable = false
|
||
const match = FIELD.exec(line)
|
||
if (!match) continue
|
||
const label = match[1] as string
|
||
const value = match[2] as string
|
||
if (label.length > MAX_LABEL_CHARS || label.split(/\s+/u).length > MAX_LABEL_WORDS) continue
|
||
if (line.length > MAX_LINE_CHARS) continue
|
||
found.push({ label, value, quote: line.trim() })
|
||
}
|
||
return found
|
||
}
|
||
|
||
type Part = { type?: string; text?: string }
|
||
|
||
/** The page text of an extraction request: what lies between the prompt's tags in the user's message. */
|
||
export function pageTextOf(request: Record<string, unknown>): string | undefined {
|
||
const messages = (request.messages ?? []) as Array<{ role?: string; content?: string | Part[] }>
|
||
const user = [...messages].reverse().find((message) => message.role === "user")
|
||
const content = user?.content
|
||
const text = typeof content === "string" ? content : (content ?? []).map((part) => part.text ?? "").join("\n")
|
||
const from = text.indexOf(PAGE_TEXT_OPEN)
|
||
const to = text.lastIndexOf(PAGE_TEXT_CLOSE)
|
||
return from < 0 || to < from ? undefined : text.slice(from + PAGE_TEXT_OPEN.length + 1, to - 1)
|
||
}
|
||
|
||
/** What an extraction request is answered with by the deterministic reader. */
|
||
export const deterministicModel = (request: Record<string, unknown>): FakeChatReply => {
|
||
const text = pageTextOf(request)
|
||
return { content: JSON.stringify({ lines: text === undefined ? [] : keyValueLines(text) }) }
|
||
}
|