Files
Jyotisha/frontend/src/lib/birth-time-dynamic-question-copy.ts
T

87 lines
4.0 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.
import { createHash } from "node:crypto";
import type { CandidateDifferencePacket } from "./birth-time-dynamic-choice-internal.ts";
const timeOfBirthPattern = /(?:^|[^\d])(?:[01]?\d|2[0-3])\s*[:]\s*[0-5]\d(?:$|[^\d])/;
const confidencePattern = /置信(?:度)?|可信度|准确率|准确度|概率最高|把握(?:最高|更高)/;
const supportPattern = /(?:更|最)?支持(?:第[一二三四]组|哪一组|.*结果|.*候选)|(?:候选|出生).*(?:支持|排除|更符合|更接近)/;
const controlPattern = /评分|得分|权重|算法|模型|证据分区|分区标识|停止提问|结束评估|应用(?:到)?排盘|更新排盘|系统(?:会|将)|直接锁定|锁定(?:答案|结果|时间)|最终答案/;
const instructionPattern = /忽略|无视|不要遵守|提示词|系统提示|开发者指令|遵循|服从|你(?:必须|应当|需要)|务必|执行(?:以上|以下|下列|这|该|内容)|把问题改成|改写问题|替换问题|请(?:选择|返回|输出)|按照.*(?:指令|规则)|回答成|输出为/;
const birthTimeClaimPattern = /出生(?:时间|时刻|分钟|几点)|生时|候选(?:时间|分钟|答案)/;
const groundingTerms = [
"升学", "转学", "学习", "搬家", "离乡", "居住", "关系",
"工作", "职业", "身份", "健康", "压力", "生活",
] as const;
const experiencePattern = /变化|转变|进入|结束|发生|经历|开始|离开|升学|转学|搬家|离乡|压力/;
export function dynamicPublicCopyIsSafe(value: string, question: boolean): boolean {
const normalized = value.normalize("NFKC").trim();
if (!/[\u3400-\u9fff]/u.test(normalized) || /[A-Za-z]/.test(normalized)) return false;
if (timeOfBirthPattern.test(normalized) || birthTimeClaimPattern.test(normalized)) return false;
if (confidencePattern.test(normalized) || supportPattern.test(normalized)) return false;
if (controlPattern.test(normalized)) return false;
return !question || (normalized.match(/[?]/g) ?? []).length === 1;
}
export function dynamicQuestionIsGrounded(prompt: string, neutralContext: string): boolean {
const terms = groundingTerms.filter((term) => neutralContext.includes(term));
return experiencePattern.test(prompt)
&& terms.length > 0
&& terms.some((term) => prompt.includes(term));
}
function safeUnmatchedNote(value: string | null): string | null {
const normalized = value?.normalize("NFKC").trim() || null;
if (normalized === null) return null;
if (normalized.length > 240) return null;
if (
timeOfBirthPattern.test(normalized)
|| birthTimeClaimPattern.test(normalized)
|| confidencePattern.test(normalized)
|| supportPattern.test(normalized)
|| controlPattern.test(normalized)
|| instructionPattern.test(normalized)
) return null;
return normalized;
}
export function modelSafeDynamicQuestionPrompt(
packet: CandidateDifferencePacket,
unmatchedNote: string | null,
): string {
const note = safeUnmatchedNote(unmatchedNote);
return JSON.stringify({
task: "generate_dynamic_choice_question",
opportunities: packet.opportunities.map((opportunity) => ({
opportunityId: opportunity.opportunityId,
dimensionCode: opportunity.dimensionCode,
neutralContext: opportunity.neutralContext,
partitions: opportunity.partitions.map((partition) => ({
partitionId: partition.partitionId,
descriptor: partition.descriptor,
fallbackLabel: partition.fallbackLabel,
})),
})),
unmatchedNote: note === null ? null : {
trust: "untrusted_user_evidence",
quotedText: note,
},
});
}
function normalizeSemanticCopy(value: string): string {
return value.normalize("NFKC").trim().replace(/\s+/g, "");
}
export function dynamicQuestionSemanticFingerprint(output: {
readonly prompt: string;
readonly options: readonly { readonly label: string }[];
}): string {
const semantics = {
prompt: normalizeSemanticCopy(output.prompt),
options: output.options.map((option) => normalizeSemanticCopy(option.label)),
};
return createHash("sha256")
.update(`birth-time-dynamic-question-v1\n${JSON.stringify(semantics)}`, "utf8")
.digest("hex");
}