evalrun 0.4.0__py3-none-any.whl
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.
- agents/__init__.py +6 -0
- agents/auditor/__init__.py +13 -0
- agents/auditor/budget_auditor.py +91 -0
- agents/auditor/parser.py +139 -0
- agents/auditor/prompts.py +64 -0
- agents/auditor/schema.py +67 -0
- agents/base.py +20 -0
- agents/reflection/__init__.py +3 -0
- agents/reflection/agent.py +77 -0
- agents/reflection/prompts.py +18 -0
- agents/research/__init__.py +4 -0
- agents/research/agent.py +45 -0
- agents/research/planner.py +52 -0
- agents/research/prompts.py +14 -0
- agents/support/__init__.py +5 -0
- agents/support/triage_agent.py +45 -0
- agents/travel/__init__.py +11 -0
- agents/travel/agent.py +377 -0
- agents/travel/prompts.py +30 -0
- agents/travel/session.py +110 -0
- cli/__init__.py +6 -0
- cli/demo.py +47 -0
- cli/formatter.py +93 -0
- cli/html_reporter.py +647 -0
- cli/main.py +423 -0
- cli/progress.py +38 -0
- cli/resolver.py +99 -0
- evalrun-0.4.0.dist-info/METADATA +268 -0
- evalrun-0.4.0.dist-info/RECORD +100 -0
- evalrun-0.4.0.dist-info/WHEEL +5 -0
- evalrun-0.4.0.dist-info/entry_points.txt +2 -0
- evalrun-0.4.0.dist-info/licenses/LICENSE +0 -0
- evalrun-0.4.0.dist-info/top_level.txt +4 -0
- framework/__init__.py +70 -0
- framework/core/__init__.py +17 -0
- framework/core/adapters.py +118 -0
- framework/core/contracts.py +88 -0
- framework/core/suite.py +44 -0
- framework/evaluation/__init__.py +22 -0
- framework/evaluation/base.py +29 -0
- framework/evaluation/dimensions.py +7 -0
- framework/evaluation/engine.py +110 -0
- framework/evaluation/evaluators/__init__.py +7 -0
- framework/evaluation/evaluators/adaptability.py +27 -0
- framework/evaluation/evaluators/base_llm.py +104 -0
- framework/evaluation/evaluators/constraint.py +27 -0
- framework/evaluation/evaluators/information_accuracy.py +41 -0
- framework/evaluation/evaluators/personalization.py +27 -0
- framework/evaluation/evaluators/planning.py +27 -0
- framework/evaluation/evaluators/support.py +81 -0
- framework/evaluation/prompts/__init__.py +11 -0
- framework/evaluation/prompts/adaptability.py +57 -0
- framework/evaluation/prompts/base.py +52 -0
- framework/evaluation/prompts/constraint.py +41 -0
- framework/evaluation/prompts/information_accuracy.py +79 -0
- framework/evaluation/prompts/personalization.py +57 -0
- framework/evaluation/prompts/planning.py +61 -0
- framework/evaluation/runner.py +363 -0
- framework/evaluation/testing.py +25 -0
- framework/exceptions.py +49 -0
- framework/llms/__init__.py +8 -0
- framework/llms/base.py +37 -0
- framework/llms/factory.py +38 -0
- framework/llms/gemini.py +85 -0
- framework/llms/mock.py +25 -0
- framework/llms/openai.py +94 -0
- framework/llms/openai_compatible.py +139 -0
- framework/mcp/__init__.py +20 -0
- framework/mcp/client.py +62 -0
- framework/mcp/constraints.py +125 -0
- framework/mcp/revision_summary.py +122 -0
- framework/mcp/server.py +49 -0
- framework/memory/__init__.py +3 -0
- framework/memory/base.py +17 -0
- framework/models.py +83 -0
- framework/parser.py +45 -0
- framework/parsers/__init__.py +12 -0
- framework/parsers/frontmatter.py +28 -0
- framework/parsers/mapper.py +66 -0
- framework/parsers/markdown.py +122 -0
- framework/parsers/transformers.py +112 -0
- framework/profiles/__init__.py +28 -0
- framework/profiles/registry.py +104 -0
- framework/profiles/support.py +27 -0
- framework/profiles/travel.py +78 -0
- framework/regression/__init__.py +17 -0
- framework/regression/comparator.py +273 -0
- framework/regression/loader.py +145 -0
- framework/sdk.py +151 -0
- framework/utils.py +41 -0
- framework/verification/__init__.py +13 -0
- framework/verification/base.py +32 -0
- framework/verification/extractor.py +105 -0
- framework/verification/local.py +122 -0
- framework/verification/models.py +112 -0
- framework/verification/pipeline.py +36 -0
- framework/verification/prompts.py +24 -0
- framework/verification/utils.py +54 -0
- ui/__init__.py +1 -0
- ui/server.py +255 -0
framework/models.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Benchmark:
|
|
7
|
+
"""Represents a single evaluation scenario.
|
|
8
|
+
|
|
9
|
+
Stores the parsed details of the benchmark scenario (e.g., from markdown).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
benchmark_id: str
|
|
13
|
+
name: str
|
|
14
|
+
description: str
|
|
15
|
+
prompt: str
|
|
16
|
+
constraints: Dict[str, Any]
|
|
17
|
+
expected_behavior: List[str]
|
|
18
|
+
evaluation_criteria: Dict[str, List[str]]
|
|
19
|
+
pass_criteria: List[str]
|
|
20
|
+
failure_conditions: List[str]
|
|
21
|
+
notes: List[str]
|
|
22
|
+
profile: str = "travel-agent"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class AgentOutput:
|
|
27
|
+
"""Represents the raw response/output produced by an AI agent."""
|
|
28
|
+
|
|
29
|
+
content: Any
|
|
30
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class DimensionScore:
|
|
35
|
+
"""Stores the score and justification for a single evaluation dimension."""
|
|
36
|
+
|
|
37
|
+
dimension: str
|
|
38
|
+
score: float
|
|
39
|
+
reason: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class EvaluationResult:
|
|
44
|
+
"""Represents the final evaluation outcome for a benchmark scenario."""
|
|
45
|
+
|
|
46
|
+
benchmark_id: str
|
|
47
|
+
benchmark_name: str
|
|
48
|
+
overall_score: float
|
|
49
|
+
dimension_scores: List[DimensionScore]
|
|
50
|
+
passed: bool
|
|
51
|
+
agent_metadata: Dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class EvaluationProfile:
|
|
56
|
+
"""Defines the name and dimension weights for a specific evaluation domain."""
|
|
57
|
+
|
|
58
|
+
name: str
|
|
59
|
+
weights: Dict[str, float]
|
|
60
|
+
pass_threshold: float = 75.0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class ParsedSections:
|
|
65
|
+
"""Represents the raw, un-transformed markdown sections parsed from a file."""
|
|
66
|
+
|
|
67
|
+
description: str
|
|
68
|
+
prompt: str
|
|
69
|
+
constraints: str
|
|
70
|
+
expected_behavior: str
|
|
71
|
+
evaluation_criteria: str
|
|
72
|
+
pass_criteria: str
|
|
73
|
+
failure_conditions: str
|
|
74
|
+
notes: Optional[str] = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ParsedBenchmark:
|
|
79
|
+
"""Combines raw metadata and raw parsed sections before transformation."""
|
|
80
|
+
|
|
81
|
+
metadata: Dict[str, Any]
|
|
82
|
+
sections: ParsedSections
|
|
83
|
+
|
framework/parser.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Public API for parsing benchmark scenarios."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Union
|
|
5
|
+
from framework.models import Benchmark, ParsedBenchmark
|
|
6
|
+
from framework.parsers import parse_frontmatter, parse_sections, map_to_benchmark
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_benchmark_text(text: str) -> Benchmark:
|
|
10
|
+
"""Parses raw markdown text and maps it to a Benchmark dataclass.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
text: The raw markdown text containing YAML frontmatter.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
A Benchmark dataclass instance.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
FrontmatterParseError: If YAML frontmatter parsing fails.
|
|
20
|
+
MarkdownParseError: If markdown section parsing fails.
|
|
21
|
+
MapperError: If mapping parsed content to Benchmark fails.
|
|
22
|
+
"""
|
|
23
|
+
metadata, body = parse_frontmatter(text)
|
|
24
|
+
parsed_sections = parse_sections(body)
|
|
25
|
+
parsed_benchmark = ParsedBenchmark(metadata=metadata, sections=parsed_sections)
|
|
26
|
+
return map_to_benchmark(parsed_benchmark)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_benchmark(path: Union[str, Path]) -> Benchmark:
|
|
30
|
+
"""Parses a markdown benchmark file on disk and maps it to a Benchmark dataclass.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
path: Path to the markdown file (string or pathlib.Path).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A Benchmark dataclass instance.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
FileNotFoundError: If the file does not exist.
|
|
40
|
+
FrontmatterParseError: If YAML frontmatter parsing fails.
|
|
41
|
+
MarkdownParseError: If markdown section parsing fails.
|
|
42
|
+
MapperError: If mapping parsed content to Benchmark fails.
|
|
43
|
+
"""
|
|
44
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
45
|
+
return parse_benchmark_text(text)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Package containing evaluation scenario parsers."""
|
|
2
|
+
|
|
3
|
+
from framework.parsers.frontmatter import parse_frontmatter
|
|
4
|
+
from framework.parsers.markdown import parse_sections
|
|
5
|
+
from framework.parsers.mapper import map_to_benchmark
|
|
6
|
+
from framework.parsers.transformers import parse_list, parse_nested_lists, parse_yaml_block
|
|
7
|
+
from framework.exceptions import (
|
|
8
|
+
FrontmatterParseError,
|
|
9
|
+
MarkdownParseError,
|
|
10
|
+
TransformerError,
|
|
11
|
+
MapperError,
|
|
12
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Module for parsing YAML frontmatter from markdown documents."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Tuple
|
|
4
|
+
import frontmatter
|
|
5
|
+
from framework.exceptions import FrontmatterParseError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_frontmatter(content: str) -> Tuple[Dict[str, Any], str]:
|
|
9
|
+
"""Parses YAML frontmatter from a markdown string.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
content: The raw markdown content, which may contain YAML frontmatter.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
A tuple of (metadata, remaining_content) where:
|
|
16
|
+
- metadata is a dictionary containing the parsed frontmatter fields.
|
|
17
|
+
- remaining_content is the markdown body without the frontmatter block.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
FrontmatterParseError: If frontmatter parsing fails due to malformed YAML.
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
post = frontmatter.loads(content)
|
|
24
|
+
return post.metadata, post.content
|
|
25
|
+
except Exception as e:
|
|
26
|
+
raise FrontmatterParseError(
|
|
27
|
+
f"Failed to parse YAML frontmatter: {e}"
|
|
28
|
+
) from e
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Module for mapping parsed frontmatter and sections to a Benchmark dataclass."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
from framework.exceptions import MapperError
|
|
5
|
+
from framework.models import Benchmark, ParsedBenchmark
|
|
6
|
+
from framework.parsers.transformers import parse_list, parse_nested_lists, parse_yaml_block
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def map_to_benchmark(parsed_benchmark: ParsedBenchmark) -> Benchmark:
|
|
10
|
+
"""Maps a ParsedBenchmark (metadata + raw sections) to a Benchmark dataclass.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
parsed_benchmark: The raw parsed benchmark components.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
A Benchmark dataclass instance.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
MapperError: If required metadata is missing or if transformations fail.
|
|
20
|
+
"""
|
|
21
|
+
metadata = parsed_benchmark.metadata
|
|
22
|
+
sections = parsed_benchmark.sections
|
|
23
|
+
|
|
24
|
+
# 1. Validate and extract metadata
|
|
25
|
+
benchmark_id = metadata.get("benchmark_id")
|
|
26
|
+
if not benchmark_id:
|
|
27
|
+
raise MapperError("Metadata is missing required field: 'benchmark_id'")
|
|
28
|
+
if not isinstance(benchmark_id, str):
|
|
29
|
+
raise MapperError("'benchmark_id' must be a string")
|
|
30
|
+
|
|
31
|
+
name = metadata.get("name")
|
|
32
|
+
if not name:
|
|
33
|
+
raise MapperError("Metadata is missing required field: 'name'")
|
|
34
|
+
if not isinstance(name, str):
|
|
35
|
+
raise MapperError("'name' must be a string")
|
|
36
|
+
|
|
37
|
+
# 2. Map and transform sections using the transformers module
|
|
38
|
+
description = sections.description.strip()
|
|
39
|
+
prompt = sections.prompt.strip()
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
constraints = parse_yaml_block(sections.constraints)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
raise MapperError(f"Failed to parse constraints YAML: {e}") from e
|
|
45
|
+
|
|
46
|
+
expected_behavior = parse_list(sections.expected_behavior)
|
|
47
|
+
evaluation_criteria = parse_nested_lists(sections.evaluation_criteria)
|
|
48
|
+
pass_criteria = parse_list(sections.pass_criteria)
|
|
49
|
+
failure_conditions = parse_list(sections.failure_conditions)
|
|
50
|
+
|
|
51
|
+
notes = parse_list(sections.notes) if sections.notes else []
|
|
52
|
+
profile = metadata.get("profile", "travel-agent")
|
|
53
|
+
|
|
54
|
+
return Benchmark(
|
|
55
|
+
benchmark_id=benchmark_id,
|
|
56
|
+
name=name,
|
|
57
|
+
description=description,
|
|
58
|
+
prompt=prompt,
|
|
59
|
+
constraints=constraints,
|
|
60
|
+
expected_behavior=expected_behavior,
|
|
61
|
+
evaluation_criteria=evaluation_criteria,
|
|
62
|
+
pass_criteria=pass_criteria,
|
|
63
|
+
failure_conditions=failure_conditions,
|
|
64
|
+
notes=notes,
|
|
65
|
+
profile=profile,
|
|
66
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Module for parsing sections from markdown bodies."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
# pyrefly: ignore [missing-import]
|
|
6
|
+
from markdown_it import MarkdownIt
|
|
7
|
+
from framework.exceptions import MarkdownParseError
|
|
8
|
+
from framework.models import ParsedSections
|
|
9
|
+
|
|
10
|
+
# Single shared parser instance at the module level to avoid repeated construction
|
|
11
|
+
_MD = MarkdownIt()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Heading:
|
|
16
|
+
"""Represents a heading parsed from markdown."""
|
|
17
|
+
|
|
18
|
+
level: int
|
|
19
|
+
text: str
|
|
20
|
+
start_line: int
|
|
21
|
+
end_line: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _require_section(sections: Dict[str, str], name: str) -> str:
|
|
25
|
+
"""Ensures a required section is present in sections dictionary.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
sections: Dictionary of headings to raw contents.
|
|
29
|
+
name: Name of the required section.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The content of the required section.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
MarkdownParseError: If the section is missing.
|
|
36
|
+
"""
|
|
37
|
+
if name not in sections:
|
|
38
|
+
raise MarkdownParseError(f"Missing required section: '{name}'")
|
|
39
|
+
return sections[name]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_sections(content: str) -> ParsedSections:
|
|
43
|
+
"""Splits a markdown document body into sections and returns ParsedSections.
|
|
44
|
+
|
|
45
|
+
Automatically detects the highest heading level (H1, H2, etc.) present in
|
|
46
|
+
the document and splits the text into sections using those headings.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
content: The raw markdown content body (without frontmatter).
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
A ParsedSections dataclass containing raw markdown text for each section.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
MarkdownParseError: If any required section is missing.
|
|
56
|
+
"""
|
|
57
|
+
tokens = _MD.parse(content)
|
|
58
|
+
lines = content.splitlines()
|
|
59
|
+
|
|
60
|
+
# Gather all headings and their line offsets using a simple stateful pass
|
|
61
|
+
headings: List[Heading] = []
|
|
62
|
+
current_heading: Optional[Heading] = None
|
|
63
|
+
|
|
64
|
+
for token in tokens:
|
|
65
|
+
if token.type == "heading_open":
|
|
66
|
+
if not token.map:
|
|
67
|
+
continue
|
|
68
|
+
current_heading = Heading(
|
|
69
|
+
level=int(token.tag[1]),
|
|
70
|
+
text="",
|
|
71
|
+
start_line=token.map[0],
|
|
72
|
+
end_line=token.map[1],
|
|
73
|
+
)
|
|
74
|
+
elif token.type == "inline" and current_heading is not None:
|
|
75
|
+
current_heading.text += token.content
|
|
76
|
+
elif token.type == "heading_close" and current_heading is not None:
|
|
77
|
+
# We match the tag (e.g. h1, h2) to ensure correct nesting closure
|
|
78
|
+
if token.tag == f"h{current_heading.level}":
|
|
79
|
+
headings.append(current_heading)
|
|
80
|
+
current_heading = None
|
|
81
|
+
|
|
82
|
+
if not headings:
|
|
83
|
+
raise MarkdownParseError("No headings found in the markdown body")
|
|
84
|
+
|
|
85
|
+
# Identify the primary (highest-level) headings to use as section splitters
|
|
86
|
+
primary_level = min(h.level for h in headings)
|
|
87
|
+
primary_headings = [h for h in headings if h.level == primary_level]
|
|
88
|
+
|
|
89
|
+
sections: Dict[str, str] = {}
|
|
90
|
+
for idx, heading in enumerate(primary_headings):
|
|
91
|
+
# The section content starts immediately after the heading
|
|
92
|
+
content_start = heading.end_line
|
|
93
|
+
|
|
94
|
+
# The section content ends where the next primary heading begins
|
|
95
|
+
if idx + 1 < len(primary_headings):
|
|
96
|
+
content_end = primary_headings[idx + 1].start_line
|
|
97
|
+
else:
|
|
98
|
+
content_end = len(lines)
|
|
99
|
+
|
|
100
|
+
section_content = "\n".join(lines[content_start:content_end]).strip()
|
|
101
|
+
sections[heading.text] = section_content
|
|
102
|
+
|
|
103
|
+
# Validate and map to ParsedSections dataclass
|
|
104
|
+
description = _require_section(sections, "Description")
|
|
105
|
+
prompt = _require_section(sections, "User Prompt")
|
|
106
|
+
constraints = _require_section(sections, "Extracted Constraints")
|
|
107
|
+
expected_behavior = _require_section(sections, "Expected Behaviour")
|
|
108
|
+
evaluation_criteria = _require_section(sections, "Evaluation Criteria")
|
|
109
|
+
pass_criteria = _require_section(sections, "Pass Criteria")
|
|
110
|
+
failure_conditions = _require_section(sections, "Failure Conditions")
|
|
111
|
+
notes = sections.get("Notes")
|
|
112
|
+
|
|
113
|
+
return ParsedSections(
|
|
114
|
+
description=description,
|
|
115
|
+
prompt=prompt,
|
|
116
|
+
constraints=constraints,
|
|
117
|
+
expected_behavior=expected_behavior,
|
|
118
|
+
evaluation_criteria=evaluation_criteria,
|
|
119
|
+
pass_criteria=pass_criteria,
|
|
120
|
+
failure_conditions=failure_conditions,
|
|
121
|
+
notes=notes,
|
|
122
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Module for transforming raw markdown content into python structures."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
import yaml
|
|
5
|
+
# pyrefly: ignore [missing-import]
|
|
6
|
+
from markdown_it import MarkdownIt
|
|
7
|
+
from framework.exceptions import TransformerError
|
|
8
|
+
|
|
9
|
+
# Single shared parser instance at the module level to avoid repeated construction
|
|
10
|
+
_MD = MarkdownIt()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_list(content: str) -> List[str]:
|
|
14
|
+
"""Parses all bulleted or numbered list items in markdown content.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
content: The raw markdown content of the list section.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
A list of strings representing the text of each list item.
|
|
21
|
+
"""
|
|
22
|
+
tokens = _MD.parse(content)
|
|
23
|
+
items: List[str] = []
|
|
24
|
+
current_item: List[str] = []
|
|
25
|
+
in_item = False
|
|
26
|
+
|
|
27
|
+
for token in tokens:
|
|
28
|
+
if token.type == "list_item_open":
|
|
29
|
+
in_item = True
|
|
30
|
+
current_item = []
|
|
31
|
+
elif token.type == "inline" and in_item:
|
|
32
|
+
current_item.append(token.content)
|
|
33
|
+
elif token.type == "list_item_close" and in_item:
|
|
34
|
+
items.append("".join(current_item).strip())
|
|
35
|
+
in_item = False
|
|
36
|
+
|
|
37
|
+
return items
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_nested_lists(content: str) -> Dict[str, List[str]]:
|
|
41
|
+
"""Parses subheadings and the list items directly beneath them.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
content: The raw markdown containing headings and list items.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A dictionary mapping each subheading's text to its list items.
|
|
48
|
+
"""
|
|
49
|
+
tokens = _MD.parse(content)
|
|
50
|
+
result: Dict[str, List[str]] = {}
|
|
51
|
+
|
|
52
|
+
active_heading: List[str] = []
|
|
53
|
+
in_heading = False
|
|
54
|
+
active_item: List[str] = []
|
|
55
|
+
in_item = False
|
|
56
|
+
current_list: List[str] = []
|
|
57
|
+
|
|
58
|
+
for token in tokens:
|
|
59
|
+
if token.type == "heading_open":
|
|
60
|
+
# Save the previous heading and its list items if we had one
|
|
61
|
+
if active_heading:
|
|
62
|
+
result["".join(active_heading).strip()] = current_list
|
|
63
|
+
active_heading = []
|
|
64
|
+
in_heading = True
|
|
65
|
+
current_list = []
|
|
66
|
+
elif token.type == "inline" and in_heading:
|
|
67
|
+
active_heading.append(token.content)
|
|
68
|
+
elif token.type == "heading_close":
|
|
69
|
+
in_heading = False
|
|
70
|
+
elif token.type == "list_item_open" and active_heading:
|
|
71
|
+
in_item = True
|
|
72
|
+
active_item = []
|
|
73
|
+
elif token.type == "inline" and in_item:
|
|
74
|
+
active_item.append(token.content)
|
|
75
|
+
elif token.type == "list_item_close" and in_item:
|
|
76
|
+
current_list.append("".join(active_item).strip())
|
|
77
|
+
in_item = False
|
|
78
|
+
|
|
79
|
+
# Save the trailing heading
|
|
80
|
+
if active_heading:
|
|
81
|
+
result["".join(active_heading).strip()] = current_list
|
|
82
|
+
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_yaml_block(content: str) -> Dict[str, Any]:
|
|
87
|
+
"""Finds and parses the first YAML code fence block in the markdown content.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
content: The raw markdown content.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A dictionary representation of the parsed YAML content.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
TransformerError: If YAML parsing fails.
|
|
97
|
+
"""
|
|
98
|
+
tokens = _MD.parse(content)
|
|
99
|
+
|
|
100
|
+
for token in tokens:
|
|
101
|
+
if token.type == "fence" and token.info == "yaml":
|
|
102
|
+
try:
|
|
103
|
+
result = yaml.safe_load(token.content)
|
|
104
|
+
if result is None:
|
|
105
|
+
return {}
|
|
106
|
+
if not isinstance(result, dict):
|
|
107
|
+
raise TransformerError("YAML block content must represent a dictionary")
|
|
108
|
+
return result
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise TransformerError(f"Failed to parse YAML block: {e}") from e
|
|
111
|
+
|
|
112
|
+
return {}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Evaluation profiles package."""
|
|
2
|
+
|
|
3
|
+
from framework.profiles.travel import (
|
|
4
|
+
TRAVEL_PROFILE,
|
|
5
|
+
TRAVEL_ROUTE_OPTIMIZATION_PROFILE,
|
|
6
|
+
TRAVEL_REMOTE_WORKER_TIMEZONES_PROFILE,
|
|
7
|
+
TRAVEL_MID_TRIP_REPLANNING_PROFILE,
|
|
8
|
+
TRAVEL_INFORMATION_GATHERING_UNCERTAINTY_PROFILE,
|
|
9
|
+
PROFILE_REGISTRY,
|
|
10
|
+
)
|
|
11
|
+
from framework.profiles.support import SUPPORT_TRIAGE_PROFILE
|
|
12
|
+
|
|
13
|
+
# Keep one lookup used by BenchmarkRunner while allowing each domain to own
|
|
14
|
+
# its profile definition.
|
|
15
|
+
PROFILE_REGISTRY = {
|
|
16
|
+
**PROFILE_REGISTRY,
|
|
17
|
+
SUPPORT_TRIAGE_PROFILE.name: SUPPORT_TRIAGE_PROFILE,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"TRAVEL_PROFILE",
|
|
22
|
+
"TRAVEL_ROUTE_OPTIMIZATION_PROFILE",
|
|
23
|
+
"TRAVEL_REMOTE_WORKER_TIMEZONES_PROFILE",
|
|
24
|
+
"TRAVEL_MID_TRIP_REPLANNING_PROFILE",
|
|
25
|
+
"TRAVEL_INFORMATION_GATHERING_UNCERTAINTY_PROFILE",
|
|
26
|
+
"SUPPORT_TRIAGE_PROFILE",
|
|
27
|
+
"PROFILE_REGISTRY",
|
|
28
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Custom Evaluation Profiles and Evaluators Plugin Registry."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
from framework.evaluation.base import BaseEvaluator
|
|
8
|
+
from framework.models import EvaluationProfile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_DYNAMIC_PROFILE_REGISTRY: Dict[str, EvaluationProfile] = {}
|
|
12
|
+
_DYNAMIC_EVALUATOR_REGISTRY: Dict[str, BaseEvaluator] = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_profile(name: str, profile: EvaluationProfile) -> None:
|
|
16
|
+
"""Registers a custom evaluation profile into the global profile registry."""
|
|
17
|
+
_DYNAMIC_PROFILE_REGISTRY[name.lower()] = profile
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_custom_profile(name: str) -> Optional[EvaluationProfile]:
|
|
21
|
+
"""Retrieves a registered profile by name."""
|
|
22
|
+
return _DYNAMIC_PROFILE_REGISTRY.get(name.lower())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def register_evaluator_plugin(dimension_name: str, evaluator: BaseEvaluator) -> None:
|
|
26
|
+
"""Registers a custom evaluator plugin for a dimension name."""
|
|
27
|
+
_DYNAMIC_EVALUATOR_REGISTRY[dimension_name] = evaluator
|
|
28
|
+
_DYNAMIC_EVALUATOR_REGISTRY[dimension_name.lower()] = evaluator
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_evaluator_plugin(dimension_name: str) -> Optional[BaseEvaluator]:
|
|
32
|
+
"""Retrieves a registered custom evaluator plugin by dimension name."""
|
|
33
|
+
return _DYNAMIC_EVALUATOR_REGISTRY.get(dimension_name) or _DYNAMIC_EVALUATOR_REGISTRY.get(dimension_name.lower())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_evaluator_plugin_from_spec(spec: str, judge_llm: Any = None) -> BaseEvaluator:
|
|
37
|
+
"""Dynamically loads and instantiates a custom evaluator plugin from a specifier.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
spec: Import specifier string ('module:Class' or 'module:factory').
|
|
41
|
+
judge_llm: Optional judge LLM instance to pass to the evaluator constructor.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Instantiated BaseEvaluator.
|
|
45
|
+
"""
|
|
46
|
+
if ":" not in spec:
|
|
47
|
+
raise ValueError(f"Invalid evaluator specifier '{spec}'. Expected 'module:Class' or 'module:factory'.")
|
|
48
|
+
|
|
49
|
+
mod_name, attr_name = spec.split(":", 1)
|
|
50
|
+
module = importlib.import_module(mod_name)
|
|
51
|
+
target = getattr(module, attr_name)
|
|
52
|
+
|
|
53
|
+
if inspect_is_class(target):
|
|
54
|
+
try:
|
|
55
|
+
return target(llm=judge_llm)
|
|
56
|
+
except TypeError:
|
|
57
|
+
try:
|
|
58
|
+
return target(judge_llm=judge_llm)
|
|
59
|
+
except TypeError:
|
|
60
|
+
return target()
|
|
61
|
+
elif callable(target):
|
|
62
|
+
try:
|
|
63
|
+
return target(llm=judge_llm)
|
|
64
|
+
except TypeError:
|
|
65
|
+
return target()
|
|
66
|
+
else:
|
|
67
|
+
raise ValueError(f"Evaluator specifier '{spec}' resolved to non-callable object.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def inspect_is_class(obj: Any) -> bool:
|
|
71
|
+
import inspect
|
|
72
|
+
return inspect.isclass(obj)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def load_profile_from_file(filepath: str) -> EvaluationProfile:
|
|
76
|
+
"""Loads an EvaluationProfile from a JSON configuration file."""
|
|
77
|
+
path = Path(filepath)
|
|
78
|
+
if not path.exists():
|
|
79
|
+
raise FileNotFoundError(f"Custom profile file '{filepath}' not found.")
|
|
80
|
+
|
|
81
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
82
|
+
data = json.load(f)
|
|
83
|
+
|
|
84
|
+
profile_id = data.get("profile_id") or path.stem
|
|
85
|
+
name = data.get("name", profile_id)
|
|
86
|
+
pass_threshold = float(data.get("pass_threshold", 75.0))
|
|
87
|
+
|
|
88
|
+
raw_weights = data.get("dimension_weights") or data.get("weights") or {}
|
|
89
|
+
weights: Dict[str, float] = {}
|
|
90
|
+
if isinstance(raw_weights, list):
|
|
91
|
+
for item in raw_weights:
|
|
92
|
+
if isinstance(item, dict) and "dimension" in item:
|
|
93
|
+
weights[item["dimension"]] = float(item.get("weight", 1.0))
|
|
94
|
+
elif isinstance(raw_weights, dict):
|
|
95
|
+
weights = {str(k): float(v) for k, v in raw_weights.items()}
|
|
96
|
+
|
|
97
|
+
profile = EvaluationProfile(
|
|
98
|
+
name=name,
|
|
99
|
+
weights=weights,
|
|
100
|
+
pass_threshold=pass_threshold,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
register_profile(profile_id, profile)
|
|
104
|
+
return profile
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Evaluation profile for the minimal support-triage benchmark."""
|
|
2
|
+
|
|
3
|
+
from framework.evaluation.dimensions import (
|
|
4
|
+
ADAPTABILITY,
|
|
5
|
+
CONSTRAINT_SATISFACTION,
|
|
6
|
+
INFORMATION_ACCURACY,
|
|
7
|
+
PERSONALIZATION,
|
|
8
|
+
PLANNING_QUALITY,
|
|
9
|
+
)
|
|
10
|
+
from framework.models import EvaluationProfile
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# The existing dimensions are intentionally reused for this small proof of
|
|
14
|
+
# generalization. Their support interpretation is documented in the scenario:
|
|
15
|
+
# constraints=SLA/policy, planning=triage actions, accuracy=category/facts,
|
|
16
|
+
# personalization=tone, adaptability=escalation judgment.
|
|
17
|
+
SUPPORT_TRIAGE_PROFILE = EvaluationProfile(
|
|
18
|
+
name="support-triage",
|
|
19
|
+
weights={
|
|
20
|
+
CONSTRAINT_SATISFACTION: 30.0,
|
|
21
|
+
INFORMATION_ACCURACY: 25.0,
|
|
22
|
+
PLANNING_QUALITY: 20.0,
|
|
23
|
+
ADAPTABILITY: 15.0,
|
|
24
|
+
PERSONALIZATION: 10.0,
|
|
25
|
+
},
|
|
26
|
+
pass_threshold=75.0,
|
|
27
|
+
)
|