@qvac/harness 1.0.0-rc.2 → 1.0.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -12
- package/bench/README.md +94 -0
- package/bench/bfcl/checker.ts +313 -0
- package/bench/bfcl/data.ts +93 -0
- package/bench/bfcl/observe.ts +93 -0
- package/bench/bfcl/run.ts +213 -0
- package/bench/bfcl/tools.ts +72 -0
- package/bench/bridge/bridge.ts +392 -0
- package/bench/bridge/openai.ts +150 -0
- package/bench/bridge/python/qvac_bench/__init__.py +3 -0
- package/bench/bridge/python/qvac_bench/airline.py +42 -0
- package/bench/bridge/python/qvac_bench/approval.py +26 -0
- package/bench/bridge/python/qvac_bench/bfcl_multi_turn.py +152 -0
- package/bench/bridge/python/qvac_bench/client.py +88 -0
- package/bench/bridge/python/qvac_bench/harbor_agent.py +85 -0
- package/bench/bridge/python/qvac_bench/session.py +107 -0
- package/bench/bridge/python/qvac_bench/speed.py +30 -0
- package/bench/bridge/python/qvac_bench/tau2_agent.py +106 -0
- package/bench/bridge/python/qvac_bench/tools.py +11 -0
- package/bench/bridge/python/run_bfcl_multi_turn.py +95 -0
- package/bench/bridge/python/run_tau2.py +150 -0
- package/bench/bridge/python/smoke.py +47 -0
- package/bench/bridge/python/test_airline.py +28 -0
- package/bench/bridge/python/test_approval.py +57 -0
- package/bench/bridge/python/test_bfcl_multi_turn.py +185 -0
- package/bench/bridge/python/test_harbor_agent.py +107 -0
- package/bench/bridge/serve.ts +52 -0
- package/bench/bridge/transcript.ts +52 -0
- package/bench/bun-tsconfig.json +15 -0
- package/bench/cpu-main.mjs +216 -0
- package/bench/cpu.ts +457 -0
- package/bench/evidence/2026-09-28-review15.md +225 -0
- package/bench/evidence/redact-bun1.3.14.json +92 -0
- package/bench/evidence/redact-node24.11.json +92 -0
- package/bench/graph-metrics.ts +43 -0
- package/bench/harness.ts +73 -0
- package/bench/issue129-matrix.mjs +722 -0
- package/bench/redact-adversarial.ts +256 -0
- package/bench/repeat-cpu.mjs +281 -0
- package/bench/report.ts +1075 -0
- package/bench/skill-eval/harness.ts +30 -0
- package/bench/skill-eval/laws.ts +37 -0
- package/bench/skill-eval/mermaid/client.ts +132 -0
- package/bench/skill-eval/mermaid/validate.mjs +115 -0
- package/bench/skill-eval/pack.ts +61 -0
- package/bench/skill-eval/packs/diagrams/cases.ts +281 -0
- package/bench/skill-eval/packs/diagrams/index.ts +50 -0
- package/bench/skill-eval/packs/diagrams/score.ts +74 -0
- package/bench/skill-eval/packs/index.ts +5 -0
- package/bench/skill-eval/packs/weather/cases.ts +279 -0
- package/bench/skill-eval/packs/weather/fixtures/wttr.json +566 -0
- package/bench/skill-eval/packs/weather/fixtures.ts +84 -0
- package/bench/skill-eval/packs/weather/index.ts +55 -0
- package/bench/skill-eval/packs/weather/score.ts +92 -0
- package/bench/skill-eval/report.ts +194 -0
- package/bench/skill-eval/run.ts +316 -0
- package/bench/speed.ts +80 -0
- package/package.json +4 -13
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { replyFailures } from '../../laws.ts'
|
|
2
|
+
import { openMermaidOracle } from '../../mermaid/client.ts'
|
|
3
|
+
import type { PackWorld, SkillPack } from '../../pack.ts'
|
|
4
|
+
import { CASES, LEAKED_TOOL, SKILL, TICKET_CASE, type Expect } from './cases.ts'
|
|
5
|
+
import { mermaidBlocks, structureFailures } from './score.ts'
|
|
6
|
+
|
|
7
|
+
// the skill grants no tools, so the only tool surface is the `skill` loader QVAC-24436 gets wrong
|
|
8
|
+
function world(): PackWorld<Expect> {
|
|
9
|
+
const oracle = openMermaidOracle()
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
tools: [],
|
|
13
|
+
reset() {},
|
|
14
|
+
async judge(evalCase, observed) {
|
|
15
|
+
const blocks = mermaidBlocks(observed.reply)
|
|
16
|
+
const verdicts = await Promise.all(blocks.map((block) => oracle.parse(block)))
|
|
17
|
+
const syntax = verdicts
|
|
18
|
+
.filter((verdict) => !verdict.ok)
|
|
19
|
+
.map((verdict) => `syntax: ${verdict.message}`)
|
|
20
|
+
|
|
21
|
+
const renders = blocks.length === 0 ? null : verdicts.every((verdict) => verdict.ok)
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
failures: [
|
|
25
|
+
...structureFailures(evalCase.expect, blocks),
|
|
26
|
+
...syntax,
|
|
27
|
+
...replyFailures(observed.reply, evalCase.expect.reply)
|
|
28
|
+
],
|
|
29
|
+
// nothing here is answered from a fixture, so a run can never be an eval defect
|
|
30
|
+
gap: false,
|
|
31
|
+
renders,
|
|
32
|
+
// what the user saw: a drawable diagram, or none when none was wanted, with no tool framing
|
|
33
|
+
outcome:
|
|
34
|
+
(evalCase.expect.type === '' ? blocks.length === 0 : renders === true) &&
|
|
35
|
+
!LEAKED_TOOL.test(observed.reply)
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
save() {},
|
|
39
|
+
close() {
|
|
40
|
+
oracle.close()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const diagrams: SkillPack<Expect> = {
|
|
46
|
+
skill: SKILL,
|
|
47
|
+
focus: TICKET_CASE,
|
|
48
|
+
cases: CASES,
|
|
49
|
+
world
|
|
50
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ToolCall } from '../../pack.ts'
|
|
2
|
+
import type { Expect } from './cases.ts'
|
|
3
|
+
|
|
4
|
+
// matches the renderers' case-sensitive language check, the same rule lib/diagram-repair.ts uses
|
|
5
|
+
const FENCE = /^[ \t]{0,3}(`{3,}|~{3,})[ \t]*mermaid[ \t]*\r?\n([\s\S]*?)^[ \t]{0,3}\1[ \t]*$/gm
|
|
6
|
+
|
|
7
|
+
// a label never spans lines: without \n here the scan runs on through the next transition
|
|
8
|
+
const LABEL = /[[({]"?([^"\]})\n]{1,200})"?[\])}]/g
|
|
9
|
+
const NODE_ID = /^\s*([A-Za-z]\w*)\s*[[({]/gm
|
|
10
|
+
|
|
11
|
+
export const LABEL_LIMIT = 40
|
|
12
|
+
|
|
13
|
+
export function mermaidBlocks(reply: string): string[] {
|
|
14
|
+
FENCE.lastIndex = 0
|
|
15
|
+
return [...reply.matchAll(FENCE)].map((match) => match[2] ?? '').filter((body) => body.trim())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function diagramType(block: string): string {
|
|
19
|
+
const first = block.trim().split(/\r?\n/)[0]?.trim() ?? ''
|
|
20
|
+
// `flowchart TD` and `stateDiagram-v2` both key on the first word
|
|
21
|
+
return first.split(/\s+/)[0] ?? ''
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// mermaid accepts a reused id (the later label wins), so the oracle never sees it
|
|
25
|
+
export function duplicateIds(block: string): string[] {
|
|
26
|
+
NODE_ID.lastIndex = 0
|
|
27
|
+
const seen = new Set<string>()
|
|
28
|
+
const twice = new Set<string>()
|
|
29
|
+
for (const match of block.matchAll(NODE_ID)) {
|
|
30
|
+
const id = match[1]
|
|
31
|
+
if (seen.has(id)) {
|
|
32
|
+
twice.add(id)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
seen.add(id)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [...twice]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function longLabels(block: string): string[] {
|
|
42
|
+
LABEL.lastIndex = 0
|
|
43
|
+
return [...block.matchAll(LABEL)]
|
|
44
|
+
.map((match) => match[1].trim())
|
|
45
|
+
.filter((label) => label.length > LABEL_LIMIT)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function structureFailures(expect: Expect, blocks: string[]): string[] {
|
|
49
|
+
const failures: string[] = []
|
|
50
|
+
const [low, high] = Array.isArray(expect.blocks) ? expect.blocks : [expect.blocks, expect.blocks]
|
|
51
|
+
if (blocks.length < low || blocks.length > high) {
|
|
52
|
+
failures.push(`blocks: ${blocks.length} not ${low === high ? low : `${low}-${high}`}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const block of blocks) {
|
|
56
|
+
const type = diagramType(block)
|
|
57
|
+
if (expect.type && type !== expect.type) {
|
|
58
|
+
failures.push(`type: ${type || 'none'} not ${expect.type}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// a class member like `speak()` reads as a node id, and one per class is not a duplicate
|
|
62
|
+
if (type === 'flowchart') {
|
|
63
|
+
for (const id of duplicateIds(block)) {
|
|
64
|
+
failures.push(`duplicate id: ${id}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const label of longLabels(block)) {
|
|
69
|
+
failures.push(`label over ${LABEL_LIMIT}: ${label.slice(0, 50)}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return failures
|
|
74
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { skillLoadedTurn } from '../../../../lib/capabilities/tool-turns.ts'
|
|
2
|
+
import type { ReplyLaws } from '../../laws.ts'
|
|
3
|
+
import { assistant, user, type EvalCase as Case } from '../../pack.ts'
|
|
4
|
+
import type { Suffix } from './fixtures.ts'
|
|
5
|
+
|
|
6
|
+
export const CATEGORIES = [
|
|
7
|
+
'city-country',
|
|
8
|
+
'ambiguous',
|
|
9
|
+
'unambiguous',
|
|
10
|
+
'unknown',
|
|
11
|
+
'forecast',
|
|
12
|
+
'casual',
|
|
13
|
+
'no-location',
|
|
14
|
+
'follow-up'
|
|
15
|
+
] as const
|
|
16
|
+
|
|
17
|
+
export type Category = (typeof CATEGORIES)[number]
|
|
18
|
+
|
|
19
|
+
export interface Expect {
|
|
20
|
+
// http_request calls only; a skill load is never counted
|
|
21
|
+
readonly calls: number | readonly [number, number]
|
|
22
|
+
// normalised as wttrRequest() does; any listed form passes
|
|
23
|
+
readonly location?: ReadonlyArray<string> | undefined
|
|
24
|
+
readonly suffix?: ReadonlyArray<Suffix> | undefined
|
|
25
|
+
// the reply must state a temperature the fixture served, in °C or converted to °F
|
|
26
|
+
readonly echoes?: boolean | undefined
|
|
27
|
+
readonly reply?: ReplyLaws | undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type EvalCase = Case<Expect>
|
|
31
|
+
|
|
32
|
+
export const SKILL = 'weather'
|
|
33
|
+
export const TICKET_CASE = 'nassau-bahamas'
|
|
34
|
+
|
|
35
|
+
export const TEMPERATURE =
|
|
36
|
+
/[+-]?\d{1,3}(?:\.\d)?\s?(?:°|º|˚|degrees?)(?:\s?(?:c|f|celsius|fahrenheit)\b)?/i
|
|
37
|
+
const QUESTION = /\?/
|
|
38
|
+
const WHICH =
|
|
39
|
+
/\b(?:which|do you mean|did you mean|clarify|specify|more than one|several|multiple)\b/i
|
|
40
|
+
const HEDGE = /\b(?:do you mean|did you mean|which (?:one|city|location|place))\b/i
|
|
41
|
+
const ASK_LOCATION = /\b(?:city|location|place|town|where)\b/i
|
|
42
|
+
const NOT_FOUND =
|
|
43
|
+
/\b(?:not|no|unable|cannot|can't|couldn't|unknown|invalid|unrecogni[sz]ed|failed?|error|500)\b|n't\b/i
|
|
44
|
+
|
|
45
|
+
interface Lookup {
|
|
46
|
+
readonly category: Category
|
|
47
|
+
readonly prompt: string
|
|
48
|
+
readonly location: ReadonlyArray<string>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const current = (id: string, lookup: Lookup): EvalCase => ({
|
|
52
|
+
id,
|
|
53
|
+
category: lookup.category,
|
|
54
|
+
messages: [user(lookup.prompt)],
|
|
55
|
+
expect: {
|
|
56
|
+
calls: 1,
|
|
57
|
+
location: lookup.location,
|
|
58
|
+
suffix: ['format=3'],
|
|
59
|
+
echoes: true,
|
|
60
|
+
reply: { none: [HEDGE] }
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
interface Ask {
|
|
65
|
+
readonly category: Category
|
|
66
|
+
readonly prompt: string
|
|
67
|
+
readonly names: ReadonlyArray<string>
|
|
68
|
+
readonly asks: RegExp
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const asks = (id: string, ask: Ask): EvalCase => ({
|
|
72
|
+
id,
|
|
73
|
+
category: ask.category,
|
|
74
|
+
messages: [user(ask.prompt)],
|
|
75
|
+
expect: {
|
|
76
|
+
calls: 0,
|
|
77
|
+
reply: { laws: { has: ask.names }, all: [QUESTION, ask.asks], none: [TEMPERATURE] }
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
interface Ambiguous {
|
|
82
|
+
readonly prompt: string
|
|
83
|
+
readonly city: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ambiguous = (id: string, ask: Ambiguous): EvalCase =>
|
|
87
|
+
asks(id, { category: 'ambiguous', prompt: ask.prompt, names: [ask.city], asks: WHICH })
|
|
88
|
+
|
|
89
|
+
interface UnknownPlace {
|
|
90
|
+
readonly prompt: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// declining to look up an obviously invented name is as good as asking wttr.in and relaying its miss
|
|
94
|
+
const unknown = (id: string, ask: UnknownPlace): EvalCase => ({
|
|
95
|
+
id,
|
|
96
|
+
category: 'unknown',
|
|
97
|
+
messages: [user(ask.prompt)],
|
|
98
|
+
expect: { calls: [0, 2], suffix: ['format=3'], reply: { all: [NOT_FOUND], none: [TEMPERATURE] } }
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
interface Forecast {
|
|
102
|
+
readonly prompt: string
|
|
103
|
+
readonly location: ReadonlyArray<string>
|
|
104
|
+
readonly suffix: ReadonlyArray<Suffix>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const forecast = (id: string, ask: Forecast): EvalCase => ({
|
|
108
|
+
id,
|
|
109
|
+
category: 'forecast',
|
|
110
|
+
messages: [user(ask.prompt)],
|
|
111
|
+
expect: { calls: 1, location: ask.location, suffix: ask.suffix, reply: { none: [HEDGE] } }
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
export const CASES: ReadonlyArray<EvalCase> = [
|
|
115
|
+
current(TICKET_CASE, {
|
|
116
|
+
category: 'city-country',
|
|
117
|
+
prompt: 'Whats the weather like in Nassau, Bahamas?',
|
|
118
|
+
location: ['nassau bahamas', 'nassau the bahamas']
|
|
119
|
+
}),
|
|
120
|
+
current('paris-texas', {
|
|
121
|
+
category: 'city-country',
|
|
122
|
+
prompt: 'weather in Paris, Texas',
|
|
123
|
+
location: ['paris texas', 'paris tx']
|
|
124
|
+
}),
|
|
125
|
+
current('cambridge-uk', {
|
|
126
|
+
category: 'city-country',
|
|
127
|
+
prompt: 'is it raining in Cambridge, UK right now',
|
|
128
|
+
location: ['cambridge uk', 'cambridge united kingdom', 'cambridge england']
|
|
129
|
+
}),
|
|
130
|
+
current('san-jose-cr', {
|
|
131
|
+
category: 'city-country',
|
|
132
|
+
prompt: 'temperature in San Jose, Costa Rica',
|
|
133
|
+
location: ['san jose costa rica']
|
|
134
|
+
}),
|
|
135
|
+
current('portland-oregon', {
|
|
136
|
+
category: 'city-country',
|
|
137
|
+
prompt: 'Portland, Oregon weather please',
|
|
138
|
+
location: ['portland oregon', 'portland or']
|
|
139
|
+
}),
|
|
140
|
+
current('georgia-country', {
|
|
141
|
+
category: 'city-country',
|
|
142
|
+
prompt: 'How hot is it in Georgia, the country?',
|
|
143
|
+
location: ['tbilisi', 'tbilisi georgia']
|
|
144
|
+
}),
|
|
145
|
+
|
|
146
|
+
ambiguous('nassau-bare', { prompt: "what's the weather in Nassau", city: 'nassau' }),
|
|
147
|
+
ambiguous('springfield', { prompt: 'Springfield weather', city: 'springfield' }),
|
|
148
|
+
ambiguous('georgia', { prompt: "how's the weather in Georgia today", city: 'georgia' }),
|
|
149
|
+
ambiguous('portland', { prompt: 'Portland weather forecast for tomorrow', city: 'portland' }),
|
|
150
|
+
ambiguous('cambridge', { prompt: 'weather in Cambridge', city: 'cambridge' }),
|
|
151
|
+
|
|
152
|
+
current('london', {
|
|
153
|
+
category: 'unambiguous',
|
|
154
|
+
prompt: 'weather in London',
|
|
155
|
+
location: ['london', 'london uk', 'london united kingdom', 'london england']
|
|
156
|
+
}),
|
|
157
|
+
current('tokyo', {
|
|
158
|
+
category: 'unambiguous',
|
|
159
|
+
prompt: 'tokyo weather rn',
|
|
160
|
+
location: ['tokyo', 'tokyo japan']
|
|
161
|
+
}),
|
|
162
|
+
current('berlin', {
|
|
163
|
+
category: 'unambiguous',
|
|
164
|
+
prompt: 'is it cold in Berlin?',
|
|
165
|
+
location: ['berlin', 'berlin germany']
|
|
166
|
+
}),
|
|
167
|
+
current('reykjavik', {
|
|
168
|
+
category: 'unambiguous',
|
|
169
|
+
prompt: "how's the weather in Reykjavik",
|
|
170
|
+
location: ['reykjavik', 'reykjavik iceland']
|
|
171
|
+
}),
|
|
172
|
+
current('cairo', {
|
|
173
|
+
category: 'unambiguous',
|
|
174
|
+
prompt: 'hows the weather in Cairo',
|
|
175
|
+
location: ['cairo', 'cairo egypt']
|
|
176
|
+
}),
|
|
177
|
+
current('paris', {
|
|
178
|
+
category: 'unambiguous',
|
|
179
|
+
prompt: 'weather in Paris',
|
|
180
|
+
location: ['paris', 'paris france']
|
|
181
|
+
}),
|
|
182
|
+
|
|
183
|
+
unknown('xyzzyqwerty', { prompt: 'weather in Xyzzyqwerty' }),
|
|
184
|
+
unknown('zzyzxville', { prompt: "what's the weather like in Zzyzxville" }),
|
|
185
|
+
unknown('qxzvtown', { prompt: 'how is the weather in Qxzvtown' }),
|
|
186
|
+
unknown('vrblxstan', { prompt: 'Vrblxstan weather today' }),
|
|
187
|
+
|
|
188
|
+
forecast('lisbon-weekend', {
|
|
189
|
+
prompt: 'will it rain in Lisbon, Portugal this weekend',
|
|
190
|
+
location: ['lisbon portugal'],
|
|
191
|
+
suffix: ['2T', 'T']
|
|
192
|
+
}),
|
|
193
|
+
forecast('rome-tomorrow', {
|
|
194
|
+
prompt: "what's the weather in Rome tomorrow?",
|
|
195
|
+
location: ['rome', 'rome italy'],
|
|
196
|
+
suffix: ['2T', 'T']
|
|
197
|
+
}),
|
|
198
|
+
forecast('oslo-today', {
|
|
199
|
+
prompt: "what's today's weather forecast for Oslo?",
|
|
200
|
+
location: ['oslo', 'oslo norway'],
|
|
201
|
+
suffix: ['1T', 'format=3']
|
|
202
|
+
}),
|
|
203
|
+
forecast('madrid-3-days', {
|
|
204
|
+
prompt: 'Madrid weather for the next 3 days',
|
|
205
|
+
location: ['madrid', 'madrid spain'],
|
|
206
|
+
suffix: ['2T', 'T']
|
|
207
|
+
}),
|
|
208
|
+
{
|
|
209
|
+
id: 'madrid-7-days',
|
|
210
|
+
category: 'forecast',
|
|
211
|
+
messages: [user('Madrid weather for the next 7 days')],
|
|
212
|
+
expect: {
|
|
213
|
+
calls: [0, 1],
|
|
214
|
+
location: ['madrid', 'madrid spain'],
|
|
215
|
+
suffix: ['2T', 'T'],
|
|
216
|
+
reply: { all: [/\b3\b|three/i] }
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
current('nyc', {
|
|
221
|
+
category: 'casual',
|
|
222
|
+
prompt: 'yo whats the weather like in nyc',
|
|
223
|
+
location: [
|
|
224
|
+
'nyc',
|
|
225
|
+
'new york',
|
|
226
|
+
'new york city',
|
|
227
|
+
'new york ny',
|
|
228
|
+
'new york usa',
|
|
229
|
+
'nyc usa',
|
|
230
|
+
'new york city usa'
|
|
231
|
+
]
|
|
232
|
+
}),
|
|
233
|
+
current('sao-paulo', {
|
|
234
|
+
category: 'casual',
|
|
235
|
+
prompt: 'temp in São Paulo?',
|
|
236
|
+
location: ['sao paulo', 'sao paulo brazil']
|
|
237
|
+
}),
|
|
238
|
+
current('rio', {
|
|
239
|
+
category: 'casual',
|
|
240
|
+
prompt: 'weather Rio de Janeiro, Brazil',
|
|
241
|
+
location: ['rio de janeiro brazil']
|
|
242
|
+
}),
|
|
243
|
+
current('zurich', {
|
|
244
|
+
category: 'casual',
|
|
245
|
+
prompt: 'Wetter in Zürich, Schweiz?',
|
|
246
|
+
location: ['zurich schweiz', 'zurich switzerland']
|
|
247
|
+
}),
|
|
248
|
+
|
|
249
|
+
asks('no-location', {
|
|
250
|
+
category: 'no-location',
|
|
251
|
+
prompt: "what's the weather like?",
|
|
252
|
+
names: [],
|
|
253
|
+
asks: ASK_LOCATION
|
|
254
|
+
}),
|
|
255
|
+
asks('rain-later', {
|
|
256
|
+
category: 'no-location',
|
|
257
|
+
prompt: 'is it going to rain later?',
|
|
258
|
+
names: [],
|
|
259
|
+
asks: ASK_LOCATION
|
|
260
|
+
}),
|
|
261
|
+
|
|
262
|
+
// the host replays an earlier load as a name-only stub, so the model must reload before calling
|
|
263
|
+
{
|
|
264
|
+
id: 'nassau-follow-up',
|
|
265
|
+
category: 'follow-up',
|
|
266
|
+
messages: [
|
|
267
|
+
user('weather in Nassau?'),
|
|
268
|
+
{ role: 'tool', content: skillLoadedTurn(SKILL) },
|
|
269
|
+
assistant('Which Nassau do you mean: the one in the Bahamas, or Nassau County in New York?'),
|
|
270
|
+
user('the one in the Bahamas')
|
|
271
|
+
],
|
|
272
|
+
expect: {
|
|
273
|
+
calls: 1,
|
|
274
|
+
location: ['nassau bahamas', 'nassau the bahamas'],
|
|
275
|
+
suffix: ['format=3'],
|
|
276
|
+
echoes: true
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
]
|