Repository navigation
Expand file tree
/
Copy patheval-question.ts
More file actions
122 lines (110 loc) · 3.48 KB
/
Copy patheval-question.ts
File metadata and controls
122 lines (110 loc) · 3.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import type { AdaptifEvent } from "@adaptif/core";
/** Situations answer "yes"/"no"; intents answer an option id. */
export type EvalAnswer = string;
export interface RuleInput<State> {
state: State;
events: readonly AdaptifEvent[];
signalNames: readonly string[];
}
interface CaseDefinition<State, Expected> {
id: string;
/** Cases sharing a pair produce the same signals but have different labels. */
pair?: string;
state: State;
timeline: readonly AdaptifEvent[];
/** When the question is asked, as `"m:ss"` from the start of the session. */
askedAt: string;
expected: Expected;
/** Why this label, written before the model ever sees the case. */
why: string;
}
export interface EvalCase {
id: string;
pair: string | null;
state: Record<string, unknown>;
timeline: readonly AdaptifEvent[];
askedAt: string;
expected: EvalAnswer;
why: string;
}
export interface EvalQuestion {
id: string;
kind: "situation" | "intent";
text: string;
options: Readonly<Record<string, string>> | null;
/** A question rules should handle as well as the model; a big model win here suggests biased cases. */
isControl: boolean;
answerByRule(input: RuleInput<Record<string, unknown>>): EvalAnswer;
cases: readonly EvalCase[];
}
export function toSituationAnswer(isTrue: boolean): EvalAnswer {
return isTrue ? "yes" : "no";
}
export function defineSituation<State extends Record<string, unknown>>(definition: {
id: string;
text: string;
isControl?: boolean;
rule: (input: RuleInput<State>) => boolean;
cases: ReadonlyArray<CaseDefinition<State, boolean>>;
}): EvalQuestion {
return {
id: definition.id,
kind: "situation",
text: definition.text,
options: null,
isControl: definition.isControl ?? false,
answerByRule: (input) => toSituationAnswer(definition.rule(input as RuleInput<State>)),
cases: definition.cases.map((evalCase) => ({
...normalizeCase(evalCase),
expected: toSituationAnswer(evalCase.expected),
})),
};
}
export function defineIntent<
State extends Record<string, unknown>,
const Options extends Record<string, string>,
>(definition: {
id: string;
text: string;
options: Options;
rule: (input: RuleInput<State>) => keyof Options & string;
cases: ReadonlyArray<CaseDefinition<State, keyof Options & string>>;
}): EvalQuestion {
return {
id: definition.id,
kind: "intent",
text: definition.text,
options: definition.options,
isControl: false,
answerByRule: (input) => definition.rule(input as RuleInput<State>),
cases: definition.cases.map((evalCase) => ({
...normalizeCase(evalCase),
expected: evalCase.expected,
})),
};
}
function normalizeCase<State extends Record<string, unknown>>(
evalCase: CaseDefinition<State, unknown>,
): Omit<EvalCase, "expected"> {
return {
id: evalCase.id,
pair: evalCase.pair ?? null,
state: evalCase.state,
timeline: evalCase.timeline,
askedAt: evalCase.askedAt,
why: evalCase.why,
};
}
export function hasEvent(
events: readonly AdaptifEvent[],
matches: (event: AdaptifEvent) => boolean,
): boolean {
return events.some(matches);
}
export function countCustomEvents(events: readonly AdaptifEvent[], name: string): number {
return events.filter((event) => event.kind === "custom" && event.name === name).length;
}
export function containsAnyKeyword(text: string, keywords: readonly string[]): boolean {
const lowered = text.toLowerCase();
return keywords.some((keyword) => lowered.includes(keyword));
}