rulesmith 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.
rulesmith/grade.py ADDED
@@ -0,0 +1,85 @@
1
+ """Grade a graph's answers with a stronger model, where no label says what is right."""
2
+
3
+ import json
4
+ import math
5
+
6
+ import dspy
7
+
8
+ from rulesmith.graph import Example, State
9
+ from rulesmith.optimize import Outcome, text_adapter
10
+ from rulesmith.runtime import DecisionProgram, undecided
11
+
12
+
13
+ def model_name(model: str) -> str:
14
+ """A model without its provider or publisher prefix, which two services write differently."""
15
+ return model.rsplit("/", 1)[-1].lower()
16
+
17
+
18
+ class Grader(dspy.Module):
19
+ """A model's grade, from 0 to 1, for what a graph answered, with the reason for it. Each
20
+ answer to each input is graded once: search scores the same answers over and over."""
21
+
22
+ def __init__(self, lm: dspy.LM, description: str):
23
+ super().__init__()
24
+ self.lm = lm
25
+ signature = dspy.make_signature(
26
+ {
27
+ "state": (State, dspy.InputField()),
28
+ "outputs": (dict, dspy.InputField()),
29
+ "reference": (
30
+ str | None,
31
+ dspy.InputField(desc="The labels a person judged right, when there are any"),
32
+ ),
33
+ "score": (float, dspy.OutputField(desc="0 is wrong, 1 is right")),
34
+ "reason": (str, dspy.OutputField(desc="What makes the outputs right or wrong")),
35
+ },
36
+ f"Grade the outputs a decision program gave for this input. The task: {description}",
37
+ )
38
+ self.predict = dspy.Predict(signature)
39
+ self.grades: dict[str, tuple[float, str]] = {}
40
+
41
+ def forward(self, state: State, outputs: dict, reference: str | None) -> tuple[float, str]:
42
+ key = json.dumps([state, outputs, reference], sort_keys=True)
43
+ if key not in self.grades:
44
+ with dspy.context(lm=self.lm, adapter=text_adapter()):
45
+ graded = self.predict(state=state, outputs=outputs, reference=reference)
46
+ if not (math.isfinite(graded.score) and 0 <= graded.score <= 1):
47
+ raise ValueError(f"the grader scored {graded.score}, outside 0 to 1")
48
+ self.grades[key] = (graded.score, graded.reason)
49
+ return self.grades[key]
50
+
51
+
52
+ class Graded:
53
+ """Scores an answer by a grader's judgment. A graph that fails to answer scores zero
54
+ without being graded."""
55
+
56
+ def __init__(self, grader: Grader):
57
+ self.grader = grader
58
+
59
+ def __call__(self, program: DecisionProgram, example: Example) -> Outcome:
60
+ prediction = program(state=example.state)
61
+ error = undecided(program.plan, prediction)
62
+ record = {"id": example.id, "expected": example.label, **dict(prediction)}
63
+ trace = {"Inputs": example.state, "Generated Outputs": dict(prediction)}
64
+ if error:
65
+ return Outcome(0.0, record, trace | {"Feedback": error}, prediction.models, error)
66
+ # A graph that asks the grader's own model can learn to ask it the grader's question,
67
+ # which scores perfectly and means nothing.
68
+ grading = model_name(self.grader.lm.model)
69
+ if grading in {model_name(model) for model in prediction.models}:
70
+ raise ValueError(
71
+ f"the grader {self.grader.lm.model} is the model the graph asks; "
72
+ "grade with a different, stronger model"
73
+ )
74
+ right = [example.label, *example.accepted] if example.label else []
75
+ score, reason = self.grader(
76
+ state=example.state,
77
+ outputs=prediction.outputs,
78
+ reference=", ".join(right) or None,
79
+ )
80
+ return Outcome(
81
+ score=score,
82
+ record=record | {"grade": score, "reason": reason},
83
+ trace=trace | {"Feedback": f"A grader scored these outputs {score:g} of 1: {reason}"},
84
+ models=prediction.models,
85
+ )