Files
template-app/test/support/fake-model.ts
T

74 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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) }) }
}