kiln-ai 0.0.4__py3-none-any.whl → 0.5.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.

Potentially problematic release.


This version of kiln-ai might be problematic. Click here for more details.

Files changed (33) hide show
  1. kiln_ai/adapters/base_adapter.py +168 -0
  2. kiln_ai/adapters/langchain_adapters.py +113 -0
  3. kiln_ai/adapters/ml_model_list.py +436 -0
  4. kiln_ai/adapters/prompt_builders.py +122 -0
  5. kiln_ai/adapters/repair/repair_task.py +71 -0
  6. kiln_ai/adapters/repair/test_repair_task.py +248 -0
  7. kiln_ai/adapters/test_langchain_adapter.py +50 -0
  8. kiln_ai/adapters/test_ml_model_list.py +99 -0
  9. kiln_ai/adapters/test_prompt_adaptors.py +167 -0
  10. kiln_ai/adapters/test_prompt_builders.py +315 -0
  11. kiln_ai/adapters/test_saving_adapter_results.py +168 -0
  12. kiln_ai/adapters/test_structured_output.py +218 -0
  13. kiln_ai/datamodel/__init__.py +362 -2
  14. kiln_ai/datamodel/basemodel.py +372 -0
  15. kiln_ai/datamodel/json_schema.py +45 -0
  16. kiln_ai/datamodel/test_basemodel.py +277 -0
  17. kiln_ai/datamodel/test_datasource.py +107 -0
  18. kiln_ai/datamodel/test_example_models.py +644 -0
  19. kiln_ai/datamodel/test_json_schema.py +124 -0
  20. kiln_ai/datamodel/test_models.py +190 -0
  21. kiln_ai/datamodel/test_nested_save.py +205 -0
  22. kiln_ai/datamodel/test_output_rating.py +88 -0
  23. kiln_ai/utils/config.py +170 -0
  24. kiln_ai/utils/formatting.py +5 -0
  25. kiln_ai/utils/test_config.py +245 -0
  26. {kiln_ai-0.0.4.dist-info → kiln_ai-0.5.0.dist-info}/METADATA +20 -1
  27. kiln_ai-0.5.0.dist-info/RECORD +29 -0
  28. kiln_ai/__init.__.py +0 -3
  29. kiln_ai/coreadd.py +0 -3
  30. kiln_ai/datamodel/project.py +0 -15
  31. kiln_ai-0.0.4.dist-info/RECORD +0 -8
  32. {kiln_ai-0.0.4.dist-info → kiln_ai-0.5.0.dist-info}/LICENSE.txt +0 -0
  33. {kiln_ai-0.0.4.dist-info → kiln_ai-0.5.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,124 @@
1
+ import pytest
2
+ from kiln_ai.datamodel.json_schema import (
3
+ JsonObjectSchema,
4
+ schema_from_json_str,
5
+ validate_schema,
6
+ )
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class ExampleModel(BaseModel):
11
+ x_schema: JsonObjectSchema | None = None
12
+
13
+
14
+ json_joke_schema = """{
15
+ "type": "object",
16
+ "properties": {
17
+ "setup": {
18
+ "description": "The setup of the joke",
19
+ "title": "Setup",
20
+ "type": "string"
21
+ },
22
+ "punchline": {
23
+ "description": "The punchline to the joke",
24
+ "title": "Punchline",
25
+ "type": "string"
26
+ },
27
+ "rating": {
28
+ "anyOf": [
29
+ {
30
+ "type": "integer"
31
+ },
32
+ {
33
+ "type": "null"
34
+ }
35
+ ],
36
+ "default": null,
37
+ "description": "How funny the joke is, from 1 to 10",
38
+ "title": "Rating"
39
+ }
40
+ },
41
+ "required": [
42
+ "setup",
43
+ "punchline"
44
+ ]
45
+ }
46
+ """
47
+
48
+
49
+ def test_json_schema():
50
+ o = ExampleModel(x_schema=json_joke_schema)
51
+ parsed_schema = schema_from_json_str(o.x_schema)
52
+ assert parsed_schema is not None
53
+ assert parsed_schema["type"] == "object"
54
+ assert parsed_schema["required"] == ["setup", "punchline"]
55
+ assert parsed_schema["properties"]["setup"]["type"] == "string"
56
+ assert parsed_schema["properties"]["punchline"]["type"] == "string"
57
+ assert parsed_schema["properties"]["rating"] is not None
58
+
59
+ # Not json schema
60
+ with pytest.raises(ValueError):
61
+ o = ExampleModel(x_schema="hello")
62
+ with pytest.raises(ValueError):
63
+ o = ExampleModel(x_schema="{'asdf':{}}")
64
+ with pytest.raises(ValueError):
65
+ o = ExampleModel(x_schema="{asdf")
66
+
67
+
68
+ def test_validate_schema_content():
69
+ o = {"setup": "asdf", "punchline": "asdf", "rating": 1}
70
+ validate_schema(o, json_joke_schema)
71
+ o = {"setup": "asdf"}
72
+ with pytest.raises(Exception):
73
+ validate_schema(0, json_joke_schema)
74
+ o = {"setup": "asdf", "punchline": "asdf"}
75
+ validate_schema(o, json_joke_schema)
76
+ o = {"setup": "asdf", "punchline": "asdf", "rating": "1"}
77
+ with pytest.raises(Exception):
78
+ validate_schema(o, json_joke_schema)
79
+
80
+
81
+ json_triangle_schema = """{
82
+ "type": "object",
83
+ "properties": {
84
+ "a": {
85
+ "description": "length of side a",
86
+ "title": "A",
87
+ "type": "integer"
88
+ },
89
+ "b": {
90
+ "description": "length of side b",
91
+ "title": "B",
92
+ "type": "integer"
93
+ },
94
+ "c": {
95
+ "description": "length of side c",
96
+ "title": "C",
97
+ "type": "integer"
98
+ }
99
+ },
100
+ "required": [
101
+ "a",
102
+ "b",
103
+ "c"
104
+ ]
105
+ }
106
+ """
107
+
108
+
109
+ def test_triangle_schema():
110
+ o = ExampleModel(x_schema=json_joke_schema)
111
+ parsed_schema = schema_from_json_str(o.x_schema)
112
+ assert parsed_schema is not None
113
+
114
+ o = ExampleModel(x_schema=json_triangle_schema)
115
+ schema = schema_from_json_str(o.x_schema)
116
+
117
+ assert schema is not None
118
+ assert schema["properties"]["a"]["type"] == "integer"
119
+ assert schema["properties"]["b"]["type"] == "integer"
120
+ assert schema["properties"]["c"]["type"] == "integer"
121
+ assert schema["required"] == ["a", "b", "c"]
122
+ validate_schema({"a": 1, "b": 2, "c": 3}, json_triangle_schema)
123
+ with pytest.raises(Exception):
124
+ validate_schema({"a": 1, "b": 2, "c": "3"}, json_triangle_schema)
@@ -0,0 +1,190 @@
1
+ import json
2
+
3
+ import pytest
4
+ from kiln_ai.datamodel import Priority, Project, Task, TaskDeterminism
5
+ from kiln_ai.datamodel.test_json_schema import json_joke_schema
6
+ from pydantic import ValidationError
7
+
8
+
9
+ @pytest.fixture
10
+ def test_project_file(tmp_path):
11
+ test_file_path = tmp_path / "project.kiln"
12
+ data = {"v": 1, "name": "Test Project", "model_type": "project"}
13
+
14
+ with open(test_file_path, "w") as file:
15
+ json.dump(data, file, indent=4)
16
+
17
+ return test_file_path
18
+
19
+
20
+ @pytest.fixture
21
+ def test_task_file(tmp_path):
22
+ test_file_path = tmp_path / "task.json"
23
+ data = {
24
+ "v": 1,
25
+ "name": "Test Task",
26
+ "instruction": "Test Instruction",
27
+ "model_type": "task",
28
+ }
29
+
30
+ with open(test_file_path, "w") as file:
31
+ json.dump(data, file, indent=4)
32
+
33
+ return test_file_path
34
+
35
+
36
+ def test_load_from_file(test_project_file):
37
+ project = Project.load_from_file(test_project_file)
38
+ assert project.v == 1
39
+ assert project.name == "Test Project"
40
+ assert project.path == test_project_file
41
+
42
+
43
+ def test_project_init():
44
+ project = Project(name="test")
45
+ assert project.name == "test"
46
+
47
+
48
+ def test_save_to_file(test_project_file):
49
+ project = Project(
50
+ name="Test Project", description="Test Description", path=test_project_file
51
+ )
52
+ project.save_to_file()
53
+
54
+ with open(test_project_file, "r") as file:
55
+ data = json.load(file)
56
+
57
+ assert data["v"] == 1
58
+ assert data["name"] == "Test Project"
59
+ assert data["description"] == "Test Description"
60
+
61
+
62
+ def test_task_defaults():
63
+ task = Task(name="Test Task", instruction="Test Instruction")
64
+ assert task.description == ""
65
+ assert task.priority == Priority.p2
66
+ assert task.determinism == TaskDeterminism.flexible
67
+
68
+
69
+ def test_task_serialization(test_project_file):
70
+ project = Project.load_from_file(test_project_file)
71
+ task = Task(
72
+ parent=project,
73
+ name="Test Task",
74
+ description="Test Description",
75
+ determinism=TaskDeterminism.semantic_match,
76
+ priority=Priority.p0,
77
+ instruction="Test Base Task Instruction",
78
+ )
79
+
80
+ task.save_to_file()
81
+
82
+ parsed_task = Task.all_children_of_parent_path(test_project_file)[0]
83
+ assert parsed_task.name == "Test Task"
84
+ assert parsed_task.description == "Test Description"
85
+ assert parsed_task.instruction == "Test Base Task Instruction"
86
+ assert parsed_task.determinism == TaskDeterminism.semantic_match
87
+ assert parsed_task.priority == Priority.p0
88
+
89
+
90
+ def test_save_to_file_without_path():
91
+ project = Project(name="Test Project")
92
+ with pytest.raises(ValueError):
93
+ project.save_to_file()
94
+
95
+
96
+ def test_name_validation():
97
+ Project(name="Test Project")
98
+ Project(name="Te st_Proj- 1234567890")
99
+ Project(name=("a" * 120)) # longest
100
+
101
+ # a string with 120 characters
102
+
103
+ with pytest.raises(ValueError):
104
+ Project(name="Test Project!")
105
+ Project(name="Test.Project")
106
+ Project(name=("a" * 121)) # too long
107
+ Project(name=("a")) # too short
108
+
109
+
110
+ def test_auto_type_name():
111
+ model = Project(name="Test Project")
112
+ assert model.model_type == "project"
113
+
114
+
115
+ def test_load_tasks(test_project_file):
116
+ # Set up a project model
117
+ project = Project.load_from_file(test_project_file)
118
+
119
+ # Set up multiple task models under the project
120
+ task1 = Task(parent=project, name="Task1", instruction="Task 1 instruction")
121
+ task2 = Task(parent=project, name="Task2", instruction="Task 2 instruction")
122
+ task3 = Task(parent=project, name="Task3", instruction="Task 3 instruction")
123
+
124
+ # Ensure the tasks are saved correctly
125
+ task1.save_to_file()
126
+ task2.save_to_file()
127
+ task3.save_to_file()
128
+
129
+ # Load tasks from the project
130
+ tasks = project.tasks()
131
+
132
+ # Verify that all tasks are loaded correctly
133
+ assert len(tasks) == 3
134
+ names = [task.name for task in tasks]
135
+ assert "Task1" in names
136
+ assert "Task2" in names
137
+ assert "Task3" in names
138
+ assert all(task.model_type == "task" for task in tasks)
139
+ assert all(task.instruction != "" for task in tasks)
140
+
141
+
142
+ # verify no error on non-saved model
143
+ def test_load_children_no_path():
144
+ project = Project(name="Test Project")
145
+ assert len(project.tasks()) == 0
146
+
147
+
148
+ def test_check_model_type(test_project_file, test_task_file):
149
+ project = Project.load_from_file(test_project_file)
150
+ task = Task.load_from_file(test_task_file)
151
+ assert project.model_type == "project"
152
+ assert task.model_type == "task"
153
+ assert task.instruction == "Test Instruction"
154
+
155
+ with pytest.raises(ValueError):
156
+ project = Project.load_from_file(test_task_file)
157
+
158
+ with pytest.raises(ValueError):
159
+ task = Task.load_from_file(test_project_file)
160
+
161
+
162
+ def test_task_output_schema(tmp_path):
163
+ path = tmp_path / "task.kiln"
164
+ task = Task(name="Test Task", path=path, instruction="Test Instruction")
165
+ task.save_to_file()
166
+ assert task.output_schema() is None
167
+ task = Task(
168
+ name="Test Task",
169
+ instruction="Test Instruction",
170
+ output_json_schema=json_joke_schema,
171
+ input_json_schema=json_joke_schema,
172
+ path=path,
173
+ )
174
+ task.save_to_file()
175
+ schemas = [task.output_schema(), task.input_schema()]
176
+ for schema in schemas:
177
+ assert schema is not None
178
+ assert schema["properties"]["setup"]["type"] == "string"
179
+ assert schema["properties"]["punchline"]["type"] == "string"
180
+ assert schema["properties"]["rating"] is not None
181
+
182
+ # Not json schema
183
+ with pytest.raises(ValidationError):
184
+ task = Task(name="Test Task", output_json_schema="hello", path=path)
185
+ with pytest.raises(ValidationError):
186
+ task = Task(name="Test Task", output_json_schema='{"asdf":{}}', path=path)
187
+ with pytest.raises(ValidationError):
188
+ task = Task(name="Test Task", output_json_schema="{'asdf':{}}", path=path)
189
+ with pytest.raises(ValidationError):
190
+ task = Task(name="Test Task", input_json_schema="{asdf", path=path)
@@ -0,0 +1,205 @@
1
+ import pytest
2
+ from kiln_ai.datamodel.basemodel import KilnParentedModel, KilnParentModel
3
+ from pydantic import Field, ValidationError
4
+
5
+
6
+ class ModelC(KilnParentedModel):
7
+ code: str = Field(..., pattern=r"^[A-Z]{3}$")
8
+
9
+ @classmethod
10
+ def relationship_name(cls) -> str:
11
+ return "cs"
12
+
13
+ @classmethod
14
+ def parent_type(cls):
15
+ return ModelB
16
+
17
+
18
+ class ModelB(KilnParentedModel, KilnParentModel, parent_of={"cs": ModelC}):
19
+ value: int = Field(..., ge=0)
20
+
21
+ @classmethod
22
+ def relationship_name(cls) -> str:
23
+ return "bs"
24
+
25
+ @classmethod
26
+ def parent_type(cls):
27
+ return ModelA
28
+
29
+
30
+ # Define the hierarchy
31
+ class ModelA(KilnParentModel, parent_of={"bs": ModelB}):
32
+ name: str = Field(..., min_length=3)
33
+
34
+
35
+ def test_validation_error_in_c_level():
36
+ data = {
37
+ "name": "Root",
38
+ "bs": [
39
+ {
40
+ "value": 10,
41
+ "cs": [
42
+ {"code": "ABC"},
43
+ {"code": "DEF"},
44
+ {"code": "invalid"}, # This should cause a validation error
45
+ ],
46
+ }
47
+ ],
48
+ }
49
+
50
+ with pytest.raises(ValidationError) as exc_info:
51
+ ModelA.validate_and_save_with_subrelations(data)
52
+
53
+ assert "String should match pattern" in str(exc_info.value)
54
+
55
+
56
+ def test_persist_three_level_hierarchy(tmp_path):
57
+ # Set up temporary paths
58
+ root_path = tmp_path / "model_a.kiln"
59
+
60
+ data = {
61
+ "name": "Root",
62
+ "bs": [
63
+ {"value": 10, "cs": [{"code": "ABC"}, {"code": "DEF"}]},
64
+ {"value": 20, "cs": [{"code": "XYZ"}]},
65
+ ],
66
+ }
67
+
68
+ instance = ModelA.validate_and_save_with_subrelations(data, path=root_path)
69
+
70
+ assert isinstance(instance, ModelA)
71
+ assert instance.name == "Root"
72
+ assert instance.path == root_path
73
+ assert len(instance.bs()) == 2
74
+
75
+ # Load the instance back from the file to double-check
76
+ instance = ModelA.load_from_file(root_path)
77
+
78
+ bs = instance.bs()
79
+ assert len(bs) == 2
80
+
81
+ # Check for the existence of both expected B models
82
+ b_values = [b.value for b in bs]
83
+ assert 10 in b_values
84
+ assert 20 in b_values
85
+
86
+ # Find the B models by their values
87
+ b10 = next(b for b in bs if b.value == 10)
88
+ b20 = next(b for b in bs if b.value == 20)
89
+
90
+ assert len(b10.cs()) == 2
91
+ assert len(b20.cs()) == 1
92
+
93
+ # Check C models for b10
94
+ c_codes_b10 = [c.code for c in b10.cs()]
95
+ assert "ABC" in c_codes_b10
96
+ assert "DEF" in c_codes_b10
97
+
98
+ # Check C model for b20
99
+ c_codes_b20 = [c.code for c in b20.cs()]
100
+ assert "XYZ" in c_codes_b20
101
+
102
+ # Check that all objects have their parent set correctly
103
+ assert all(b.parent == instance for b in bs)
104
+ assert all(c.parent.id == b10.id for c in b10.cs())
105
+ assert all(c.parent.id == b20.id for c in b20.cs())
106
+
107
+
108
+ def test_persist_model_a_without_children(tmp_path):
109
+ # Set up temporary path
110
+ root_path = tmp_path / "model_a_no_children.kiln"
111
+
112
+ data = {"name": "RootNoChildren"}
113
+
114
+ instance = ModelA.validate_and_save_with_subrelations(data, path=root_path)
115
+
116
+ assert isinstance(instance, ModelA)
117
+ assert instance.name == "RootNoChildren"
118
+ assert instance.path == root_path
119
+ assert len(instance.bs()) == 0
120
+
121
+ # Verify that the file was created
122
+ assert root_path.exists()
123
+
124
+ # Load the instance back from the file to double-check
125
+ loaded_instance = ModelA.load_from_file(root_path)
126
+ assert loaded_instance.name == "RootNoChildren"
127
+ assert len(loaded_instance.bs()) == 0
128
+
129
+
130
+ def test_validate_without_saving(tmp_path):
131
+ data = {
132
+ "name": "ValidateOnly",
133
+ "bs": [
134
+ {"value": 30, "cs": [{"code": "GHI"}, {"code": "JKL"}]},
135
+ {"value": 40, "cs": [{"code": "MNO"}]},
136
+ ],
137
+ }
138
+
139
+ # Validate the data without saving
140
+ ModelA._validate_nested(data, save=False)
141
+
142
+ data = {
143
+ "name": "ValidateOnly",
144
+ "bs": [
145
+ {"value": 30, "cs": [{"code": "GHI"}, {"code": "JKL"}]},
146
+ {"value": 40, "cs": [{"code": 123}]},
147
+ ],
148
+ }
149
+
150
+ with pytest.raises(ValidationError):
151
+ ModelA._validate_nested(data, save=False)
152
+
153
+
154
+ def test_validation_error_in_multiple_levels():
155
+ data = {
156
+ "missing_name": "Root",
157
+ "bs": [
158
+ {
159
+ "value": -1,
160
+ "cs": [
161
+ {"code": "ABC"},
162
+ {"code": "DEF"},
163
+ {"code": "invalid"},
164
+ ],
165
+ }
166
+ ],
167
+ }
168
+
169
+ with pytest.raises(ValidationError) as exc_info:
170
+ ModelA.validate_and_save_with_subrelations(data)
171
+
172
+ assert len(exc_info.value.errors()) == 3
173
+
174
+ first = exc_info.value.errors()[0]
175
+ assert "Field required" in first["msg"]
176
+ assert first["loc"] == ("name",)
177
+
178
+ second = exc_info.value.errors()[1]
179
+ assert "Input should be greater than or equal to 0" in second["msg"]
180
+ assert second["loc"] == ("bs", 0, "value")
181
+
182
+ third = exc_info.value.errors()[2]
183
+ assert "String should match pattern" in third["msg"]
184
+ assert third["loc"] == ("bs", 0, "cs", 2, "code")
185
+
186
+
187
+ def test_validation_error_in_c_level_length():
188
+ data = {
189
+ "name": "Root",
190
+ "bs": [
191
+ {
192
+ "value": 10,
193
+ "cs": [
194
+ {"code": "ABC"},
195
+ {"code": "DEF"},
196
+ {"code": "GE"}, # This should cause a validation error
197
+ ],
198
+ }
199
+ ],
200
+ }
201
+
202
+ with pytest.raises(ValidationError) as exc_info:
203
+ ModelA.validate_and_save_with_subrelations(data)
204
+
205
+ assert "String should match pattern" in str(exc_info.value)
@@ -0,0 +1,88 @@
1
+ import pytest
2
+ from kiln_ai.datamodel import TaskOutputRating, TaskOutputRatingType
3
+ from pydantic import ValidationError
4
+
5
+
6
+ def test_valid_task_output_rating():
7
+ rating = TaskOutputRating(value=4.0, requirement_ratings={"req1": 5.0, "req2": 3.0})
8
+ assert rating.type == TaskOutputRatingType.five_star
9
+ assert rating.value == 4.0
10
+ assert rating.requirement_ratings == {"req1": 5.0, "req2": 3.0}
11
+
12
+
13
+ def test_invalid_rating_type():
14
+ with pytest.raises(ValidationError, match="Input should be"):
15
+ TaskOutputRating(type="invalid_type", value=4.0)
16
+
17
+
18
+ def test_invalid_rating_value():
19
+ with pytest.raises(
20
+ ValidationError,
21
+ match="Overall rating of type five_star must be an integer value",
22
+ ):
23
+ TaskOutputRating(value=3.5)
24
+
25
+
26
+ def test_rating_out_of_range():
27
+ with pytest.raises(
28
+ ValidationError,
29
+ match="Overall rating of type five_star must be between 1 and 5 stars",
30
+ ):
31
+ TaskOutputRating(value=6.0)
32
+
33
+
34
+ def test_rating_below_range():
35
+ with pytest.raises(
36
+ ValidationError,
37
+ match="Overall rating of type five_star must be between 1 and 5 stars",
38
+ ):
39
+ TaskOutputRating(value=0.0)
40
+
41
+
42
+ def test_valid_requirement_ratings():
43
+ rating = TaskOutputRating(
44
+ value=4.0, requirement_ratings={"req1": 5.0, "req2": 3.0, "req3": 1.0}
45
+ )
46
+ assert rating.requirement_ratings == {"req1": 5.0, "req2": 3.0, "req3": 1.0}
47
+
48
+
49
+ def test_invalid_requirement_rating_value():
50
+ with pytest.raises(
51
+ ValidationError,
52
+ match="Requirement rating for req1 of type five_star must be an integer value",
53
+ ):
54
+ TaskOutputRating(value=4.0, requirement_ratings={"req1": 3.5})
55
+
56
+
57
+ def test_requirement_rating_out_of_range():
58
+ with pytest.raises(
59
+ ValidationError,
60
+ match="Requirement rating for req1 of type five_star must be between 1 and 5 stars",
61
+ ):
62
+ TaskOutputRating(value=4.0, requirement_ratings={"req1": 6.0})
63
+
64
+
65
+ def test_empty_requirement_ratings():
66
+ rating = TaskOutputRating(value=4.0)
67
+ assert rating.requirement_ratings == {}
68
+
69
+
70
+ def test_invalid_id_type():
71
+ with pytest.raises(ValidationError):
72
+ TaskOutputRating(
73
+ value=4.0,
74
+ requirement_ratings={
75
+ 123: 4.0 # Assuming ID_TYPE is str
76
+ },
77
+ )
78
+
79
+
80
+ def test_valid_custom_rating():
81
+ rating = TaskOutputRating(
82
+ type=TaskOutputRatingType.custom,
83
+ value=31.459,
84
+ requirement_ratings={"req1": 42.0, "req2": 3.14},
85
+ )
86
+ assert rating.type == TaskOutputRatingType.custom
87
+ assert rating.value == 31.459
88
+ assert rating.requirement_ratings == {"req1": 42.0, "req2": 3.14}