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.
Files changed (100) hide show
  1. agents/__init__.py +6 -0
  2. agents/auditor/__init__.py +13 -0
  3. agents/auditor/budget_auditor.py +91 -0
  4. agents/auditor/parser.py +139 -0
  5. agents/auditor/prompts.py +64 -0
  6. agents/auditor/schema.py +67 -0
  7. agents/base.py +20 -0
  8. agents/reflection/__init__.py +3 -0
  9. agents/reflection/agent.py +77 -0
  10. agents/reflection/prompts.py +18 -0
  11. agents/research/__init__.py +4 -0
  12. agents/research/agent.py +45 -0
  13. agents/research/planner.py +52 -0
  14. agents/research/prompts.py +14 -0
  15. agents/support/__init__.py +5 -0
  16. agents/support/triage_agent.py +45 -0
  17. agents/travel/__init__.py +11 -0
  18. agents/travel/agent.py +377 -0
  19. agents/travel/prompts.py +30 -0
  20. agents/travel/session.py +110 -0
  21. cli/__init__.py +6 -0
  22. cli/demo.py +47 -0
  23. cli/formatter.py +93 -0
  24. cli/html_reporter.py +647 -0
  25. cli/main.py +423 -0
  26. cli/progress.py +38 -0
  27. cli/resolver.py +99 -0
  28. evalrun-0.4.0.dist-info/METADATA +268 -0
  29. evalrun-0.4.0.dist-info/RECORD +100 -0
  30. evalrun-0.4.0.dist-info/WHEEL +5 -0
  31. evalrun-0.4.0.dist-info/entry_points.txt +2 -0
  32. evalrun-0.4.0.dist-info/licenses/LICENSE +0 -0
  33. evalrun-0.4.0.dist-info/top_level.txt +4 -0
  34. framework/__init__.py +70 -0
  35. framework/core/__init__.py +17 -0
  36. framework/core/adapters.py +118 -0
  37. framework/core/contracts.py +88 -0
  38. framework/core/suite.py +44 -0
  39. framework/evaluation/__init__.py +22 -0
  40. framework/evaluation/base.py +29 -0
  41. framework/evaluation/dimensions.py +7 -0
  42. framework/evaluation/engine.py +110 -0
  43. framework/evaluation/evaluators/__init__.py +7 -0
  44. framework/evaluation/evaluators/adaptability.py +27 -0
  45. framework/evaluation/evaluators/base_llm.py +104 -0
  46. framework/evaluation/evaluators/constraint.py +27 -0
  47. framework/evaluation/evaluators/information_accuracy.py +41 -0
  48. framework/evaluation/evaluators/personalization.py +27 -0
  49. framework/evaluation/evaluators/planning.py +27 -0
  50. framework/evaluation/evaluators/support.py +81 -0
  51. framework/evaluation/prompts/__init__.py +11 -0
  52. framework/evaluation/prompts/adaptability.py +57 -0
  53. framework/evaluation/prompts/base.py +52 -0
  54. framework/evaluation/prompts/constraint.py +41 -0
  55. framework/evaluation/prompts/information_accuracy.py +79 -0
  56. framework/evaluation/prompts/personalization.py +57 -0
  57. framework/evaluation/prompts/planning.py +61 -0
  58. framework/evaluation/runner.py +363 -0
  59. framework/evaluation/testing.py +25 -0
  60. framework/exceptions.py +49 -0
  61. framework/llms/__init__.py +8 -0
  62. framework/llms/base.py +37 -0
  63. framework/llms/factory.py +38 -0
  64. framework/llms/gemini.py +85 -0
  65. framework/llms/mock.py +25 -0
  66. framework/llms/openai.py +94 -0
  67. framework/llms/openai_compatible.py +139 -0
  68. framework/mcp/__init__.py +20 -0
  69. framework/mcp/client.py +62 -0
  70. framework/mcp/constraints.py +125 -0
  71. framework/mcp/revision_summary.py +122 -0
  72. framework/mcp/server.py +49 -0
  73. framework/memory/__init__.py +3 -0
  74. framework/memory/base.py +17 -0
  75. framework/models.py +83 -0
  76. framework/parser.py +45 -0
  77. framework/parsers/__init__.py +12 -0
  78. framework/parsers/frontmatter.py +28 -0
  79. framework/parsers/mapper.py +66 -0
  80. framework/parsers/markdown.py +122 -0
  81. framework/parsers/transformers.py +112 -0
  82. framework/profiles/__init__.py +28 -0
  83. framework/profiles/registry.py +104 -0
  84. framework/profiles/support.py +27 -0
  85. framework/profiles/travel.py +78 -0
  86. framework/regression/__init__.py +17 -0
  87. framework/regression/comparator.py +273 -0
  88. framework/regression/loader.py +145 -0
  89. framework/sdk.py +151 -0
  90. framework/utils.py +41 -0
  91. framework/verification/__init__.py +13 -0
  92. framework/verification/base.py +32 -0
  93. framework/verification/extractor.py +105 -0
  94. framework/verification/local.py +122 -0
  95. framework/verification/models.py +112 -0
  96. framework/verification/pipeline.py +36 -0
  97. framework/verification/prompts.py +24 -0
  98. framework/verification/utils.py +54 -0
  99. ui/__init__.py +1 -0
  100. 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
+ )