python-yama 0.1.0__py3-none-any.whl → 0.2.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.
- python_yama-0.2.0.dist-info/METADATA +99 -0
- {python_yama-0.1.0.dist-info → python_yama-0.2.0.dist-info}/RECORD +11 -11
- yama/cli.py +11 -12
- yama/llm.py +4 -1
- yama/mocks.py +22 -1
- yama/report.py +1 -1
- yama/runner.py +33 -6
- yama/tools/bash/fs.py +16 -5
- python_yama-0.1.0.dist-info/METADATA +0 -98
- {python_yama-0.1.0.dist-info → python_yama-0.2.0.dist-info}/WHEEL +0 -0
- {python_yama-0.1.0.dist-info → python_yama-0.2.0.dist-info}/entry_points.txt +0 -0
- {python_yama-0.1.0.dist-info → python_yama-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-yama
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A reproducible evaluation runner for tool-using Agent skills
|
|
5
|
+
Project-URL: Repository, https://github.com/world-sim-dev/yama
|
|
6
|
+
Project-URL: Issues, https://github.com/world-sim-dev/yama/issues
|
|
7
|
+
Author-email: Farmer Sun <podpodiumapp@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent,evaluation,llm,skill,testing
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: fastapi>=0.139.0
|
|
19
|
+
Requires-Dist: jinja2>=3.1
|
|
20
|
+
Requires-Dist: litellm<2.0,>=1.80
|
|
21
|
+
Requires-Dist: orjson>=3.11.9
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: rich<15.0,>=14.1
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# yama
|
|
27
|
+
|
|
28
|
+
English | [简体中文](https://github.com/world-sim-dev/yama/blob/main/README.zh-CN.md)
|
|
29
|
+
|
|
30
|
+
`yama` is a reproducible evaluation framework for tool-using Agent Skills: a declarative YAML Case describes what the model sees — the system prompt, Skill metadata, tool schemas, and multi-turn user messages — then yama calls the LLM through LiteLLM, drives the tool loop, and scores the transcript with deterministic hard checks or an LLM judge. See the [design doc](https://github.com/world-sim-dev/yama/blob/main/docs/agent-skill-test-framework-design.md) (Chinese) for the full Case schema and execution contract.
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Run from a plugin root; collects __evals__/cases/**/*.yaml by default
|
|
36
|
+
cd dingding-simple
|
|
37
|
+
OPENAI_API_KEY=... uv run --project yama yama
|
|
38
|
+
|
|
39
|
+
# Or, from a workspace root (a directory with yama.toml), pick a plugin by name
|
|
40
|
+
OPENAI_API_KEY=... uv run --project yama yama --plugin dingding-simple
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Writing a Case
|
|
44
|
+
|
|
45
|
+
A Case is a YAML file organized as `context` (what the model sees) → `mocks` (how tool calls get executed) → `steps` (user turns sent in order, each with assertions) → `outcome` (the pass bar for the whole Case):
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
context:
|
|
49
|
+
system_prompt: { default: true } # use SYSTEM.md from the plugin root
|
|
50
|
+
|
|
51
|
+
tools:
|
|
52
|
+
- file: tools/read-dsl.yaml # tool schema, resolved relative to __evals__
|
|
53
|
+
|
|
54
|
+
mocks:
|
|
55
|
+
tools:
|
|
56
|
+
read_dsl:
|
|
57
|
+
respond:
|
|
58
|
+
result:
|
|
59
|
+
visualStyle: { name: Vintage Film }
|
|
60
|
+
|
|
61
|
+
steps:
|
|
62
|
+
- id: request-directions
|
|
63
|
+
user: Give me a few creative directions
|
|
64
|
+
assert:
|
|
65
|
+
hard:
|
|
66
|
+
- tool_called: { name: read_dsl }
|
|
67
|
+
- assistant_contains: creative direction
|
|
68
|
+
|
|
69
|
+
outcome:
|
|
70
|
+
require:
|
|
71
|
+
hard_checks: all_pass
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `context.tools` is the tool schema sent to the model; `mocks.tools` is how the runner responds when a tool call arrives. The two sets of tool names must match exactly.
|
|
75
|
+
- To use a Skill, declare it by name under `context.skills` and also declare `{builtin: skill}` under `context.tools`; the model reads the Skill body via `skill(name, file?)`.
|
|
76
|
+
- To simulate command-line tools, declare `{builtin: bash}` and configure per-command output under `mocks.cli`.
|
|
77
|
+
- `steps[].assert.hard` are deterministic checks (nine types, including `tool_called`, `tool_arguments`, and `assistant_contains`); add `assert.judge` for LLM scoring.
|
|
78
|
+
|
|
79
|
+
For the full schema (every Skill/tool/mock form, the `bash` sandbox, message injection, judge configuration, and more), see the [design doc](https://github.com/world-sim-dev/yama/blob/main/docs/agent-skill-test-framework-design.md) (Chinese).
|
|
80
|
+
|
|
81
|
+
## CLI usage
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
uv run --project yama yama --plugin dingding-simple --report
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Flag | Meaning |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `paths` (positional, repeatable) | Explicit Case YAML paths to run |
|
|
90
|
+
| `--plugin-root PATH` | Use the given path as the single plugin root |
|
|
91
|
+
| `--plugin NAME` (repeatable) | Select plugins by name from `yama.toml` |
|
|
92
|
+
| `--all-plugins` | Run every plugin configured in `yama.toml` |
|
|
93
|
+
| `--result-dir PATH` | Override the artifact root (default `<plugin_root>/.yama/runs`) |
|
|
94
|
+
| `--report [PATH]` | Also generate a single-file HTML report (default `.yama/reports/latest.html`) |
|
|
95
|
+
| `--list` | Only print the collected Cases, without running them |
|
|
96
|
+
| `--no-artifacts` | Write no artifact files at all |
|
|
97
|
+
| `--json` | Emit machine-readable JSON instead of Rich tables |
|
|
98
|
+
|
|
99
|
+
`--plugin`, `--all-plugins`, and `--plugin-root` are mutually exclusive. When none of them is given, yama walks up from the current directory to the nearest directory containing `yama.toml` and uses it as the workspace root (falling back to the cwd), running it as a single plugin root.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
yama/__init__.py,sha256=UYhoZTSu1HiMT54LOZCdvLKMQ_L0qec8RT8jDfde_cY,1156
|
|
2
2
|
yama/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
|
|
3
3
|
yama/assertions.py,sha256=0x1r82W2dtAY1mTk4LC86VulD5Q9blS7QeRCTysb4kE,21736
|
|
4
|
-
yama/cli.py,sha256=
|
|
5
|
-
yama/llm.py,sha256=
|
|
6
|
-
yama/mocks.py,sha256=
|
|
4
|
+
yama/cli.py,sha256=RJcffqSX2DTUU8i6PB2ZyPMXuACCpBuj_Ifm-uQ4j9g,11736
|
|
5
|
+
yama/llm.py,sha256=XqGE4aKPOimy9n3oNF5rUvqWdJlpi0EVzMlD-FBim6A,13570
|
|
6
|
+
yama/mocks.py,sha256=YyiU6Y7NgttxHYJBsqGPILKYlTsM4IMTIilMNRkUuzs,14973
|
|
7
7
|
yama/models.py,sha256=-ry0QUI2e0ialL6A9NUEAM0QpwCQg8oXEfPBXn3CvNI,3964
|
|
8
|
-
yama/report.py,sha256=
|
|
9
|
-
yama/runner.py,sha256=
|
|
8
|
+
yama/report.py,sha256=bjp_aev2KHx3i2pqYRGE1VHTuBmiakXgbqYjN3RAGEg,37558
|
|
9
|
+
yama/runner.py,sha256=dJi-hxXxpUawZ3g14Hk9NFZy7HQNvcGpxpxvj8QKbX8,21694
|
|
10
10
|
yama/workspace.py,sha256=rHQ9kQUkAo32gJpqRf7pSd5JhBc05xgxA1wPRC4bOQ8,3618
|
|
11
11
|
yama/loaders/__init__.py,sha256=DIwfa8QlIlKt6NpFMZJ_OcqbJPPrmKLBzd9_e-BCuBs,174
|
|
12
12
|
yama/loaders/case.py,sha256=gas5J9jtPuaqo43IO5gk-4uVJ49JWc7sNMXFI8I_7AA,14255
|
|
@@ -17,11 +17,11 @@ yama/tools/base.py,sha256=ONFuGzTSWVsA0k4-UUREncqAHmyQI2uHpVOU2Dh_XiQ,446
|
|
|
17
17
|
yama/tools/bash/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
18
|
yama/tools/bash/_cli_shim.py,sha256=f3Rom5UipW0dA0KRYavqIa9HJDKT336ssDZGWeVmkvo,2494
|
|
19
19
|
yama/tools/bash/bash.py,sha256=4GlFvmqTUS4k6DgJ5Io91YF5fB6wF6e3e7p2P-pmbEQ,10358
|
|
20
|
-
yama/tools/bash/fs.py,sha256=
|
|
20
|
+
yama/tools/bash/fs.py,sha256=ncLZf4_gErRP3HHXinLEQeN6chrf7JaSiaTuadVl8MY,21314
|
|
21
21
|
yama/tools/skill/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
yama/tools/skill/skill.py,sha256=P2rDz6VqRLfhiabWjOU3ZBaWLw30Eywf3o_OxVfFbPI,1992
|
|
23
|
-
python_yama-0.
|
|
24
|
-
python_yama-0.
|
|
25
|
-
python_yama-0.
|
|
26
|
-
python_yama-0.
|
|
27
|
-
python_yama-0.
|
|
23
|
+
python_yama-0.2.0.dist-info/METADATA,sha256=TuKyzkSIHUvjSVNTBj6YYErZjK91mznGMs6gFBFLCmg,4598
|
|
24
|
+
python_yama-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
25
|
+
python_yama-0.2.0.dist-info/entry_points.txt,sha256=N5se3y1FflUcDU3TmUrH2Tu9dzsrBBo7sUP-Gu_2cVs,39
|
|
26
|
+
python_yama-0.2.0.dist-info/licenses/LICENSE,sha256=cMAJJAs7R1lemJPPc7qMNv4w_sqn5QncFf4aXKYyEc4,1067
|
|
27
|
+
python_yama-0.2.0.dist-info/RECORD,,
|
yama/cli.py
CHANGED
|
@@ -19,7 +19,7 @@ from .workspace import (
|
|
|
19
19
|
load_workspace_config,
|
|
20
20
|
resolve_plugin_root,
|
|
21
21
|
)
|
|
22
|
-
from .llm import LLMError, LiteLLMClient
|
|
22
|
+
from .llm import LLMError, LiteLLMClient
|
|
23
23
|
from .loaders import CollectionError, resolve_case
|
|
24
24
|
from .report import write_html_report
|
|
25
25
|
from .runner import YamaRunner
|
|
@@ -190,9 +190,6 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
190
190
|
parser.add_argument(
|
|
191
191
|
"--all-plugins", action="store_true", help="Run all configured plugins"
|
|
192
192
|
)
|
|
193
|
-
parser.add_argument(
|
|
194
|
-
"--response-script", type=Path, help="Use deterministic scripted LLM responses"
|
|
195
|
-
)
|
|
196
193
|
parser.add_argument("--result-dir", type=Path, help="Override artifact directory")
|
|
197
194
|
parser.add_argument(
|
|
198
195
|
"--report",
|
|
@@ -255,11 +252,7 @@ async def _run(args: argparse.Namespace) -> int:
|
|
|
255
252
|
Console(stderr=True).print("[bold red]No eval cases collected.[/bold red]")
|
|
256
253
|
return 2
|
|
257
254
|
|
|
258
|
-
llm = (
|
|
259
|
-
ScriptedLLMClient.from_file(args.response_script.resolve())
|
|
260
|
-
if args.response_script
|
|
261
|
-
else LiteLLMClient()
|
|
262
|
-
)
|
|
255
|
+
llm = LiteLLMClient()
|
|
263
256
|
all_results = []
|
|
264
257
|
result_roots = []
|
|
265
258
|
for case in collected:
|
|
@@ -275,8 +268,6 @@ async def _run(args: argparse.Namespace) -> int:
|
|
|
275
268
|
write_artifacts=not args.no_artifacts,
|
|
276
269
|
)
|
|
277
270
|
all_results.append(await runner.run_case(case))
|
|
278
|
-
if isinstance(llm, ScriptedLLMClient):
|
|
279
|
-
llm.assert_consumed()
|
|
280
271
|
report_target = args.report
|
|
281
272
|
if report_target == DEFAULT_REPORT:
|
|
282
273
|
report_target = result_roots[0].parent / "reports" / "latest.html"
|
|
@@ -309,10 +300,18 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
309
300
|
args = _parser().parse_args(argv)
|
|
310
301
|
try:
|
|
311
302
|
return asyncio.run(_run(args))
|
|
312
|
-
except (ConfigurationError, CollectionError,
|
|
303
|
+
except (ConfigurationError, CollectionError, ValueError) as exc:
|
|
313
304
|
Console(stderr=True).print(
|
|
314
305
|
Text.assemble(("Collection error: ", "bold red"), (str(exc), "red"))
|
|
315
306
|
)
|
|
316
307
|
return 2
|
|
308
|
+
except LLMError as exc:
|
|
309
|
+
# Not a collection problem: cases were already collected (and possibly
|
|
310
|
+
# executed) when the LLM layer failed -- label it accordingly so the
|
|
311
|
+
# user debugs the right layer.
|
|
312
|
+
Console(stderr=True).print(
|
|
313
|
+
Text.assemble(("LLM error: ", "bold red"), (str(exc), "red"))
|
|
314
|
+
)
|
|
315
|
+
return 2
|
|
317
316
|
except KeyboardInterrupt:
|
|
318
317
|
return 130
|
yama/llm.py
CHANGED
|
@@ -337,7 +337,10 @@ class ScriptedLLMClient(LLMClient):
|
|
|
337
337
|
if not isinstance(expectation, dict):
|
|
338
338
|
raise LLMError("script expect must be an object")
|
|
339
339
|
if "tools" in expectation:
|
|
340
|
-
|
|
340
|
+
# A request with no tools omits the "tools" key entirely (see
|
|
341
|
+
# _litellm_call_arguments), so expect: {tools: []} must read it
|
|
342
|
+
# as an empty list instead of raising KeyError.
|
|
343
|
+
actual = [tool["function"]["name"] for tool in request.get("tools", [])]
|
|
341
344
|
if actual != expectation["tools"]:
|
|
342
345
|
raise LLMError(
|
|
343
346
|
f"script expected tools {expectation['tools']}, got {actual}"
|
yama/mocks.py
CHANGED
|
@@ -178,6 +178,27 @@ def _matches(actual: Any, expected: Any) -> bool:
|
|
|
178
178
|
_MISSING = object()
|
|
179
179
|
|
|
180
180
|
|
|
181
|
+
def result_is_error(result: Any) -> bool:
|
|
182
|
+
"""Semantic error status of a tool result envelope.
|
|
183
|
+
|
|
184
|
+
A result is an error only when it carries a non-null ``error`` payload,
|
|
185
|
+
or when it is a bash-style ``{stdout, stderr, exit_code}`` envelope with a
|
|
186
|
+
non-zero exit code. A literal ``error: null`` field is a successful result.
|
|
187
|
+
"""
|
|
188
|
+
if not isinstance(result, dict):
|
|
189
|
+
return False
|
|
190
|
+
if result.get("error") is not None:
|
|
191
|
+
return True
|
|
192
|
+
if (
|
|
193
|
+
isinstance(result.get("stdout"), str)
|
|
194
|
+
and isinstance(result.get("stderr"), str)
|
|
195
|
+
and isinstance(result.get("exit_code"), int)
|
|
196
|
+
and not isinstance(result.get("exit_code"), bool)
|
|
197
|
+
):
|
|
198
|
+
return result["exit_code"] != 0
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
181
202
|
def _state_diff(before: Any, after: Any, path: str = "$") -> list[dict[str, Any]]:
|
|
182
203
|
if before == after:
|
|
183
204
|
return []
|
|
@@ -353,7 +374,7 @@ class MockToolRegistry:
|
|
|
353
374
|
"match_failed": match_failed,
|
|
354
375
|
"arguments": arguments,
|
|
355
376
|
"result": result,
|
|
356
|
-
"is_error": is_error or (
|
|
377
|
+
"is_error": is_error or result_is_error(result),
|
|
357
378
|
"duration_ms": round((time.perf_counter() - started) * 1000, 3),
|
|
358
379
|
"state_diff": _state_diff(before, after),
|
|
359
380
|
}
|
yama/report.py
CHANGED
|
@@ -418,7 +418,7 @@ def _render_step(
|
|
|
418
418
|
"</span>"
|
|
419
419
|
"</summary>"
|
|
420
420
|
'<div class="step-body">'
|
|
421
|
-
'<details class="subsection timeline-section"
|
|
421
|
+
'<details class="subsection timeline-section">'
|
|
422
422
|
'<summary><span>Execution timeline</span>'
|
|
423
423
|
f'<span class="muted">{len(events)} events</span></summary>'
|
|
424
424
|
f'<div class="timeline">{"".join(events)}</div></details>'
|
yama/runner.py
CHANGED
|
@@ -14,9 +14,15 @@ from typing import Any
|
|
|
14
14
|
import yaml
|
|
15
15
|
|
|
16
16
|
from .assertions import run_hard_assertions, run_judge
|
|
17
|
-
from .tools.bash.fs import cleanup_session
|
|
17
|
+
from .tools.bash.fs import case_workspace_root, cleanup_session
|
|
18
18
|
from .llm import LLMClient
|
|
19
|
-
from .mocks import
|
|
19
|
+
from .mocks import (
|
|
20
|
+
MockToolRegistry,
|
|
21
|
+
ToolMockContext,
|
|
22
|
+
_matches,
|
|
23
|
+
load_python_callable,
|
|
24
|
+
result_is_error,
|
|
25
|
+
)
|
|
20
26
|
from .models import ResolvedCase, StepResult, ToolCallRecord, sanitize_case_key
|
|
21
27
|
|
|
22
28
|
|
|
@@ -109,6 +115,7 @@ class YamaRunner:
|
|
|
109
115
|
repeat = int(case.execution.get("repeat", 1))
|
|
110
116
|
if repeat < 1:
|
|
111
117
|
raise ValueError("execution.repeat must be at least 1")
|
|
118
|
+
self._clean_case_dirs(case)
|
|
112
119
|
runs = [
|
|
113
120
|
await self._run_once(case, run_index) for run_index in range(1, repeat + 1)
|
|
114
121
|
]
|
|
@@ -351,7 +358,7 @@ class YamaRunner:
|
|
|
351
358
|
call_index=call_index,
|
|
352
359
|
call=call,
|
|
353
360
|
result=result,
|
|
354
|
-
is_error=
|
|
361
|
+
is_error=result_is_error(result),
|
|
355
362
|
)
|
|
356
363
|
records.append(record)
|
|
357
364
|
messages.append(
|
|
@@ -452,12 +459,32 @@ class YamaRunner:
|
|
|
452
459
|
return False
|
|
453
460
|
return True
|
|
454
461
|
|
|
462
|
+
def _clean_case_dirs(self, case: ResolvedCase) -> None:
|
|
463
|
+
"""Drop leftovers of earlier executions before the first run starts.
|
|
464
|
+
|
|
465
|
+
Both the bash-sandbox workspaces (`.yama/fs-runs/<case>`) and the run
|
|
466
|
+
artifacts (`.yama/runs/<case>`) are keyed by run index, which restarts
|
|
467
|
+
at 1 on every execution -- without this wipe a later execution would
|
|
468
|
+
reuse stale workspaces (old .mockbin scripts, files written by a
|
|
469
|
+
previous model) and leave orphaned run-00N artifact dirs behind when
|
|
470
|
+
execution.repeat shrinks.
|
|
471
|
+
"""
|
|
472
|
+
fs_root = case_workspace_root(case.plugin_root, case.case_key)
|
|
473
|
+
if fs_root.exists():
|
|
474
|
+
shutil.rmtree(fs_root)
|
|
475
|
+
if self.write_artifacts:
|
|
476
|
+
case_dir = self._case_artifact_root(case)
|
|
477
|
+
if case_dir.exists():
|
|
478
|
+
shutil.rmtree(case_dir)
|
|
479
|
+
|
|
480
|
+
def _case_artifact_root(self, case: ResolvedCase) -> Path:
|
|
481
|
+
root = self.result_root or case.plugin_root / ".yama" / "runs"
|
|
482
|
+
return root / sanitize_case_key(case.case_key)
|
|
483
|
+
|
|
455
484
|
def _artifact_dir(self, case: ResolvedCase, run_index: int) -> Path | None:
|
|
456
485
|
if not self.write_artifacts:
|
|
457
486
|
return None
|
|
458
|
-
|
|
459
|
-
key = sanitize_case_key(case.case_key)
|
|
460
|
-
return root / key / f"run-{run_index:03d}"
|
|
487
|
+
return self._case_artifact_root(case) / f"run-{run_index:03d}"
|
|
461
488
|
|
|
462
489
|
def _write_artifacts(self, case: ResolvedCase, run: RunResult) -> None:
|
|
463
490
|
assert run.artifact_dir is not None
|
yama/tools/bash/fs.py
CHANGED
|
@@ -463,6 +463,15 @@ class ShellSession:
|
|
|
463
463
|
pass
|
|
464
464
|
|
|
465
465
|
|
|
466
|
+
def case_workspace_root(plugin_root: Path, case_key: str) -> Path:
|
|
467
|
+
"""Parent directory of all bash-sandbox workspaces of one case.
|
|
468
|
+
|
|
469
|
+
The runner wipes it before a case's first run so a fresh execution never
|
|
470
|
+
reuses stale workspaces from an earlier one.
|
|
471
|
+
"""
|
|
472
|
+
return plugin_root / ".yama" / "fs-runs" / sanitize_case_key(case_key)
|
|
473
|
+
|
|
474
|
+
|
|
466
475
|
def workspace_dir(context: ToolMockContext) -> Path:
|
|
467
476
|
"""Bash-sandbox workspace directory of the current run.
|
|
468
477
|
|
|
@@ -471,10 +480,7 @@ def workspace_dir(context: ToolMockContext) -> Path:
|
|
|
471
480
|
first `bash` call actually materializes the directory.
|
|
472
481
|
"""
|
|
473
482
|
return (
|
|
474
|
-
context.plugin_root
|
|
475
|
-
/ ".yama"
|
|
476
|
-
/ "fs-runs"
|
|
477
|
-
/ sanitize_case_key(context.case_key)
|
|
483
|
+
case_workspace_root(context.plugin_root, context.case_key)
|
|
478
484
|
/ f"run-{context.run_index:03d}"
|
|
479
485
|
)
|
|
480
486
|
|
|
@@ -485,7 +491,12 @@ async def _get_session(
|
|
|
485
491
|
session = _SESSIONS.get(id(context.state))
|
|
486
492
|
if session is None or session.closed:
|
|
487
493
|
workspace = workspace_dir(context)
|
|
488
|
-
|
|
494
|
+
# Keyed on the per-run state marker, not directory existence: a
|
|
495
|
+
# leftover directory from an earlier execution (or one pre-created by
|
|
496
|
+
# a python handler) must still get this run's files and .mockbin
|
|
497
|
+
# scripts, while a session restart mid-run must not clobber what the
|
|
498
|
+
# model already wrote.
|
|
499
|
+
if "__fs__" not in context.state:
|
|
489
500
|
materialize_workspace(workspace, fs_config)
|
|
490
501
|
session = ShellSession(workspace, fs_config)
|
|
491
502
|
await session.start()
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-yama
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: A reproducible evaluation runner for tool-using Agent skills
|
|
5
|
-
Project-URL: Repository, https://github.com/world-sim-dev/yama
|
|
6
|
-
Project-URL: Issues, https://github.com/world-sim-dev/yama/issues
|
|
7
|
-
Author-email: Farmer Sun <podpodiumapp@gmail.com>
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Keywords: agent,evaluation,llm,skill,testing
|
|
11
|
-
Classifier: Development Status :: 3 - Alpha
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Classifier: Topic :: Software Development :: Testing
|
|
17
|
-
Requires-Python: >=3.12
|
|
18
|
-
Requires-Dist: fastapi>=0.139.0
|
|
19
|
-
Requires-Dist: jinja2>=3.1
|
|
20
|
-
Requires-Dist: litellm<2.0,>=1.80
|
|
21
|
-
Requires-Dist: orjson>=3.11.9
|
|
22
|
-
Requires-Dist: pyyaml>=6.0
|
|
23
|
-
Requires-Dist: rich<15.0,>=14.1
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
|
-
# yama
|
|
27
|
-
|
|
28
|
-
`yama` 是一个可复现的 Agent Skill 评测框架:用声明式 YAML Case 描述模型看到的 System Prompt、Skill metadata、Tool Schema 与多轮 User Message,通过 LiteLLM 统一调用 LLM 并驱动 Tool Loop,再对 Transcript 做确定性 Hard Check 或 LLM Judge 评分。完整 Case Schema 与执行契约见[设计文档](docs/agent-skill-test-framework-design.md)。
|
|
29
|
-
|
|
30
|
-
## 快速开始
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
# 在 Plugin 根目录下直接运行,默认收集 __evals__/cases/**/*.yaml
|
|
34
|
-
cd dingding-simple
|
|
35
|
-
OPENAI_API_KEY=... uv run --project yama yama
|
|
36
|
-
|
|
37
|
-
# 在 Workspace 根目录(存在 yama.toml)按名字指定 Plugin
|
|
38
|
-
OPENAI_API_KEY=... uv run --project yama yama --plugin dingding-simple
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## 编写一个 Case
|
|
42
|
-
|
|
43
|
-
一个 Case 是一份 YAML 文件,按 `context`(模型看到什么)→ `mocks`(Tool 怎么被执行)→ `steps`(依次发送的用户回合与断言)→ `outcome`(整个 Case 的通过门槛)的结构组织:
|
|
44
|
-
|
|
45
|
-
```yaml
|
|
46
|
-
context:
|
|
47
|
-
system_prompt: { default: true } # 使用 Plugin 根目录下的 SYSTEM.md
|
|
48
|
-
|
|
49
|
-
tools:
|
|
50
|
-
- file: tools/read-dsl.yaml # Tool Schema,相对 __evals__ 目录解析
|
|
51
|
-
|
|
52
|
-
mocks:
|
|
53
|
-
tools:
|
|
54
|
-
read_dsl:
|
|
55
|
-
respond:
|
|
56
|
-
result:
|
|
57
|
-
visualStyle: { name: 复古胶片 }
|
|
58
|
-
|
|
59
|
-
steps:
|
|
60
|
-
- id: request-directions
|
|
61
|
-
user: 给我几个创意方向
|
|
62
|
-
assert:
|
|
63
|
-
hard:
|
|
64
|
-
- tool_called: { name: read_dsl }
|
|
65
|
-
- assistant_contains: 创意方向
|
|
66
|
-
|
|
67
|
-
outcome:
|
|
68
|
-
require:
|
|
69
|
-
hard_checks: all_pass
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
- `context.tools` 是发给模型的 Tool Schema,`mocks.tools` 是 Runner 收到 Tool Call 后如何返回结果;两者的 Tool 名字集合必须完全一致。
|
|
73
|
-
- 需要 Skill 时在 `context.skills` 中按名字声明,并在 `context.tools` 中同时声明 `{builtin: skill}`;模型通过 `skill(name, file?)` 读取正文。
|
|
74
|
-
- 需要模拟命令行工具时声明 `{builtin: bash}`,在 `mocks.cli` 中按命令配置输出。
|
|
75
|
-
- `steps[].assert.hard` 是确定性检查(`tool_called`、`tool_arguments`、`assistant_contains` 等九种类型);还可以加 `assert.judge` 做 LLM 打分。
|
|
76
|
-
|
|
77
|
-
完整 Schema(Skill/Tool/Mock 的全部写法、`bash` 沙箱、Message Injection、Judge 配置等)见[设计文档](docs/agent-skill-test-framework-design.md)。
|
|
78
|
-
|
|
79
|
-
## 命令行使用
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
uv run --project yama yama --plugin dingding-simple --report
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
| 参数 | 含义 |
|
|
86
|
-
| --- | --- |
|
|
87
|
-
| `paths`(位置参数,可多个) | 显式指定要跑的 Case YAML 路径 |
|
|
88
|
-
| `--plugin-root PATH` | 以指定路径作为单一 Plugin Root |
|
|
89
|
-
| `--plugin NAME`(可重复) | 按 `yama.toml` 中的 Plugin 名字选择 |
|
|
90
|
-
| `--all-plugins` | 运行 `yama.toml` 中配置的全部 Plugin |
|
|
91
|
-
| `--response-script PATH` | 用脚本化 LLM 响应回放,不调用真实模型 |
|
|
92
|
-
| `--result-dir PATH` | 覆盖产物根目录(默认 `<plugin_root>/.yama/runs`) |
|
|
93
|
-
| `--report [PATH]` | 额外生成单文件 HTML 报告(默认 `.yama/reports/latest.html`) |
|
|
94
|
-
| `--list` | 只打印匹配到的 Case,不运行 |
|
|
95
|
-
| `--no-artifacts` | 不写任何产物文件 |
|
|
96
|
-
| `--json` | 输出机器可读 JSON 而不是 Rich 表格 |
|
|
97
|
-
|
|
98
|
-
`--plugin`/`--all-plugins`/`--plugin-root` 三者互斥;都不提供时默认用当前目录向上搜索到的 `yama.toml` 所在目录作为 Workspace Root(找不到则用 cwd),把该目录当作单一 Plugin Root 运行。
|
|
File without changes
|
|
File without changes
|
|
File without changes
|