Skip to content

Commit b3ced6a

Browse files
committed
docs: document eval matrix
1 parent 9c1eea8 commit b3ced6a

1 file changed

Lines changed: 292 additions & 0 deletions

File tree

packages/langium-ai-tools/README.md

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,298 @@ You can also define custom evaluators that are more tuned to the needs of your D
151151
152152
In general we stick to focusing on what Langium can do to help with evaluation, but leave the opportunity open for you to extend, supplement, or modify evaluation logic as you see fit.
153153
154+
### Evaluation Matrix
155+
156+
The Evaluation Matrix provides a framework for testing multiple model configurations against a set of test cases using Langium AI evaluators. This is particularly helpful when comparing across models, prompt strategies, RAG setups, or other variations in your AI stack.
157+
158+
In practice an evaluation matrix can be helpful when deciding between which models or services to use up front, but this can also be done externally by levering the evaluator directly yourself.
159+
160+
#### Overview
161+
162+
The evaluation matrix orchestrates three key components:
163+
- **Runners**: Functions that execute prompts against models, services, or other response generators
164+
- **Cases**: Test scenarios with expected outputs, which can be defined in code or loaded from YAML files
165+
- **Evaluators**: Metrics that score actual responses against expected responses
166+
167+
The matrix fires off each runner against each test case, evaluates the results with all configured evaluators, and produces reports with aggregated metrics.
168+
169+
#### Runners
170+
171+
A runner is an interface that takes a prompt and message history, returning a response string. This serves as an abstraction so you can test your own setup as it stands
172+
173+
Runners can wrap:
174+
- Direct model calls (Ollama, OpenAI, Anthropic, etc.)
175+
- RAG pipelines with vector database lookups
176+
- Multi-step agent workflows
177+
- Really any system that produces some text output
178+
179+
```ts
180+
import { type Runner, type Message } from 'langium-ai-tools/evaluator';
181+
182+
const myRunner: Runner = {
183+
name: 'my-model',
184+
runner: async (prompt: string, messages: Message[]) => {
185+
// call your model/service here
186+
const response = await myModel.generate(prompt, messages);
187+
return response;
188+
}
189+
};
190+
```
191+
192+
#### Cases
193+
194+
Test cases define the input and expected output for evaluation:
195+
196+
```ts
197+
import { type EvalCase } from 'langium-ai-tools/evaluator';
198+
199+
const testCase: EvalCase = {
200+
name: 'hello-world-grammar',
201+
prompt: 'Generate a Langium grammar for a simple hello world DSL',
202+
expected_response: `grammar HelloWorld
203+
entry Greeting: 'hello' name=ID;
204+
terminal ID: /[_a-zA-Z][\\w_]*/;`,
205+
// optional fields
206+
history: [{
207+
role: 'system',
208+
content: 'You are an expert in Langium grammars.'
209+
}],
210+
tags: ['grammar', 'beginner'],
211+
only_check_codeblocks: false
212+
};
213+
```
214+
215+
Cases can also be loaded from YAML files for easier management:
216+
217+
```yaml
218+
# eval-cases.yaml
219+
eval_cases:
220+
- name: "hello-world-grammar"
221+
prompt: "Generate a Langium grammar for a simple hello world DSL"
222+
expected_response: |
223+
grammar HelloWorld
224+
entry Greeting: 'hello' name=ID;
225+
terminal ID: /[_a-zA-Z][\w_]*/;
226+
tags:
227+
- "grammar"
228+
- "beginner"
229+
```
230+
231+
And can be loaded like so:
232+
233+
```ts
234+
import { loadFromYaml } from 'langium-ai-tools/evaluator';
235+
import { readFileSync } from 'fs';
236+
237+
const yamlContent = readFileSync('eval-cases.yaml', 'utf-8');
238+
const cases = loadFromYaml(yamlContent);
239+
```
240+
241+
##### Evaluators
242+
243+
Evaluators score responses against expected outputs. You can use built-in evaluators or create custom ones:
244+
245+
```ts
246+
import { LangiumEvaluator, mergeEvaluators } from 'langium-ai-tools/evaluator';
247+
import { createMyDSLServices } from './my-dsl';
248+
249+
const services = createMyDSLServices(EmptyFileSystem).MyDSL;
250+
251+
// use the built-in Langium evaluator
252+
const langiumEval = new LangiumEvaluator(services);
253+
254+
// or create a custom evaluator
255+
class CustomEvaluator extends Evaluator {
256+
async evaluate(response: string, expected: string): Promise<Partial<EvaluatorResult>> {
257+
return {
258+
data: {
259+
custom_metric: calculateMetric(response, expected)
260+
}
261+
};
262+
}
263+
}
264+
265+
// merge multiple evaluators
266+
const combinedEval = mergeEvaluators(
267+
langiumEval,
268+
new CustomEvaluator()
269+
);
270+
```
271+
272+
#### Setting Up an Evaluation Matrix
273+
274+
Create and run an evaluation matrix by combining your runners, cases, and evaluators:
275+
276+
```ts
277+
import { EvalMatrix } from 'langium-ai-tools/evaluator';
278+
279+
const matrix = new EvalMatrix({
280+
config: {
281+
name: 'Model Comparison',
282+
description: 'Comparing different models for DSL generation',
283+
history_folder: '.eval-history',
284+
num_runs: 3 // number of times to run each combination
285+
},
286+
runners: [
287+
myModelRunner,
288+
myModelWithRAGRunner,
289+
competitorModelRunner
290+
],
291+
evaluators: [
292+
{
293+
name: 'Langium Parser + Validation',
294+
eval: new LangiumEvaluator(services)
295+
},
296+
{
297+
name: 'Edit Distance',
298+
eval: new EditDistanceEvaluator()
299+
}
300+
],
301+
cases: testCases
302+
});
303+
304+
// run the matrix and get results
305+
const results = await matrix.run();
306+
```
307+
308+
#### Configuration Options
309+
310+
The `EvalMatrixConfig` provides several options:
311+
312+
- **name**: Descriptive name for this evaluation run
313+
- **description**: Longer description of what's being evaluated
314+
- **history_folder**: Directory where results will be saved as timestamped JSON files
315+
- **num_runs**: Number of times to run each runner-case-evaluator combination (for averaging)
316+
317+
#### Working with Results
318+
319+
The matrix produces detailed results that include:
320+
321+
```ts
322+
type EvaluatorResult = {
323+
// combined runner-case-evaluator name
324+
name: string;
325+
metadata: {
326+
runner: string;
327+
evaluator: string;
328+
testCase: EvalCase;
329+
actual_response: string;
330+
// runtime in seconds
331+
duration: number;
332+
run_count: number;
333+
};
334+
data: {
335+
// runtime in seconds
336+
runtime: number;
337+
// evaluator-specific metrics (errors, warnings, edit_distance, etc.)
338+
...
339+
};
340+
};
341+
```
342+
343+
Results are automatically saved to the history folder with timestamps. You can then process these results using utility functions:
344+
345+
```ts
346+
import {
347+
averageAcrossCases,
348+
averageAcrossRunners,
349+
loadLastResults
350+
} from 'langium-ai-tools/evaluator';
351+
352+
// average results across multiple runs of the same case
353+
const avgResults = averageAcrossCases(results);
354+
355+
// average across all cases for each runner
356+
const runnerAvgs = averageAcrossRunners(results);
357+
358+
// load results from previous runs
359+
const lastResults = loadLastResults('.eval-history');
360+
const last3Runs = loadLastResults('.eval-history', 3);
361+
362+
// display results
363+
console.table(avgResults.map(r => ({
364+
name: r.name,
365+
...r.data
366+
})));
367+
```
368+
369+
#### Complete Example
370+
371+
And here's a full example combining all the parts listed above:
372+
373+
```ts
374+
import {
375+
EvalMatrix,
376+
LangiumEvaluator,
377+
averageAcrossCases,
378+
type Runner,
379+
type EvalCase
380+
} from 'langium-ai-tools/evaluator';
381+
import { createMyDSLServices } from './my-dsl';
382+
import { EmptyFileSystem } from 'langium';
383+
import ollama from 'ollama';
384+
385+
// setup services
386+
const services = createMyDSLServices(EmptyFileSystem).MyDSL;
387+
388+
// define runners
389+
const baseRunner: Runner = {
390+
name: 'llama3.2-base',
391+
runner: async (prompt: string, messages: Message[]) => {
392+
const response = await ollama.chat({
393+
model: 'llama3.2',
394+
messages: [...messages, { role: 'user', content: prompt }]
395+
});
396+
return response.message.content;
397+
}
398+
};
399+
400+
// define test cases
401+
const cases: EvalCase[] = [
402+
{
403+
name: 'simple-grammar',
404+
prompt: 'Create a grammar for arithmetic expressions',
405+
expected_response: `grammar Arithmetic
406+
entry Expression: Addition;
407+
Addition: Multiplication ({infer BinaryExpression.left=current} operator=('+' | '-') right=Multiplication)*;
408+
Multiplication: Primary ({infer BinaryExpression.left=current} operator=('*' | '/') right=Primary)*;
409+
Primary: '(' Expression ')' | value=NUMBER;
410+
terminal NUMBER returns number: /[0-9]+/;`
411+
},
412+
// more cases...
413+
];
414+
415+
// create and run matrix
416+
const matrix = new EvalMatrix({
417+
config: {
418+
name: 'DSL Generation Test',
419+
description: 'Testing model performance on grammar generation',
420+
history_folder: '.eval-results',
421+
num_runs: 5
422+
},
423+
runners: [baseRunner],
424+
evaluators: [{
425+
name: 'Langium Evaluator',
426+
eval: new LangiumEvaluator(services)
427+
}],
428+
cases
429+
});
430+
431+
const results = await matrix.run();
432+
433+
// process and display results
434+
const averaged = averageAcrossCases(results);
435+
console.log('\nAveraged Results:');
436+
console.table(averaged.map(r => ({
437+
name: r.name,
438+
errors: r.data.errors,
439+
warnings: r.data.warnings,
440+
runtime: r.data.runtime
441+
})));
442+
```
443+
444+
For more complete examples, see the [example-dsl-evaluator](../examples/example-dsl-evaluator) project.
445+
154446
## Contributing
155447
156448
If you want to help feel free to open an issue or a PR. As a general note we're open to accept changes that focus on improving how we can support AI application development for Langium DSLs. But we don't want to provide explicit bindings to actual services/providers at this time, such as LLamaIndex, Ollama, LangChain, or others. Similarly this package doesn't provide direct bindings for AI providers such as OpenAI and Anthropic here. Instead these changes will go into a separate package under Langium AI that is intended for this purpose.

0 commit comments

Comments
 (0)