Skip to content

Commit ed3f6e5

Browse files
authored
Add role-based model eval workload
1 parent ef3fb7e commit ed3f6e5

5 files changed

Lines changed: 423 additions & 53 deletions

File tree

evals/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,29 @@
22

33
Manual-only workload checks for candidate models.
44

5+
This suite is a local role-fit gate, not a public benchmark or leaderboard. It
6+
is intentionally shaped around production assistant usage patterns: short
7+
Telegram-style commands, calendar/tasks/memory/health/files/web tool routing,
8+
admin/debug advice, model-routing tradeoffs, and compaction.
9+
510
These evals are intentionally not wired into CI, schedulers, or background jobs.
611
The default runner refuses cloud paid models unless `--allow-paid` is passed.
712
OpenRouter models are allowed by default only when the model id ends with
813
`:free`; local and Ollama models are allowed by default.
914

15+
The suite uses deterministic expectations:
16+
17+
- literal required/forbidden content
18+
- "one of these phrases" groups for less brittle synonym checks
19+
- expected tool names
20+
- top-level JSON tool argument checks
21+
- max tool calls
22+
- max response length for simple-role checks
23+
24+
Tool cases use synthetic tool declarations. They validate whether a model picks
25+
the right tool and argument shape; they do not execute the real calendar,
26+
Todoist, memory, health, filesystem, or web tools.
27+
1028
Examples:
1129

1230
```bash

evals/workload.json

Lines changed: 235 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,81 @@
11
{
2-
"version": 1,
3-
"name": "personal-assistant-core-workloads",
2+
"version": 2,
3+
"name": "personal-assistant-role-workloads",
44
"cases": [
55
{
6-
"id": "ru-chat-concise",
7-
"category": "russian_chat",
8-
"system_prompt": "Reply in Russian. Be concise and practical.",
9-
"prompt": "Пользователь спрашивает: \"Что лучше сделать сначала: проверить логи сервиса или сразу перезапустить контейнер?\" Ответь коротко, как инженер.",
6+
"id": "simple-ping",
7+
"category": "simple",
8+
"purpose": "Checks that a small model can handle the very common short Telegram liveness turn without tools or filler.",
9+
"limitations": ["Does not measure reasoning quality."],
10+
"system_prompt": "Reply in Russian unless the user writes in another language. Be concise and practical.",
11+
"prompt": "Ping?",
1012
"expect": {
11-
"must_contain": ["логи"],
13+
"must_contain_any": [["pong", "на связи", "да", "работаю"]],
1214
"must_not_contain": ["As an AI"],
13-
"no_tool_call": true
15+
"no_tool_call": true,
16+
"max_chars": 120
1417
}
1518
},
1619
{
17-
"id": "tool-calendar-intent",
20+
"id": "simple-ops-logs-first",
21+
"category": "simple",
22+
"purpose": "Checks concise operational advice for a real admin/debug pattern.",
23+
"limitations": ["Does not prove the model can inspect logs itself."],
24+
"system_prompt": "Reply in Russian. Give the next action first. Do not use tools.",
25+
"prompt": "Бот не отвечает. Что первым проверить?",
26+
"expect": {
27+
"must_contain_any": [["логи", "лог"], ["контейнер", "сервис", "процесс", "health"]],
28+
"must_not_contain": ["сразу перезапусти все", "As an AI"],
29+
"no_tool_call": true,
30+
"max_chars": 260
31+
}
32+
},
33+
{
34+
"id": "default-admin-empty-models",
35+
"category": "default",
36+
"purpose": "Checks admin/model-catalog diagnosis without brittle English-only wording.",
37+
"limitations": ["Does not query the real database."],
38+
"system_prompt": "Act as a pragmatic senior engineer. Give the next diagnostic step first.",
39+
"prompt": "После деплоя админка открывается, но список моделей пустой. Что проверишь первым?",
40+
"expect": {
41+
"must_contain_any": [
42+
["model_capabilities", "capabilities", "кэш моделей", "таблиц"],
43+
["логи", "запрос", "refresh", "hydration", "openrouter"]
44+
],
45+
"must_not_contain": ["перезапусти все"],
46+
"no_tool_call": true,
47+
"max_chars": 700
48+
}
49+
},
50+
{
51+
"id": "default-safe-deploy-smoke",
52+
"category": "default",
53+
"purpose": "Checks that deployment advice includes service state, health endpoint, and logs instead of only reporting CI success.",
54+
"limitations": ["Does not run the deployment."],
55+
"system_prompt": "Reply in Russian. Be concrete and operational.",
56+
"prompt": "CI прошел, давай деплоить. Какие проверки после docker compose up обязательны?",
57+
"expect": {
58+
"must_contain_any": [
59+
["health", "healthz"],
60+
["логи", "logs"],
61+
["docker compose ps", "контейнер", "статус"]
62+
],
63+
"must_not_contain": ["ничего проверять не нужно"],
64+
"no_tool_call": true,
65+
"max_chars": 900
66+
}
67+
},
68+
{
69+
"id": "tool-calendar-morning-window",
1870
"category": "tool_use",
19-
"system_prompt": "Use tools when the user asks for data that only a tool can know.",
20-
"prompt": "Проверь мой календарь на завтра утром и скажи, есть ли свободное окно на 30 минут.",
71+
"purpose": "Checks calendar lookup routing with fixed dates, avoiding ambiguous relative-date behavior.",
72+
"limitations": ["Only validates tool selection and top-level arguments; it does not execute a real calendar read."],
73+
"system_prompt": "Use tools for calendar data. Use the exact ISO date range requested by the user.",
74+
"prompt": "Посмотри мой календарь 2026-06-17 утром с 09:00 до 12:00 по Белграду и скажи, есть ли окно на 30 минут.",
2175
"tools": [
2276
{
23-
"name": "calendar_list_events",
24-
"description": "List calendar events for a date range.",
77+
"name": "calendar__get_events",
78+
"description": "List calendar events for an exact date-time range.",
2579
"input_schema": {
2680
"type": "object",
2781
"properties": {
@@ -33,50 +87,196 @@
3387
}
3488
],
3589
"expect": {
36-
"tool_call": "calendar_list_events"
90+
"tool_call": "calendar__get_events",
91+
"tool_args": {
92+
"date_from": "2026-06-17T09:00:00+02:00",
93+
"date_to": "2026-06-17T12:00:00+02:00"
94+
},
95+
"max_tool_calls": 1
96+
}
97+
},
98+
{
99+
"id": "tool-tasks-today",
100+
"category": "tool_use",
101+
"purpose": "Checks task-list routing for short daily-planning prompts seen in production.",
102+
"limitations": ["Only validates tool call shape, not task ranking quality."],
103+
"system_prompt": "Use tools when the user asks about their tasks. Keep tool arguments minimal and explicit.",
104+
"prompt": "Что мне нужно сделать на 2026-06-17?",
105+
"tools": [
106+
{
107+
"name": "tasks__get_tasks",
108+
"description": "Get Todoist tasks for a date or project.",
109+
"input_schema": {
110+
"type": "object",
111+
"properties": {
112+
"date": {"type": "string"}
113+
},
114+
"required": ["date"]
115+
}
116+
}
117+
],
118+
"expect": {
119+
"tool_call": "tasks__get_tasks",
120+
"tool_args": {
121+
"date": "2026-06-17"
122+
},
123+
"max_tool_calls": 1
37124
}
38125
},
39126
{
40-
"id": "tool-web-fetch-intent",
41-
"category": "web_fetch",
42-
"system_prompt": "Use tools when the user asks you to inspect a URL. Do not invent page contents.",
43-
"prompt": "Открой https://example.com/release-notes и кратко перечисли изменения в последнем релизе.",
127+
"id": "tool-memory-recall",
128+
"category": "tool_use",
129+
"purpose": "Checks personal-memory recall for short 'check memory' style prompts.",
130+
"limitations": ["Does not validate the factual answer after memory retrieval."],
131+
"system_prompt": "Use personal memory tools when the user asks what you remember.",
132+
"prompt": "Проверь память: что я сохранял про измерение продуктивности разработчиков?",
44133
"tools": [
45134
{
46-
"name": "web_fetch",
47-
"description": "Fetch a URL and return readable page text.",
135+
"name": "personal-memory__recall_facts",
136+
"description": "Recall saved user facts by semantic query.",
48137
"input_schema": {
49138
"type": "object",
50139
"properties": {
51-
"url": {"type": "string"}
140+
"query": {"type": "string"}
52141
},
53-
"required": ["url"]
142+
"required": ["query"]
54143
}
55144
}
56145
],
57146
"expect": {
58-
"tool_call": "web_fetch"
147+
"tool_call": "personal-memory__recall_facts",
148+
"max_tool_calls": 1
59149
}
60150
},
61151
{
62-
"id": "compaction-summary",
152+
"id": "tool-health-sleep-summary",
153+
"category": "tool_use",
154+
"purpose": "Checks health-dashboard routing for the recurring sleep/health questions in production.",
155+
"limitations": ["Does not judge medical correctness; it only checks that private health data is fetched through the tool."],
156+
"system_prompt": "Use health-dashboard tools for the user's health data. Do not infer medical causes from tool-free context.",
157+
"prompt": "Как я спал 2026-06-14?",
158+
"tools": [
159+
{
160+
"name": "health-dashboard__get_sleep_summary",
161+
"description": "Get a sleep summary for a specific date.",
162+
"input_schema": {
163+
"type": "object",
164+
"properties": {
165+
"date": {"type": "string"}
166+
},
167+
"required": ["date"]
168+
}
169+
}
170+
],
171+
"expect": {
172+
"tool_call": "health-dashboard__get_sleep_summary",
173+
"tool_args": {
174+
"date": "2026-06-14"
175+
},
176+
"max_tool_calls": 1
177+
}
178+
},
179+
{
180+
"id": "tool-file-document-search",
181+
"category": "tool_use",
182+
"purpose": "Checks file/document lookup routing for short prompts about remembered documents.",
183+
"limitations": ["Does not validate filesystem permissions or final document content."],
184+
"system_prompt": "Use filesystem search when the user asks to find something in their documents.",
185+
"prompt": "Посмотри в документах, где была заметка про Тулу.",
186+
"tools": [
187+
{
188+
"name": "fs_search",
189+
"description": "Search files under the assistant context root.",
190+
"input_schema": {
191+
"type": "object",
192+
"properties": {
193+
"query": {"type": "string"}
194+
},
195+
"required": ["query"]
196+
}
197+
}
198+
],
199+
"expect": {
200+
"tool_call": "fs_search",
201+
"max_tool_calls": 1
202+
}
203+
},
204+
{
205+
"id": "tool-web-freshness-prooflinks",
206+
"category": "tool_use",
207+
"purpose": "Checks web-search routing for freshness/prooflink prompts seen in news conversations.",
208+
"limitations": ["Does not validate final source quality."],
209+
"system_prompt": "Use web search for current events and proof links. Do not invent sources.",
210+
"prompt": "А что в Белграде произошло 2026-05-01? Дай пруфлинки.",
211+
"tools": [
212+
{
213+
"name": "web_search",
214+
"description": "Search the web for current or source-backed information.",
215+
"input_schema": {
216+
"type": "object",
217+
"properties": {
218+
"query": {"type": "string"}
219+
},
220+
"required": ["query"]
221+
}
222+
}
223+
],
224+
"expect": {
225+
"tool_call": "web_search",
226+
"max_tool_calls": 1
227+
}
228+
},
229+
{
230+
"id": "complex-model-routing-tradeoff",
231+
"category": "complex",
232+
"purpose": "Checks model-selection reasoning without fabricated benchmark numbers.",
233+
"limitations": ["Does not prove the recommendation is globally optimal."],
234+
"system_prompt": "Reply in Russian. Be explicit about uncertainty. Do not fabricate benchmark numbers.",
235+
"prompt": "Стоит ли полностью исключить free модели из роутинга, если часть из них внезапно становится платной или недоступной?",
236+
"expect": {
237+
"must_contain_any": [
238+
["не стоит", "не полностью", "оставить"],
239+
["провер", "healthcheck", "availability", "доступност"],
240+
["fallback", "резерв"],
241+
["стоимост", "цена", "paid", "платн"]
242+
],
243+
"must_not_contain": ["100%", "гарантированно всегда"],
244+
"no_tool_call": true,
245+
"max_chars": 1200
246+
}
247+
},
248+
{
249+
"id": "compaction-preserve-state",
63250
"category": "compaction",
64-
"system_prompt": "Summarize the conversation state for future turns. Preserve decisions, unresolved tasks, and concrete identifiers.",
65-
"prompt": "Сожми историю: решили вынести ARM docker build в manual workflow; сделали compact model catalog; free модели проверяем перед назначением; остался watchlist и eval pack. Нужно продолжить без потери контекста.",
251+
"purpose": "Checks that compaction preserves decisions, unresolved tasks, and caveats from a long working session.",
252+
"limitations": ["Does not test semantic clustering over real stored embeddings."],
253+
"system_prompt": "Summarize the conversation state for future turns. Preserve decisions, unresolved tasks, concrete identifiers, and safety caveats.",
254+
"prompt": "Сожми историю: PR #19 задеплоен; evals/workload.json теперь попадает в Docker image; бесплатные модели можно проверять вручную; paid eval только после подтверждения; нужно заменить синтетические evals на role-based suite из продовых паттернов; здоровье нельзя интерпретировать как диагноз без caveat.",
66255
"expect": {
67-
"must_contain": ["ARM", "watchlist", "eval"],
68-
"no_tool_call": true
256+
"must_contain_any": [
257+
["PR #19", "19"],
258+
["evals/workload.json", "workload"],
259+
["paid eval", "платн"],
260+
["role-based", "role based", "ролев"],
261+
["здоров", "диагноз", "caveat"]
262+
],
263+
"must_not_contain": ["уже заменили role-based suite"],
264+
"no_tool_call": true,
265+
"max_chars": 1000
69266
}
70267
},
71268
{
72-
"id": "admin-debug-next-step",
73-
"category": "admin_debug",
74-
"system_prompt": "Act as a pragmatic senior engineer. Give the next diagnostic step first.",
75-
"prompt": "После деплоя админка открывается, но список моделей пустой. Что проверишь первым?",
269+
"id": "multimodal-absent-image",
270+
"category": "multimodal_readiness",
271+
"purpose": "Checks that a model does not hallucinate screenshot contents when no image is attached.",
272+
"limitations": ["Actual image understanding requires a later multimodal eval runner."],
273+
"system_prompt": "Reply in Russian. If an image is not provided, do not claim to see it.",
274+
"prompt": "Посмотри фотку предыдущую и скажи, что на ней не так.",
76275
"expect": {
77-
"must_contain": ["capabilities"],
78-
"must_not_contain": ["перезапусти все"],
79-
"no_tool_call": true
276+
"must_contain_any": [["не вижу", "нет изображения", "не прикреплена", "нужна фот", "пришли"]],
277+
"must_not_contain": ["на фото видно", "я вижу"],
278+
"no_tool_call": true,
279+
"max_chars": 360
80280
}
81281
}
82282
]

internal/adminapi/templates/evals.html

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,21 @@ <h2 class="usage-section-h">Free model checks</h2>
140140
<div class="ops-pre">{{.SuiteErr}}</div>
141141
{{else}}
142142
<div class="ops-summary">
143-
<span class="ops-badge">{{.Suite.Name}}</span>
144-
<span class="ops-badge">version {{.Suite.Version}}</span>
145-
<span class="ops-badge">{{len .Suite.Cases}} cases</span>
146-
</div>
143+
<span class="ops-badge">{{.Suite.Name}}</span>
144+
<span class="ops-badge">version {{.Suite.Version}}</span>
145+
<span class="ops-badge">{{len .Suite.Cases}} cases</span>
146+
<span class="ops-badge">manual only</span>
147+
</div>
147148
{{range .Suite.Cases}}
148149
<details class="ops-detail">
149150
<summary><span class="model-id">{{.ID}}</span> · {{.Category}}</summary>
151+
{{if .Purpose}}<div style="margin-top:0.5rem;color:var(--text-secondary);font-size:0.82rem;">purpose</div><div class="ops-pre">{{.Purpose}}</div>{{end}}
152+
{{if .Limitations}}
153+
<div style="margin-top:0.5rem;color:var(--text-secondary);font-size:0.82rem;">limitations</div>
154+
<ul>
155+
{{range .Limitations}}<li>{{.}}</li>{{end}}
156+
</ul>
157+
{{end}}
150158
{{if .SystemPrompt}}<div style="margin-top:0.5rem;color:var(--text-secondary);font-size:0.82rem;">system prompt</div><div class="ops-pre">{{.SystemPrompt}}</div>{{end}}
151159
<div style="margin-top:0.5rem;color:var(--text-secondary);font-size:0.82rem;">prompt</div>
152160
<div class="ops-pre">{{.Prompt}}</div>
@@ -159,9 +167,13 @@ <h2 class="usage-section-h">Free model checks</h2>
159167
<div style="margin-top:0.5rem;color:var(--text-secondary);font-size:0.82rem;">expectations</div>
160168
<ul>
161169
{{range .Expect.MustContain}}<li>must contain <code>{{.}}</code></li>{{end}}
170+
{{range .Expect.MustContainAny}}<li>must contain one of {{range .}}<code>{{.}}</code> {{end}}</li>{{end}}
162171
{{range .Expect.MustNotContain}}<li>must not contain <code>{{.}}</code></li>{{end}}
163172
{{if .Expect.ToolCall}}<li>tool call <code>{{.Expect.ToolCall}}</code></li>{{end}}
173+
{{if .Expect.ToolArgs}}<li>tool args {{range $key, $value := .Expect.ToolArgs}}<code>{{$key}}={{$value}}</code> {{end}}</li>{{end}}
164174
{{if .Expect.NoToolCall}}<li>no tool call</li>{{end}}
175+
{{if .Expect.MaxToolCalls}}<li>max tool calls <code>{{.Expect.MaxToolCalls}}</code></li>{{end}}
176+
{{if .Expect.MaxChars}}<li>max chars <code>{{.Expect.MaxChars}}</code></li>{{end}}
165177
</ul>
166178
</details>
167179
{{end}}

0 commit comments

Comments
 (0)