k8math 0.1.0__tar.gz

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.
@@ -0,0 +1,19 @@
1
+ Copyright © 2026 Ramiro Santa Ana Anguiano
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the “Software”), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
k8math-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: k8math
3
+ Version: 0.1.0
4
+ Summary: Generate math word problems with AI
5
+ Keywords: k8math,ai,math,math word problem,generator
6
+ Author: ram
7
+ Author-email: ram <hi@colima.press>
8
+ License-Expression: MIT
9
+ License-File: LICENSE.md
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Education
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Education
17
+ Requires-Dist: anthropic>=1.2.0
18
+ Requires-Dist: simpleeval>=1.0.7
19
+ Maintainer: ram
20
+ Maintainer-email: ram <hi@colima.press>
21
+ Requires-Python: >=3.12
22
+ Project-URL: Repository, https://gitlab.com/perritotuerto/codigo/k8math
23
+ Project-URL: Changelog, https://gitlab.com/perritotuerto/codigo/k8math/-/blob/main/CHANGELOG.md
24
+ Project-URL: Releases, https://gitlab.com/perritotuerto/codigo/k8math/-/releases
25
+ Description-Content-Type: text/markdown
26
+
27
+ # K8Math
28
+
29
+ Generate math word problems with AI.
30
+
31
+ **NOTE**. Requires Anthropic API key.
32
+
33
+ ## Usage
34
+
35
+ Generate wit CLI:
36
+
37
+ ```
38
+ uv run k8math -k $ANTHROPIC_API_KEY -l english "3 + 4 = 7"
39
+ ```
40
+
41
+ Print help:
42
+
43
+ ```
44
+ uv run k8math -h
45
+ ```
46
+
47
+ Import as a module:
48
+
49
+ ```
50
+ import os
51
+ from k8math import K8Math
52
+
53
+ k8m = K8Math(
54
+ expressions=["3 + 4 = 7"],
55
+ api_key=os.environ.get("ANTHROPIC_API_KEY"),
56
+ language="english",
57
+ )
58
+ k8m.generate()
59
+ ```
60
+
61
+ ## Development
62
+
63
+ Clone:
64
+
65
+ ```
66
+ git clone TODO
67
+ ```
68
+
69
+ Format:
70
+
71
+ ```
72
+ uv run ruff format .
73
+ uv run isort .
74
+ ```
75
+
76
+ Check:
77
+
78
+ ```
79
+ uv run ruff check .
80
+ ```
81
+
82
+ Test:
83
+
84
+ ```
85
+ uv run python -m pytest -s --durations 0
86
+ ```
87
+
88
+ Tag version:
89
+
90
+ ```
91
+ git tag v1.0.0
92
+ git push origin tag v1.0.0
93
+ ```
k8math-0.1.0/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # K8Math
2
+
3
+ Generate math word problems with AI.
4
+
5
+ **NOTE**. Requires Anthropic API key.
6
+
7
+ ## Usage
8
+
9
+ Generate wit CLI:
10
+
11
+ ```
12
+ uv run k8math -k $ANTHROPIC_API_KEY -l english "3 + 4 = 7"
13
+ ```
14
+
15
+ Print help:
16
+
17
+ ```
18
+ uv run k8math -h
19
+ ```
20
+
21
+ Import as a module:
22
+
23
+ ```
24
+ import os
25
+ from k8math import K8Math
26
+
27
+ k8m = K8Math(
28
+ expressions=["3 + 4 = 7"],
29
+ api_key=os.environ.get("ANTHROPIC_API_KEY"),
30
+ language="english",
31
+ )
32
+ k8m.generate()
33
+ ```
34
+
35
+ ## Development
36
+
37
+ Clone:
38
+
39
+ ```
40
+ git clone TODO
41
+ ```
42
+
43
+ Format:
44
+
45
+ ```
46
+ uv run ruff format .
47
+ uv run isort .
48
+ ```
49
+
50
+ Check:
51
+
52
+ ```
53
+ uv run ruff check .
54
+ ```
55
+
56
+ Test:
57
+
58
+ ```
59
+ uv run python -m pytest -s --durations 0
60
+ ```
61
+
62
+ Tag version:
63
+
64
+ ```
65
+ git tag v1.0.0
66
+ git push origin tag v1.0.0
67
+ ```
@@ -0,0 +1,55 @@
1
+ [project]
2
+ name = "k8math"
3
+ version = "0.1.0"
4
+ description = "Generate math word problems with AI"
5
+ readme = "README.md"
6
+ keywords = [
7
+ "k8math",
8
+ "ai",
9
+ "math",
10
+ "math word problem",
11
+ "generator",
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Education",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Topic :: Education",
21
+ ]
22
+ license = "MIT"
23
+ license-files = ["LICENSE.md"]
24
+ requires-python = ">=3.12"
25
+ dependencies = [
26
+ "anthropic>=1.2.0",
27
+ "simpleeval>=1.0.7",
28
+ ]
29
+
30
+ [[project.authors]]
31
+ name = "ram"
32
+ email = "hi@colima.press"
33
+
34
+ [[project.maintainers]]
35
+ name = "ram"
36
+ email = "hi@colima.press"
37
+
38
+ [project.scripts]
39
+ k8math = "k8math.cli:main"
40
+
41
+ [project.urls]
42
+ Repository = "https://gitlab.com/perritotuerto/codigo/k8math"
43
+ Changelog = "https://gitlab.com/perritotuerto/codigo/k8math/-/blob/main/CHANGELOG.md"
44
+ Releases = "https://gitlab.com/perritotuerto/codigo/k8math/-/releases"
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "isort>=8.0.1",
49
+ "pytest>=9.0.3",
50
+ "ruff>=0.15.11",
51
+ ]
52
+
53
+ [build-system]
54
+ requires = ["uv_build>=0.11.8,<0.13"]
55
+ build-backend = "uv_build"
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "k8math"
3
+ version = "0.1.0"
4
+ description = "Generate math word problems with AI"
5
+ readme = "README.md"
6
+ authors = [{ name = "ram", email = "hi@colima.press" }]
7
+ maintainers = [{ name = "ram", email = "hi@colima.press" }]
8
+ keywords = ["k8math", "ai", "math", "math word problem", "generator"]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Developers",
13
+ "Intended Audience :: Education",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Topic :: Education",
17
+ ]
18
+ license = "MIT"
19
+ license-files = ["LICENSE.md"]
20
+ requires-python = ">=3.12"
21
+ dependencies = ["anthropic>=1.2.0", "simpleeval>=1.0.7"]
22
+
23
+ [dependency-groups]
24
+ dev = ["isort>=8.0.1", "pytest>=9.0.3", "ruff>=0.15.11"]
25
+
26
+ [project.scripts]
27
+ k8math = "k8math.cli:main"
28
+
29
+ [project.urls]
30
+ Repository = "https://gitlab.com/perritotuerto/codigo/k8math"
31
+ Changelog = "https://gitlab.com/perritotuerto/codigo/k8math/-/blob/main/CHANGELOG.md"
32
+ Releases = "https://gitlab.com/perritotuerto/codigo/k8math/-/releases"
33
+
34
+ [build-system]
35
+ requires = ["uv_build>=0.11.8,<0.13"]
36
+ build-backend = "uv_build"
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from k8math!"
@@ -0,0 +1,67 @@
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ from k8math.core import K8Math
5
+
6
+
7
+ def main() -> None:
8
+ parser = argparse.ArgumentParser(
9
+ prog=Path(__file__).stem,
10
+ description="Generate math word problems with AI",
11
+ epilog="hi@colima.press",
12
+ )
13
+ parser.add_argument(
14
+ "math_expressions",
15
+ help="one or more math expressions; e.g., '1 * 2 / 2 = 1'",
16
+ metavar="EXP",
17
+ nargs="+",
18
+ )
19
+ parser.add_argument(
20
+ "-k",
21
+ "--key",
22
+ help="Anthropic API key",
23
+ metavar="KEY",
24
+ required=True,
25
+ )
26
+ parser.add_argument(
27
+ "-l",
28
+ "--language",
29
+ help="problem language",
30
+ metavar="LANG",
31
+ choices=["english", "spanish"],
32
+ )
33
+ parser.add_argument(
34
+ "-s",
35
+ "--subject",
36
+ help="one or more subjects",
37
+ metavar="SUBJ",
38
+ action="append",
39
+ )
40
+ parser.add_argument(
41
+ "-o",
42
+ "--object",
43
+ help="one or more objects",
44
+ metavar="OBJ",
45
+ action="append",
46
+ )
47
+ parser.add_argument(
48
+ "-v",
49
+ "--verb",
50
+ help="one or more verbs",
51
+ metavar="VERB",
52
+ action="append",
53
+ )
54
+ args = parser.parse_args()
55
+ k8m = K8Math(
56
+ expressions=args.math_expressions,
57
+ api_key=args.key,
58
+ language=args.language,
59
+ subjects=args.subject,
60
+ objects=args.object,
61
+ verbs=args.verb,
62
+ )
63
+ k8m.generate()
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
@@ -0,0 +1,172 @@
1
+ import io
2
+ import json
3
+ import tokenize
4
+
5
+ from anthropic import Anthropic
6
+ from simpleeval import simple_eval
7
+
8
+
9
+ class K8Math:
10
+ MODEL = "claude-haiku-4-5-20251001" # cheapest with structured-output
11
+ SCHEMA = { # noqa: RUF012
12
+ "type": "array",
13
+ "items": {
14
+ "type": "object",
15
+ "properties": {
16
+ "text": {"type": "string"},
17
+ "index": {"type": "integer"},
18
+ "num": {"type": "number"},
19
+ "op": {"enum": ["+", "-", "*", "/"]},
20
+ "par": {"enum": ["(", ")"]},
21
+ "subject": {"type": "string"},
22
+ "subject_in_text": {"type": "string"},
23
+ "object": {"type": "string"},
24
+ "object_in_text": {"type": "string"},
25
+ "verb": {"type": "string"},
26
+ "verb_in_text": {"type": "string"},
27
+ },
28
+ "additionalProperties": False,
29
+ },
30
+ }
31
+ SYSTEM_PROMPT = """
32
+ You write short kinder and elementary-school (K-8) word problems encoded as a
33
+ JSON array.
34
+
35
+ Rules:
36
+ - Reproduce every given math token exactly (same num/op/par value, same index).
37
+ - NEVER alter, add, or remove a math token.
38
+ - Every object except "par" objects must have a non-empty "text".
39
+ - "par" objects never have "text" and never appear in the narrated sentence.
40
+ - "subject", "object" and "verb" objects have to appear at least once in "text"
41
+ - Every "*_in_text" field must be a literal substring of that same object's
42
+ "text".
43
+ - The verb may be conjugated freely in "verb_in_text" regardless of the form
44
+ given in "verb".
45
+ - If the change described is small or trivial (like going from 1 to 2), phrase
46
+ it the way someone would naturally say it in conversation — don't reach for
47
+ formal math language like 'doubles' or 'multiplies by' when a simpler phrase
48
+ fits better.
49
+ - MUST make sense in real world and it should not seem like a verbose math
50
+ expression.
51
+ - ATTACH every number to a concrete, countable noun the reader can picture.
52
+ - ALWAYS write naturally and for a kid.
53
+ - ALWAYS leave the punctuation, such as periods and commas, at the end of
54
+ "text".
55
+ - ALWAYS be clear and specific in what you ask.
56
+ - NEVER use the name of the operation.
57
+ - NEVER use dashes nor semicolons.
58
+ - NEVER hide a quantity behind a vague pronoun like 'it', 'them', or
59
+ 'everything'.
60
+ - NEVER use the name of the math operator in the word problem.
61
+ - NEVER give the result and answer away.
62
+ - Respond with ONLY the JSON array. No explanation, no markdown, no code
63
+ fences.
64
+
65
+ Example 1:
66
+ Math tokens (reproduce exactly, with its order in "index"):
67
+ ("9", "-", "7" "=", "2")
68
+
69
+ Output:
70
+ [
71
+ {"text":"Rosa","subject":"Rosa","subject_in_text":"Rosa"},
72
+ {"text":"has","verb":"have","verb_in_text":"has"},
73
+ {"text":"9","index":0,"num":9},
74
+ {"text":"apples.","object":"apple","object_in_text":"apples"},
75
+ {"text":"She","subject":"Rosa","subject_in_text":"She"},
76
+ {"text":"ate","index":1,"op":"-","verb":"eat","verb_in_text":"ate"},
77
+ {"text":"7.","index":2,"num":7},
78
+ {"text":"How many does she have left?"}
79
+ ]
80
+
81
+ Example 2:
82
+ Math tokens (reproduce exactly, with its order in "index"):
83
+ ("(", "3", "+", "7", ")", "/", "5", "=", "2")
84
+
85
+ Output:
86
+ [
87
+ {"text":"Tom","subject":"Tom","subject_in_text":"Tom"},
88
+ {"index":0,"par":"("},
89
+ {"text":"buys","verb":"buy","verb_in_text":"buys"},
90
+ {"text":"3","index":1,"num":3},
91
+ {"text":"boxes","object":"box","object_in_text":"boxes"},
92
+ {"text":"and","index":2,"op":"+"},
93
+ {"text":"7","index":3,"num":7},
94
+ {"text":"more boxes,","object":"box","object_in_text":"boxes"},
95
+ {"index":4,"par":")"},
96
+ {"text":"then splits them evenly among","index":5,"op":"/"},
97
+ {"text":"5","index":6,"num":5},
98
+ {"text":"friends. How many boxes does each friend get?"}
99
+ ]
100
+ """
101
+ USER_PROMPT = """
102
+ Math tokens (reproduce exactly, with its order in "index"): %s
103
+
104
+ Now produce the JSON array for a new, different word problem in
105
+ %s using these exact math tokens.
106
+ """
107
+
108
+ def __init__( # noqa: PLR0913,PLR0917
109
+ self,
110
+ expressions: list[str] | tuple[str],
111
+ api_key: str,
112
+ language: str | None = None,
113
+ subjects: list | None = None,
114
+ objects: list | None = None,
115
+ verbs: list | None = None,
116
+ ):
117
+ self.exps = [self._get_exp(exp) for exp in expressions]
118
+ self._key = api_key
119
+ self.lang = language if language else "english"
120
+ self.subjects = subjects
121
+ self.objects = objects
122
+ self.verbs = verbs
123
+
124
+ def _get_exp(self, raw_exp: str) -> tuple:
125
+ if "=" not in raw_exp:
126
+ raise ValueError(f"No equality symbol found in '{raw_exp}'")
127
+ tokens = tokenize.generate_tokens(io.StringIO(raw_exp).readline)
128
+ tokens = tuple(t.string for t in tokens if t.string.strip())
129
+ py_str = "".join(tuple("==" if t == "=" else t for t in tokens))
130
+ if not simple_eval(py_str):
131
+ raise ValueError(f"Invalid result '{raw_exp}'")
132
+ return tokens
133
+
134
+ def _get_prompt(self, exp: tuple, lang: str) -> str:
135
+ prompt = f"\n{exp}"
136
+ if self.subjects or self.objects or self.verbs:
137
+ prompt += "\n\nWith this content:"
138
+ if self.subjects:
139
+ prompt += f"\n- subjects: {self.subjects}"
140
+ if self.objects:
141
+ prompt += f"\n- objects: {self.objects}"
142
+ if self.verbs:
143
+ prompt += f"\n- verbs: {self.verbs}"
144
+ return self.USER_PROMPT % (prompt, lang)
145
+
146
+ def _print(self, tokens: list, math_expression: tuple):
147
+ problem = " ".join([t.get("text", "").strip() for t in tokens])
148
+ answer = " ".join(math_expression)
149
+ print(f"{problem}\n\nAnswer: {answer}")
150
+
151
+ def generate(self, language: str | None = None):
152
+ self.problems = []
153
+ lang = language if language else self.lang
154
+ client = Anthropic(api_key=self._key)
155
+ for exp in self.exps:
156
+ user_prompt = self._get_prompt(exp, lang)
157
+ response = client.messages.create(
158
+ model=self.MODEL,
159
+ max_tokens=1024,
160
+ system=self.SYSTEM_PROMPT,
161
+ messages=[{"role": "user", "content": user_prompt}],
162
+ output_config={
163
+ "format": {
164
+ "type": "json_schema",
165
+ "schema": self.SCHEMA,
166
+ }
167
+ },
168
+ )
169
+ text_block = next(b for b in response.content if b.type == "text")
170
+ parsed = json.loads(text_block.text)
171
+ self._print(parsed, exp)
172
+ self.problems.append(parsed)
File without changes