coatlearn 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. coatlearn/__init__.py +26 -0
  2. coatlearn/__main__.py +5 -0
  3. coatlearn/annotation/__init__.py +12 -0
  4. coatlearn/annotation/batch.py +90 -0
  5. coatlearn/annotation/dedup.py +135 -0
  6. coatlearn/annotation/definitions.py +216 -0
  7. coatlearn/annotation/judge.py +117 -0
  8. coatlearn/annotation/measurement.py +639 -0
  9. coatlearn/annotation/prediction.py +134 -0
  10. coatlearn/annotation/quality.py +128 -0
  11. coatlearn/annotation/text.py +290 -0
  12. coatlearn/annotation/types.py +388 -0
  13. coatlearn/api.py +1577 -0
  14. coatlearn/causality/__init__.py +5 -0
  15. coatlearn/causality/adjustment.py +183 -0
  16. coatlearn/causality/effects.py +631 -0
  17. coatlearn/causality/graph.py +447 -0
  18. coatlearn/causality/independence.py +19 -0
  19. coatlearn/causality/interface.py +85 -0
  20. coatlearn/causality/learning.py +167 -0
  21. coatlearn/causality/postprocessing.py +126 -0
  22. coatlearn/causality/priors.py +65 -0
  23. coatlearn/causality/statistics.py +83 -0
  24. coatlearn/cli/__init__.py +60 -0
  25. coatlearn/configuration.py +569 -0
  26. coatlearn/discovery/__init__.py +4 -0
  27. coatlearn/discovery/adj.py +703 -0
  28. coatlearn/discovery/mb.py +2521 -0
  29. coatlearn/discovery/pag.py +782 -0
  30. coatlearn/execution.py +200 -0
  31. coatlearn/models/__init__.py +8 -0
  32. coatlearn/models/jev.py +4 -0
  33. coatlearn/models/kev.py +65 -0
  34. coatlearn/models/kev_server.py +119 -0
  35. coatlearn/models/openai.py +531 -0
  36. coatlearn/models/presets.py +121 -0
  37. coatlearn/models/systemone.py +372 -0
  38. coatlearn/utils/__init__.py +1 -0
  39. coatlearn/utils/constants.py +107 -0
  40. coatlearn/utils/logging.py +64 -0
  41. coatlearn/utils/sampling.py +93 -0
  42. coatlearn-0.1.0.dist-info/METADATA +150 -0
  43. coatlearn-0.1.0.dist-info/RECORD +53 -0
  44. coatlearn-0.1.0.dist-info/WHEEL +5 -0
  45. coatlearn-0.1.0.dist-info/entry_points.txt +2 -0
  46. coatlearn-0.1.0.dist-info/licenses/LICENSE +202 -0
  47. coatlearn-0.1.0.dist-info/licenses/NOTICE +21 -0
  48. coatlearn-0.1.0.dist-info/licenses/data/LICENSE +22 -0
  49. coatlearn-0.1.0.dist-info/licenses/docs/assets/vendor/LICENSE-docsify +21 -0
  50. coatlearn-0.1.0.dist-info/licenses/docs/assets/vendor/LICENSE-docsify-copy-code +21 -0
  51. coatlearn-0.1.0.dist-info/licenses/docs/assets/vendor/LICENSE-docsify-pagination +21 -0
  52. coatlearn-0.1.0.dist-info/licenses/docs/assets/vendor/LICENSE-prismjs +21 -0
  53. coatlearn-0.1.0.dist-info/top_level.txt +1 -0
coatlearn/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """COAT: causal discovery over unstructured data with large language models.
2
+
3
+ The public surface is intentionally small::
4
+
5
+ from coatlearn import ModelConfig, RunConfig, run_mb, run_pag, run_adj
6
+
7
+ ``run_mb`` / ``run_pag`` / ``run_adj`` wrap the core experiment classes
8
+ (``COATMB`` / ``COATPAG`` / ``COATADJ``) without re-implementing any
9
+ algorithm. They validate inputs first, then resolve model clients, and
10
+ return a strictly JSON-safe result dictionary with a conservative
11
+ ``status`` field.
12
+
13
+ Importing this package has no side effects: no model client is
14
+ constructed, no credentials are read, no experiment object is created,
15
+ no NLTK corpus is downloaded, and no dataset is accessed. Heavy
16
+ dependencies (causal-learn and friends) are imported only when a ``run_*``
17
+ function is actually called.
18
+
19
+ Independent model, annotation and causal operations are available from their
20
+ functional subpackages. Importing those interfaces starts no model inference.
21
+ """
22
+
23
+ from coatlearn.models.openai import ModelConfig
24
+ from .api import RunConfig, run_mb, run_pag, run_adj
25
+
26
+ __all__ = ["ModelConfig", "RunConfig", "run_mb", "run_pag", "run_adj"]
coatlearn/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Run the installed command with python -m coatlearn."""
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,12 @@
1
+ """Measure user-defined factors without running causal discovery."""
2
+ from .types import MeasurementSpec, FactorAnnotation, UnsupportedMeasurement
3
+ from .definitions import factor_definition_to_spec, general_definition_to_spec
4
+ from .measurement import JevAnnotator, KevAnnotator, FactorAnnotator
5
+ from .batch import annotate, AnnotationResult
6
+ from .judge import JevFactorJudge, review_definition
7
+ from .dedup import JevFactorDeduplicator, compare_definitions
8
+
9
+ __all__ = ["annotate", "AnnotationResult", "MeasurementSpec", "FactorAnnotation",
10
+ "UnsupportedMeasurement", "factor_definition_to_spec", "general_definition_to_spec",
11
+ "FactorAnnotator", "JevAnnotator", "KevAnnotator", "JevFactorJudge",
12
+ "JevFactorDeduplicator", "review_definition", "compare_definitions"]
@@ -0,0 +1,90 @@
1
+ """Batch measurement of fixed factor definitions, with no discovery or screening."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Mapping
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ from dataclasses import dataclass
7
+ import pandas as pd
8
+
9
+ from .definitions import factor_definition_to_spec
10
+ from .types import MeasurementSpec
11
+
12
+
13
+ @dataclass
14
+ class AnnotationResult:
15
+ """Aligned values, per-measurement metadata and the finalized definitions."""
16
+
17
+ values: pd.DataFrame
18
+ metadata: pd.DataFrame
19
+ specs: dict[str, MeasurementSpec]
20
+
21
+
22
+ def resolve_annotator(model):
23
+ """Resolve a preset or model configuration after local data validation."""
24
+ from coatlearn.models.presets import ModelPreset
25
+ from coatlearn.models.jev import JevConfig
26
+ from coatlearn.models.systemone import SystemOneClient
27
+ from .measurement import JevAnnotator, decision_annotator
28
+
29
+ if isinstance(model, ModelPreset):
30
+ model = model._resolve("annotation")
31
+ if isinstance(model, JevConfig):
32
+ return decision_annotator(model)
33
+ if callable(getattr(model, "annotate_record", None)):
34
+ return model
35
+ if isinstance(model, SystemOneClient):
36
+ annotator = JevAnnotator(model.config)
37
+ annotator._system_one = model._system_one
38
+ annotator.stats = model.stats
39
+ return annotator
40
+ raise TypeError("model must be a Jev/Kev configuration, preset, System One client or factor annotator")
41
+
42
+
43
+ def annotate(texts, definitions: Mapping[str, str | MeasurementSpec], *, model,
44
+ concurrency: int = 16, batch_size: int = 8) -> AnnotationResult:
45
+ """Measure fixed definitions in row order, without judging or deduplicating them.
46
+
47
+ Each request contains one text and up to batch_size factors. A pandas Series
48
+ retains its index, including repeated labels. Transport failures propagate;
49
+ a mention gate can independently record a missing measurement as NaN.
50
+ """
51
+ if not isinstance(concurrency, int) or isinstance(concurrency, bool) or concurrency < 1:
52
+ raise ValueError("concurrency must be a positive integer")
53
+ if not isinstance(batch_size, int) or isinstance(batch_size, bool) or not 1 <= batch_size <= 8:
54
+ raise ValueError("batch_size must be between 1 and 8")
55
+ if isinstance(texts, str):
56
+ raise TypeError("texts must be a sequence, not a single string")
57
+ series = texts.copy() if isinstance(texts, pd.Series) else pd.Series(list(texts), dtype=object)
58
+ if series.empty or not series.map(lambda text: isinstance(text, str) and bool(text.strip())).all():
59
+ raise ValueError("texts must contain nonempty strings")
60
+ if not isinstance(definitions, Mapping) or not definitions:
61
+ raise ValueError("definitions must map factor names to definitions")
62
+ specs = {}
63
+ for name, definition in definitions.items():
64
+ spec = definition if isinstance(definition, MeasurementSpec) else factor_definition_to_spec(name, definition)
65
+ if spec.factor_name != name:
66
+ raise ValueError("Definition names must match their mapping keys")
67
+ specs[name] = spec.validate()
68
+ backend = resolve_annotator(model)
69
+ prepared = {name: backend.prepare_factor(name, spec) for name, spec in specs.items()}
70
+ names = list(prepared)
71
+
72
+ def measure(item):
73
+ row, text = item
74
+ values, metadata = {}, []
75
+ for start in range(0, len(names), batch_size):
76
+ chunk = {name: prepared[name] for name in names[start:start + batch_size]}
77
+ answers = backend.annotate_record(text, chunk, sample_id=row)
78
+ for name in chunk:
79
+ answer = answers[name]
80
+ values[name] = answer.value
81
+ snapshot = getattr(backend, "annotation_metadata", None)
82
+ record = snapshot(row, name) if callable(snapshot) else None
83
+ metadata.append({**(record if record is not None else answer.to_metadata()), "row": row})
84
+ return values, metadata
85
+
86
+ with ThreadPoolExecutor(max_workers=concurrency) as pool:
87
+ measured = list(pool.map(measure, enumerate(series.tolist())))
88
+ values = pd.DataFrame([row for row, _ in measured], index=series.index, columns=names)
89
+ metadata = pd.DataFrame([record for _, records in measured for record in records])
90
+ return AnnotationResult(values, metadata, dict(prepared))
@@ -0,0 +1,135 @@
1
+ """Jev concept-level comparison, usable standalone or via opt-in COAT dedup.
2
+
3
+ Compare original definitions without rewriting them or reusing labels across
4
+ non-equivalent rubrics. UNCERTAIN and technical ERROR both retain a candidate,
5
+ but are distinct audit outcomes. This is not a definition/label merge engine.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import asdict
10
+
11
+ from coatlearn.annotation.types import MeasurementSpec
12
+ from coatlearn.models.jev import JevConfig
13
+
14
+ DEFAULT_TASK_CONTEXT = "从非结构化文本中识别候选因子;比较完整定义对应的核心属性,而非预测目标值或判断因果关系。"
15
+
16
+
17
+ def normalize_factor_name(name: str) -> str:
18
+ """Legacy exact-name normalization only; no substring or WordNet veto."""
19
+ return " ".join(name.lower().replace("_", " ").replace("-", " ").split())
20
+
21
+
22
+ def configured_deduplicator(fields):
23
+ """Validate optional core config without constructing an HTTP client."""
24
+ if fields is None:
25
+ return None, DEFAULT_TASK_CONTEXT
26
+ if not isinstance(fields, dict) or fields.get("backend") not in {"jev", "kev"}:
27
+ raise ValueError("factor_dedup requires backend='jev' or 'kev'")
28
+ fields = dict(fields)
29
+ context = fields.pop("task_context", DEFAULT_TASK_CONTEXT)
30
+ if not isinstance(context, str) or not context.strip():
31
+ raise ValueError("factor_dedup.task_context must be a nonempty string")
32
+ from coatlearn.models.kev import decision_config
33
+ return JevFactorDeduplicator(decision_config(fields)), context
34
+
35
+
36
+ def core_dedup_policy(comparator, context):
37
+ """Record actual orchestration separately from the standalone pair policy."""
38
+ if comparator is None:
39
+ return None
40
+ return {**comparator.policy(), "integrated_into_coat": True,
41
+ "task_context": context, "comparison_order": "existing_A_new_B",
42
+ "simple_rules": ["exact_normalized_name", "cross_cluster_exact_definition"],
43
+ "representative": "first_valid_measured_factor",
44
+ "candidate_order": "cluster_index_then_proposal_order",
45
+ "reverse_check": False, "confidence_threshold": None}
46
+
47
+ DEDUP_INSTRUCTIONS = """判断两个候选因子是否描述同一个核心属性。
48
+ 同一属性的措辞、评价角度、取值范围或判定细节有所变化,不足以构成新的独立因子。例如,属性本身与针对该属性的评价,可以属于概念重复。
49
+ 但仅仅讨论同一对象、主题相关、存在因果联系或统计相关,不构成重复。不同主体的评价或不同结果维度,也不能仅因相关而合并。
50
+ 比较两份完整定义,不仅比较名称。
51
+ 不改写定义,不判断哪个定义更好。"""
52
+
53
+
54
+ def dedup_question() -> dict:
55
+ return {
56
+ "type": "choice",
57
+ "instructions": DEDUP_INSTRUCTIONS,
58
+ "criteria": {
59
+ "DUPLICATE": "能够明确判断为同一核心属性的重复表达。",
60
+ "DISTINCT": "能够明确指出不同的核心属性。",
61
+ "UNCERTAIN": "现有定义不足以明确判断。",
62
+ },
63
+ }
64
+
65
+
66
+ def build_state(definition_a: str, definition_b: str, task_context: str) -> str:
67
+ """Only task context and verbatim definitions enter state; no reference labels."""
68
+ for text in (definition_a, definition_b, task_context):
69
+ if not isinstance(text, str) or not text.strip():
70
+ raise ValueError("Two nonempty definitions and task context are required")
71
+ return ("# Task context\n\n" + task_context
72
+ + "\n\n# Factor A (verbatim)\n\n" + definition_a
73
+ + "\n\n# Factor B (verbatim)\n\n" + definition_b)
74
+
75
+
76
+ class JevFactorDeduplicator:
77
+ """One native Choice request per pair; no main LLM or discovery calls.
78
+
79
+ Original request and response are local audit artifacts, not public results.
80
+ No confidence cutoff, automatic retries beyond config, or label rewriting.
81
+ """
82
+
83
+ def __init__(self, config: JevConfig):
84
+ from coatlearn.annotation.measurement import decision_annotator
85
+ self.backend = decision_annotator(config)
86
+ question = dedup_question()
87
+ self.spec = MeasurementSpec(
88
+ factor_name="relation", jev_type="choice",
89
+ instructions=question["instructions"],
90
+ criteria=tuple(question["criteria"].items()),
91
+ choice_values={"DUPLICATE": 1, "DISTINCT": 0, "UNCERTAIN": -1},
92
+ ).validate()
93
+
94
+ def policy(self) -> dict:
95
+ backend = self.backend.config.redacted().get("backend", "jev")
96
+ return {"version": "concept-dedup-v1", "backend": backend,
97
+ "config": self.backend.config.redacted(),
98
+ "questions": {"relation": dedup_question()},
99
+ "uncertain_action": "retain", "error_action": "retain",
100
+ "rewrites_definitions": False, "integrated_into_coat": False}
101
+
102
+ def stats(self) -> dict:
103
+ return asdict(self.backend.stats())
104
+
105
+ def evaluate(self, definition_a: str, definition_b: str, task_context: str) -> dict:
106
+ request = {"model": self.backend.config.model,
107
+ "state": build_state(definition_a, definition_b, task_context),
108
+ "questions": {"relation": dedup_question()}}
109
+ result = {"status": "error", "decision": "ERROR", "action": "retain",
110
+ "request": request, "response": None}
111
+ try:
112
+ response = self.backend._system_one(request)
113
+ result["response"] = response
114
+ if (response.get("model") != self.backend.config.model
115
+ or set(response.get("answers", {})) != {"relation"}):
116
+ raise ValueError("Unexpected comparator model or answer keys")
117
+ answer = self.backend._interpret_answer(
118
+ "relation", self.spec, response["answers"]["relation"], None)
119
+ decision = answer.raw["choice"]
120
+ result.update(status="ok", decision=decision,
121
+ action="deduplicate" if decision == "DUPLICATE" else "retain",
122
+ probabilities=answer.probabilities, confidence=answer.confidence)
123
+ except Exception as exc:
124
+ # Transport exceptions may contain secrets: record type only.
125
+ result["error_type"] = type(exc).__name__
126
+ return result
127
+
128
+
129
+ def compare_definitions(definition_a, definition_b, *, model, task_context=DEFAULT_TASK_CONTEXT):
130
+ """Compare two full definitions without rewriting them or running discovery."""
131
+ from .batch import resolve_annotator
132
+ backend = resolve_annotator(model)
133
+ comparator = JevFactorDeduplicator(backend.config)
134
+ comparator.backend = backend
135
+ return comparator.evaluate(definition_a, definition_b, task_context)
@@ -0,0 +1,216 @@
1
+ """Deterministic translation of COAT's extracted ternary factor definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Mapping
7
+ from typing import Any
8
+
9
+ from coatlearn.annotation.types import MeasurementSpec, UnsupportedMeasurement
10
+
11
+ __all__ = ["factor_definition_to_spec", "general_definition_to_spec"]
12
+
13
+ _GENERAL_TYPE = re.compile(r"^[-+*•]?\s*type\s*:\s*(score|choice)\s*$", re.I)
14
+ _VALUES = re.compile(r"^[-+*•]?\s*values\s*:\s*(.+)$", re.I)
15
+ _MENTIONED = re.compile(r"^[-+*•]?\s*mentioned\s+when\s*:\s*(.+)$", re.I)
16
+ _LEVEL = re.compile(r"^[-+*•]?\s*(\d+)\s*:\s*(.+)$")
17
+ _OPTION = re.compile(r"^[-+*•]?\s*([^:]+?)\s*:\s*(.+)$")
18
+
19
+
20
+ def _unwrap(text: str) -> str:
21
+ text = text.strip()
22
+ while True:
23
+ for marker in ("**", "__", "`", "*", "_"):
24
+ if (
25
+ text.startswith(marker)
26
+ and text.endswith(marker)
27
+ and len(text) >= 2 * len(marker)
28
+ and text.find(marker, len(marker)) == len(text) - len(marker)
29
+ ):
30
+ text = text[len(marker) : -len(marker)].strip()
31
+ break
32
+ else:
33
+ return text
34
+
35
+
36
+ def _unbullet(text: str) -> str:
37
+ return re.sub(r"^(?:[-+*•]\s+|\d+[.)]\s+)", "", text.strip()).strip()
38
+
39
+
40
+ def _name(text: str) -> str:
41
+ text = re.sub(r"^#{1,6}\s+", "", _unbullet(_unwrap(text)))
42
+ return " ".join(_unwrap(text.rstrip(":")).rstrip(":").split())
43
+
44
+
45
+ def factor_definition_to_spec(factor_name: str, factor_definition: Any) -> MeasurementSpec:
46
+ """Parse exactly one named {-1, 0, 1} factor without inference or requests.
47
+
48
+ Accept the extracted four-line text or its exact ``name``/``description``
49
+ record. Markdown decoration and whitespace are presentation only; criterion
50
+ wording and label associations are retained. Unsupported input raises
51
+ :class:`UnsupportedMeasurement` before any backend is involved.
52
+ """
53
+ if not isinstance(factor_name, str) or not factor_name.strip():
54
+ raise UnsupportedMeasurement(factor_name, "factor_name must be a nonempty string")
55
+ name = _name(factor_name)
56
+ if not name:
57
+ raise UnsupportedMeasurement(factor_name, "factor_name must contain a name")
58
+ if isinstance(factor_definition, Mapping):
59
+ if set(factor_definition) != {"name", "description"}:
60
+ raise UnsupportedMeasurement(factor_name, "expected exactly name and description")
61
+ record_name = factor_definition["name"]
62
+ if not isinstance(record_name, str) or _name(record_name).casefold() != name.casefold():
63
+ raise UnsupportedMeasurement(factor_name, "factor record name does not match")
64
+ factor_definition = factor_definition["description"]
65
+ if not isinstance(factor_definition, str):
66
+ raise UnsupportedMeasurement(factor_name, "factor definition must be text or a factor record")
67
+ if any(_GENERAL_TYPE.match(line.strip()) for line in factor_definition.splitlines()):
68
+ return general_definition_to_spec(factor_name, factor_definition)
69
+ lines = [line.strip() for line in factor_definition.splitlines() if line.strip()]
70
+ if len(lines) >= 2 and lines[0] in ("```", "```text", "```markdown") and lines[-1] == "```":
71
+ lines = lines[1:-1]
72
+ if len(lines) != 4:
73
+ raise UnsupportedMeasurement(factor_name, "expected a name and exactly three criterion lines")
74
+ if _name(lines[0]).casefold() != name.casefold():
75
+ raise UnsupportedMeasurement(factor_name, "definition name does not match factor_name")
76
+ criteria = {}
77
+ for line in lines[1:]:
78
+ line = _unwrap(_unbullet(line))
79
+ line = re.sub(r"^(\*\*|__|`|\*|_)([-−]?\d+\s*:?)\1", r"\2", line)
80
+ match = re.fullmatch(r"([-−]?\d+)\s*:\s*(.*)", line)
81
+ if match is None:
82
+ raise UnsupportedMeasurement(factor_name, "expected a numeric label and criterion")
83
+ label = match.group(1).replace("−", "-")
84
+ description = " ".join(match.group(2).split())
85
+ if label not in ("1", "0", "-1") or label in criteria:
86
+ raise UnsupportedMeasurement(factor_name, "labels must be exactly 1, 0 and -1 without duplicates")
87
+ if not _unwrap(description):
88
+ raise UnsupportedMeasurement(factor_name, "criteria must not be empty")
89
+ criteria[label] = description
90
+ if set(criteria) != {"1", "0", "-1"}:
91
+ raise UnsupportedMeasurement(factor_name, "labels must be exactly 1, 0 and -1")
92
+ return MeasurementSpec(
93
+ factor_name=factor_name,
94
+ jev_type="choice",
95
+ instructions=f"Factor: {name}",
96
+ criteria=tuple((label, criteria[label]) for label in ("1", "0", "-1")),
97
+ choice_values={"1": 1.0, "0": 0.0, "-1": -1.0},
98
+ source="parsed",
99
+ notes="COAT ternary criteria parsed verbatim; value is the selected label, not probability or confidence.",
100
+ ).validate()
101
+
102
+
103
+ def general_definition_to_spec(factor_name: str, factor_definition: str) -> MeasurementSpec:
104
+ """Parse a general-template factor with an explicit measurement type.
105
+
106
+ The proposal declares ``Type: score|choice``, ``Values: ...`` and
107
+ ``Mentioned when: ...``; the mention sentence becomes a noul gate so that
108
+ "text does not mention this attribute" is recorded as missing rather than
109
+ folded into a value. Parsing is deterministic and makes no model calls.
110
+ """
111
+ if not isinstance(factor_name, str) or not factor_name.strip():
112
+ raise UnsupportedMeasurement(factor_name, "factor_name must be a nonempty string")
113
+ name = _name(factor_name)
114
+ if not name:
115
+ raise UnsupportedMeasurement(factor_name, "factor_name must contain a name")
116
+ if not isinstance(factor_definition, str):
117
+ raise UnsupportedMeasurement(factor_name, "factor definition must be text")
118
+ lines = [line.strip() for line in factor_definition.splitlines() if line.strip()]
119
+ if len(lines) >= 2 and lines[0] in ("```", "```text", "```markdown") and lines[-1] == "```":
120
+ lines = lines[1:-1]
121
+ if len(lines) < 4 or _name(lines[0]).casefold() != name.casefold():
122
+ raise UnsupportedMeasurement(factor_name, "expected a name line and a general definition")
123
+ kind = values_text = mentioned = None
124
+ body = []
125
+ for line in lines[1:]:
126
+ stripped = _unwrap(_unbullet(line))
127
+ match = _GENERAL_TYPE.match(stripped)
128
+ if match and kind is None:
129
+ kind = match.group(1).lower()
130
+ continue
131
+ match = _VALUES.match(stripped)
132
+ if match and values_text is None:
133
+ values_text = match.group(1).strip()
134
+ continue
135
+ match = _MENTIONED.match(stripped)
136
+ if match and mentioned is None:
137
+ mentioned = " ".join(match.group(1).split())
138
+ continue
139
+ body.append(stripped)
140
+ if kind is None or values_text is None or mentioned is None:
141
+ raise UnsupportedMeasurement(
142
+ factor_name, "general definition needs Type, Values and Mentioned when"
143
+ )
144
+ if not _unwrap(mentioned):
145
+ raise UnsupportedMeasurement(factor_name, "Mentioned when must not be empty")
146
+
147
+ gate = MeasurementSpec(
148
+ factor_name=factor_name,
149
+ jev_type="noul",
150
+ instructions=f"Does the text explicitly mention this attribute? Factor: {name}",
151
+ criteria=(mentioned, "The text does not explicitly mention this attribute."),
152
+ source="parsed",
153
+ notes="Mention gate: text without an explicit statement is recorded as missing.",
154
+ )
155
+
156
+ if kind == "score":
157
+ try:
158
+ value_map = tuple(float(v.strip()) for v in values_text.split(","))
159
+ except ValueError:
160
+ raise UnsupportedMeasurement(factor_name, "score Values must be comma-separated numbers") from None
161
+ levels = {}
162
+ for line in body:
163
+ match = _LEVEL.match(line)
164
+ if match is None:
165
+ raise UnsupportedMeasurement(factor_name, "expected numbered score levels")
166
+ index = int(match.group(1))
167
+ if index in levels:
168
+ raise UnsupportedMeasurement(factor_name, "duplicate score level")
169
+ levels[index] = " ".join(match.group(2).split())
170
+ if len(value_map) < 2 or len(levels) != len(value_map):
171
+ raise UnsupportedMeasurement(
172
+ factor_name, "score needs one level per Values entry")
173
+ # Level labels may be 0..n-1 or the numeric values themselves; either way
174
+ # they are ordered ascending and paired with Values in that order.
175
+ ordered = sorted(levels)
176
+ return MeasurementSpec(
177
+ factor_name=factor_name,
178
+ jev_type="score",
179
+ instructions=f"Factor: {name}",
180
+ criteria=tuple(levels[key] for key in ordered),
181
+ value_map=value_map,
182
+ source="parsed",
183
+ notes="General score parsed verbatim; value is the probability-weighted expectation over Values.",
184
+ gate=gate,
185
+ ).validate()
186
+
187
+ options = {}
188
+ for line in body:
189
+ match = _OPTION.match(line)
190
+ if match is None:
191
+ raise UnsupportedMeasurement(factor_name, "expected named choice options")
192
+ option, description = match.group(1).strip(), " ".join(match.group(2).split())
193
+ if option in options:
194
+ raise UnsupportedMeasurement(factor_name, "duplicate choice option")
195
+ options[option] = description
196
+ choice_values = {}
197
+ for item in values_text.split(","):
198
+ if "=" not in item:
199
+ raise UnsupportedMeasurement(factor_name, "choice Values must use option=number")
200
+ option, value = item.split("=", 1)
201
+ try:
202
+ choice_values[option.strip()] = float(value.strip())
203
+ except ValueError:
204
+ raise UnsupportedMeasurement(factor_name, "choice value must be numeric") from None
205
+ if set(choice_values) != set(options):
206
+ raise UnsupportedMeasurement(factor_name, "Values must cover exactly the declared options")
207
+ return MeasurementSpec(
208
+ factor_name=factor_name,
209
+ jev_type="choice",
210
+ instructions=f"Factor: {name}",
211
+ criteria=tuple((option, options[option]) for option in options),
212
+ choice_values=choice_values,
213
+ source="parsed",
214
+ notes="General choice parsed verbatim; value is the selected option's declared number.",
215
+ gate=gate,
216
+ ).validate()
@@ -0,0 +1,117 @@
1
+ """Opt-in Jev factor-definition judge; separate from per-review annotation.
2
+
3
+ One native request, two independent Choice questions, both must PASS. The
4
+ wording-clarity instruction is the author's exact text. No schema/main LLM,
5
+ confidence threshold, or implicit rewrite of the proposed definition is used.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import asdict
10
+ from typing import Any
11
+
12
+ from coatlearn.annotation.types import MeasurementSpec, UnsupportedMeasurement
13
+ from coatlearn.models.jev import JevConfig
14
+ from coatlearn.annotation.definitions import factor_definition_to_spec
15
+
16
+ CLARITY_INSTRUCTIONS = "结合任务示例,因子名称和各取值说明是否有明确、可理解的含义?仅因实质性的文字歧义拒绝,例如指代不明、修饰语或否定范围存在不同合理解释。"
17
+ FORMAT_INSTRUCTIONS = "因子定义的格式是否正确:一个非空的因子名称,随后是标记为 1、0、-1 的三条非空取值说明,三个标签各出现一次且没有额外取值或内容?Markdown 修饰和空白不影响格式。"
18
+ FORMAT_INSTRUCTIONS_GENERAL = "因子定义的格式是否正确:一个非空的因子名称,随后声明 Type(score 或 choice)、Values、Mentioned when,并且每个取值或选项都有非空的说明?Markdown 修饰和空白不影响格式。"
19
+
20
+
21
+ def judge_questions(template: str = "simple") -> dict:
22
+ """Fresh native question objects, safe to freeze in audit/provenance."""
23
+ format_instructions = (
24
+ FORMAT_INSTRUCTIONS_GENERAL if template == "general" else FORMAT_INSTRUCTIONS
25
+ )
26
+ return {
27
+ "clarity": {"type": "choice", "instructions": CLARITY_INSTRUCTIONS, "criteria": {
28
+ "PASS": "因子名称和各取值说明有明确、可理解的含义。",
29
+ "REJECT": "存在实质性的文字歧义。"}},
30
+ "format": {"type": "choice", "instructions": format_instructions, "criteria": {
31
+ "PASS": "格式正确。", "REJECT": "格式不正确。"}},
32
+ }
33
+
34
+
35
+ def proposal_examples(prompt: str) -> str:
36
+ """Extract the actual data section, never regenerate/resample examples."""
37
+ examples = prompt.split("# Data\n\n", 1)[1].split("# Tasks: Factor Abstraction.", 1)[0]
38
+ for heading in ("\n# Existing Factors", "\n# Previously Rejected Factors"):
39
+ examples = examples.split(heading, 1)[0]
40
+ return examples
41
+
42
+
43
+ def configured_judge(fields, template="simple"):
44
+ """Choose a typed backend without changing judge questions or acceptance."""
45
+ if fields is None:
46
+ return None
47
+ if not isinstance(fields, dict) or fields.get("backend") not in {"jev", "kev"}:
48
+ raise ValueError("factor_judge requires backend='jev' or 'kev'")
49
+ from coatlearn.models.kev import decision_config
50
+ return JevFactorJudge(decision_config(fields), template=template)
51
+
52
+
53
+ class JevFactorJudge:
54
+ """Reuse the existing native transport and strict Choice response parser.
55
+
56
+ ``evaluate`` returns a local audit record, which can contain example text
57
+ and final responses. It must NOT be put into the public result dictionary.
58
+ API/response errors are ERROR, not a semantic rejection or a normal zero.
59
+ """
60
+
61
+ def __init__(self, config: JevConfig, template: str = "simple"):
62
+ from coatlearn.annotation.measurement import decision_annotator
63
+ self.backend = decision_annotator(config)
64
+ self.template = template
65
+ self.specs = {
66
+ name: MeasurementSpec(factor_name=name, jev_type="choice", instructions=q["instructions"],
67
+ criteria=tuple(q["criteria"].items()), choice_values={"PASS": 1, "REJECT": 0}).validate()
68
+ for name, q in judge_questions(template).items()
69
+ }
70
+
71
+ def policy(self) -> dict:
72
+ backend = self.backend.config.redacted().get("backend", "jev")
73
+ return {"backend": backend, "version": "clarity-format-v1", "template": self.template,
74
+ "config": self.backend.config.redacted(),
75
+ "questions": judge_questions(self.template), "acceptance": "both_PASS", "trials": 1,
76
+ "replaces": "legacy_quality_prescoring_and_three_LLM_judges"}
77
+
78
+ def stats(self) -> dict:
79
+ return asdict(self.backend.stats())
80
+
81
+ def evaluate(self, factor_name: str, definition: str, examples: str) -> dict[str, Any]:
82
+ result = {"status": "ok", "decision": "REJECT", "local_format": "PASS", "response": None,
83
+ "request": {"model": self.backend.config.model,
84
+ "state": "# Candidate factor definition (verbatim)\n\n" + definition
85
+ + "\n\n# Original proposal examples (verbatim data section)\n" + examples,
86
+ "questions": judge_questions(self.template)}}
87
+ try:
88
+ factor_definition_to_spec(factor_name, definition)
89
+ except UnsupportedMeasurement:
90
+ result.update(local_format="REJECT", rejected_checks=["local_format"])
91
+ return result
92
+ try:
93
+ if not isinstance(examples, str) or not examples.strip():
94
+ raise ValueError("Original proposal examples are required")
95
+ response = self.backend._system_one(result["request"])
96
+ result["response"] = response # retain even malformed final JSON in local audit
97
+ if response.get("model") != self.backend.config.model or set(response.get("answers", {})) != set(self.specs):
98
+ raise ValueError("Unexpected judge model or answer keys")
99
+ answers = {}
100
+ for name, spec in self.specs.items():
101
+ annotation = self.backend._interpret_answer(name, spec, response["answers"][name], None)
102
+ answers[name] = {"decision": annotation.raw["choice"], "probabilities": annotation.probabilities,
103
+ "confidence": annotation.confidence}
104
+ rejected = [name for name, a in answers.items() if a["decision"] == "REJECT"]
105
+ result.update(answers=answers, rejected_checks=rejected, decision="REJECT" if rejected else "PASS")
106
+ except Exception as exc:
107
+ result.update(status="error", decision="ERROR", error_type=type(exc).__name__)
108
+ return result
109
+
110
+
111
+ def review_definition(name, definition, *, examples, model, template="general"):
112
+ """Review one definition independently of annotation or discovery."""
113
+ from .batch import resolve_annotator
114
+ backend = resolve_annotator(model)
115
+ judge = JevFactorJudge(backend.config, template=template)
116
+ judge.backend = backend
117
+ return judge.evaluate(name, definition, examples)