ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
.env
__pycache__/
*.pyc
last_run.json
+416
View File
@@ -0,0 +1,416 @@
# Experiment 5-2: Code Tools for Logic / 实验 5-2:用代码生成工具提升逻辑思考能力
> Companion lab for *AI Agents in Depth*, Chapter 5 — Knights & Knaves as CSP with `python-constraint`; pure reasoning vs code-assisted vs offline solver.
> 《深入理解 AI Agent》第 5 章配套:骑士与无赖谜题转 CSP,对比纯思考 / 代码辅助 / 离线约束求解。
← [Chapter 5 index / 返回第 5 章目录](../README.md)
## Formal manuscript result (canonical)
The canonical run uses the pinned revision of
`K-and-K/perturbed-knights-and-knaves`, stratified across six perturbations and
28 people (84 paired tasks). Every code trajectory invoked
`python-constraint`, but observed accuracy was 39.3% for code assistance versus
75.0% for pure reasoning (p=2.27e-7 in the opposite direction). The campaign is
complete; the manuscript's >90% and significant-improvement hypothesis was not
observed. Evidence: [`validation/real_ark_doubao_flash_hf84_20260730.json`](validation/real_ark_doubao_flash_hf84_20260730.json).
正式活动固定 K&K 数据集版本,按六类扰动与 2–8 人规模分层抽取 84 道配对题。代码臂
每题都调用了 `python-constraint`,但实测准确率为 39.3%,低于纯思考的 75.0%
(p=2.27e-7,方向与预期相反)。因此下文离线求解器 100% 的表格只能证明确定性 CSP
机制正确,不能替代真实 LLM 对照或正文的 >90% 验收结论。
---
## English
### Overview
This lab evaluates whether an Agent can use **constraint-solving** code to support logical thinking. The same LLM gets a Code Interpreter preloaded with `python-constraint`, and turns Knights & Knaves (K&K) puzzles into formal **constraint satisfaction problems (CSP)**—variables (each islander is knight or knave), constraints (“knights tell truth, knaves lie”), then a solver search.
On a set of 12 K&K puzzles (25 people, each with a unique truth assignment), three modes are compared:
- **Pure thinking (`pure`)**: natural-language chain-of-thought only; answer directly.
- **Code-assisted (`code`)**: use `run_python` to write a constraint model and call the solver, then answer from the result.
- **Constraint solver (`solver`)**: **offline baseline**—solve structured statements with `python-constraint` only, no API/network. Deterministic; theoretically 100% correct; validates “translate puzzle → constraints → solve” (see real results below).
### Core idea: why code helps
The key modeling rule for K&K is one **biconditional (equivalence)** per resident X:
```
X is knight (True) <=> X's statement is true
```
i.e. `X == (semantic truth of that statement)`. Hand this to a deterministic solver that **enumerates** all Boolean assignments and logic cannot “slip”; pure thinking often fails on multi-person, counting (“exactly two knights”), or self-referential (“A and B are the same type”) puzzles when propagating truth values by hand.
### Files
| File | Role |
| --- | --- |
| `demo.py` | Main: pure / code / solver comparison; accuracy table |
| `csp_solver.py` | Offline CSP solver: structured statement DSL + `python-constraint` (shared by demo solver mode and `build_puzzles` checks) |
| `sandbox.py` | Minimal Code Interpreter: subprocess sandbox for model-generated Python (python-constraint preinstalled) |
| `puzzles.json` | 12 puzzles: stems + structured statements + unique solutions (LLM sees stems only) |
| `build_puzzles.py` | Generate/validate puzzles: solve with `python-constraint`, assert unique solution; export curated or random sets |
| `requirements.txt` | Dependencies (openai + python-constraint) |
| `env.example` | Env var sample |
| `last_run.json` | Full per-problem record after each run (including model-generated code) for review |
### Quick start
```bash
# From the repository root: use the shared Chapter 5 environment
uv sync --locked --python 3.12 --extra ch5
# Activate it before changing directories:
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Windows cmd: .venv\Scripts\activate.bat
# pip fallback when uv is not installed:
# python -m pip install -e ".[ch5]"
cd chapter5/code-for-logic
# Single-project compatibility path, still supported during migration:
# python -m pip install -r requirements.txt
```
#### 1) Offline solver baseline (no API key; recommended first)
```bash
python demo.py --mode solver # offline solve all 12 with python-constraint
python demo.py --mode solver --min-people 4 # only puzzles with >=4 people
```
Fully offline and deterministic; demonstrates “puzzle → constraints → solve” at 100% accuracy.
#### 2) LLM comparison (needs `OPENAI_API_KEY` or `OPENROUTER_API_KEY`)
```bash
cp env.example .env # then edit .env with OPENAI_API_KEY
# or: export OPENAI_API_KEY=your-openai-api-key
python demo.py # default both: pure vs code, all 12
python demo.py --mode pure # pure baseline only
python demo.py --limit 4 # first 4 only (cheap smoke)
python demo.py --max-people 3 # only puzzles with <=3 people
python demo.py --model gpt-4o-mini # model (default gpt-4o-mini)
python demo.py --puzzles my.json --output run.json # custom data / output path
```
**OpenRouter fallback**: if `OPENAI_API_KEY` is unset but `OPENROUTER_API_KEY` is set, traffic goes through OpenRouter (`gpt-*``openai/*`). Default `gpt-4o-mini` works on direct OpenAI; OpenRouter is preferred when you switch `--model` to gpt-5.x models that need org verification and `OPENROUTER_API_KEY` is set.
Full flags: `python demo.py --help` (Chinese help text).
#### 3) Build / expand the puzzle set
```bash
python build_puzzles.py # export built-in 12 curated puzzles (default)
python build_puzzles.py --generate 20 --min-people 3 --max-people 5 --seed 7
python build_puzzles.py --generate 20 --output my.json
```
The random generator solves each candidate with `python-constraint` and keeps only unique-solution puzzles.
`sandbox.py` / `csp_solver.py` can also be run alone for self-tests:
`python sandbox.py`, `python csp_solver.py` each solve a minimal puzzle with python-constraint.
### Real results (1): offline solver (`--mode solver`, no API)
Actual output of `python demo.py --mode solver` (12 curated puzzles, offline, deterministic):
```
== 约束求解(solver,离线) ==
[solver] kk01 (2人) ✓ 解数=1 预测={'A': 'knight', 'B': 'knave'}
[solver] kk05 (3人) ✓ 解数=1 预测={'A': 'knave', 'B': 'knave', 'C': 'knight'}
[solver] kk11 (5人) ✓ 解数=1 预测={'A': 'knight', 'B': 'knight', 'C': 'knave', 'D': 'knave', 'E': 'knight'}
...(其余题略)
------------------------------------------------------------
准确率 100.0%
============================================================
约束求解 准确率: 100.0% (12/12)
```
This path translates each structured statement into `python-constraint` and enumerates—12/12 correct. It proves determinism of “puzzle → constraints → solve”; if the LLM translates correctly, it gets the same 100%. Random puzzles from `build_puzzles.py --generate` also solve 100% and match the unique solutions recorded at generation time.
### Real results (2): LLM comparison (gpt-4o-mini, 12 puzzles)
```
准确率对比表
============================================================
题号 人数 纯思考 代码辅助
------------------------------------------------------------
kk01 2 ✓ ✓
kk02 2 ✓ ✓
kk03 2 ✓ ✓
kk04 3 ✓ ✓
kk05 3 ✗ ✓
kk06 3 ✗ ✓
kk07 3 ✗ ✓
kk08 4 ✗ ✓
kk09 4 ✗ ✓
kk10 4 ✓ ✓
kk11 5 ✗ ✓
kk12 5 ✓ ✓
------------------------------------------------------------
准确率 50.0% 100.0%
============================================================
纯思考 准确率: 50.0% (6/12)
代码辅助 准确率: 100.0% (12/12)
提升(代码辅助 - 纯思考): +50.0 个百分点
```
> A weaker `gpt-4o-mini` is used on purpose: pure thinking got **6/12 (50%)**, with errors concentrated on ≥3 people and counting/self-reference (kk05kk09, kk11)—exactly where mental truth propagation fails; code-assisted maps each sentence to biconditionals and lets `python-constraint` enumerate for **12/12** and **+50 percentage points**. Correctness no longer depends on the models own reasoning strength. There is some run-to-run noise on individual items, but “pure ≪ code-assisted” is stable.
> **Model ↔ harness tradeoff**: stronger models need thinner harnesses; weaker models need more (e.g. offload logic to code/solvers). With weak `gpt-4o-mini` the contrast is visible; with strong reasoners like `gpt-5.6-luna`, pure thinking can also full-solve and code gains can go to 0. Code-assisted (and offline solver) turn correctness into something **deterministic and model-strength-independent**.
#### Example constraint code (model-generated, kk11, 5 people + count)
Stem: A says “B is a knight”; B says “C is a knave”; C says “D is a knight”; D says “E is a knave”;
E says “at least two of us five are knights”.
```python
from constraint import Problem
p = Problem()
for name in ['A', 'B', 'C', 'D', 'E']:
p.addVariable(name, [True, False]) # True=knight (truth), False=knave (lie)
# Each sentence: X == (truth value of the claim)
p.addConstraint(lambda a, b: a == (b == True), ['A', 'B']) # A:"B is knight"
p.addConstraint(lambda b, c: b == (c == False), ['B', 'C']) # B:"C is knave"
p.addConstraint(lambda c, d: c == (d == True), ['C', 'D']) # C:"D is knight"
p.addConstraint(lambda d, e: d == (e == False), ['D', 'E']) # D:"E is knave"
p.addConstraint(lambda a, b, c, d, e: e == ((a + b + c + d + e) >= 2),
['A', 'B', 'C', 'D', 'E']) # E:"at least two knights"
for s in p.getSolutions():
print({k: ('knight' if v else 'knave') for k, v in s.items()})
# Output: {'A': 'knight', 'B': 'knight', 'C': 'knave', 'D': 'knave', 'E': 'knight'}
```
The solver enumerates \(2^5=32\) assignments and returns the unique solution—the kind of chain pure thinking most often gets wrong.
### Notes
- **Cost**: default `gpt-4o-mini` (weaker model for contrast); 12 puzzles × two modes is cheap; override with `MODEL` / `--model`.
- **API key**: `OPENAI_API_KEY` or `OPENROUTER_API_KEY` from env / `.env`; `MODEL` to switch models.
- **Sandbox**: `sandbox.py` uses subprocess + timeout—teaching minimal sandbox; production should use containers/gVisor etc.
- **Puzzle reliability**: `build_puzzles.py` solves each puzzle (curated or random) with `python-constraint` and asserts unique solution before writing; extend via `CURATED` or `--generate`.
---
## 中文
### 概述
本实验评估 Agent 通过**约束求解**代码来辅助逻辑思考的能力:为同一个 LLM 配备一个预装
`python-constraint` 的 Code Interpreter,让它把「骑士与无赖」(Knights & Knaves) 逻辑谜题
转化为形式化的**约束满足问题(CSP)**——识别变量(每个岛民是骑士还是无赖)、定义约束
(“骑士说真话、无赖说假话”),再调用求解器搜索满足所有约束的解。
我们用一组 12 道 K&K 谜题(2~5 人,均带唯一真值解)对比三种模式:
- **纯思考(pure)**:LLM 只用自然语言链式推理,直接给答案;
- **代码辅助(code)**LLM 用 `run_python` 工具写约束模型并调求解器,再据结果作答;
- **约束求解(solver)****离线基线**,直接用 `python-constraint` 求解结构化陈述,
不需要任何 API/网络——它是确定性求解器路径本身,理论上 100% 正确,用来验证
「把谜题翻译成约束程序并求解」这一核心论点(见下方真实运行结果)。
### 核心思想:为什么代码辅助更强
K&K 谜题的关键建模规则只有一条——对每位居民 X 加一条**双条件(等价)约束**:
```
X 是骑士(True) <=> X 说的那句话为真
```
`X == (该陈述的语义真值)`。把它交给确定性求解器**穷举**所有布尔组合,逻辑上不会出错;
而纯思考在多人、含计数(“恰好两个骑士”)或自指(“我和 B 同类”)的谜题上,很容易在心算
真值传播时出错。
### 文件说明
| 文件 | 作用 |
| --- | --- |
| `demo.py` | 主程序:跑 纯思考/代码辅助/约束求解 的对照实验,打印准确率对比表 |
| `csp_solver.py` | 离线约束求解器:结构化陈述 DSL + `python-constraint` 求解(供 demo 的 solver 模式与 build_puzzles 校验共用) |
| `sandbox.py` | 极简 Code Interpreter:子进程沙箱执行模型生成的 Python(预装 python-constraint) |
| `puzzles.json` | 12 道谜题的题面 + 结构化陈述 + 唯一真值解(给 LLM 的只有题面) |
| `build_puzzles.py` | 生成/校验谜题:用 `python-constraint` 求解并断言每题“解唯一”,可导出精选题或随机生成 |
| `requirements.txt` | 依赖(openai + python-constraint) |
| `env.example` | 环境变量样例 |
| `last_run.json` | 每次运行后自动保存的逐题完整记录(含模型生成的代码),便于复盘 |
### 快速开始
```bash
# 在仓库根目录使用统一的第 5 章环境
uv sync --locked --python 3.12 --extra ch5
# 切换目录前先激活环境:
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell.\.venv\Scripts\Activate.ps1
# Windows cmd.venv\Scripts\activate.bat
# 未安装 uv 时可用 pip 兜底:
# python -m pip install -e ".[ch5]"
cd chapter5/code-for-logic
# 迁移期间仍支持单项目兼容路径:
# python -m pip install -r requirements.txt
```
#### 1) 离线约束求解基线(不需要 API Key,推荐先跑)
```bash
python demo.py --mode solver # 用 python-constraint 离线求解全部 12 题
python demo.py --mode solver --min-people 4 # 只跑 >=4 人的难题
```
这条路径完全离线、确定性,直接演示「谜题→约束程序→求解」的核心论点,准确率 100%。
#### 2) LLM 对照实验(需要 OPENAI_API_KEY 或 OPENROUTER_API_KEY
```bash
cp env.example .env # 然后编辑 .env 填入 OPENAI_API_KEY
# 或直接 export OPENAI_API_KEY=your-openai-api-key
python demo.py # 默认 both:纯思考 vs 代码辅助,全部 12 题
python demo.py --mode pure # 只跑纯思考基线
python demo.py --limit 4 # 只跑前 4 题(省钱冒烟测试)
python demo.py --max-people 3 # 只跑 <=3 人的谜题(按难度筛选)
python demo.py --model gpt-4o-mini # 指定模型(默认 gpt-4o-mini)
python demo.py --puzzles my.json --output run.json # 换数据集/输出路径
```
**通用 OpenRouter 兜底**:未配置 `OPENAI_API_KEY` 时,只要设置了 `OPENROUTER_API_KEY`
即自动改走 OpenRouter`gpt-*``openai/*`)。默认模型 `gpt-4o-mini` 是普通 gpt id,可
直连 OpenAI;仅当把 `--model` 换成 `gpt-5.x` 这类需组织实名认证的模型、且设置了
`OPENROUTER_API_KEY` 时,才会优先走 OpenRouter。
完整参数见 `python demo.py --help`(中文说明)。
#### 3) 生成/扩充谜题数据集
```bash
python build_puzzles.py # 导出内置 12 道精选题(默认)
python build_puzzles.py --generate 20 --min-people 3 --max-people 5 --seed 7
python build_puzzles.py --generate 20 --output my.json
```
随机生成器会用 `python-constraint` 求解每个候选谜题,只保留「解唯一」的题目。
`sandbox.py` / `csp_solver.py` 也可单独运行做自测:
`python sandbox.py``python csp_solver.py` 都会用 python-constraint 求解一道最简谜题。
### 真实运行结果(一):离线约束求解基线(`--mode solver`,无需 API
`python demo.py --mode solver` 的真实输出(12 道精选题,完全离线、确定性):
```
== 约束求解(solver,离线) ==
[solver] kk01 (2人) ✓ 解数=1 预测={'A': 'knight', 'B': 'knave'}
[solver] kk05 (3人) ✓ 解数=1 预测={'A': 'knave', 'B': 'knave', 'C': 'knight'}
[solver] kk11 (5人) ✓ 解数=1 预测={'A': 'knight', 'B': 'knight', 'C': 'knave', 'D': 'knave', 'E': 'knight'}
...(其余题略)
------------------------------------------------------------
准确率 100.0%
============================================================
约束求解 准确率: 100.0% (12/12)
```
这条路径把每题的结构化陈述翻译成 `python-constraint` 约束并穷举求解,12/12 全对——
它直接证明了「谜题→约束程序→求解」的确定性;LLM 只要把谜题正确翻译成同样的约束,
就能拿到同样 100% 的结果(下节)。随机生成的谜题(`build_puzzles.py --generate`)经
solver 复核同样 100% 解出且与生成时的唯一解一致。
### 真实运行结果(二):LLM 对照实验(gpt-4o-mini12 题)
```
准确率对比表
============================================================
题号 人数 纯思考 代码辅助
------------------------------------------------------------
kk01 2 ✓ ✓
kk02 2 ✓ ✓
kk03 2 ✓ ✓
kk04 3 ✓ ✓
kk05 3 ✗ ✓
kk06 3 ✗ ✓
kk07 3 ✗ ✓
kk08 4 ✗ ✓
kk09 4 ✗ ✓
kk10 4 ✓ ✓
kk11 5 ✗ ✓
kk12 5 ✓ ✓
------------------------------------------------------------
准确率 50.0% 100.0%
============================================================
纯思考 准确率: 50.0% (6/12)
代码辅助 准确率: 100.0% (12/12)
提升(代码辅助 - 纯思考): +50.0 个百分点
```
> 说明:这里刻意选用能力较弱的 `gpt-4o-mini` 来暴露对照——纯思考只做对了 **6/12
> (50%)**,且错误集中在 3 人及以上、含计数/自指的谜题上(kk05~kk09、kk11),正是心算
> 真值传播最容易出错的题型;而代码辅助把每句话翻译成双条件约束、交给 `python-constraint`
> 穷举求解,**12/12 全对**,一举把准确率拉满,净提升 **+50 个百分点**。这正是本实验想
> 说明的核心:把逻辑外包给确定性求解器,正确性不再依赖模型自己的推理强弱。`gpt-4o-mini`
> 有一定随机性,多次运行个别题目可能有小幅波动,但“纯思考明显低于代码辅助”的整体格局稳定。
> **模型与脚手架(harness)是此消彼长的关系**:模型足够强时,脚手架可以更薄——模型自己
> 就能算对;模型不够强时,就需要在脚手架里做更多事(如把逻辑交给代码/求解器)来兜住
> 正确性。本实验刻意用较弱的 `gpt-4o-mini`,正是为了让这一对照可见——换成 `gpt-5.6-luna`
> 这类强推理模型,纯思考也能全解,代码增益会收敛为 0。换句话说,代码辅助(乃至离线
> solver)真正的价值,是把正确性变成**确定性、与模型强弱无关**:对更弱的模型或更大/更难
> 的谜题,纯思考会随人数增加而掉分,而“翻译成约束程序 + 求解器穷举”的路径始终稳定给出正确解。
#### 一道谜题的约束建模代码(模型自动生成,kk11,5 人链式+计数)
题面:A 说“B 是骑士”;B 说“C 是无赖”;C 说“D 是骑士”;D 说“E 是无赖”;
E 说“我们五人当中至少有两个骑士”。
```python
from constraint import Problem
p = Problem()
for name in ['A', 'B', 'C', 'D', 'E']:
p.addVariable(name, [True, False]) # True=骑士(说真话), False=无赖(说假话)
# 每句话都写成「X == (那句话的真值)」的双条件约束
p.addConstraint(lambda a, b: a == (b == True), ['A', 'B']) # A:"B 是骑士"
p.addConstraint(lambda b, c: b == (c == False), ['B', 'C']) # B:"C 是无赖"
p.addConstraint(lambda c, d: c == (d == True), ['C', 'D']) # C:"D 是骑士"
p.addConstraint(lambda d, e: d == (e == False), ['D', 'E']) # D:"E 是无赖"
p.addConstraint(lambda a, b, c, d, e: e == ((a + b + c + d + e) >= 2),
['A', 'B', 'C', 'D', 'E']) # E:"至少两个骑士"
for s in p.getSolutions():
print({k: ('knight' if v else 'knave') for k, v in s.items()})
# 输出: {'A': 'knight', 'B': 'knight', 'C': 'knave', 'D': 'knave', 'E': 'knight'}
```
求解器直接穷举 2^5=32 种组合,返回满足全部约束的唯一解——这正是纯思考在链式真值
传播中最容易算错的题型。
### 注意事项
- **成本**:默认 `gpt-4o-mini`(刻意选用较弱模型以显现对照,见上文),跑完 12 题两种模式的开销很小;用 `MODEL`/`--model` 可换更便宜或更强的模型。
- **API Key**:从环境变量或 `.env``OPENAI_API_KEY`(或 `OPENROUTER_API_KEY` 兜底);用 `MODEL` 可换模型。
- **沙箱**`sandbox.py` 用子进程 + 超时执行代码,属教学用极简沙箱;生产环境应换成
容器/gVisor 等更强隔离。
- **谜题可靠性**`build_puzzles.py``python-constraint` 求解每题(内置精选题或随机生成)
断言“解唯一”后才写出,确保真值解无歧义;想自己加题就改 `CURATED` 或用 `--generate`
---
## Notes / 说明
- Run `--mode solver` first for a free offline baseline. / 建议先跑 `--mode solver` 离线基线。
- Commands, code, paths, and env vars are identical in both language sections. / 命令、代码、路径与环境变量在中英文两侧保持一致。
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""Build the frozen Experiment 5-2 test set from the named Hugging Face dataset.
The manuscript explicitly names K-and-K/perturbed-knights-and-knaves. This
builder downloads a revision-pinned, stratified sample from every test
perturbation and every 2--8-person difficulty cell. It retains source identity
and hashes and independently checks every published label with the local
python-constraint implementation before writing the benchmark JSON.
"""
from __future__ import annotations
import argparse
import ast
import hashlib
import json
import random
import urllib.request
from pathlib import Path
from typing import Any
from csp_solver import solve_labeled
DATASET = "K-and-K/perturbed-knights-and-knaves"
REVISION = "bc7ee75a15ee8196ccbdb7df3ab46284340412e2"
LICENSE = "CC-BY-NC-SA-4.0"
PERTURBATIONS = (
"perturbed_leaf",
"perturbed_statement",
"reorder_statement",
"random_pair",
"uncommon_name",
"flip_role",
)
def _source_path(perturbation: str, people: int) -> str:
return f"test/{perturbation}/people{people}_num100.jsonl"
def _download(path: str) -> bytes:
url = (
"https://huggingface.co/datasets/"
f"{DATASET}/resolve/{REVISION}/{path}?download=true"
)
request = urllib.request.Request(url, headers={"User-Agent": "ai-agent-book-exp5-2/1.0"})
with urllib.request.urlopen(request, timeout=60) as response:
return response.read()
def convert_expression(node: Any, names: list[str]) -> list[Any]:
"""Convert the dataset's published tuple AST into the lab's JSON DSL."""
if not isinstance(node, tuple) or not node:
raise ValueError(f"invalid statement AST node: {node!r}")
tag = node[0]
if tag in {"lying", "telling-truth"}:
if len(node) != 2 or not isinstance(node[1], int):
raise ValueError(f"invalid identity node: {node!r}")
role = "knave" if tag == "lying" else "knight"
return ["is", names[node[1]], role]
if tag == "not" and len(node) == 2:
return ["not", convert_expression(node[1], names)]
binary = {"and": "and", "or": "or", "->": "implies", "<=>": "iff"}
if tag in binary and len(node) == 3:
return [
binary[tag],
convert_expression(node[1], names),
convert_expression(node[2], names),
]
raise ValueError(f"unsupported statement AST node: {node!r}")
def convert_row(
row: dict[str, Any], *, perturbation: str, people: int, source_path: str,
source_sha256: str, source_row: int,
) -> dict[str, Any]:
names = list(row["names"])
if len(names) != people:
raise ValueError(f"row {source_row}: expected {people} names, got {len(names)}")
statements = ast.literal_eval(row["statements"])
if not isinstance(statements, tuple) or len(statements) != len(names):
raise ValueError(f"row {source_row}: statement count does not match names")
structs = {
speaker: convert_expression(statement, names)
for speaker, statement in zip(names, statements)
}
gold = {
name: ("knight" if truth else "knave")
for name, truth in zip(names, row["solution"])
}
independently_solved = solve_labeled(names, structs)
if len(independently_solved) != 1 or independently_solved[0] != gold:
raise ValueError(
f"row {source_row}: published label failed independent CSP check: "
f"gold={gold!r}, solved={independently_solved!r}"
)
return {
"id": f"{perturbation}-p{people}-r{source_row:03d}",
"num_people": people,
"names": names,
"description": row["quiz"],
"solution": gold,
"statements_struct": structs,
"source": {
"dataset": DATASET,
"revision": REVISION,
"license": LICENSE,
"config": "test",
"split": perturbation,
"path": source_path,
"file_sha256": source_sha256,
"row": source_row,
"dataset_index": row.get("index"),
},
}
def build(*, per_cell: int, seed: int) -> tuple[list[dict[str, Any]], dict[str, Any]]:
if not 1 <= per_cell <= 100:
raise ValueError("per_cell must be between 1 and 100")
puzzles: list[dict[str, Any]] = []
files: list[dict[str, Any]] = []
for perturbation in PERTURBATIONS:
for people in range(2, 9):
path = _source_path(perturbation, people)
raw = _download(path)
sha256 = hashlib.sha256(raw).hexdigest()
rows = [json.loads(line) for line in raw.decode("utf-8").splitlines() if line]
if len(rows) < per_cell:
raise ValueError(
f"{path}: only {len(rows)} published rows, cannot sample {per_cell}"
)
cell_seed = int.from_bytes(
hashlib.sha256(f"{seed}:{path}".encode()).digest()[:8], "big"
)
indices = sorted(random.Random(cell_seed).sample(range(len(rows)), per_cell))
for index in indices:
puzzles.append(convert_row(
rows[index], perturbation=perturbation, people=people,
source_path=path, source_sha256=sha256, source_row=index,
))
files.append({
"path": path,
"sha256": sha256,
"published_rows": len(rows),
"sampled_rows": indices,
})
manifest = {
"schema_version": "1.0",
"experiment": "5-2",
"dataset": DATASET,
"revision": REVISION,
"license": LICENSE,
"sampling": {
"split": "test",
"perturbations": list(PERTURBATIONS),
"people": list(range(2, 9)),
"per_cell": per_cell,
"seed": seed,
"cells": len(PERTURBATIONS) * 7,
"total": len(puzzles),
},
"source_files": files,
"label_validation": "all rows independently solved with python-constraint",
}
return puzzles, manifest
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--per-cell", type=int, default=2)
parser.add_argument("--seed", type=int, default=512)
parser.add_argument("--output", type=Path, default=Path("hf_test_stratified_84.json"))
parser.add_argument("--manifest", type=Path, default=Path("hf_test_stratified_84.manifest.json"))
args = parser.parse_args()
puzzles, manifest = build(per_cell=args.per_cell, seed=args.seed)
args.output.write_text(json.dumps(puzzles, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
args.manifest.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({
"output": str(args.output), "manifest": str(args.manifest),
"puzzles": len(puzzles), "revision": REVISION,
}, ensure_ascii=False))
if __name__ == "__main__":
main()
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
生成/校验「骑士与无赖」(Knights and Knaves)谜题,并导出 puzzles.json。
每道谜题的每句话都用 csp_solver.py 里的结构化 DSL 表示(见该文件顶部说明),
既能渲染成中文题面(给 LLM 看),也能直接翻译成 python-constraint 约束来求解。
本脚本用 python-constraint 校验每题「解唯一」后才写出——这确保真值解无歧义,
同时演示了实验 5-2 的核心:把谜题形式化为 CSP 并用求解器离线求解。
约定:骑士(knight)永远说真话,无赖(knave)永远说假话。t[name]=True 表示骑士。
用法:
python build_puzzles.py # 导出内置的 12 道精选谜题(默认)
python build_puzzles.py --generate 20 # 随机生成 20 道解唯一的谜题
python build_puzzles.py --generate 20 --min-people 3 --max-people 5 --seed 7
python build_puzzles.py --output my.json # 指定输出文件
"""
import argparse
import json
import random
from csp_solver import render_nl, solve, solve_labeled
# 每题:id, 名字列表, dict{name: 结构化陈述}。中文题面由结构化陈述自动渲染,
# 但精选题保留手写的更自然的中文(见 STATEMENTS_NL 覆盖)。
CURATED = [
("kk01", ["A", "B"], {
"A": ["is", "B", "knave"],
"B": ["and", ["is", "A", "knave"], ["is", "B", "knave"]]}),
("kk02", ["A", "B"], {
"A": ["same", "A", "B"],
"B": ["diff", "A", "B"]}),
("kk03", ["A", "B"], {
"A": ["count", "knight", ">=", 1],
"B": ["is", "A", "knave"]}),
("kk04", ["A", "B", "C"], {
"A": ["is", "B", "knave"],
"B": ["is", "C", "knave"],
"C": ["and", ["is", "A", "knave"], ["is", "B", "knave"]]}),
("kk05", ["A", "B", "C"], {
"A": ["is", "B", "knight"],
"B": ["is", "C", "knave"],
"C": ["same", "A", "B"]}),
("kk06", ["A", "B", "C"], {
"A": ["same", "B", "C"],
"B": ["is", "A", "knave"],
"C": ["same", "C", "A"]}),
("kk07", ["A", "B", "C"], {
"A": ["or", ["is", "A", "knave"], ["is", "B", "knight"]],
"B": ["is", "A", "knight"],
"C": ["is", "B", "knave"]}),
("kk08", ["A", "B", "C", "D"], {
"A": ["same", "B", "D"],
"B": ["is", "C", "knave"],
"C": ["is", "D", "knight"],
"D": ["diff", "B", "C"]}),
("kk09", ["A", "B", "C", "D"], {
"A": ["is", "B", "knight"],
"B": ["is", "C", "knave"],
"C": ["is", "D", "knight"],
"D": ["diff", "A", "B"]}),
("kk10", ["A", "B", "C", "D"], {
"A": ["count", "knave", ">=", 3],
"B": ["is", "A", "knave"],
"C": ["is", "B", "knight"],
"D": ["is", "C", "knave"]}),
("kk11", ["A", "B", "C", "D", "E"], {
"A": ["is", "B", "knight"],
"B": ["is", "C", "knave"],
"C": ["is", "D", "knight"],
"D": ["is", "E", "knave"],
"E": ["count", "knight", ">=", 2]}),
("kk12", ["A", "B", "C", "D", "E"], {
"A": ["is", "B", "knight"],
"B": ["is", "C", "knave"],
"C": ["is", "D", "knave"],
"D": ["is", "E", "knight"],
"E": ["same", "A", "C"]}),
]
# 精选题的手写中文题面(比自动渲染更自然)。未覆盖的句子回退到 render_nl。
STATEMENTS_NL = {
("kk01", "B"): "我们两人都不是骑士。",
("kk02", "A"): "我和 B 是同一类人(要么都是骑士,要么都是无赖)。",
("kk02", "B"): "我和 A 是不同类人。",
("kk03", "A"): "我们当中至少有一个骑士。",
("kk04", "C"): "A 和 B 都是无赖。",
("kk06", "C"): "我和 A 是同一类人。",
("kk07", "A"): "我是无赖,或者 B 是骑士。",
("kk09", "D"): "A 和 B 不是同一类人。",
("kk10", "A"): "我们四人当中至少有三个无赖。",
("kk11", "E"): "我们五人当中至少有两个骑士。",
("kk12", "E"): "A 和 C 是同一类人。",
}
def build_puzzle(pid, names, structs, nl_overrides=None):
"""求解校验(要求解唯一)并组装成写入 puzzles.json 的一条记录。"""
sols = solve_labeled(names, structs)
if len(sols) != 1:
raise ValueError(f"{pid} 解不唯一: {len(sols)} 个解 -> {sols}")
solution = sols[0]
nl_overrides = nl_overrides or {}
statements = {n: nl_overrides.get(n, render_nl(structs[n])) for n in names}
lines = [f"{n}: 「{statements[n]}" for n in names]
desc = (
f"这座岛上有 {len(names)} 位居民:{', '.join(names)}"
"每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。"
"他们各自说了如下的话:\n" + "\n".join(lines)
)
return dict(id=pid, num_people=len(names), names=names,
statements=statements, statements_struct=structs,
description=desc, solution=solution)
# ---------------- 随机生成器 ----------------
def _random_stmt(speaker, names, rng):
"""为 speaker 随机生成一句合法的结构化陈述。"""
others = [n for n in names if n != speaker]
# Solo resident: only count statements are valid (no "others" to name).
if not others:
role = rng.choice(["knight", "knave"])
op = rng.choice([">=", "<=", "=="])
return ["count", role, op, rng.randint(1, len(names))]
kind = rng.choice(["is", "is", "same", "diff", "count"])
if kind == "is":
return ["is", rng.choice(others), rng.choice(["knight", "knave"])]
if kind == "same":
return ["same", speaker, rng.choice(others)]
if kind == "diff":
return ["diff", speaker, rng.choice(others)]
# count:全体中某角色的人数满足某比较
role = rng.choice(["knight", "knave"])
op = rng.choice([">=", "<=", "=="])
k = rng.randint(1, len(names))
return ["count", role, op, k]
def generate(count, min_people, max_people, seed):
"""随机生成 count 道「解唯一」的谜题(用 python-constraint 过滤)。"""
rng = random.Random(seed)
names_pool = ["A", "B", "C", "D", "E", "F", "G"]
puzzles = []
attempts = 0
while len(puzzles) < count and attempts < count * 2000:
attempts += 1
n = rng.randint(min_people, max_people)
names = names_pool[:n]
structs = {sp: _random_stmt(sp, names, rng) for sp in names}
if len(solve(names, structs)) != 1: # 只保留解唯一的谜题
continue
pid = f"gen{len(puzzles) + 1:03d}"
puzzles.append(build_puzzle(pid, names, structs))
if len(puzzles) < count:
print(f"警告:{attempts} 次尝试只生成了 {len(puzzles)}/{count} 道解唯一的谜题。")
return puzzles
def build_curated():
out = []
for pid, names, structs in CURATED:
nl = {n: STATEMENTS_NL[(pid, n)]
for n in names if (pid, n) in STATEMENTS_NL}
out.append(build_puzzle(pid, names, structs, nl))
return out
def main():
ap = argparse.ArgumentParser(
description="生成/校验骑士与无赖谜题并导出 puzzles.json"
"(用 python-constraint 离线求解,校验每题解唯一)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__)
ap.add_argument("--generate", type=int, metavar="N", default=0,
help="随机生成 N 道解唯一的谜题(默认 0=导出内置 12 道精选题)")
ap.add_argument("--min-people", type=int, default=2,
help="随机生成时每题最少居民数(默认 2)")
ap.add_argument("--max-people", type=int, default=5,
help="随机生成时每题最多居民数(难度上限,默认 5)")
ap.add_argument("--seed", type=int, default=42,
help="随机种子,保证可复现(默认 42)")
ap.add_argument("--output", default="puzzles.json",
help="输出文件路径(默认 puzzles.json)")
args = ap.parse_args()
if args.generate > 0:
print(f"随机生成 {args.generate} 道谜题"
f"({args.min_people}~{args.max_people} 人,seed={args.seed})...")
out = generate(args.generate, args.min_people, args.max_people, args.seed)
else:
out = build_curated()
for p in out:
print(f"{p['id']}: OK 唯一解 = {p['solution']}")
with open(args.output, "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
print(f"\n已写出 {len(out)} 题到 {args.output}")
if __name__ == "__main__":
main()
+127
View File
@@ -0,0 +1,127 @@
"""
离线约束求解器:把「骑士与无赖」谜题的结构化陈述翻译成约束满足问题(CSP),
用 python-constraint 库求解——这是实验 5-2 想论证的「代码求解」路径的确定性参考实现。
它不依赖任何 LLM / 网络,可完全离线运行,因此既用于 build_puzzles.py 校验谜题
「解唯一」,也用于 demo.py 的 solver 模式给出约束求解基线(理论上 100% 正确)。
【结构化陈述 DSL】每句话用一个 JSON 可序列化的列表表示,节点形式如下
(True=骑士/说真话,False=无赖/说假话):
["is", target, "knight"|"knave"] # target 是骑士 / 无赖
["same", a, b] # a 和 b 是同一类人
["diff", a, b] # a 和 b 是不同类人
["count", "knight"|"knave", op, k] # 全体中该角色的人数 op k, op ∈ {">=","<=","=="}
["and", s1, s2] # 合取
["or", s1, s2] # 析取
["not", s1] # 否定
["implies", s1, s2] # 蕴含 s1 -> s2
["iff", s1, s2] # 双条件 s1 <-> s2
关键建模规则:对每位说话者 X 加一条【双条件约束】 `t[X] == eval_stmt(X 的话)`——
X 是骑士当且仅当他的话为真。绝不能把话本身当作硬约束。
"""
from constraint import Problem
_OPS = {">=": lambda a, b: a >= b,
"<=": lambda a, b: a <= b,
"==": lambda a, b: a == b}
def eval_stmt(node, t):
"""在赋值 t(name->bool, True=骑士) 下求某句话的语义真值。"""
tag = node[0]
if tag == "is":
_, target, role = node
return t[target] if role == "knight" else (not t[target])
if tag == "same":
return t[node[1]] == t[node[2]]
if tag == "diff":
return t[node[1]] != t[node[2]]
if tag == "count":
_, role, op, k = node
want = (role == "knight")
cnt = sum(1 for v in t.values() if v == want)
return _OPS[op](cnt, k)
if tag == "and":
return eval_stmt(node[1], t) and eval_stmt(node[2], t)
if tag == "or":
return eval_stmt(node[1], t) or eval_stmt(node[2], t)
if tag == "not":
return not eval_stmt(node[1], t)
if tag == "implies":
return (not eval_stmt(node[1], t)) or eval_stmt(node[2], t)
if tag == "iff":
return eval_stmt(node[1], t) == eval_stmt(node[2], t)
raise ValueError(f"未知的陈述节点: {node!r}")
def solve(names, structs):
"""用 python-constraint 求解,返回所有满足约束的赋值(dict name->bool)列表。
names : 居民名字列表
structs : dict name -> 该居民陈述的结构化 DSL
"""
problem = Problem()
for n in names:
problem.addVariable(n, [True, False])
# 对每位说话者加一条双条件约束:t[X] == (X 的话为真)
for speaker in names:
stmt = structs.get(speaker)
if stmt is None:
continue
def make_constraint(speaker=speaker, stmt=stmt):
def constraint(*values):
t = dict(zip(names, values))
return t[speaker] == eval_stmt(stmt, t)
return constraint
problem.addConstraint(make_constraint(), names)
return problem.getSolutions()
def solve_labeled(names, structs):
"""求解并把布尔解转成 {name: 'knight'/'knave'}。返回解列表(通常唯一)。"""
out = []
for sol in solve(names, structs):
out.append({n: ("knight" if sol[n] else "knave") for n in names})
return out
def render_nl(node):
"""把结构化陈述渲染成中文题面(供随机生成的谜题使用)。"""
tag = node[0]
if tag == "is":
role = "骑士" if node[2] == "knight" else "无赖"
return f"{node[1]}{role}"
if tag == "same":
return f"{node[1]}{node[2]} 是同一类人。"
if tag == "diff":
return f"{node[1]}{node[2]} 是不同类人。"
if tag == "count":
role = "骑士" if node[1] == "knight" else "无赖"
word = {">=": "至少", "<=": "至多", "==": "恰好"}[node[2]]
return f"我们当中{word}{node[3]}{role}"
if tag == "and":
return f"{render_nl(node[1])[:-1]},并且 {render_nl(node[2])}"
if tag == "or":
return f"{render_nl(node[1])[:-1]},或者 {render_nl(node[2])}"
if tag == "not":
return f"以下说法不成立:{render_nl(node[1])}"
if tag == "implies":
return f"如果 {render_nl(node[1])[:-1]},那么 {render_nl(node[2])}"
if tag == "iff":
return f"{render_nl(node[1])[:-1]},当且仅当 {render_nl(node[2])}"
raise ValueError(f"未知的陈述节点: {node!r}")
if __name__ == "__main__":
# 自测:kk01 —— A 说"B 是无赖"B 说"我们都不是骑士"
names = ["A", "B"]
structs = {
"A": ["is", "B", "knave"],
"B": ["and", ["is", "A", "knave"], ["is", "B", "knave"]],
}
print("求解结果:", solve_labeled(names, structs))
+640
View File
@@ -0,0 +1,640 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
实验 5-2:用代码生成工具提升逻辑思考能力
对比在三种模式下求解「骑士与无赖」(Knights & Knaves) 谜题的准确率:
1) 纯思考(pure) —— LLM 仅靠自然语言链式推理直接给出答案;
2) 代码辅助(code) —— LLM 配备 Code Interpreter(预装 python-constraint)
把谜题形式化为约束满足问题(CSP),调用求解器搜索答案;
3) 约束求解(solver) —— 【离线,无需 API】直接用 python-constraint 求解结构化
陈述,作为确定性基线(理论上 100% 正确)。
结论预期:约束求解把逻辑推理外包给确定性求解器,准确率应达 90%+
且显著高于纯思考模式(纯思考在多人、含计数/自指的谜题上容易出错)。
用法:
# 离线约束求解基线(不花钱、不联网,演示核心论点):
python demo.py --mode solver
# LLM 对照实验(需要 OPENAI_API_KEY)
export OPENAI_API_KEY=your-openai-api-key
python demo.py # 默认 both:跑 纯思考 vs 代码辅助 全部题目
python demo.py --mode pure # 只跑纯思考
python demo.py --limit 4 # 只跑前 4 题(省钱冒烟测试)
python demo.py --max-people 3 # 只跑不超过 3 人的谜题(按难度筛选)
python demo.py --model gpt-4o-mini # 指定模型(默认 gpt-4o-mini)
python demo.py --puzzles my.json # 换一份谜题数据集
"""
import argparse
import datetime as dt
import json
import math
import os
import re
import sys
import time
from pathlib import Path
from csp_solver import solve_labeled
from sandbox import run_python
# ---- 读取 .env(如果存在)。避免额外依赖,手写一个极简解析器。----
def _load_dotenv(path=".env"):
if not os.path.exists(path):
return
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
_load_dotenv()
MODEL = os.environ.get("MODEL", "gpt-4o-mini")
PROVIDER = "unknown"
# --- 通用 OpenRouter 兜底:无直连 key 时自动改走 OpenRouter ---
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
def map_model_to_openrouter(model: str) -> str:
"""把直连模型名映射为 OpenRouter 上的 id(非可映射 id 统一兜底到当前廉价旗舰)。"""
if not model or "/" in model:
return model or "openai/gpt-5.6-luna"
m = model.lower()
if m.startswith(("gpt-", "o1", "o3", "o4")):
return "openai/" + model
if m.startswith("claude"):
if "haiku" in m:
return "anthropic/claude-haiku-4.5"
if "sonnet" in m:
return "anthropic/claude-sonnet-4.6"
return "anthropic/claude-opus-4.8"
if m.startswith("gemini"):
return "google/" + model
return "openai/gpt-5.6-luna"
def build_client_and_model(provider="auto"):
"""构造 OpenAI 客户端并返回 (client, model)。
- 有 OPENAI_API_KEY:直连(默认模型 gpt-4o-mini 是普通 gpt id,可直连 OpenAI)。
仅当模型是 gpt-5.x 且同时设置了 OPENROUTER_API_KEY 时才优先走 OpenRouter
(直连 gpt-5.x 需组织实名认证)。
- 无 OPENAI_API_KEY 但有 OPENROUTER_API_KEY:整体改走 OpenRouter。
"""
from openai import OpenAI
global MODEL, PROVIDER
choices = {
"ollama": ("ollama", os.environ.get(
"OLLAMA_BASE_URL", "http://127.0.0.1:11434/v1"
), MODEL),
"openai": (os.environ.get("OPENAI_API_KEY"),
os.environ.get("OPENAI_BASE_URL"), MODEL),
"openrouter": (os.environ.get("OPENROUTER_API_KEY"),
OPENROUTER_BASE_URL, map_model_to_openrouter(MODEL)),
"moonshot": (os.environ.get("MOONSHOT_API_KEY"),
"https://api.moonshot.cn/v1", MODEL),
"ark": (os.environ.get("ARK_API_KEY"),
"https://ark.cn-beijing.volces.com/api/v3", MODEL),
}
if provider == "auto":
provider = next(
(name for name in ("openai", "openrouter", "moonshot", "ark")
if choices[name][0]),
"openai",
)
if provider not in choices:
raise ValueError(f"unsupported provider: {provider}")
api_key, base_url, MODEL = choices[provider]
if not api_key:
raise SystemExit(f"错误:provider={provider} 缺少对应 API key")
PROVIDER = provider
kw = {"api_key": api_key, "timeout": 180.0, "max_retries": 5}
if base_url:
kw["base_url"] = base_url
return OpenAI(**kw), MODEL
def _reasoning(model: str) -> bool:
"""推理模型(gpt-5 / o 系列 / *thinking 等)不接受 temperature=0。"""
return any(k in (model or "").lower()
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
# run_python 工具的 function calling 定义
TOOLS = [{
"type": "function",
"function": {
"name": "run_python",
"description": (
"在预装了 python-constraint 库的沙箱中执行 Python 代码,返回 stdout/stderr。"
"用它把逻辑谜题建模为约束满足问题并求解。记得用 print() 打印结果。"
),
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "要执行的完整 Python 代码"}
},
"required": ["code"],
},
},
}]
ANSWER_HINT = (
'推理结束后,请在最后单独用一行输出 JSON 形式的最终答案,'
'键为每个居民的名字,值为 "knight""knave",例如:'
'{"A": "knight", "B": "knave"}'
)
PURE_SYSTEM = (
"你是逻辑推理专家。在「骑士与无赖」谜题中,骑士永远说真话,无赖永远说假话。"
"请仅凭自己的推理,逐步分析每位居民的身份,找出满足所有陈述的唯一解。\n" + ANSWER_HINT
)
CODE_SYSTEM = (
"你是逻辑推理专家,擅长把谜题转化为形式化约束并用代码求解。"
"在「骑士与无赖」谜题中,骑士永远说真话,无赖永远说假话。\n"
"请务必使用 run_python 工具,用 python-constraint 库把谜题建模为约束满足问题(CSP)来求解。\n\n"
"【最关键的建模规则】不要把某人的陈述直接当成事实约束!"
"正确做法是对每位居民 X 加一条【双条件(等价)约束】:\n"
" X 的布尔值 == (X 那句话在语义上为真)\n"
"含义:X 是骑士(True) 当且仅当 他的话为真;X 是无赖(False) 当且仅当 他的话为假。\n"
"这条规则对每一句话都适用,包括计数类('恰好有两个骑士')和自指类('我和 B 同类')的陈述——"
"都要写成 `X == (那句话的真值表达式)`,绝不能把 `(那句话的真值表达式)` 单独当作硬约束。\n\n"
"示例(设 True=骑士)\n"
" from constraint import Problem\n"
" p = Problem()\n"
" for name in ['A','B','C']:\n"
" p.addVariable(name, [True, False])\n"
" # A 说'我们中恰好有一个骑士' -> A == ( (A+B+C)==1 )\n"
" p.addConstraint(lambda a,b,c: a == ((a+b+c)==1), ['A','B','C'])\n"
" # B 说'C 是无赖' -> B == (not C)\n"
" p.addConstraint(lambda b,c: b == (not c), ['B','C'])\n"
" # C 说'我和 A 是同一类人' -> C == (C == A)\n"
" p.addConstraint(lambda a,c: c == (c == a), ['A','C'])\n"
" for s in p.getSolutions():\n"
" print({k:('knight' if v else 'knave') for k,v in s.items()})\n\n"
"步骤:1) 每人一个布尔变量;2) 每句话写成上面的双条件约束;"
"3) 调用 getSolutions() 枚举所有解并 print。\n"
"最终答案必须严格采用求解器打印出的解,不要用自己的直觉去推翻它。"
"若求解器输出为空,说明约束建错了(很可能漏了双条件),请检查并重跑。\n" + ANSWER_HINT
)
def parse_answer(text, names):
"""从模型输出里提取最后一个形如 {name: knight/knave} 的 JSON 答案。"""
norm = {
"knight": "knight",
"knave": "knave",
"骑士": "knight",
"无赖": "knave",
"true": "knight",
"false": "knave",
"1": "knight",
"0": "knave",
}
# 找出所有 {...} 片段,从后往前尝试解析
for m in reversed(list(re.finditer(r"\{[^{}]*\}", text))):
try:
obj = json.loads(m.group(0))
except json.JSONDecodeError:
try:
import ast
obj = ast.literal_eval(m.group(0))
except (SyntaxError, ValueError):
continue
if not isinstance(obj, dict):
continue
got = {}
for n in names:
if n not in obj:
break
v = str(obj[n]).strip().lower()
v = norm.get(v, norm.get(str(obj[n]).strip(), None))
if v is None:
break
got[n] = v
else:
return got
return None
def call_model(client, system, user, use_tools):
"""Run one trajectory and retain credential-free provider receipts."""
messages = [{"role": "system", "content": system},
{"role": "user", "content": user}]
codes = []
receipts = []
for turn in range(8): # 最多 8 轮,防止无限循环
kwargs = (dict(model=MODEL, messages=messages, temperature=1, max_tokens=8192)
if _reasoning(MODEL)
else dict(model=MODEL, messages=messages, temperature=0))
if use_tools:
kwargs.update(
tools=TOOLS,
tool_choice="required" if not codes else "auto",
)
resp = client.chat.completions.create(**kwargs)
msg = resp.choices[0].message
usage = getattr(resp, "usage", None)
receipts.append({
"turn": turn + 1,
"response_id": getattr(resp, "id", None),
"response_model": getattr(resp, "model", None),
"finish_reason": getattr(resp.choices[0], "finish_reason", None),
"usage": {
"prompt_tokens": getattr(usage, "prompt_tokens", None),
"completion_tokens": getattr(usage, "completion_tokens", None),
"total_tokens": getattr(usage, "total_tokens", None),
"cached_prompt_tokens": getattr(
getattr(usage, "prompt_tokens_details", None),
"cached_tokens", None,
),
},
"tool_calls": len(getattr(msg, "tool_calls", None) or []),
})
if use_tools and msg.tool_calls:
messages.append(msg)
for tc in msg.tool_calls:
try:
code = json.loads(tc.function.arguments).get("code", "")
except json.JSONDecodeError:
code = ""
codes.append(code)
result = run_python(code)
messages.append({"role": "tool", "tool_call_id": tc.id,
"content": result})
continue
return msg.content or "", codes, receipts
return "", codes, receipts
def run_mode(client, puzzles, mode, existing=None, checkpoint=None):
"""跑一种 LLM 模式(pure/code),返回逐题记录列表。"""
system = CODE_SYSTEM if mode == "code" else PURE_SYSTEM
existing_by_id = {
record["id"]: record for record in (existing or [])
if record.get("id")
}
records = []
for p in puzzles:
previous = existing_by_id.get(p["id"])
if (
previous
and not previous.get("provider_error")
and previous.get("provider_receipts")
and (mode != "code" or previous.get("codes"))
):
record = previous
else:
started = time.monotonic()
try:
text, codes, receipts = call_model(
client, system, p["description"], mode == "code"
)
pred = parse_answer(text, p["names"])
record = dict(
id=p["id"], num=p["num_people"], pred=pred,
gold=p["solution"], correct=pred == p["solution"],
source=p.get("source"), codes=codes, text=text,
used_python_constraint=any(
re.search(r"(^|\s)(from|import)\s+constraint\b", code)
for code in codes
),
duration_s=round(time.monotonic() - started, 3),
provider_receipts=receipts,
provider_error=None,
)
except Exception as exc:
record = dict(
id=p["id"], num=p["num_people"], pred=None,
gold=p["solution"], correct=False,
source=p.get("source"), codes=[], text="",
used_python_constraint=False,
duration_s=round(time.monotonic() - started, 3),
provider_receipts=[],
provider_error=f"{type(exc).__name__}: {exc}",
)
records.append(record)
if checkpoint:
checkpoint(mode, records)
pred = record.get("pred")
correct = bool(record.get("correct"))
mark = "" if correct else ""
print(f" [{mode:6}] {p['id']} ({p['num_people']}人) {mark} "
f"预测={pred}")
return records
def run_solver(puzzles):
"""离线约束求解模式:直接用 python-constraint 求解结构化陈述,无需 LLM/API。"""
records = []
for p in puzzles:
struct = p.get("statements_struct")
if not struct:
sys.exit(f"错误:谜题 {p['id']} 缺少 statements_struct 字段,"
"请用新版 build_puzzles.py 重新生成 puzzles.json。")
sols = solve_labeled(p["names"], struct)
pred = sols[0] if len(sols) == 1 else None
correct = pred == p["solution"]
records.append(dict(id=p["id"], num=p["num_people"], pred=pred,
gold=p["solution"], correct=correct,
codes=[], text="", num_solutions=len(sols)))
mark = "" if correct else ""
print(f" [solver] {p['id']} ({p['num_people']}人) {mark} "
f"解数={len(sols)} 预测={pred}")
return records
LABELS = {"pure": "纯思考", "code": "代码辅助", "solver": "约束求解"}
def _wilson(successes, total, z=1.959963984540054):
if total <= 0:
return [None, None]
p = successes / total
denominator = 1 + z * z / total
center = (p + z * z / (2 * total)) / denominator
half = z * math.sqrt(p * (1 - p) / total + z * z / (4 * total * total)) / denominator
return [center - half, center + half]
def paired_statistics(pure, code):
"""Preregistered paired accuracy analysis (exact McNemar/binomial test)."""
if [r["id"] for r in pure] != [r["id"] for r in code]:
raise ValueError("paired modes do not contain the same ordered task ids")
pure_only = sum(a["correct"] and not b["correct"] for a, b in zip(pure, code))
code_only = sum(not a["correct"] and b["correct"] for a, b in zip(pure, code))
discordant = pure_only + code_only
if discordant:
tail = sum(math.comb(discordant, i) for i in range(min(pure_only, code_only) + 1))
p_value = min(1.0, 2 * tail / (2 ** discordant))
else:
p_value = 1.0
code_ok = sum(r["correct"] for r in code)
pure_ok = sum(r["correct"] for r in pure)
code_accuracy = code_ok / len(code)
pure_accuracy = pure_ok / len(pure)
library_rate = sum(r["used_python_constraint"] for r in code) / len(code)
return {
"test": "two-sided exact McNemar/binomial test on discordant pairs",
"n": len(code),
"contingency": {"pure_only": pure_only, "code_only": code_only,
"discordant": discordant},
"pure_accuracy": pure_accuracy,
"code_accuracy": code_accuracy,
"accuracy_delta": code_accuracy - pure_accuracy,
"code_accuracy_wilson_95": _wilson(code_ok, len(code)),
"p_value": p_value,
"python_constraint_tool_use_rate": library_rate,
"acceptance": {
"code_accuracy_over_90_percent": code_accuracy > 0.90,
"code_significantly_higher_than_pure": (
code_accuracy > pure_accuracy and p_value < 0.05
),
"all_code_trajectories_used_python_constraint": library_rate == 1.0,
},
}
def campaign_completion(results, puzzles, manifest, mode):
"""Check exact protocol execution without requiring a positive result."""
expected_ids = [puzzle["id"] for puzzle in puzzles]
manifest_exact = bool(
manifest
and manifest.get("dataset") == "K-and-K/perturbed-knights-and-knaves"
and manifest.get("revision")
== "bc7ee75a15ee8196ccbdb7df3ab46284340412e2"
and (manifest.get("sampling") or {}).get("total") == 84
and (manifest.get("sampling") or {}).get("cells") == 42
and (manifest.get("sampling") or {}).get("per_cell") == 2
and len(manifest.get("source_files") or []) == 42
and manifest.get("label_validation")
== "all rows independently solved with python-constraint"
)
exact_coverage = (
len(expected_ids) == 84 and len(set(expected_ids)) == 84
)
errors = []
arm_checks = {}
for arm in ("pure", "code"):
records = results.get(arm) or []
arm_errors = [
{"id": record.get("id"), "error": record.get("provider_error")}
for record in records if record.get("provider_error")
]
errors.extend({"arm": arm, **error} for error in arm_errors)
arm_checks[f"all_{arm}_trajectories_complete"] = (
len(records) == 84
and [record.get("id") for record in records] == expected_ids
and not arm_errors
and all(record.get("provider_receipts") for record in records)
)
code_records = results.get("code") or []
checks = {
"mode_is_full_paired_campaign": mode == "both",
"exact_pinned_stratified_dataset_manifest": manifest_exact,
"all_84_unique_tasks_present": exact_coverage,
**arm_checks,
"zero_provider_errors": not errors,
"every_code_trajectory_used_python_constraint": (
len(code_records) == 84
and all(record.get("used_python_constraint") for record in code_records)
),
}
return {
"status": "complete" if all(checks.values()) else "incomplete",
"checks": checks,
"provider_errors": errors,
"expected_tasks_per_arm": 84,
}
def print_table(columns, puzzles):
"""打印多列准确率对比表。columns = [(mode, records), ...],顺序即列顺序。"""
accs = {m: sum(r["correct"] for r in recs) / len(recs) for m, recs in columns}
header = f"{'题号':<8}{'人数':<6}" + "".join(f"{LABELS[m]:<10}" for m, _ in columns)
print("\n" + "=" * 60)
print("准确率对比表")
print("=" * 60)
print(header)
print("-" * 60)
n = len(puzzles)
for i in range(n):
row = f"{puzzles[i]['id']:<8}{puzzles[i]['num_people']:<6}"
for _, recs in columns:
row += f"{('' if recs[i]['correct'] else ''):<10}"
print(row)
print("-" * 60)
tail = f"{'准确率':<8}{'':<6}" + "".join(
f"{accs[m]*100:>6.1f}% " for m, _ in columns)
print(tail)
print("=" * 60)
for m, recs in columns:
n_ok = sum(r["correct"] for r in recs)
print(f"{LABELS[m]:<6} 准确率: {accs[m]*100:5.1f}% ({n_ok}/{len(recs)})")
# 若同时有 solver/code 与 pure,报告提升幅度
baseline = next((m for m in ("pure",) if m in accs), None)
best = next((m for m in ("solver", "code") if m in accs), None)
if baseline and best and best != baseline:
print(f"提升({LABELS[best]} - {LABELS[baseline]}): "
f"{(accs[best]-accs[baseline])*100:+.1f} 个百分点")
def main():
global MODEL
ap = argparse.ArgumentParser(
description="实验 5-2:对比纯思考 / 代码辅助 / 约束求解 三种模式求解"
"「骑士与无赖」逻辑谜题的准确率",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__)
ap.add_argument("--mode", choices=["both", "pure", "code", "solver"],
default="both",
help="运行模式:both=纯思考+代码辅助(默认);pure=仅纯思考;"
"code=仅代码辅助;solver=离线约束求解基线(无需 API)")
ap.add_argument("--model", default=MODEL,
help=f"LLM 模型名(默认 {MODEL}solver 模式忽略)")
ap.add_argument(
"--provider",
choices=["auto", "ollama", "openai", "openrouter", "moonshot", "ark"],
default="auto",
help="explicit API provider; recorded in the saved evidence",
)
ap.add_argument("--limit", type=int, default=0,
help="只跑前 N 题(0=全部)")
ap.add_argument("--min-people", type=int, default=0,
help="只跑居民数 >= 该值的谜题(按难度筛选,0=不限)")
ap.add_argument("--max-people", type=int, default=0,
help="只跑居民数 <= 该值的谜题(按难度筛选,0=不限)")
ap.add_argument("--puzzles", default="puzzles.json",
help="谜题数据集路径(默认 puzzles.json)")
ap.add_argument("--output", default="last_run.json",
help="逐题完整记录的输出路径(默认 last_run.json)")
ap.add_argument(
"--manifest", default=None,
help="optional dataset manifest; defaults to the matching .manifest.json",
)
ap.add_argument(
"--resume", action="store_true",
help="resume successful per-arm rows from OUTPUT.checkpoint.json",
)
args = ap.parse_args()
MODEL = args.model
with open(args.puzzles, encoding="utf-8") as f:
puzzles = json.load(f)
if args.min_people:
puzzles = [p for p in puzzles if p["num_people"] >= args.min_people]
if args.max_people:
puzzles = [p for p in puzzles if p["num_people"] <= args.max_people]
if args.limit:
puzzles = puzzles[:args.limit]
if not puzzles:
sys.exit("错误:筛选后没有任何谜题,请放宽 --min-people/--max-people/--limit。")
# solver 模式完全离线,不需要 API;其余模式需要 OPENAI_API_KEY。
llm_modes = {"both": ["pure", "code"], "pure": ["pure"],
"code": ["code"], "solver": []}[args.mode]
results = {}
checkpoint_path = Path(str(args.output) + ".checkpoint.json")
resumed_results = {}
if args.resume and checkpoint_path.is_file():
prior = json.loads(checkpoint_path.read_text(encoding="utf-8"))
if prior.get("provider") != args.provider or prior.get("model") != MODEL:
raise ValueError("resume rejected: provider/model changed")
if prior.get("puzzle_ids") != [puzzle["id"] for puzzle in puzzles]:
raise ValueError("resume rejected: selected puzzle set changed")
resumed_results = prior.get("results") or {}
checkpoint_results = {
arm: list(records) for arm, records in resumed_results.items()
}
def persist_checkpoint(arm, records):
checkpoint_results[arm] = list(records)
checkpoint_path.parent.mkdir(parents=True, exist_ok=True)
checkpoint_path.write_text(json.dumps({
"schema_version": "1.0", "experiment": "5-2",
"provider": args.provider, "model": MODEL,
"puzzle_ids": [puzzle["id"] for puzzle in puzzles],
"results": checkpoint_results,
}, ensure_ascii=False, indent=2), encoding="utf-8")
if args.mode == "solver":
print(f"离线约束求解基线 题目数:{len(puzzles)}\n")
print("== 约束求解(solver,离线) ==")
results["solver"] = run_solver(puzzles)
else:
client, MODEL = build_client_and_model(args.provider)
print(f"供应商:{PROVIDER} 模型:{MODEL} 题目数:{len(puzzles)} 模式:{args.mode}\n")
for m in llm_modes:
print(f"== {LABELS[m]}({m}) ==")
results[m] = run_mode(
client, puzzles, m,
existing=resumed_results.get(m),
checkpoint=persist_checkpoint,
)
print()
# ---- 准确率对比表(按 pure -> code -> solver 的固定列序) ----
columns = [(m, results[m]) for m in ["pure", "code", "solver"] if m in results]
print_table(columns, puzzles)
# ---- 展示一题的约束建模代码与求解结果 ----
code_recs = results.get("code")
if code_recs:
sample = next((r for r in code_recs if r["correct"] and r["codes"]), None)
if sample:
print("\n" + "=" * 60)
print(f"示例:{sample['id']} 的约束建模代码(模型生成)")
print("=" * 60)
print(sample["codes"][0])
print("-- 求解 & 最终答案 --")
print(f"预测={sample['pred']} 真值={sample['gold']}")
# 保存完整记录,便于复盘
puzzle_path = Path(args.puzzles)
manifest_path = (
Path(args.manifest) if args.manifest
else puzzle_path.with_name(puzzle_path.stem + ".manifest.json")
)
manifest = None
manifest_sha256 = None
if manifest_path.is_file():
import hashlib
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest_sha256 = hashlib.sha256(manifest_path.read_bytes()).hexdigest()
payload = dict(
schema_version="2.0",
experiment="5-2",
generated_at_utc=dt.datetime.now(dt.timezone.utc).isoformat(),
provider=PROVIDER,
model=MODEL,
mode=args.mode,
tasks=len(puzzles),
dataset_manifest=manifest,
dataset_manifest_sha256=manifest_sha256,
)
for m, recs in results.items():
payload[m] = recs
payload[f"{m}_acc"] = sum(r["correct"] for r in recs) / len(recs)
if "pure" in results and "code" in results:
payload["paired_analysis"] = paired_statistics(results["pure"], results["code"])
payload["completion"] = campaign_completion(
results, puzzles, manifest, args.mode
)
payload["official_complete"] = payload["completion"]["status"] == "complete"
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
print(f"\n完整逐题记录已保存到 {args.output}")
if __name__ == "__main__":
main()
+14
View File
@@ -0,0 +1,14 @@
# 复制本文件为 .env 并填入你的密钥
# 必填其一:OpenAI API Key(直连)
OPENAI_API_KEY=your-openai-api-key
# 通用兜底:未配置 OPENAI_API_KEY 时自动改走 OpenRouterroute openai/gpt-4o-mini)。
# 仅当把 MODEL 换成 gpt-5.x 这类需组织实名认证的模型、且设置了本 key 时,
# 才会优先走 OpenRouter。
# OPENROUTER_API_KEY=your-openrouter-api-key
# 可选:切换到兼容 OpenAI 协议的服务端点
# OPENAI_BASE_URL=https://api.openai.com/v1
# 可选:默认模型(不填则用 gpt-4o-mini)
MODEL=gpt-4o-mini
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,412 @@
{
"schema_version": "1.0",
"experiment": "5-2",
"dataset": "K-and-K/perturbed-knights-and-knaves",
"revision": "bc7ee75a15ee8196ccbdb7df3ab46284340412e2",
"license": "CC-BY-NC-SA-4.0",
"sampling": {
"split": "test",
"perturbations": [
"perturbed_leaf",
"perturbed_statement",
"reorder_statement",
"random_pair",
"uncommon_name",
"flip_role"
],
"people": [
2,
3,
4,
5,
6,
7,
8
],
"per_cell": 2,
"seed": 512,
"cells": 42,
"total": 84
},
"source_files": [
{
"path": "test/perturbed_leaf/people2_num100.jsonl",
"sha256": "541c5dadb60c54e4c9973cafa0dbd69635538fb44aa5dcb28dc64ea7a78a56be",
"published_rows": 76,
"sampled_rows": [
6,
11
]
},
{
"path": "test/perturbed_leaf/people3_num100.jsonl",
"sha256": "4c3d767065edb62d19728da7a4e4e4c63f550c1719c3600c868f50f48715711e",
"published_rows": 93,
"sampled_rows": [
6,
92
]
},
{
"path": "test/perturbed_leaf/people4_num100.jsonl",
"sha256": "b3811d1ea4bbdb2ade55dde240361470516e0541c58b5c526609f99ad6a038d7",
"published_rows": 94,
"sampled_rows": [
66,
82
]
},
{
"path": "test/perturbed_leaf/people5_num100.jsonl",
"sha256": "17529110798b0a659286983d0cf4e0d3b5a2b560dfb6ab3b3acb1b61ac9d7da7",
"published_rows": 98,
"sampled_rows": [
16,
41
]
},
{
"path": "test/perturbed_leaf/people6_num100.jsonl",
"sha256": "032c0c253bbe574335c741bb5ba7d07b48f622e217dc939f72a633ef9fbe46b0",
"published_rows": 100,
"sampled_rows": [
17,
51
]
},
{
"path": "test/perturbed_leaf/people7_num100.jsonl",
"sha256": "f62b1f0c3c45e3a62660951fc12b9716679f1466bdc313066d85d08d6fe3c767",
"published_rows": 100,
"sampled_rows": [
79,
80
]
},
{
"path": "test/perturbed_leaf/people8_num100.jsonl",
"sha256": "01947375b3a16ce4fb340d6f0e1545d5aeadfc6f695c49cf2a6f436fcc5ba099",
"published_rows": 100,
"sampled_rows": [
11,
69
]
},
{
"path": "test/perturbed_statement/people2_num100.jsonl",
"sha256": "18479f9b2c327e9ce51e901479e9e7ffcd4397094e91fdca430871a3fc131fc3",
"published_rows": 100,
"sampled_rows": [
8,
94
]
},
{
"path": "test/perturbed_statement/people3_num100.jsonl",
"sha256": "cb8832b4aae19426e10a039236f0e4fb7dad797598edb3850db8067ff7cda1a6",
"published_rows": 100,
"sampled_rows": [
0,
24
]
},
{
"path": "test/perturbed_statement/people4_num100.jsonl",
"sha256": "ec8821279ff09db70098b97c5b47f452f2b9a6059b3d32ff22c9fce8342e4c30",
"published_rows": 100,
"sampled_rows": [
27,
28
]
},
{
"path": "test/perturbed_statement/people5_num100.jsonl",
"sha256": "791a14c3a7fea7713904786cdbd24ccc94901c0508c1ed3daf5abe3794857ed4",
"published_rows": 100,
"sampled_rows": [
95,
99
]
},
{
"path": "test/perturbed_statement/people6_num100.jsonl",
"sha256": "afadf008aa8aba0753bb198baca07ea51ebd9879045b619861225eea56559134",
"published_rows": 100,
"sampled_rows": [
47,
52
]
},
{
"path": "test/perturbed_statement/people7_num100.jsonl",
"sha256": "bd3900ae3527887659a1a0da1146780e0a47e4d96ff301e1c2039f35bcfff849",
"published_rows": 100,
"sampled_rows": [
15,
28
]
},
{
"path": "test/perturbed_statement/people8_num100.jsonl",
"sha256": "c3d91ba046bbeb5ec0d64c9147d3d2c76c1f631716da47d32b8379ebfcbe35a0",
"published_rows": 100,
"sampled_rows": [
37,
86
]
},
{
"path": "test/reorder_statement/people2_num100.jsonl",
"sha256": "c116f384f984150969554d783ccf7cecf4c6e56c59d2f5d326bfcfa722bb149c",
"published_rows": 100,
"sampled_rows": [
60,
76
]
},
{
"path": "test/reorder_statement/people3_num100.jsonl",
"sha256": "ff772d57162fb500e608ef28c59eaf57ba1d80f60cf3f73902162c3476f6b0a2",
"published_rows": 100,
"sampled_rows": [
0,
64
]
},
{
"path": "test/reorder_statement/people4_num100.jsonl",
"sha256": "be12e4b5a38792785264c16c11117f7f10f955517e32c92ea37949017a4da0cb",
"published_rows": 100,
"sampled_rows": [
68,
97
]
},
{
"path": "test/reorder_statement/people5_num100.jsonl",
"sha256": "04a492522cdeb64a960632652b54e3fd27cc505fa8a1c48cf9c666cc8e7d6fb0",
"published_rows": 100,
"sampled_rows": [
55,
92
]
},
{
"path": "test/reorder_statement/people6_num100.jsonl",
"sha256": "4b6131d749c47ce1580d0443e373112cc5f0e20f1b0245455b3df63b106580c5",
"published_rows": 100,
"sampled_rows": [
80,
85
]
},
{
"path": "test/reorder_statement/people7_num100.jsonl",
"sha256": "d3051b151ef7e5c47a9cd69b09b52f1ec94c7f899821ec685e286e578c7932c5",
"published_rows": 100,
"sampled_rows": [
67,
92
]
},
{
"path": "test/reorder_statement/people8_num100.jsonl",
"sha256": "3710c66680d201ebf3d6d243fa9f54ee7e5ce35570ed44f87cd5111b05dc1d9b",
"published_rows": 100,
"sampled_rows": [
2,
14
]
},
{
"path": "test/random_pair/people2_num100.jsonl",
"sha256": "92b9d18d12ea3eaaa0a49daa03f519b5c5f66614bf724e541e9c00e1b9be24ae",
"published_rows": 100,
"sampled_rows": [
13,
14
]
},
{
"path": "test/random_pair/people3_num100.jsonl",
"sha256": "6eb60022244be6fa25d372d1580eb8dddfb06914d7290dbe1d64c9cb57c1f612",
"published_rows": 100,
"sampled_rows": [
54,
75
]
},
{
"path": "test/random_pair/people4_num100.jsonl",
"sha256": "1cafedf8ea1c5bf393d2366a6d798e66730059fdd5290c8b2a8cca35de6c4589",
"published_rows": 100,
"sampled_rows": [
68,
90
]
},
{
"path": "test/random_pair/people5_num100.jsonl",
"sha256": "9036535b0ace7b3cfe1b8cf94fbb898b0c07e8d88d23cbc914707ed6d0221ebc",
"published_rows": 100,
"sampled_rows": [
62,
79
]
},
{
"path": "test/random_pair/people6_num100.jsonl",
"sha256": "9d3a4bcd28f465b311bd5cdc96958b69529240f05e00a79d80ea7a54b8d53002",
"published_rows": 100,
"sampled_rows": [
22,
45
]
},
{
"path": "test/random_pair/people7_num100.jsonl",
"sha256": "389713889ec22d14bf64d1bb9b846d1fb7a4adba44004b6ce9db1e0ac3d54190",
"published_rows": 100,
"sampled_rows": [
64,
91
]
},
{
"path": "test/random_pair/people8_num100.jsonl",
"sha256": "8885fe0d63bfe5910617bd84e6be646f7b62a26978a649b4173889601d0f9229",
"published_rows": 100,
"sampled_rows": [
68,
71
]
},
{
"path": "test/uncommon_name/people2_num100.jsonl",
"sha256": "25437adfc79438934dbaa113f4a2b06378d3f899a3cc9d8193202a3e0d746f29",
"published_rows": 100,
"sampled_rows": [
16,
82
]
},
{
"path": "test/uncommon_name/people3_num100.jsonl",
"sha256": "087b47837520c8da31d78deac08781c3a62ad7961bae246d68f81843b3795ab0",
"published_rows": 100,
"sampled_rows": [
37,
40
]
},
{
"path": "test/uncommon_name/people4_num100.jsonl",
"sha256": "8e0edc42da3d5739a4e24f82725c6332e71189016391810b5e80d549c71d1004",
"published_rows": 100,
"sampled_rows": [
44,
57
]
},
{
"path": "test/uncommon_name/people5_num100.jsonl",
"sha256": "c1e7ade7a1791ab17a1d951f7548ce8c54767190f0402e4e6785889810694f3f",
"published_rows": 100,
"sampled_rows": [
5,
47
]
},
{
"path": "test/uncommon_name/people6_num100.jsonl",
"sha256": "4046b49fe876c7f4eab6eddc35ff0a9f01ad487faf59e9edb5605a39b3e56530",
"published_rows": 100,
"sampled_rows": [
8,
56
]
},
{
"path": "test/uncommon_name/people7_num100.jsonl",
"sha256": "752a97113d999fcff5f2910c78fca761133822b99b5ae6ec9070421e0e51b2dc",
"published_rows": 100,
"sampled_rows": [
34,
69
]
},
{
"path": "test/uncommon_name/people8_num100.jsonl",
"sha256": "951228b33ff7b4afffcbd247eeb67390d9af5ff2def9f9cdb0f8433564fffc15",
"published_rows": 100,
"sampled_rows": [
12,
68
]
},
{
"path": "test/flip_role/people2_num100.jsonl",
"sha256": "2885eb7b356f9ab67ff2ccef9372dcfa7ebcf6ed2b8c5e720db1d74f03840c8d",
"published_rows": 100,
"sampled_rows": [
7,
80
]
},
{
"path": "test/flip_role/people3_num100.jsonl",
"sha256": "31026489710814617c66a6879408accc099c34f395425b41d0bd56743d933461",
"published_rows": 100,
"sampled_rows": [
36,
79
]
},
{
"path": "test/flip_role/people4_num100.jsonl",
"sha256": "a5d56cc8724a40fd674dba7730f52ca8dec26966f07ff434302bd62b1260a458",
"published_rows": 100,
"sampled_rows": [
36,
97
]
},
{
"path": "test/flip_role/people5_num100.jsonl",
"sha256": "1429f88b80794e12f731bcd9cd2502384a3c4ac1a3292010103baccd86e93ae7",
"published_rows": 100,
"sampled_rows": [
22,
73
]
},
{
"path": "test/flip_role/people6_num100.jsonl",
"sha256": "ccfee7ea7e2f24376e88193a979a20ac6410ec2458c534af7e4daedab4ff47ff",
"published_rows": 100,
"sampled_rows": [
39,
62
]
},
{
"path": "test/flip_role/people7_num100.jsonl",
"sha256": "b5720e5a434747f323f90086ac6bae7ba33d1b6ba8d617424f599bfabdbcdeca",
"published_rows": 100,
"sampled_rows": [
1,
91
]
},
{
"path": "test/flip_role/people8_num100.jsonl",
"sha256": "d668973f128b119ad065e0b58f33099516dbd801f0e7957c00e876115e8119dc",
"published_rows": 100,
"sampled_rows": [
64,
96
]
}
],
"label_validation": "all rows independently solved with python-constraint"
}
+505
View File
@@ -0,0 +1,505 @@
[
{
"id": "kk01",
"num_people": 2,
"names": [
"A",
"B"
],
"statements": {
"A": "B 是无赖。",
"B": "我们两人都不是骑士。"
},
"statements_struct": {
"A": [
"is",
"B",
"knave"
],
"B": [
"and",
[
"is",
"A",
"knave"
],
[
"is",
"B",
"knave"
]
]
},
"description": "这座岛上有 2 位居民:A, B。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是无赖。」\nB: 「我们两人都不是骑士。」",
"solution": {
"A": "knight",
"B": "knave"
}
},
{
"id": "kk02",
"num_people": 2,
"names": [
"A",
"B"
],
"statements": {
"A": "我和 B 是同一类人(要么都是骑士,要么都是无赖)。",
"B": "我和 A 是不同类人。"
},
"statements_struct": {
"A": [
"same",
"A",
"B"
],
"B": [
"diff",
"A",
"B"
]
},
"description": "这座岛上有 2 位居民:A, B。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「我和 B 是同一类人(要么都是骑士,要么都是无赖)。」\nB: 「我和 A 是不同类人。」",
"solution": {
"A": "knave",
"B": "knight"
}
},
{
"id": "kk03",
"num_people": 2,
"names": [
"A",
"B"
],
"statements": {
"A": "我们当中至少有一个骑士。",
"B": "A 是无赖。"
},
"statements_struct": {
"A": [
"count",
"knight",
">=",
1
],
"B": [
"is",
"A",
"knave"
]
},
"description": "这座岛上有 2 位居民:A, B。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「我们当中至少有一个骑士。」\nB: 「A 是无赖。」",
"solution": {
"A": "knight",
"B": "knave"
}
},
{
"id": "kk04",
"num_people": 3,
"names": [
"A",
"B",
"C"
],
"statements": {
"A": "B 是无赖。",
"B": "C 是无赖。",
"C": "A 和 B 都是无赖。"
},
"statements_struct": {
"A": [
"is",
"B",
"knave"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"and",
[
"is",
"A",
"knave"
],
[
"is",
"B",
"knave"
]
]
},
"description": "这座岛上有 3 位居民:A, B, C。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是无赖。」\nB: 「C 是无赖。」\nC: 「A 和 B 都是无赖。」",
"solution": {
"A": "knave",
"B": "knight",
"C": "knave"
}
},
{
"id": "kk05",
"num_people": 3,
"names": [
"A",
"B",
"C"
],
"statements": {
"A": "B 是骑士。",
"B": "C 是无赖。",
"C": "A 和 B 是同一类人。"
},
"statements_struct": {
"A": [
"is",
"B",
"knight"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"same",
"A",
"B"
]
},
"description": "这座岛上有 3 位居民:A, B, C。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是骑士。」\nB: 「C 是无赖。」\nC: 「A 和 B 是同一类人。」",
"solution": {
"A": "knave",
"B": "knave",
"C": "knight"
}
},
{
"id": "kk06",
"num_people": 3,
"names": [
"A",
"B",
"C"
],
"statements": {
"A": "B 和 C 是同一类人。",
"B": "A 是无赖。",
"C": "我和 A 是同一类人。"
},
"statements_struct": {
"A": [
"same",
"B",
"C"
],
"B": [
"is",
"A",
"knave"
],
"C": [
"same",
"C",
"A"
]
},
"description": "这座岛上有 3 位居民:A, B, C。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 和 C 是同一类人。」\nB: 「A 是无赖。」\nC: 「我和 A 是同一类人。」",
"solution": {
"A": "knight",
"B": "knave",
"C": "knave"
}
},
{
"id": "kk07",
"num_people": 3,
"names": [
"A",
"B",
"C"
],
"statements": {
"A": "我是无赖,或者 B 是骑士。",
"B": "A 是骑士。",
"C": "B 是无赖。"
},
"statements_struct": {
"A": [
"or",
[
"is",
"A",
"knave"
],
[
"is",
"B",
"knight"
]
],
"B": [
"is",
"A",
"knight"
],
"C": [
"is",
"B",
"knave"
]
},
"description": "这座岛上有 3 位居民:A, B, C。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「我是无赖,或者 B 是骑士。」\nB: 「A 是骑士。」\nC: 「B 是无赖。」",
"solution": {
"A": "knight",
"B": "knight",
"C": "knave"
}
},
{
"id": "kk08",
"num_people": 4,
"names": [
"A",
"B",
"C",
"D"
],
"statements": {
"A": "B 和 D 是同一类人。",
"B": "C 是无赖。",
"C": "D 是骑士。",
"D": "B 和 C 是不同类人。"
},
"statements_struct": {
"A": [
"same",
"B",
"D"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"is",
"D",
"knight"
],
"D": [
"diff",
"B",
"C"
]
},
"description": "这座岛上有 4 位居民:A, B, C, D。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 和 D 是同一类人。」\nB: 「C 是无赖。」\nC: 「D 是骑士。」\nD: 「B 和 C 是不同类人。」",
"solution": {
"A": "knave",
"B": "knave",
"C": "knight",
"D": "knight"
}
},
{
"id": "kk09",
"num_people": 4,
"names": [
"A",
"B",
"C",
"D"
],
"statements": {
"A": "B 是骑士。",
"B": "C 是无赖。",
"C": "D 是骑士。",
"D": "A 和 B 不是同一类人。"
},
"statements_struct": {
"A": [
"is",
"B",
"knight"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"is",
"D",
"knight"
],
"D": [
"diff",
"A",
"B"
]
},
"description": "这座岛上有 4 位居民:A, B, C, D。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是骑士。」\nB: 「C 是无赖。」\nC: 「D 是骑士。」\nD: 「A 和 B 不是同一类人。」",
"solution": {
"A": "knight",
"B": "knight",
"C": "knave",
"D": "knave"
}
},
{
"id": "kk10",
"num_people": 4,
"names": [
"A",
"B",
"C",
"D"
],
"statements": {
"A": "我们四人当中至少有三个无赖。",
"B": "A 是无赖。",
"C": "B 是骑士。",
"D": "C 是无赖。"
},
"statements_struct": {
"A": [
"count",
"knave",
">=",
3
],
"B": [
"is",
"A",
"knave"
],
"C": [
"is",
"B",
"knight"
],
"D": [
"is",
"C",
"knave"
]
},
"description": "这座岛上有 4 位居民:A, B, C, D。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「我们四人当中至少有三个无赖。」\nB: 「A 是无赖。」\nC: 「B 是骑士。」\nD: 「C 是无赖。」",
"solution": {
"A": "knave",
"B": "knight",
"C": "knight",
"D": "knave"
}
},
{
"id": "kk11",
"num_people": 5,
"names": [
"A",
"B",
"C",
"D",
"E"
],
"statements": {
"A": "B 是骑士。",
"B": "C 是无赖。",
"C": "D 是骑士。",
"D": "E 是无赖。",
"E": "我们五人当中至少有两个骑士。"
},
"statements_struct": {
"A": [
"is",
"B",
"knight"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"is",
"D",
"knight"
],
"D": [
"is",
"E",
"knave"
],
"E": [
"count",
"knight",
">=",
2
]
},
"description": "这座岛上有 5 位居民:A, B, C, D, E。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是骑士。」\nB: 「C 是无赖。」\nC: 「D 是骑士。」\nD: 「E 是无赖。」\nE: 「我们五人当中至少有两个骑士。」",
"solution": {
"A": "knight",
"B": "knight",
"C": "knave",
"D": "knave",
"E": "knight"
}
},
{
"id": "kk12",
"num_people": 5,
"names": [
"A",
"B",
"C",
"D",
"E"
],
"statements": {
"A": "B 是骑士。",
"B": "C 是无赖。",
"C": "D 是无赖。",
"D": "E 是骑士。",
"E": "A 和 C 是同一类人。"
},
"statements_struct": {
"A": [
"is",
"B",
"knight"
],
"B": [
"is",
"C",
"knave"
],
"C": [
"is",
"D",
"knave"
],
"D": [
"is",
"E",
"knight"
],
"E": [
"same",
"A",
"C"
]
},
"description": "这座岛上有 5 位居民:A, B, C, D, E。每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。他们各自说了如下的话:\nA: 「B 是骑士。」\nB: 「C 是无赖。」\nC: 「D 是无赖。」\nD: 「E 是骑士。」\nE: 「A 和 C 是同一类人。」",
"solution": {
"A": "knave",
"B": "knave",
"C": "knight",
"D": "knave",
"E": "knave"
}
}
]
+2
View File
@@ -0,0 +1,2 @@
openai>=1.30.0
python-constraint2>=2.0.2
+52
View File
@@ -0,0 +1,52 @@
"""
极简 Code Interpreter 沙箱:在子进程中执行模型生成的 Python 代码。
- 用独立子进程运行,避免污染主进程、并可强制超时。
- 子进程使用与主程序相同的解释器(sys.executable),因此已预装 python-constraint。
- 捕获 stdout / stderr 一并返回给模型,让它能看到求解结果或报错信息。
"""
import subprocess
import sys
import tempfile
import os
def run_python(code: str, timeout: int = 20) -> str:
"""在子进程沙箱中执行 code,返回合并后的 stdout/stderr 文本。"""
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False,
encoding="utf-8") as f:
f.write(code)
path = f.name
try:
proc = subprocess.run(
[sys.executable, path],
capture_output=True, text=True, timeout=timeout,
)
out = proc.stdout
if proc.stderr.strip():
out += "\n[stderr]\n" + proc.stderr
if not out.strip():
out = "(代码已执行,但没有任何输出。记得用 print() 打印结果。)"
return out.strip()
except subprocess.TimeoutExpired:
return f"[错误] 代码执行超时(超过 {timeout} 秒)。"
finally:
os.unlink(path)
if __name__ == "__main__":
# 自测:用 python-constraint 求解一个最简单的 K&K 谜题
demo = """
from constraint import Problem
p = Problem()
# True=骑士(说真话), False=无赖(说假话)
p.addVariable('A', [True, False])
p.addVariable('B', [True, False])
# A 说"B 是无赖":A 的真值 == (B 是无赖) 即 A == (not B)
p.addConstraint(lambda a, b: a == (not b), ['A', 'B'])
# B 说"我们都不是骑士"B == (not A and not B)
p.addConstraint(lambda a, b: b == ((not a) and (not b)), ['A', 'B'])
for s in p.getSolutions():
print({k: 'knight' if v else 'knave' for k, v in s.items()})
"""
print(run_python(demo))
@@ -0,0 +1,27 @@
from build_hf_puzzles import convert_expression, convert_row
def test_convert_dataset_expression_and_label():
row = {
"quiz": "A says B is not lying. B says B is truthful iff A is lying.",
"names": ["A", "B"],
"solution": [False, False],
"statements": "(('not', ('lying', 1)), ('<=>', ('telling-truth', 1), ('lying', 0)))",
"index": 7,
}
puzzle = convert_row(
row,
perturbation="perturbed_leaf",
people=2,
source_path="test/perturbed_leaf/people2_num100.jsonl",
source_sha256="abc",
source_row=7,
)
assert puzzle["solution"] == {"A": "knave", "B": "knave"}
assert puzzle["source"]["dataset_index"] == 7
def test_convert_implication_and_biconditional():
names = ["A", "B"]
assert convert_expression(("->", ("lying", 0), ("telling-truth", 1)), names)[0] == "implies"
assert convert_expression(("<=>", ("lying", 0), ("telling-truth", 1)), names)[0] == "iff"
@@ -0,0 +1,32 @@
import sys
from pathlib import Path
# Ensure demo module can be resolved regardless of working directory
sys.path.insert(0, str(Path(__file__).parent))
from demo import parse_answer
def test_parse_answer_supports_boolean_json_values():
"""Verify parse_answer maps JSON boolean true/false to knight/knave.
Contract: LLMs outputting JSON solutions with boolean values like {"A": true, "B": false}
must be normalized to {"A": "knight", "B": "knave"} instead of returning None.
Locks out KeyError or unparsed boolean JSON answers.
"""
text = 'The solution is {"A": true, "B": false}'
ans = parse_answer(text, ["A", "B"])
assert ans == {"A": "knight", "B": "knave"}
def test_parse_answer_supports_numeric_and_str_boolean():
"""Verify parse_answer maps numeric 1/0 and string "true"/"false" booleans to knight/knave.
Contract: 1 and "true" map to "knight"; 0 and "false" map to "knave".
Locks out unparsed numeric or string boolean representation in model output.
"""
text1 = '{"A": 1, "B": 0}'
assert parse_answer(text1, ["A", "B"]) == {"A": "knight", "B": "knave"}
text2 = '{"A": "true", "B": "false"}'
assert parse_answer(text2, ["A", "B"]) == {"A": "knight", "B": "knave"}
@@ -0,0 +1,22 @@
"""Regression: one-person puzzles must not IndexError in _random_stmt."""
import random
import sys
import types
sys.modules.setdefault("constraint", types.ModuleType("constraint"))
sys.modules["constraint"].Problem = object
cs = types.ModuleType("csp_solver")
cs.render_nl = lambda *a, **k: ""
cs.solve = lambda *a, **k: [{}]
cs.solve_labeled = lambda *a, **k: {}
sys.modules["csp_solver"] = cs
from build_puzzles import _random_stmt # noqa: E402
def test_random_stmt_one_person_returns_count():
rng = random.Random(0)
for _ in range(20):
stmt = _random_stmt("A", ["A"], rng)
assert stmt[0] == "count"
assert stmt[1] in ("knight", "knave")
@@ -0,0 +1,30 @@
import sys
from pathlib import Path
# Ensure csp_solver module can be resolved regardless of working directory
sys.path.insert(0, str(Path(__file__).parent))
from csp_solver import solve, solve_labeled
def test_csp_solver_handles_silent_resident():
"""Verify solve handles residents in names that make no statements.
Contract: In Knights and Knaves puzzles, residents listed in `names` may be
silent (spoken about by others without making statements themselves).
`solve` must skip adding speaker-statement constraints for silent residents
rather than raising KeyError.
"""
names = ["A", "B"]
# A speaks about B ("B is a knave"), but B makes no statement.
structs = {"A": ["is", "B", "knave"]}
solutions = solve(names, structs)
assert len(solutions) == 2
# If A is knight (True), B must be knave (False); if A is knave (False), B must be knight (True).
assert {"A": True, "B": False} in solutions
assert {"A": False, "B": True} in solutions
labeled = solve_labeled(names, structs)
assert {"A": "knight", "B": "knave"} in labeled
assert {"A": "knave", "B": "knight"} in labeled
@@ -0,0 +1,42 @@
from demo import campaign_completion, paired_statistics
def test_paired_statistics_detects_clear_code_gain():
pure = [{"id": str(i), "correct": i < 2} for i in range(20)]
code = [
{"id": str(i), "correct": i < 19, "used_python_constraint": True}
for i in range(20)
]
result = paired_statistics(pure, code)
assert result["code_accuracy"] == 0.95
assert result["acceptance"]["code_accuracy_over_90_percent"] is True
assert result["acceptance"]["code_significantly_higher_than_pure"] is True
def test_campaign_completion_requires_both_full_real_arms():
puzzles = [{"id": f"p-{i}"} for i in range(84)]
pure = [
{"id": p["id"], "provider_receipts": [{"response_id": "r"}],
"provider_error": None}
for p in puzzles
]
code = [
{"id": p["id"], "provider_receipts": [{"response_id": "r"}],
"provider_error": None, "used_python_constraint": True}
for p in puzzles
]
manifest = {
"dataset": "K-and-K/perturbed-knights-and-knaves",
"revision": "bc7ee75a15ee8196ccbdb7df3ab46284340412e2",
"sampling": {"total": 84, "cells": 42, "per_cell": 2},
"source_files": [{} for _ in range(42)],
"label_validation": "all rows independently solved with python-constraint",
}
result = campaign_completion(
{"pure": pure, "code": code}, puzzles, manifest, "both"
)
assert result["status"] == "complete"
code[-1]["used_python_constraint"] = False
assert campaign_completion(
{"pure": pure, "code": code}, puzzles, manifest, "both"
)["status"] == "incomplete"
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,594 @@
{
"schema_version": "2.0",
"experiment": "5-2",
"generated_at_utc": "2026-07-29T18:26:39.099268+00:00",
"provider": "ark",
"model": "doubao-seed-1-6-flash-250615",
"mode": "both",
"tasks": 1,
"dataset_manifest": {
"schema_version": "1.0",
"experiment": "5-2",
"dataset": "K-and-K/perturbed-knights-and-knaves",
"revision": "bc7ee75a15ee8196ccbdb7df3ab46284340412e2",
"license": "CC-BY-NC-SA-4.0",
"sampling": {
"split": "test",
"perturbations": [
"perturbed_leaf",
"perturbed_statement",
"reorder_statement",
"random_pair",
"uncommon_name",
"flip_role"
],
"people": [
2,
3,
4,
5,
6,
7,
8
],
"per_cell": 2,
"seed": 512,
"cells": 42,
"total": 84
},
"source_files": [
{
"path": "test/perturbed_leaf/people2_num100.jsonl",
"sha256": "541c5dadb60c54e4c9973cafa0dbd69635538fb44aa5dcb28dc64ea7a78a56be",
"published_rows": 76,
"sampled_rows": [
6,
11
]
},
{
"path": "test/perturbed_leaf/people3_num100.jsonl",
"sha256": "4c3d767065edb62d19728da7a4e4e4c63f550c1719c3600c868f50f48715711e",
"published_rows": 93,
"sampled_rows": [
6,
92
]
},
{
"path": "test/perturbed_leaf/people4_num100.jsonl",
"sha256": "b3811d1ea4bbdb2ade55dde240361470516e0541c58b5c526609f99ad6a038d7",
"published_rows": 94,
"sampled_rows": [
66,
82
]
},
{
"path": "test/perturbed_leaf/people5_num100.jsonl",
"sha256": "17529110798b0a659286983d0cf4e0d3b5a2b560dfb6ab3b3acb1b61ac9d7da7",
"published_rows": 98,
"sampled_rows": [
16,
41
]
},
{
"path": "test/perturbed_leaf/people6_num100.jsonl",
"sha256": "032c0c253bbe574335c741bb5ba7d07b48f622e217dc939f72a633ef9fbe46b0",
"published_rows": 100,
"sampled_rows": [
17,
51
]
},
{
"path": "test/perturbed_leaf/people7_num100.jsonl",
"sha256": "f62b1f0c3c45e3a62660951fc12b9716679f1466bdc313066d85d08d6fe3c767",
"published_rows": 100,
"sampled_rows": [
79,
80
]
},
{
"path": "test/perturbed_leaf/people8_num100.jsonl",
"sha256": "01947375b3a16ce4fb340d6f0e1545d5aeadfc6f695c49cf2a6f436fcc5ba099",
"published_rows": 100,
"sampled_rows": [
11,
69
]
},
{
"path": "test/perturbed_statement/people2_num100.jsonl",
"sha256": "18479f9b2c327e9ce51e901479e9e7ffcd4397094e91fdca430871a3fc131fc3",
"published_rows": 100,
"sampled_rows": [
8,
94
]
},
{
"path": "test/perturbed_statement/people3_num100.jsonl",
"sha256": "cb8832b4aae19426e10a039236f0e4fb7dad797598edb3850db8067ff7cda1a6",
"published_rows": 100,
"sampled_rows": [
0,
24
]
},
{
"path": "test/perturbed_statement/people4_num100.jsonl",
"sha256": "ec8821279ff09db70098b97c5b47f452f2b9a6059b3d32ff22c9fce8342e4c30",
"published_rows": 100,
"sampled_rows": [
27,
28
]
},
{
"path": "test/perturbed_statement/people5_num100.jsonl",
"sha256": "791a14c3a7fea7713904786cdbd24ccc94901c0508c1ed3daf5abe3794857ed4",
"published_rows": 100,
"sampled_rows": [
95,
99
]
},
{
"path": "test/perturbed_statement/people6_num100.jsonl",
"sha256": "afadf008aa8aba0753bb198baca07ea51ebd9879045b619861225eea56559134",
"published_rows": 100,
"sampled_rows": [
47,
52
]
},
{
"path": "test/perturbed_statement/people7_num100.jsonl",
"sha256": "bd3900ae3527887659a1a0da1146780e0a47e4d96ff301e1c2039f35bcfff849",
"published_rows": 100,
"sampled_rows": [
15,
28
]
},
{
"path": "test/perturbed_statement/people8_num100.jsonl",
"sha256": "c3d91ba046bbeb5ec0d64c9147d3d2c76c1f631716da47d32b8379ebfcbe35a0",
"published_rows": 100,
"sampled_rows": [
37,
86
]
},
{
"path": "test/reorder_statement/people2_num100.jsonl",
"sha256": "c116f384f984150969554d783ccf7cecf4c6e56c59d2f5d326bfcfa722bb149c",
"published_rows": 100,
"sampled_rows": [
60,
76
]
},
{
"path": "test/reorder_statement/people3_num100.jsonl",
"sha256": "ff772d57162fb500e608ef28c59eaf57ba1d80f60cf3f73902162c3476f6b0a2",
"published_rows": 100,
"sampled_rows": [
0,
64
]
},
{
"path": "test/reorder_statement/people4_num100.jsonl",
"sha256": "be12e4b5a38792785264c16c11117f7f10f955517e32c92ea37949017a4da0cb",
"published_rows": 100,
"sampled_rows": [
68,
97
]
},
{
"path": "test/reorder_statement/people5_num100.jsonl",
"sha256": "04a492522cdeb64a960632652b54e3fd27cc505fa8a1c48cf9c666cc8e7d6fb0",
"published_rows": 100,
"sampled_rows": [
55,
92
]
},
{
"path": "test/reorder_statement/people6_num100.jsonl",
"sha256": "4b6131d749c47ce1580d0443e373112cc5f0e20f1b0245455b3df63b106580c5",
"published_rows": 100,
"sampled_rows": [
80,
85
]
},
{
"path": "test/reorder_statement/people7_num100.jsonl",
"sha256": "d3051b151ef7e5c47a9cd69b09b52f1ec94c7f899821ec685e286e578c7932c5",
"published_rows": 100,
"sampled_rows": [
67,
92
]
},
{
"path": "test/reorder_statement/people8_num100.jsonl",
"sha256": "3710c66680d201ebf3d6d243fa9f54ee7e5ce35570ed44f87cd5111b05dc1d9b",
"published_rows": 100,
"sampled_rows": [
2,
14
]
},
{
"path": "test/random_pair/people2_num100.jsonl",
"sha256": "92b9d18d12ea3eaaa0a49daa03f519b5c5f66614bf724e541e9c00e1b9be24ae",
"published_rows": 100,
"sampled_rows": [
13,
14
]
},
{
"path": "test/random_pair/people3_num100.jsonl",
"sha256": "6eb60022244be6fa25d372d1580eb8dddfb06914d7290dbe1d64c9cb57c1f612",
"published_rows": 100,
"sampled_rows": [
54,
75
]
},
{
"path": "test/random_pair/people4_num100.jsonl",
"sha256": "1cafedf8ea1c5bf393d2366a6d798e66730059fdd5290c8b2a8cca35de6c4589",
"published_rows": 100,
"sampled_rows": [
68,
90
]
},
{
"path": "test/random_pair/people5_num100.jsonl",
"sha256": "9036535b0ace7b3cfe1b8cf94fbb898b0c07e8d88d23cbc914707ed6d0221ebc",
"published_rows": 100,
"sampled_rows": [
62,
79
]
},
{
"path": "test/random_pair/people6_num100.jsonl",
"sha256": "9d3a4bcd28f465b311bd5cdc96958b69529240f05e00a79d80ea7a54b8d53002",
"published_rows": 100,
"sampled_rows": [
22,
45
]
},
{
"path": "test/random_pair/people7_num100.jsonl",
"sha256": "389713889ec22d14bf64d1bb9b846d1fb7a4adba44004b6ce9db1e0ac3d54190",
"published_rows": 100,
"sampled_rows": [
64,
91
]
},
{
"path": "test/random_pair/people8_num100.jsonl",
"sha256": "8885fe0d63bfe5910617bd84e6be646f7b62a26978a649b4173889601d0f9229",
"published_rows": 100,
"sampled_rows": [
68,
71
]
},
{
"path": "test/uncommon_name/people2_num100.jsonl",
"sha256": "25437adfc79438934dbaa113f4a2b06378d3f899a3cc9d8193202a3e0d746f29",
"published_rows": 100,
"sampled_rows": [
16,
82
]
},
{
"path": "test/uncommon_name/people3_num100.jsonl",
"sha256": "087b47837520c8da31d78deac08781c3a62ad7961bae246d68f81843b3795ab0",
"published_rows": 100,
"sampled_rows": [
37,
40
]
},
{
"path": "test/uncommon_name/people4_num100.jsonl",
"sha256": "8e0edc42da3d5739a4e24f82725c6332e71189016391810b5e80d549c71d1004",
"published_rows": 100,
"sampled_rows": [
44,
57
]
},
{
"path": "test/uncommon_name/people5_num100.jsonl",
"sha256": "c1e7ade7a1791ab17a1d951f7548ce8c54767190f0402e4e6785889810694f3f",
"published_rows": 100,
"sampled_rows": [
5,
47
]
},
{
"path": "test/uncommon_name/people6_num100.jsonl",
"sha256": "4046b49fe876c7f4eab6eddc35ff0a9f01ad487faf59e9edb5605a39b3e56530",
"published_rows": 100,
"sampled_rows": [
8,
56
]
},
{
"path": "test/uncommon_name/people7_num100.jsonl",
"sha256": "752a97113d999fcff5f2910c78fca761133822b99b5ae6ec9070421e0e51b2dc",
"published_rows": 100,
"sampled_rows": [
34,
69
]
},
{
"path": "test/uncommon_name/people8_num100.jsonl",
"sha256": "951228b33ff7b4afffcbd247eeb67390d9af5ff2def9f9cdb0f8433564fffc15",
"published_rows": 100,
"sampled_rows": [
12,
68
]
},
{
"path": "test/flip_role/people2_num100.jsonl",
"sha256": "2885eb7b356f9ab67ff2ccef9372dcfa7ebcf6ed2b8c5e720db1d74f03840c8d",
"published_rows": 100,
"sampled_rows": [
7,
80
]
},
{
"path": "test/flip_role/people3_num100.jsonl",
"sha256": "31026489710814617c66a6879408accc099c34f395425b41d0bd56743d933461",
"published_rows": 100,
"sampled_rows": [
36,
79
]
},
{
"path": "test/flip_role/people4_num100.jsonl",
"sha256": "a5d56cc8724a40fd674dba7730f52ca8dec26966f07ff434302bd62b1260a458",
"published_rows": 100,
"sampled_rows": [
36,
97
]
},
{
"path": "test/flip_role/people5_num100.jsonl",
"sha256": "1429f88b80794e12f731bcd9cd2502384a3c4ac1a3292010103baccd86e93ae7",
"published_rows": 100,
"sampled_rows": [
22,
73
]
},
{
"path": "test/flip_role/people6_num100.jsonl",
"sha256": "ccfee7ea7e2f24376e88193a979a20ac6410ec2458c534af7e4daedab4ff47ff",
"published_rows": 100,
"sampled_rows": [
39,
62
]
},
{
"path": "test/flip_role/people7_num100.jsonl",
"sha256": "b5720e5a434747f323f90086ac6bae7ba33d1b6ba8d617424f599bfabdbcdeca",
"published_rows": 100,
"sampled_rows": [
1,
91
]
},
{
"path": "test/flip_role/people8_num100.jsonl",
"sha256": "d668973f128b119ad065e0b58f33099516dbd801f0e7957c00e876115e8119dc",
"published_rows": 100,
"sampled_rows": [
64,
96
]
}
],
"label_validation": "all rows independently solved with python-constraint"
},
"dataset_manifest_sha256": "2e55a7d8b07e55b035a8129889b11dff186589bcad97c02c0ce553f38249212f",
"pure": [
{
"id": "perturbed_leaf-p2-r006",
"num": 2,
"pred": {
"James": "knight",
"Sophia": "knave"
},
"gold": {
"James": "knight",
"Sophia": "knave"
},
"correct": true,
"source": {
"dataset": "K-and-K/perturbed-knights-and-knaves",
"revision": "bc7ee75a15ee8196ccbdb7df3ab46284340412e2",
"license": "CC-BY-NC-SA-4.0",
"config": "test",
"split": "perturbed_leaf",
"path": "test/perturbed_leaf/people2_num100.jsonl",
"file_sha256": "541c5dadb60c54e4c9973cafa0dbd69635538fb44aa5dcb28dc64ea7a78a56be",
"row": 6,
"dataset_index": 9
},
"codes": [],
"text": "### Step 1: Analyze James's statement\nJames says, \"Sophia is not a knight.\" Let's denote \\( J \\) as \"James is a knight\" and \\( S \\) as \"Sophia is a knight\". James's statement is \\( \\neg S \\).\n\n### Step 2: Analyze Sophia's statement\nSophia says, \"James is a knight if and only if James is a knave.\" The \"if and only if\" (iff) statement \\( J \\leftrightarrow \\neg J \\) is a contradiction because a statement cannot be both true and false simultaneously.\n\n### Step 3: Assume James is a knight\nIf \\( J \\) is true (James is a knight), then James's statement \\( \\neg S \\) is true, so \\( S \\) is false (Sophia is a knave). Now, check Sophia's statement: \\( J \\leftrightarrow \\neg J \\). Since \\( J \\) is true and \\( \\neg J \\) is false, \\( J \\leftrightarrow \\neg J \\) is false. Sophia is a knave, so she lies, which is consistent with her false statement.\n\n### Step 4: Assume James is a knave\nIf \\( J \\) is false (James is a knave), then James's statement \\( \\neg S \\) is a lie, so \\( S \\) is true (Sophia is a knight). Now, check Sophia's statement: \\( J \\leftrightarrow \\neg J \\). Since \\( J \\) is false and \\( \\neg J \\) is true, \\( J \\leftrightarrow \\neg J \\) is false. But Sophia is a knight, so she must tell the truth. This is a contradiction because her statement is false.\n\nThus, the only consistent case is James is a knight and Sophia is a knave.\n\n{\"James\": \"knight\", \"Sophia\": \"knave\"}",
"used_python_constraint": false,
"duration_s": 9.42,
"provider_receipts": [
{
"turn": 1,
"response_id": "021785349572412c2531bd3ff98f8d456851518a83e6729925db2",
"response_model": "doubao-seed-1-6-flash-250615",
"finish_reason": "stop",
"usage": {
"prompt_tokens": 237,
"completion_tokens": 1750,
"total_tokens": 1987,
"cached_prompt_tokens": 0
},
"tool_calls": 0
}
]
}
],
"pure_acc": 1.0,
"code": [
{
"id": "perturbed_leaf-p2-r006",
"num": 2,
"pred": {
"James": "knight",
"Sophia": "knave"
},
"gold": {
"James": "knight",
"Sophia": "knave"
},
"correct": true,
"source": {
"dataset": "K-and-K/perturbed-knights-and-knaves",
"revision": "bc7ee75a15ee8196ccbdb7df3ab46284340412e2",
"license": "CC-BY-NC-SA-4.0",
"config": "test",
"split": "perturbed_leaf",
"path": "test/perturbed_leaf/people2_num100.jsonl",
"file_sha256": "541c5dadb60c54e4c9973cafa0dbd69635538fb44aa5dcb28dc64ea7a78a56be",
"row": 6,
"dataset_index": 9
},
"codes": [
"",
"from constraint import Problem\n\np = Problem()\n# 定义变量,True表示骑士,False表示无赖\np.addVariable('James', [True, False])\np.addVariable('Sophia', [True, False])\n\n# James说\"Sophia不是骑士\",所以James == (Sophia是假),即James == (not Sophia)\np.addConstraint(lambda james, sophia: james == (not sophia), ['James', 'Sophia'])\n\n# Sophia说\"James是骑士当且仅当James是无赖\",即Sophia的话等价于James == not James,而James == not James恒假,所以Sophia == False\np.addConstraint(lambda sophia: sophia == False, ['Sophia'])\n\n# 寻找所有解\nsolutions = p.getSolutions()\n\n# 转换结果并打印\nfor sol in solutions:\n result = {}\n for name, val in sol.items():\n result[name] = 'knight' if val else 'knave'\n print(result)",
"from constraint import Problem\n\np = Problem()\n# 定义变量,True表示骑士,False表示无赖\np.addVariable('James', [True, False])\np.addVariable('Sophia', [True, False])\n\n# James说\"Sophia不是骑士\",所以James == (Sophia是假),即James == (not Sophia)\np.addConstraint(lambda james, sophia: james == (not sophia), ['James', 'Sophia'])\n\n# Sophia说\"James是骑士当且仅当James是无赖\",即Sophia的话等价于James == not James,而James == not James恒假,所以Sophia == False\np.addConstraint(lambda sophia: sophia == False, ['Sophia'])\n\n# 寻找所有解\nsolutions = p.getSolutions()\n\n# 转换结果并打印\nresults = []\nfor sol in solutions:\n result = {}\n for name, val in sol.items():\n result[name] = 'knight' if val else 'knave'\n results.append(result)\n\n# 这里直接输出最终结果\nprint(results)",
"from constraint import Problem\n\np = Problem()\n# 定义变量,True表示骑士,False表示无赖\np.addVariable('James', [True, False])\np.addVariable('Sophia', [True, False])\n\n# James说\"Sophia不是骑士\",所以James == (Sophia是假),即James == (not Sophia)\np.addConstraint(lambda james, sophia: james == (not sophia), ['James', 'Sophia'])\n\n# Sophia说\"James是骑士当且仅当James是无赖\",即Sophia的话等价于James == not James,而James == not James恒假,所以Sophia == False\np.addConstraint(lambda sophia: sophia == False, ['Sophia'])\n\n# 寻找所有解\nsolutions = p.getSolutions()\n\n# 转换结果并输出最终的JSON\nfinal_result = {} \nfor sol in solutions:\n for name, val in sol.items():\n final_result[name] = 'knight' if val else 'knave'\n\nprint(final_result)"
],
"text": "{\"James\": \"knight\", \"Sophia\": \"knave\"}",
"used_python_constraint": true,
"duration_s": 17.563,
"provider_receipts": [
{
"turn": 1,
"response_id": "021785349581717c2531bd3ff98f8d456851518a83e672947b029",
"response_model": "doubao-seed-1-6-flash-250615",
"finish_reason": "tool_calls",
"usage": {
"prompt_tokens": 1124,
"completion_tokens": 437,
"total_tokens": 1561,
"cached_prompt_tokens": 0
},
"tool_calls": 1
},
{
"turn": 2,
"response_id": "021785349588314c2531bd3ff98f8d456851518a83e672919a4ec",
"response_model": "doubao-seed-1-6-flash-250615",
"finish_reason": "tool_calls",
"usage": {
"prompt_tokens": 1453,
"completion_tokens": 871,
"total_tokens": 2324,
"cached_prompt_tokens": 0
},
"tool_calls": 1
},
{
"turn": 3,
"response_id": "021785349593459c2531bd3ff98f8d456851518a83e6729b3f947",
"response_model": "doubao-seed-1-6-flash-250615",
"finish_reason": "tool_calls",
"usage": {
"prompt_tokens": 1747,
"completion_tokens": 345,
"total_tokens": 2092,
"cached_prompt_tokens": 0
},
"tool_calls": 1
},
{
"turn": 4,
"response_id": "021785349596925c2531bd3ff98f8d456851518a83e672961e3df",
"response_model": "doubao-seed-1-6-flash-250615",
"finish_reason": "tool_calls",
"usage": {
"prompt_tokens": 2062,
"completion_tokens": 322,
"total_tokens": 2384,
"cached_prompt_tokens": 0
},
"tool_calls": 1
},
{
"turn": 5,
"response_id": "021785349598681c2531bd3ff98f8d456851518a83e67292205e8",
"response_model": "doubao-seed-1-6-flash-250615",
"finish_reason": "stop",
"usage": {
"prompt_tokens": 2361,
"completion_tokens": 70,
"total_tokens": 2431,
"cached_prompt_tokens": 0
},
"tool_calls": 0
}
]
}
],
"code_acc": 1.0,
"paired_analysis": {
"test": "two-sided exact McNemar/binomial test on discordant pairs",
"n": 1,
"contingency": {
"pure_only": 0,
"code_only": 0,
"discordant": 0
},
"pure_accuracy": 1.0,
"code_accuracy": 1.0,
"accuracy_delta": 0.0,
"code_accuracy_wilson_95": [
0.20654931437723745,
1.0
],
"p_value": 1.0,
"python_constraint_tool_use_rate": 1.0,
"acceptance": {
"code_accuracy_over_90_percent": true,
"code_significantly_higher_than_pure": false,
"all_code_trajectories_used_python_constraint": true
}
}
}
@@ -0,0 +1,552 @@
{
"schema_version": "2.0",
"experiment": "5-2",
"generated_at_utc": "2026-07-29T18:25:22.452238+00:00",
"provider": "ark",
"model": "doubao-seed-1-6-250615",
"mode": "both",
"tasks": 1,
"dataset_manifest": {
"schema_version": "1.0",
"experiment": "5-2",
"dataset": "K-and-K/perturbed-knights-and-knaves",
"revision": "bc7ee75a15ee8196ccbdb7df3ab46284340412e2",
"license": "CC-BY-NC-SA-4.0",
"sampling": {
"split": "test",
"perturbations": [
"perturbed_leaf",
"perturbed_statement",
"reorder_statement",
"random_pair",
"uncommon_name",
"flip_role"
],
"people": [
2,
3,
4,
5,
6,
7,
8
],
"per_cell": 2,
"seed": 512,
"cells": 42,
"total": 84
},
"source_files": [
{
"path": "test/perturbed_leaf/people2_num100.jsonl",
"sha256": "541c5dadb60c54e4c9973cafa0dbd69635538fb44aa5dcb28dc64ea7a78a56be",
"published_rows": 76,
"sampled_rows": [
6,
11
]
},
{
"path": "test/perturbed_leaf/people3_num100.jsonl",
"sha256": "4c3d767065edb62d19728da7a4e4e4c63f550c1719c3600c868f50f48715711e",
"published_rows": 93,
"sampled_rows": [
6,
92
]
},
{
"path": "test/perturbed_leaf/people4_num100.jsonl",
"sha256": "b3811d1ea4bbdb2ade55dde240361470516e0541c58b5c526609f99ad6a038d7",
"published_rows": 94,
"sampled_rows": [
66,
82
]
},
{
"path": "test/perturbed_leaf/people5_num100.jsonl",
"sha256": "17529110798b0a659286983d0cf4e0d3b5a2b560dfb6ab3b3acb1b61ac9d7da7",
"published_rows": 98,
"sampled_rows": [
16,
41
]
},
{
"path": "test/perturbed_leaf/people6_num100.jsonl",
"sha256": "032c0c253bbe574335c741bb5ba7d07b48f622e217dc939f72a633ef9fbe46b0",
"published_rows": 100,
"sampled_rows": [
17,
51
]
},
{
"path": "test/perturbed_leaf/people7_num100.jsonl",
"sha256": "f62b1f0c3c45e3a62660951fc12b9716679f1466bdc313066d85d08d6fe3c767",
"published_rows": 100,
"sampled_rows": [
79,
80
]
},
{
"path": "test/perturbed_leaf/people8_num100.jsonl",
"sha256": "01947375b3a16ce4fb340d6f0e1545d5aeadfc6f695c49cf2a6f436fcc5ba099",
"published_rows": 100,
"sampled_rows": [
11,
69
]
},
{
"path": "test/perturbed_statement/people2_num100.jsonl",
"sha256": "18479f9b2c327e9ce51e901479e9e7ffcd4397094e91fdca430871a3fc131fc3",
"published_rows": 100,
"sampled_rows": [
8,
94
]
},
{
"path": "test/perturbed_statement/people3_num100.jsonl",
"sha256": "cb8832b4aae19426e10a039236f0e4fb7dad797598edb3850db8067ff7cda1a6",
"published_rows": 100,
"sampled_rows": [
0,
24
]
},
{
"path": "test/perturbed_statement/people4_num100.jsonl",
"sha256": "ec8821279ff09db70098b97c5b47f452f2b9a6059b3d32ff22c9fce8342e4c30",
"published_rows": 100,
"sampled_rows": [
27,
28
]
},
{
"path": "test/perturbed_statement/people5_num100.jsonl",
"sha256": "791a14c3a7fea7713904786cdbd24ccc94901c0508c1ed3daf5abe3794857ed4",
"published_rows": 100,
"sampled_rows": [
95,
99
]
},
{
"path": "test/perturbed_statement/people6_num100.jsonl",
"sha256": "afadf008aa8aba0753bb198baca07ea51ebd9879045b619861225eea56559134",
"published_rows": 100,
"sampled_rows": [
47,
52
]
},
{
"path": "test/perturbed_statement/people7_num100.jsonl",
"sha256": "bd3900ae3527887659a1a0da1146780e0a47e4d96ff301e1c2039f35bcfff849",
"published_rows": 100,
"sampled_rows": [
15,
28
]
},
{
"path": "test/perturbed_statement/people8_num100.jsonl",
"sha256": "c3d91ba046bbeb5ec0d64c9147d3d2c76c1f631716da47d32b8379ebfcbe35a0",
"published_rows": 100,
"sampled_rows": [
37,
86
]
},
{
"path": "test/reorder_statement/people2_num100.jsonl",
"sha256": "c116f384f984150969554d783ccf7cecf4c6e56c59d2f5d326bfcfa722bb149c",
"published_rows": 100,
"sampled_rows": [
60,
76
]
},
{
"path": "test/reorder_statement/people3_num100.jsonl",
"sha256": "ff772d57162fb500e608ef28c59eaf57ba1d80f60cf3f73902162c3476f6b0a2",
"published_rows": 100,
"sampled_rows": [
0,
64
]
},
{
"path": "test/reorder_statement/people4_num100.jsonl",
"sha256": "be12e4b5a38792785264c16c11117f7f10f955517e32c92ea37949017a4da0cb",
"published_rows": 100,
"sampled_rows": [
68,
97
]
},
{
"path": "test/reorder_statement/people5_num100.jsonl",
"sha256": "04a492522cdeb64a960632652b54e3fd27cc505fa8a1c48cf9c666cc8e7d6fb0",
"published_rows": 100,
"sampled_rows": [
55,
92
]
},
{
"path": "test/reorder_statement/people6_num100.jsonl",
"sha256": "4b6131d749c47ce1580d0443e373112cc5f0e20f1b0245455b3df63b106580c5",
"published_rows": 100,
"sampled_rows": [
80,
85
]
},
{
"path": "test/reorder_statement/people7_num100.jsonl",
"sha256": "d3051b151ef7e5c47a9cd69b09b52f1ec94c7f899821ec685e286e578c7932c5",
"published_rows": 100,
"sampled_rows": [
67,
92
]
},
{
"path": "test/reorder_statement/people8_num100.jsonl",
"sha256": "3710c66680d201ebf3d6d243fa9f54ee7e5ce35570ed44f87cd5111b05dc1d9b",
"published_rows": 100,
"sampled_rows": [
2,
14
]
},
{
"path": "test/random_pair/people2_num100.jsonl",
"sha256": "92b9d18d12ea3eaaa0a49daa03f519b5c5f66614bf724e541e9c00e1b9be24ae",
"published_rows": 100,
"sampled_rows": [
13,
14
]
},
{
"path": "test/random_pair/people3_num100.jsonl",
"sha256": "6eb60022244be6fa25d372d1580eb8dddfb06914d7290dbe1d64c9cb57c1f612",
"published_rows": 100,
"sampled_rows": [
54,
75
]
},
{
"path": "test/random_pair/people4_num100.jsonl",
"sha256": "1cafedf8ea1c5bf393d2366a6d798e66730059fdd5290c8b2a8cca35de6c4589",
"published_rows": 100,
"sampled_rows": [
68,
90
]
},
{
"path": "test/random_pair/people5_num100.jsonl",
"sha256": "9036535b0ace7b3cfe1b8cf94fbb898b0c07e8d88d23cbc914707ed6d0221ebc",
"published_rows": 100,
"sampled_rows": [
62,
79
]
},
{
"path": "test/random_pair/people6_num100.jsonl",
"sha256": "9d3a4bcd28f465b311bd5cdc96958b69529240f05e00a79d80ea7a54b8d53002",
"published_rows": 100,
"sampled_rows": [
22,
45
]
},
{
"path": "test/random_pair/people7_num100.jsonl",
"sha256": "389713889ec22d14bf64d1bb9b846d1fb7a4adba44004b6ce9db1e0ac3d54190",
"published_rows": 100,
"sampled_rows": [
64,
91
]
},
{
"path": "test/random_pair/people8_num100.jsonl",
"sha256": "8885fe0d63bfe5910617bd84e6be646f7b62a26978a649b4173889601d0f9229",
"published_rows": 100,
"sampled_rows": [
68,
71
]
},
{
"path": "test/uncommon_name/people2_num100.jsonl",
"sha256": "25437adfc79438934dbaa113f4a2b06378d3f899a3cc9d8193202a3e0d746f29",
"published_rows": 100,
"sampled_rows": [
16,
82
]
},
{
"path": "test/uncommon_name/people3_num100.jsonl",
"sha256": "087b47837520c8da31d78deac08781c3a62ad7961bae246d68f81843b3795ab0",
"published_rows": 100,
"sampled_rows": [
37,
40
]
},
{
"path": "test/uncommon_name/people4_num100.jsonl",
"sha256": "8e0edc42da3d5739a4e24f82725c6332e71189016391810b5e80d549c71d1004",
"published_rows": 100,
"sampled_rows": [
44,
57
]
},
{
"path": "test/uncommon_name/people5_num100.jsonl",
"sha256": "c1e7ade7a1791ab17a1d951f7548ce8c54767190f0402e4e6785889810694f3f",
"published_rows": 100,
"sampled_rows": [
5,
47
]
},
{
"path": "test/uncommon_name/people6_num100.jsonl",
"sha256": "4046b49fe876c7f4eab6eddc35ff0a9f01ad487faf59e9edb5605a39b3e56530",
"published_rows": 100,
"sampled_rows": [
8,
56
]
},
{
"path": "test/uncommon_name/people7_num100.jsonl",
"sha256": "752a97113d999fcff5f2910c78fca761133822b99b5ae6ec9070421e0e51b2dc",
"published_rows": 100,
"sampled_rows": [
34,
69
]
},
{
"path": "test/uncommon_name/people8_num100.jsonl",
"sha256": "951228b33ff7b4afffcbd247eeb67390d9af5ff2def9f9cdb0f8433564fffc15",
"published_rows": 100,
"sampled_rows": [
12,
68
]
},
{
"path": "test/flip_role/people2_num100.jsonl",
"sha256": "2885eb7b356f9ab67ff2ccef9372dcfa7ebcf6ed2b8c5e720db1d74f03840c8d",
"published_rows": 100,
"sampled_rows": [
7,
80
]
},
{
"path": "test/flip_role/people3_num100.jsonl",
"sha256": "31026489710814617c66a6879408accc099c34f395425b41d0bd56743d933461",
"published_rows": 100,
"sampled_rows": [
36,
79
]
},
{
"path": "test/flip_role/people4_num100.jsonl",
"sha256": "a5d56cc8724a40fd674dba7730f52ca8dec26966f07ff434302bd62b1260a458",
"published_rows": 100,
"sampled_rows": [
36,
97
]
},
{
"path": "test/flip_role/people5_num100.jsonl",
"sha256": "1429f88b80794e12f731bcd9cd2502384a3c4ac1a3292010103baccd86e93ae7",
"published_rows": 100,
"sampled_rows": [
22,
73
]
},
{
"path": "test/flip_role/people6_num100.jsonl",
"sha256": "ccfee7ea7e2f24376e88193a979a20ac6410ec2458c534af7e4daedab4ff47ff",
"published_rows": 100,
"sampled_rows": [
39,
62
]
},
{
"path": "test/flip_role/people7_num100.jsonl",
"sha256": "b5720e5a434747f323f90086ac6bae7ba33d1b6ba8d617424f599bfabdbcdeca",
"published_rows": 100,
"sampled_rows": [
1,
91
]
},
{
"path": "test/flip_role/people8_num100.jsonl",
"sha256": "d668973f128b119ad065e0b58f33099516dbd801f0e7957c00e876115e8119dc",
"published_rows": 100,
"sampled_rows": [
64,
96
]
}
],
"label_validation": "all rows independently solved with python-constraint"
},
"dataset_manifest_sha256": "2e55a7d8b07e55b035a8129889b11dff186589bcad97c02c0ce553f38249212f",
"pure": [
{
"id": "perturbed_leaf-p2-r006",
"num": 2,
"pred": {
"James": "knight",
"Sophia": "knave"
},
"gold": {
"James": "knight",
"Sophia": "knave"
},
"correct": true,
"source": {
"dataset": "K-and-K/perturbed-knights-and-knaves",
"revision": "bc7ee75a15ee8196ccbdb7df3ab46284340412e2",
"license": "CC-BY-NC-SA-4.0",
"config": "test",
"split": "perturbed_leaf",
"path": "test/perturbed_leaf/people2_num100.jsonl",
"file_sha256": "541c5dadb60c54e4c9973cafa0dbd69635538fb44aa5dcb28dc64ea7a78a56be",
"row": 6,
"dataset_index": 9
},
"codes": [],
"text": "Let's analyze the statements of James and Sophia step by step:\n\n\n### **Step 1: Analyze Sophia's Statement** \nSophia claims: \"James is a knight if and only if James is a knave.\" \nAn \"if and only if\" (↔) statement is true only when both sides have the same truth value. Here, the two sides are \"James is a knight\" (J_knight) and \"James is a knave\" (¬J_knight, since a knave is not a knight). \n\n- If J_knight is **true**, then ¬J_knight is **false**. Thus, \"true ↔ false\" is **false**. \n- If J_knight is **false**, then ¬J_knight is **true**. Thus, \"false ↔ true\" is **false**. \n\nIn all cases, Sophias statement is **logically false** (a contradiction). \n\n\n### **Step 2: Determine Sophia's Identity** \nSince Sophias statement is false, and knaves always lie, Sophia must be a **knave**. \n\n\n### **Step 3: Analyze James's Statement** \nJames claims: \"Sophia is not a knight.\" \nWe already established Sophia is a knave, so \"Sophia is not a knight\" is a **true statement**. \n\nSince Jamess statement is true, and knights always tell the truth, James must be a **knight**. \n\n\n### **Verification** \n- James (knight) tells the truth: \"Sophia is not a knight\" is true (Sophia is a knave). \n- Sophia (knave) lies: Her statement \"James is a knight if and only if James is a knave\" is false (consistent with knaves lying). \n\n\n{\"James\": \"knight\", \"Sophia\": \"knave\"}",
"used_python_constraint": false,
"duration_s": 33.894,
"provider_receipts": [
{
"turn": 1,
"response_id": "021785349436115840fd37cfe82fdd108ca4a9d2a6525759244e1",
"response_model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"usage": {
"prompt_tokens": 265,
"completion_tokens": 1351,
"total_tokens": 1616,
"cached_prompt_tokens": 0
},
"tool_calls": 0
}
]
}
],
"pure_acc": 1.0,
"code": [
{
"id": "perturbed_leaf-p2-r006",
"num": 2,
"pred": {
"James": "knight",
"Sophia": "knave"
},
"gold": {
"James": "knight",
"Sophia": "knave"
},
"correct": true,
"source": {
"dataset": "K-and-K/perturbed-knights-and-knaves",
"revision": "bc7ee75a15ee8196ccbdb7df3ab46284340412e2",
"license": "CC-BY-NC-SA-4.0",
"config": "test",
"split": "perturbed_leaf",
"path": "test/perturbed_leaf/people2_num100.jsonl",
"file_sha256": "541c5dadb60c54e4c9973cafa0dbd69635538fb44aa5dcb28dc64ea7a78a56be",
"row": 6,
"dataset_index": 9
},
"codes": [
"from constraint import Problem\n\np = Problem()\n# 为James和Sophia创建布尔变量,True=骑士,False=无赖\np.addVariable('James', [True, False])\np.addVariable('Sophia', [True, False])\n\n# James说:\"Sophia不是骑士\" → James的身份等价于(Sophia是无赖)\n# 即 James == (not Sophia)\np.addConstraint(lambda j, s: j == (not s), ['James', 'Sophia'])\n\n# Sophia说:\"James是骑士当且仅当James是无赖\" → Sophia的身份等价于(James是骑士 ⇨⇦ James是无赖)\n# 即 Sophia == (James == (not James))\np.addConstraint(lambda j, s: s == (j == (not j)), ['James', 'Sophia'])\n\n# 求解并输出结果\nfor solution in p.getSolutions():\n result = {k: 'knight' if v else 'knave' for k, v in solution.items()}\n print(result)"
],
"text": "{\"James\": \"knight\", \"Sophia\": \"knave\"}",
"used_python_constraint": true,
"duration_s": 52.753,
"provider_receipts": [
{
"turn": 1,
"response_id": "021785349469880840fd37cfe82fdd108ca4a9d2a652575539cd2",
"response_model": "doubao-seed-1-6-250615",
"finish_reason": "tool_calls",
"usage": {
"prompt_tokens": 1152,
"completion_tokens": 1827,
"total_tokens": 2979,
"cached_prompt_tokens": 0
},
"tool_calls": 1
},
{
"turn": 2,
"response_id": "021785349507332840fd37cfe82fdd108ca4a9d2a652575ed09ce",
"response_model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"usage": {
"prompt_tokens": 1451,
"completion_tokens": 515,
"total_tokens": 1966,
"cached_prompt_tokens": 0
},
"tool_calls": 0
}
]
}
],
"code_acc": 1.0,
"paired_analysis": {
"test": "two-sided exact McNemar/binomial test on discordant pairs",
"n": 1,
"contingency": {
"pure_only": 0,
"code_only": 0,
"discordant": 0
},
"pure_accuracy": 1.0,
"code_accuracy": 1.0,
"accuracy_delta": 0.0,
"code_accuracy_wilson_95": [
0.20654931437723745,
1.0
],
"p_value": 1.0,
"python_constraint_tool_use_rate": 1.0,
"acceptance": {
"code_accuracy_over_90_percent": true,
"code_significantly_higher_than_pure": false,
"all_code_trajectories_used_python_constraint": true
}
}
}
@@ -0,0 +1,531 @@
{
"schema_version": "2.0",
"experiment": "5-2",
"generated_at_utc": "2026-07-29T18:34:11.952867+00:00",
"provider": "ollama",
"model": "qwen3:1.7b",
"mode": "both",
"tasks": 1,
"dataset_manifest": {
"schema_version": "1.0",
"experiment": "5-2",
"dataset": "K-and-K/perturbed-knights-and-knaves",
"revision": "bc7ee75a15ee8196ccbdb7df3ab46284340412e2",
"license": "CC-BY-NC-SA-4.0",
"sampling": {
"split": "test",
"perturbations": [
"perturbed_leaf",
"perturbed_statement",
"reorder_statement",
"random_pair",
"uncommon_name",
"flip_role"
],
"people": [
2,
3,
4,
5,
6,
7,
8
],
"per_cell": 2,
"seed": 512,
"cells": 42,
"total": 84
},
"source_files": [
{
"path": "test/perturbed_leaf/people2_num100.jsonl",
"sha256": "541c5dadb60c54e4c9973cafa0dbd69635538fb44aa5dcb28dc64ea7a78a56be",
"published_rows": 76,
"sampled_rows": [
6,
11
]
},
{
"path": "test/perturbed_leaf/people3_num100.jsonl",
"sha256": "4c3d767065edb62d19728da7a4e4e4c63f550c1719c3600c868f50f48715711e",
"published_rows": 93,
"sampled_rows": [
6,
92
]
},
{
"path": "test/perturbed_leaf/people4_num100.jsonl",
"sha256": "b3811d1ea4bbdb2ade55dde240361470516e0541c58b5c526609f99ad6a038d7",
"published_rows": 94,
"sampled_rows": [
66,
82
]
},
{
"path": "test/perturbed_leaf/people5_num100.jsonl",
"sha256": "17529110798b0a659286983d0cf4e0d3b5a2b560dfb6ab3b3acb1b61ac9d7da7",
"published_rows": 98,
"sampled_rows": [
16,
41
]
},
{
"path": "test/perturbed_leaf/people6_num100.jsonl",
"sha256": "032c0c253bbe574335c741bb5ba7d07b48f622e217dc939f72a633ef9fbe46b0",
"published_rows": 100,
"sampled_rows": [
17,
51
]
},
{
"path": "test/perturbed_leaf/people7_num100.jsonl",
"sha256": "f62b1f0c3c45e3a62660951fc12b9716679f1466bdc313066d85d08d6fe3c767",
"published_rows": 100,
"sampled_rows": [
79,
80
]
},
{
"path": "test/perturbed_leaf/people8_num100.jsonl",
"sha256": "01947375b3a16ce4fb340d6f0e1545d5aeadfc6f695c49cf2a6f436fcc5ba099",
"published_rows": 100,
"sampled_rows": [
11,
69
]
},
{
"path": "test/perturbed_statement/people2_num100.jsonl",
"sha256": "18479f9b2c327e9ce51e901479e9e7ffcd4397094e91fdca430871a3fc131fc3",
"published_rows": 100,
"sampled_rows": [
8,
94
]
},
{
"path": "test/perturbed_statement/people3_num100.jsonl",
"sha256": "cb8832b4aae19426e10a039236f0e4fb7dad797598edb3850db8067ff7cda1a6",
"published_rows": 100,
"sampled_rows": [
0,
24
]
},
{
"path": "test/perturbed_statement/people4_num100.jsonl",
"sha256": "ec8821279ff09db70098b97c5b47f452f2b9a6059b3d32ff22c9fce8342e4c30",
"published_rows": 100,
"sampled_rows": [
27,
28
]
},
{
"path": "test/perturbed_statement/people5_num100.jsonl",
"sha256": "791a14c3a7fea7713904786cdbd24ccc94901c0508c1ed3daf5abe3794857ed4",
"published_rows": 100,
"sampled_rows": [
95,
99
]
},
{
"path": "test/perturbed_statement/people6_num100.jsonl",
"sha256": "afadf008aa8aba0753bb198baca07ea51ebd9879045b619861225eea56559134",
"published_rows": 100,
"sampled_rows": [
47,
52
]
},
{
"path": "test/perturbed_statement/people7_num100.jsonl",
"sha256": "bd3900ae3527887659a1a0da1146780e0a47e4d96ff301e1c2039f35bcfff849",
"published_rows": 100,
"sampled_rows": [
15,
28
]
},
{
"path": "test/perturbed_statement/people8_num100.jsonl",
"sha256": "c3d91ba046bbeb5ec0d64c9147d3d2c76c1f631716da47d32b8379ebfcbe35a0",
"published_rows": 100,
"sampled_rows": [
37,
86
]
},
{
"path": "test/reorder_statement/people2_num100.jsonl",
"sha256": "c116f384f984150969554d783ccf7cecf4c6e56c59d2f5d326bfcfa722bb149c",
"published_rows": 100,
"sampled_rows": [
60,
76
]
},
{
"path": "test/reorder_statement/people3_num100.jsonl",
"sha256": "ff772d57162fb500e608ef28c59eaf57ba1d80f60cf3f73902162c3476f6b0a2",
"published_rows": 100,
"sampled_rows": [
0,
64
]
},
{
"path": "test/reorder_statement/people4_num100.jsonl",
"sha256": "be12e4b5a38792785264c16c11117f7f10f955517e32c92ea37949017a4da0cb",
"published_rows": 100,
"sampled_rows": [
68,
97
]
},
{
"path": "test/reorder_statement/people5_num100.jsonl",
"sha256": "04a492522cdeb64a960632652b54e3fd27cc505fa8a1c48cf9c666cc8e7d6fb0",
"published_rows": 100,
"sampled_rows": [
55,
92
]
},
{
"path": "test/reorder_statement/people6_num100.jsonl",
"sha256": "4b6131d749c47ce1580d0443e373112cc5f0e20f1b0245455b3df63b106580c5",
"published_rows": 100,
"sampled_rows": [
80,
85
]
},
{
"path": "test/reorder_statement/people7_num100.jsonl",
"sha256": "d3051b151ef7e5c47a9cd69b09b52f1ec94c7f899821ec685e286e578c7932c5",
"published_rows": 100,
"sampled_rows": [
67,
92
]
},
{
"path": "test/reorder_statement/people8_num100.jsonl",
"sha256": "3710c66680d201ebf3d6d243fa9f54ee7e5ce35570ed44f87cd5111b05dc1d9b",
"published_rows": 100,
"sampled_rows": [
2,
14
]
},
{
"path": "test/random_pair/people2_num100.jsonl",
"sha256": "92b9d18d12ea3eaaa0a49daa03f519b5c5f66614bf724e541e9c00e1b9be24ae",
"published_rows": 100,
"sampled_rows": [
13,
14
]
},
{
"path": "test/random_pair/people3_num100.jsonl",
"sha256": "6eb60022244be6fa25d372d1580eb8dddfb06914d7290dbe1d64c9cb57c1f612",
"published_rows": 100,
"sampled_rows": [
54,
75
]
},
{
"path": "test/random_pair/people4_num100.jsonl",
"sha256": "1cafedf8ea1c5bf393d2366a6d798e66730059fdd5290c8b2a8cca35de6c4589",
"published_rows": 100,
"sampled_rows": [
68,
90
]
},
{
"path": "test/random_pair/people5_num100.jsonl",
"sha256": "9036535b0ace7b3cfe1b8cf94fbb898b0c07e8d88d23cbc914707ed6d0221ebc",
"published_rows": 100,
"sampled_rows": [
62,
79
]
},
{
"path": "test/random_pair/people6_num100.jsonl",
"sha256": "9d3a4bcd28f465b311bd5cdc96958b69529240f05e00a79d80ea7a54b8d53002",
"published_rows": 100,
"sampled_rows": [
22,
45
]
},
{
"path": "test/random_pair/people7_num100.jsonl",
"sha256": "389713889ec22d14bf64d1bb9b846d1fb7a4adba44004b6ce9db1e0ac3d54190",
"published_rows": 100,
"sampled_rows": [
64,
91
]
},
{
"path": "test/random_pair/people8_num100.jsonl",
"sha256": "8885fe0d63bfe5910617bd84e6be646f7b62a26978a649b4173889601d0f9229",
"published_rows": 100,
"sampled_rows": [
68,
71
]
},
{
"path": "test/uncommon_name/people2_num100.jsonl",
"sha256": "25437adfc79438934dbaa113f4a2b06378d3f899a3cc9d8193202a3e0d746f29",
"published_rows": 100,
"sampled_rows": [
16,
82
]
},
{
"path": "test/uncommon_name/people3_num100.jsonl",
"sha256": "087b47837520c8da31d78deac08781c3a62ad7961bae246d68f81843b3795ab0",
"published_rows": 100,
"sampled_rows": [
37,
40
]
},
{
"path": "test/uncommon_name/people4_num100.jsonl",
"sha256": "8e0edc42da3d5739a4e24f82725c6332e71189016391810b5e80d549c71d1004",
"published_rows": 100,
"sampled_rows": [
44,
57
]
},
{
"path": "test/uncommon_name/people5_num100.jsonl",
"sha256": "c1e7ade7a1791ab17a1d951f7548ce8c54767190f0402e4e6785889810694f3f",
"published_rows": 100,
"sampled_rows": [
5,
47
]
},
{
"path": "test/uncommon_name/people6_num100.jsonl",
"sha256": "4046b49fe876c7f4eab6eddc35ff0a9f01ad487faf59e9edb5605a39b3e56530",
"published_rows": 100,
"sampled_rows": [
8,
56
]
},
{
"path": "test/uncommon_name/people7_num100.jsonl",
"sha256": "752a97113d999fcff5f2910c78fca761133822b99b5ae6ec9070421e0e51b2dc",
"published_rows": 100,
"sampled_rows": [
34,
69
]
},
{
"path": "test/uncommon_name/people8_num100.jsonl",
"sha256": "951228b33ff7b4afffcbd247eeb67390d9af5ff2def9f9cdb0f8433564fffc15",
"published_rows": 100,
"sampled_rows": [
12,
68
]
},
{
"path": "test/flip_role/people2_num100.jsonl",
"sha256": "2885eb7b356f9ab67ff2ccef9372dcfa7ebcf6ed2b8c5e720db1d74f03840c8d",
"published_rows": 100,
"sampled_rows": [
7,
80
]
},
{
"path": "test/flip_role/people3_num100.jsonl",
"sha256": "31026489710814617c66a6879408accc099c34f395425b41d0bd56743d933461",
"published_rows": 100,
"sampled_rows": [
36,
79
]
},
{
"path": "test/flip_role/people4_num100.jsonl",
"sha256": "a5d56cc8724a40fd674dba7730f52ca8dec26966f07ff434302bd62b1260a458",
"published_rows": 100,
"sampled_rows": [
36,
97
]
},
{
"path": "test/flip_role/people5_num100.jsonl",
"sha256": "1429f88b80794e12f731bcd9cd2502384a3c4ac1a3292010103baccd86e93ae7",
"published_rows": 100,
"sampled_rows": [
22,
73
]
},
{
"path": "test/flip_role/people6_num100.jsonl",
"sha256": "ccfee7ea7e2f24376e88193a979a20ac6410ec2458c534af7e4daedab4ff47ff",
"published_rows": 100,
"sampled_rows": [
39,
62
]
},
{
"path": "test/flip_role/people7_num100.jsonl",
"sha256": "b5720e5a434747f323f90086ac6bae7ba33d1b6ba8d617424f599bfabdbcdeca",
"published_rows": 100,
"sampled_rows": [
1,
91
]
},
{
"path": "test/flip_role/people8_num100.jsonl",
"sha256": "d668973f128b119ad065e0b58f33099516dbd801f0e7957c00e876115e8119dc",
"published_rows": 100,
"sampled_rows": [
64,
96
]
}
],
"label_validation": "all rows independently solved with python-constraint"
},
"dataset_manifest_sha256": "2e55a7d8b07e55b035a8129889b11dff186589bcad97c02c0ce553f38249212f",
"pure": [
{
"id": "perturbed_leaf-p2-r006",
"num": 2,
"pred": null,
"gold": {
"James": "knight",
"Sophia": "knave"
},
"correct": false,
"source": {
"dataset": "K-and-K/perturbed-knights-and-knaves",
"revision": "bc7ee75a15ee8196ccbdb7df3ab46284340412e2",
"license": "CC-BY-NC-SA-4.0",
"config": "test",
"split": "perturbed_leaf",
"path": "test/perturbed_leaf/people2_num100.jsonl",
"file_sha256": "541c5dadb60c54e4c9973cafa0dbd69635538fb44aa5dcb28dc64ea7a78a56be",
"row": 6,
"dataset_index": 9
},
"codes": [],
"text": "{\"A\": \"knight\", \"B\": \"knave\"}",
"used_python_constraint": false,
"duration_s": 7.214,
"provider_receipts": [
{
"turn": 1,
"response_id": "chatcmpl-321",
"response_model": "qwen3:1.7b",
"finish_reason": "stop",
"usage": {
"prompt_tokens": 191,
"completion_tokens": 702,
"total_tokens": 893,
"cached_prompt_tokens": null
},
"tool_calls": 0
}
]
}
],
"pure_acc": 0.0,
"code": [
{
"id": "perturbed_leaf-p2-r006",
"num": 2,
"pred": null,
"gold": {
"James": "knight",
"Sophia": "knave"
},
"correct": false,
"source": {
"dataset": "K-and-K/perturbed-knights-and-knaves",
"revision": "bc7ee75a15ee8196ccbdb7df3ab46284340412e2",
"license": "CC-BY-NC-SA-4.0",
"config": "test",
"split": "perturbed_leaf",
"path": "test/perturbed_leaf/people2_num100.jsonl",
"file_sha256": "541c5dadb60c54e4c9973cafa0dbd69635538fb44aa5dcb28dc64ea7a78a56be",
"row": 6,
"dataset_index": 9
},
"codes": [],
"text": "{\"A\": \"knight\", \"B\": \"knave\"}\n\n```python\nfrom constraint import Problem\np = Problem()\n\n# Add variables for James and Sophia\np.addVariable(\"James\", [True, False])\np.addVariable(\"Sophia\", [True, False])\n\n# James says \"Sophia is not a knight\"\np.addConstraint(lambda j, s: j == (not s), [\"James\", \"Sophia\"])\n\n# Sophia says \"James is a knight if and only if James is a knave\"\np.addConstraint(lambda j: j == (j == not j), [\"James\"])\n\nfor solution in p.getSolutions():\n print({k: 'knight' if v else 'knave' for k, v in solution.items()})\n```\n\nThe solution shows James is a knight (truth-teller) and Sophia is a knave (liar). The constraints are satisfied:\n- James's statement \"Sophia is not a knight\" is true (she's a knave).\n- Sophia's statement \"James is a knight iff James is a knave\" is false (since James is a knight, the statement is \"True iff False\", which is false, consistent with her being a knave.\n\nThe code returns:\n{\"A\": \"knight\", \"B\": \"knave\"}",
"used_python_constraint": false,
"duration_s": 7.145,
"provider_receipts": [
{
"turn": 1,
"response_id": "chatcmpl-732",
"response_model": "qwen3:1.7b",
"finish_reason": "stop",
"usage": {
"prompt_tokens": 831,
"completion_tokens": 863,
"total_tokens": 1694,
"cached_prompt_tokens": null
},
"tool_calls": 0
}
]
}
],
"code_acc": 0.0,
"paired_analysis": {
"test": "two-sided exact McNemar/binomial test on discordant pairs",
"n": 1,
"contingency": {
"pure_only": 0,
"code_only": 0,
"discordant": 0
},
"pure_accuracy": 0.0,
"code_accuracy": 0.0,
"accuracy_delta": 0.0,
"code_accuracy_wilson_95": [
0.0,
0.7934506856227626
],
"p_value": 1.0,
"python_constraint_tool_use_rate": 0.0,
"acceptance": {
"code_accuracy_over_90_percent": false,
"code_significantly_higher_than_pure": false,
"all_code_trajectories_used_python_constraint": false
}
}
}