k-eval 0.3.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 (74) hide show
  1. k_eval/__init__.py +0 -0
  2. k_eval/agent/__init__.py +0 -0
  3. k_eval/agent/domain/__init__.py +0 -0
  4. k_eval/agent/domain/agent.py +14 -0
  5. k_eval/agent/domain/factory.py +18 -0
  6. k_eval/agent/domain/observer.py +27 -0
  7. k_eval/agent/domain/result.py +18 -0
  8. k_eval/agent/domain/usage.py +12 -0
  9. k_eval/agent/infrastructure/__init__.py +0 -0
  10. k_eval/agent/infrastructure/claude_sdk.py +222 -0
  11. k_eval/agent/infrastructure/errors.py +19 -0
  12. k_eval/agent/infrastructure/factory.py +32 -0
  13. k_eval/agent/infrastructure/observer.py +50 -0
  14. k_eval/agent/infrastructure/registry.py +21 -0
  15. k_eval/cli/__init__.py +0 -0
  16. k_eval/cli/main.py +505 -0
  17. k_eval/cli/output/__init__.py +0 -0
  18. k_eval/cli/output/aggregator.py +89 -0
  19. k_eval/cli/output/eee.py +224 -0
  20. k_eval/config/__init__.py +0 -0
  21. k_eval/config/domain/__init__.py +0 -0
  22. k_eval/config/domain/agent.py +8 -0
  23. k_eval/config/domain/condition.py +12 -0
  24. k_eval/config/domain/condition_mcp_server.py +13 -0
  25. k_eval/config/domain/config.py +26 -0
  26. k_eval/config/domain/dataset.py +11 -0
  27. k_eval/config/domain/execution.py +15 -0
  28. k_eval/config/domain/judge.py +8 -0
  29. k_eval/config/domain/mcp_server.py +39 -0
  30. k_eval/config/domain/observer.py +9 -0
  31. k_eval/config/infrastructure/__init__.py +0 -0
  32. k_eval/config/infrastructure/env_interpolation.py +50 -0
  33. k_eval/config/infrastructure/errors.py +37 -0
  34. k_eval/config/infrastructure/observer.py +23 -0
  35. k_eval/config/infrastructure/yaml_loader.py +120 -0
  36. k_eval/core/__init__.py +0 -0
  37. k_eval/core/errors.py +9 -0
  38. k_eval/dataset/__init__.py +0 -0
  39. k_eval/dataset/domain/__init__.py +0 -0
  40. k_eval/dataset/domain/load_result.py +16 -0
  41. k_eval/dataset/domain/loader.py +12 -0
  42. k_eval/dataset/domain/observer.py +15 -0
  43. k_eval/dataset/domain/sample.py +13 -0
  44. k_eval/dataset/infrastructure/__init__.py +0 -0
  45. k_eval/dataset/infrastructure/errors.py +10 -0
  46. k_eval/dataset/infrastructure/jsonl_loader.py +130 -0
  47. k_eval/dataset/infrastructure/observer.py +36 -0
  48. k_eval/evaluation/__init__.py +0 -0
  49. k_eval/evaluation/application/__init__.py +0 -0
  50. k_eval/evaluation/application/runner.py +232 -0
  51. k_eval/evaluation/domain/__init__.py +0 -0
  52. k_eval/evaluation/domain/observer.py +71 -0
  53. k_eval/evaluation/domain/run.py +20 -0
  54. k_eval/evaluation/domain/summary.py +18 -0
  55. k_eval/evaluation/infrastructure/__init__.py +0 -0
  56. k_eval/evaluation/infrastructure/composite_observer.py +128 -0
  57. k_eval/evaluation/infrastructure/observer.py +129 -0
  58. k_eval/evaluation/infrastructure/progress_observer.py +370 -0
  59. k_eval/judge/__init__.py +0 -0
  60. k_eval/judge/domain/__init__.py +0 -0
  61. k_eval/judge/domain/factory.py +15 -0
  62. k_eval/judge/domain/judge.py +16 -0
  63. k_eval/judge/domain/observer.py +26 -0
  64. k_eval/judge/domain/score.py +21 -0
  65. k_eval/judge/infrastructure/__init__.py +0 -0
  66. k_eval/judge/infrastructure/errors.py +10 -0
  67. k_eval/judge/infrastructure/factory.py +30 -0
  68. k_eval/judge/infrastructure/litellm.py +205 -0
  69. k_eval/judge/infrastructure/observer.py +53 -0
  70. k_eval-0.3.0.dist-info/METADATA +188 -0
  71. k_eval-0.3.0.dist-info/RECORD +74 -0
  72. k_eval-0.3.0.dist-info/WHEEL +5 -0
  73. k_eval-0.3.0.dist-info/entry_points.txt +2 -0
  74. k_eval-0.3.0.dist-info/top_level.txt +1 -0
k_eval/__init__.py ADDED
File without changes
File without changes
File without changes
@@ -0,0 +1,14 @@
1
+ """Agent Protocol — structural interface for all agent implementations."""
2
+
3
+ from typing import Protocol
4
+
5
+ from k_eval.agent.domain.result import AgentResult
6
+
7
+
8
+ class Agent(Protocol):
9
+ """Structural interface satisfied by any agent implementation.
10
+
11
+ Each instance is constructed once per (condition, sample) evaluation.
12
+ """
13
+
14
+ async def ask(self, question: str) -> AgentResult: ...
@@ -0,0 +1,18 @@
1
+ """AgentFactory Protocol — structural interface for constructing Agent instances."""
2
+
3
+ from typing import Protocol
4
+
5
+ from k_eval.agent.domain.agent import Agent
6
+ from k_eval.config.domain.condition_mcp_server import ConditionMcpServer
7
+
8
+
9
+ class AgentFactory(Protocol):
10
+ """Constructs a new Agent instance for a given (condition, sample) pair."""
11
+
12
+ def create(
13
+ self,
14
+ condition: str,
15
+ sample_idx: str,
16
+ system_prompt: str,
17
+ mcp_servers: list[ConditionMcpServer],
18
+ ) -> Agent: ...
@@ -0,0 +1,27 @@
1
+ """AgentObserver port — domain events emitted during agent invocations."""
2
+
3
+ from typing import Protocol
4
+
5
+
6
+ class AgentObserver(Protocol):
7
+ """Observer port for agent domain events.
8
+
9
+ Implementations may log to structlog, record for tests, or emit metrics.
10
+ """
11
+
12
+ def agent_invocation_started(
13
+ self, condition: str, sample_idx: str, model: str
14
+ ) -> None: ...
15
+
16
+ def agent_invocation_completed(
17
+ self,
18
+ condition: str,
19
+ sample_idx: str,
20
+ duration_ms: int,
21
+ num_turns: int,
22
+ cost_usd: float | None,
23
+ ) -> None: ...
24
+
25
+ def agent_invocation_failed(
26
+ self, condition: str, sample_idx: str, reason: str
27
+ ) -> None: ...
@@ -0,0 +1,18 @@
1
+ """AgentResult value object — the outcome of a single agent invocation."""
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+ from k_eval.agent.domain.usage import UsageMetrics
6
+
7
+
8
+ class AgentResult(BaseModel, frozen=True):
9
+ """Immutable value object capturing all outcome data from one agent invocation."""
10
+
11
+ model_config = ConfigDict(frozen=True)
12
+
13
+ response: str
14
+ cost_usd: float | None
15
+ duration_ms: int
16
+ duration_api_ms: int
17
+ num_turns: int
18
+ usage: UsageMetrics | None
@@ -0,0 +1,12 @@
1
+ """UsageMetrics value object — token usage from an agent invocation."""
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class UsageMetrics(BaseModel, frozen=True):
7
+ """Immutable value object capturing token usage from a single agent invocation."""
8
+
9
+ model_config = ConfigDict(frozen=True)
10
+
11
+ input_tokens: int | None
12
+ output_tokens: int | None
File without changes
@@ -0,0 +1,222 @@
1
+ """ClaudeAgentSDKAgent — agent implementation using the Claude Agent SDK."""
2
+
3
+ from typing import Any
4
+
5
+ from claude_agent_sdk import query
6
+ from claude_agent_sdk._errors import ClaudeSDKError
7
+ from claude_agent_sdk.types import (
8
+ ClaudeAgentOptions,
9
+ McpHttpServerConfig,
10
+ McpSdkServerConfig,
11
+ McpSSEServerConfig,
12
+ McpStdioServerConfig,
13
+ ResultMessage,
14
+ )
15
+
16
+ from k_eval.agent.domain.observer import AgentObserver
17
+ from k_eval.agent.domain.result import AgentResult
18
+ from k_eval.agent.domain.usage import UsageMetrics
19
+ from k_eval.agent.infrastructure.errors import AgentInvocationError
20
+ from k_eval.config.domain.agent import AgentConfig
21
+ from k_eval.config.domain.condition_mcp_server import ConditionMcpServer
22
+ from k_eval.config.domain.mcp_server import HttpMcpServer, SseMcpServer, StdioMcpServer
23
+
24
+ type McpServerConfigMap = dict[
25
+ str,
26
+ McpStdioServerConfig
27
+ | McpSSEServerConfig
28
+ | McpHttpServerConfig
29
+ | McpSdkServerConfig,
30
+ ]
31
+
32
+
33
+ class ClaudeAgentSDKAgent:
34
+ """Agent implementation that delegates to the Claude Agent SDK.
35
+
36
+ One instance is constructed per (condition, sample) evaluation run.
37
+ The condition and sample_idx are injected at construction time so that
38
+ observer events carry full context without polluting the ask() signature.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ config: AgentConfig,
44
+ condition: str,
45
+ sample_idx: str,
46
+ system_prompt: str,
47
+ mcp_servers: list[ConditionMcpServer],
48
+ observer: AgentObserver,
49
+ ) -> None:
50
+ self._config = config
51
+ self._condition = condition
52
+ self._sample_idx = sample_idx
53
+ self._system_prompt = system_prompt
54
+ self._mcp_servers = mcp_servers
55
+ self._observer = observer
56
+
57
+ async def ask(self, question: str) -> AgentResult:
58
+ """Invoke the agent with a question and return the structured result.
59
+
60
+ Opens a new SDK session per call — correct for independent eval samples.
61
+
62
+ Raises:
63
+ AgentInvocationError: if the SDK raises, the agent returns an error,
64
+ or no ResultMessage is present in the response stream.
65
+ """
66
+ self._observer.agent_invocation_started(
67
+ condition=self._condition,
68
+ sample_idx=self._sample_idx,
69
+ model=self._config.model,
70
+ )
71
+
72
+ try:
73
+ options = ClaudeAgentOptions(
74
+ model=self._config.model,
75
+ system_prompt=self._system_prompt,
76
+ mcp_servers=self._build_mcp_servers(),
77
+ disallowed_tools=self._build_disallowed_tools(),
78
+ permission_mode="bypassPermissions",
79
+ setting_sources=[],
80
+ )
81
+
82
+ result_message = await self._collect_result(
83
+ prompt=question, options=options
84
+ )
85
+ except AgentInvocationError as exc:
86
+ reason = str(exc).removeprefix("Failed to invoke agent: ")
87
+ self._observer.agent_invocation_failed(
88
+ condition=self._condition,
89
+ sample_idx=self._sample_idx,
90
+ reason=reason,
91
+ )
92
+ raise
93
+
94
+ self._observer.agent_invocation_completed(
95
+ condition=self._condition,
96
+ sample_idx=self._sample_idx,
97
+ duration_ms=result_message.duration_ms,
98
+ num_turns=result_message.num_turns,
99
+ cost_usd=result_message.total_cost_usd,
100
+ )
101
+
102
+ assert result_message.result is not None # guaranteed by _collect_result
103
+ return AgentResult(
104
+ response=result_message.result,
105
+ cost_usd=result_message.total_cost_usd,
106
+ duration_ms=result_message.duration_ms,
107
+ duration_api_ms=result_message.duration_api_ms,
108
+ num_turns=result_message.num_turns,
109
+ usage=self._map_usage(raw=result_message.usage),
110
+ )
111
+
112
+ async def _collect_result(
113
+ self, prompt: str, options: ClaudeAgentOptions
114
+ ) -> ResultMessage:
115
+ """Run the SDK query and extract the single ResultMessage.
116
+
117
+ Raises:
118
+ AgentInvocationError: on SDK errors or missing/error ResultMessage.
119
+ """
120
+ result_message: ResultMessage | None = None
121
+
122
+ try:
123
+ async for message in query(prompt=prompt, options=options):
124
+ if isinstance(message, ResultMessage):
125
+ result_message = message
126
+ except ClaudeSDKError as exc:
127
+ raise AgentInvocationError(reason=str(exc), retriable=True) from exc
128
+ except Exception as exc:
129
+ # The SDK internally raises a bare Exception (not ClaudeSDKError) when
130
+ # its message reader encounters a fatal error (e.g. subprocess exit).
131
+ raise AgentInvocationError(reason=str(exc), retriable=True) from exc
132
+
133
+ if result_message is None:
134
+ raise AgentInvocationError(reason="no ResultMessage in response stream")
135
+
136
+ if result_message.is_error:
137
+ raise AgentInvocationError(
138
+ reason=f"agent returned error response: {result_message.result}"
139
+ )
140
+
141
+ if result_message.result is None:
142
+ raise AgentInvocationError(reason="ResultMessage has no result text")
143
+
144
+ return result_message
145
+
146
+ def _build_mcp_servers(self) -> McpServerConfigMap:
147
+ """Convert ConditionMcpServer list to the SDK's TypedDict format."""
148
+ servers: McpServerConfigMap = {}
149
+
150
+ for server in self._mcp_servers:
151
+ config = server.config
152
+
153
+ if isinstance(config, StdioMcpServer):
154
+ servers[server.name] = self._build_stdio_server(config=config)
155
+ elif isinstance(config, SseMcpServer):
156
+ servers[server.name] = self._build_sse_server(config=config)
157
+ elif isinstance(config, HttpMcpServer):
158
+ servers[server.name] = self._build_http_server(config=config)
159
+ else:
160
+ raise AgentInvocationError(
161
+ reason=f"unsupported MCP server type for server '{server.name}'"
162
+ )
163
+
164
+ return servers
165
+
166
+ def _build_stdio_server(self, config: StdioMcpServer) -> McpStdioServerConfig:
167
+ """Build a McpStdioServerConfig TypedDict from a StdioMcpServer model."""
168
+ server: McpStdioServerConfig = McpStdioServerConfig(command=config.command)
169
+ if config.args:
170
+ server["args"] = list(config.args)
171
+ if config.env:
172
+ server["env"] = dict(config.env)
173
+ return server
174
+
175
+ def _build_sse_server(self, config: SseMcpServer) -> McpSSEServerConfig:
176
+ """Build a McpSSEServerConfig TypedDict from a SseMcpServer model."""
177
+ server: McpSSEServerConfig = McpSSEServerConfig(type="sse", url=config.url)
178
+ if config.headers:
179
+ server["headers"] = dict(config.headers)
180
+ return server
181
+
182
+ def _build_http_server(self, config: HttpMcpServer) -> McpHttpServerConfig:
183
+ """Build a McpHttpServerConfig TypedDict from an HttpMcpServer model."""
184
+ server: McpHttpServerConfig = McpHttpServerConfig(type="http", url=config.url)
185
+ if config.headers:
186
+ server["headers"] = dict(config.headers)
187
+ return server
188
+
189
+ def _build_disallowed_tools(self) -> list[str]:
190
+ """Build the disallowed tools list — all Claude built-in tools.
191
+
192
+ allowed_tools alone does not remove built-in tools from the agent's
193
+ context; it only controls approval requirements. Explicitly disallowing
194
+ all built-in tools ensures the agent cannot use web search, file I/O,
195
+ or any other built-in capability regardless of permission_mode.
196
+ """
197
+ return [
198
+ "Bash",
199
+ "Edit",
200
+ "Glob",
201
+ "Grep",
202
+ "LS",
203
+ "MultiEdit",
204
+ "NotebookEdit",
205
+ "NotebookRead",
206
+ "Read",
207
+ "Task",
208
+ "TodoRead",
209
+ "TodoWrite",
210
+ "WebFetch",
211
+ "WebSearch",
212
+ "Write",
213
+ ]
214
+
215
+ def _map_usage(self, raw: dict[str, Any] | None) -> UsageMetrics | None:
216
+ """Map the SDK's raw usage dict to a typed UsageMetrics value object."""
217
+ if raw is None:
218
+ return None
219
+ return UsageMetrics(
220
+ input_tokens=raw.get("input_tokens"),
221
+ output_tokens=raw.get("output_tokens"),
222
+ )
@@ -0,0 +1,19 @@
1
+ """Error types raised by agent infrastructure."""
2
+
3
+ from k_eval.core.errors import KEvalError
4
+
5
+
6
+ class AgentInvocationError(KEvalError):
7
+ """Raised when the agent cannot be invoked or returns an error response."""
8
+
9
+ def __init__(self, reason: str, retriable: bool = False) -> None:
10
+ super().__init__(f"Failed to invoke agent: {reason}", retriable=retriable)
11
+
12
+
13
+ class AgentTypeNotSupportedError(KEvalError):
14
+ """Raised when the agent type specified in config is not a known agent type."""
15
+
16
+ def __init__(self, agent_type: str) -> None:
17
+ super().__init__(
18
+ f"Failed to create agent factory: unsupported agent type '{agent_type}'"
19
+ )
@@ -0,0 +1,32 @@
1
+ """ClaudeAgentSDKAgentFactory — constructs ClaudeAgentSDKAgent instances."""
2
+
3
+ from k_eval.agent.domain.agent import Agent
4
+ from k_eval.agent.domain.observer import AgentObserver
5
+ from k_eval.agent.infrastructure.claude_sdk import ClaudeAgentSDKAgent
6
+ from k_eval.config.domain.agent import AgentConfig
7
+ from k_eval.config.domain.condition_mcp_server import ConditionMcpServer
8
+
9
+
10
+ class ClaudeAgentSDKAgentFactory:
11
+ """Creates ClaudeAgentSDKAgent instances configured for a given condition and sample."""
12
+
13
+ def __init__(self, config: AgentConfig, observer: AgentObserver) -> None:
14
+ self._config = config
15
+ self._observer = observer
16
+
17
+ def create(
18
+ self,
19
+ condition: str,
20
+ sample_idx: str,
21
+ system_prompt: str,
22
+ mcp_servers: list[ConditionMcpServer],
23
+ ) -> Agent:
24
+ """Construct a new ClaudeAgentSDKAgent for the given condition and sample."""
25
+ return ClaudeAgentSDKAgent(
26
+ config=self._config,
27
+ condition=condition,
28
+ sample_idx=sample_idx,
29
+ system_prompt=system_prompt,
30
+ mcp_servers=mcp_servers,
31
+ observer=self._observer,
32
+ )
@@ -0,0 +1,50 @@
1
+ """Structlog implementation of the AgentObserver port."""
2
+
3
+ import structlog
4
+
5
+
6
+ class StructlogAgentObserver:
7
+ """Delegates agent domain events to structlog.
8
+
9
+ Satisfies the AgentObserver protocol structurally.
10
+ """
11
+
12
+ def __init__(self) -> None:
13
+ self._log = structlog.get_logger()
14
+
15
+ def agent_invocation_started(
16
+ self, condition: str, sample_idx: str, model: str
17
+ ) -> None:
18
+ self._log.info(
19
+ "agent.invocation_started",
20
+ condition=condition,
21
+ sample_idx=sample_idx,
22
+ model=model,
23
+ )
24
+
25
+ def agent_invocation_completed(
26
+ self,
27
+ condition: str,
28
+ sample_idx: str,
29
+ duration_ms: int,
30
+ num_turns: int,
31
+ cost_usd: float | None,
32
+ ) -> None:
33
+ self._log.info(
34
+ "agent.invocation_completed",
35
+ condition=condition,
36
+ sample_idx=sample_idx,
37
+ duration_ms=duration_ms,
38
+ num_turns=num_turns,
39
+ cost_usd=cost_usd,
40
+ )
41
+
42
+ def agent_invocation_failed(
43
+ self, condition: str, sample_idx: str, reason: str
44
+ ) -> None:
45
+ self._log.error(
46
+ "agent.invocation_failed",
47
+ condition=condition,
48
+ sample_idx=sample_idx,
49
+ reason=reason,
50
+ )
@@ -0,0 +1,21 @@
1
+ """AgentFactoryRegistry — maps AgentConfig.type to the correct AgentFactory."""
2
+
3
+ from k_eval.agent.domain.factory import AgentFactory
4
+ from k_eval.agent.domain.observer import AgentObserver
5
+ from k_eval.agent.infrastructure.errors import AgentTypeNotSupportedError
6
+ from k_eval.agent.infrastructure.factory import ClaudeAgentSDKAgentFactory
7
+ from k_eval.config.domain.agent import AgentConfig
8
+
9
+ _SUPPORTED_TYPE = "claude_code_sdk"
10
+
11
+
12
+ def create_agent_factory(config: AgentConfig, observer: AgentObserver) -> AgentFactory:
13
+ """Return the appropriate AgentFactory for the given AgentConfig.
14
+
15
+ Raises:
16
+ AgentTypeNotSupportedError: if config.type is not a known agent type.
17
+ """
18
+ if config.type == _SUPPORTED_TYPE:
19
+ return ClaudeAgentSDKAgentFactory(config=config, observer=observer)
20
+
21
+ raise AgentTypeNotSupportedError(agent_type=config.type)
k_eval/cli/__init__.py ADDED
File without changes