kiln-ai 0.19.0__py3-none-any.whl → 0.21.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.
- kiln_ai/adapters/__init__.py +8 -2
- kiln_ai/adapters/adapter_registry.py +43 -208
- kiln_ai/adapters/chat/chat_formatter.py +8 -12
- kiln_ai/adapters/chat/test_chat_formatter.py +6 -2
- kiln_ai/adapters/chunkers/__init__.py +13 -0
- kiln_ai/adapters/chunkers/base_chunker.py +42 -0
- kiln_ai/adapters/chunkers/chunker_registry.py +16 -0
- kiln_ai/adapters/chunkers/fixed_window_chunker.py +39 -0
- kiln_ai/adapters/chunkers/helpers.py +23 -0
- kiln_ai/adapters/chunkers/test_base_chunker.py +63 -0
- kiln_ai/adapters/chunkers/test_chunker_registry.py +28 -0
- kiln_ai/adapters/chunkers/test_fixed_window_chunker.py +346 -0
- kiln_ai/adapters/chunkers/test_helpers.py +75 -0
- kiln_ai/adapters/data_gen/test_data_gen_task.py +9 -3
- kiln_ai/adapters/docker_model_runner_tools.py +119 -0
- kiln_ai/adapters/embedding/__init__.py +0 -0
- kiln_ai/adapters/embedding/base_embedding_adapter.py +44 -0
- kiln_ai/adapters/embedding/embedding_registry.py +32 -0
- kiln_ai/adapters/embedding/litellm_embedding_adapter.py +199 -0
- kiln_ai/adapters/embedding/test_base_embedding_adapter.py +283 -0
- kiln_ai/adapters/embedding/test_embedding_registry.py +166 -0
- kiln_ai/adapters/embedding/test_litellm_embedding_adapter.py +1149 -0
- kiln_ai/adapters/eval/base_eval.py +2 -2
- kiln_ai/adapters/eval/eval_runner.py +9 -3
- kiln_ai/adapters/eval/g_eval.py +2 -2
- kiln_ai/adapters/eval/test_base_eval.py +2 -4
- kiln_ai/adapters/eval/test_g_eval.py +4 -5
- kiln_ai/adapters/extractors/__init__.py +18 -0
- kiln_ai/adapters/extractors/base_extractor.py +72 -0
- kiln_ai/adapters/extractors/encoding.py +20 -0
- kiln_ai/adapters/extractors/extractor_registry.py +44 -0
- kiln_ai/adapters/extractors/extractor_runner.py +112 -0
- kiln_ai/adapters/extractors/litellm_extractor.py +386 -0
- kiln_ai/adapters/extractors/test_base_extractor.py +244 -0
- kiln_ai/adapters/extractors/test_encoding.py +54 -0
- kiln_ai/adapters/extractors/test_extractor_registry.py +181 -0
- kiln_ai/adapters/extractors/test_extractor_runner.py +181 -0
- kiln_ai/adapters/extractors/test_litellm_extractor.py +1192 -0
- kiln_ai/adapters/fine_tune/__init__.py +1 -1
- kiln_ai/adapters/fine_tune/openai_finetune.py +14 -4
- kiln_ai/adapters/fine_tune/test_dataset_formatter.py +2 -2
- kiln_ai/adapters/fine_tune/test_fireworks_tinetune.py +2 -6
- kiln_ai/adapters/fine_tune/test_openai_finetune.py +108 -111
- kiln_ai/adapters/fine_tune/test_together_finetune.py +2 -6
- kiln_ai/adapters/ml_embedding_model_list.py +192 -0
- kiln_ai/adapters/ml_model_list.py +761 -37
- kiln_ai/adapters/model_adapters/base_adapter.py +51 -21
- kiln_ai/adapters/model_adapters/litellm_adapter.py +380 -138
- kiln_ai/adapters/model_adapters/test_base_adapter.py +193 -17
- kiln_ai/adapters/model_adapters/test_litellm_adapter.py +407 -2
- kiln_ai/adapters/model_adapters/test_litellm_adapter_tools.py +1103 -0
- kiln_ai/adapters/model_adapters/test_saving_adapter_results.py +5 -5
- kiln_ai/adapters/model_adapters/test_structured_output.py +113 -5
- kiln_ai/adapters/ollama_tools.py +69 -12
- kiln_ai/adapters/parsers/__init__.py +1 -1
- kiln_ai/adapters/provider_tools.py +205 -47
- kiln_ai/adapters/rag/deduplication.py +49 -0
- kiln_ai/adapters/rag/progress.py +252 -0
- kiln_ai/adapters/rag/rag_runners.py +844 -0
- kiln_ai/adapters/rag/test_deduplication.py +195 -0
- kiln_ai/adapters/rag/test_progress.py +785 -0
- kiln_ai/adapters/rag/test_rag_runners.py +2376 -0
- kiln_ai/adapters/remote_config.py +80 -8
- kiln_ai/adapters/repair/test_repair_task.py +12 -9
- kiln_ai/adapters/run_output.py +3 -0
- kiln_ai/adapters/test_adapter_registry.py +657 -85
- kiln_ai/adapters/test_docker_model_runner_tools.py +305 -0
- kiln_ai/adapters/test_ml_embedding_model_list.py +429 -0
- kiln_ai/adapters/test_ml_model_list.py +251 -1
- kiln_ai/adapters/test_ollama_tools.py +340 -1
- kiln_ai/adapters/test_prompt_adaptors.py +13 -6
- kiln_ai/adapters/test_prompt_builders.py +1 -1
- kiln_ai/adapters/test_provider_tools.py +254 -8
- kiln_ai/adapters/test_remote_config.py +651 -58
- kiln_ai/adapters/vector_store/__init__.py +1 -0
- kiln_ai/adapters/vector_store/base_vector_store_adapter.py +83 -0
- kiln_ai/adapters/vector_store/lancedb_adapter.py +389 -0
- kiln_ai/adapters/vector_store/test_base_vector_store.py +160 -0
- kiln_ai/adapters/vector_store/test_lancedb_adapter.py +1841 -0
- kiln_ai/adapters/vector_store/test_vector_store_registry.py +199 -0
- kiln_ai/adapters/vector_store/vector_store_registry.py +33 -0
- kiln_ai/datamodel/__init__.py +39 -34
- kiln_ai/datamodel/basemodel.py +170 -1
- kiln_ai/datamodel/chunk.py +158 -0
- kiln_ai/datamodel/datamodel_enums.py +28 -0
- kiln_ai/datamodel/embedding.py +64 -0
- kiln_ai/datamodel/eval.py +1 -1
- kiln_ai/datamodel/external_tool_server.py +298 -0
- kiln_ai/datamodel/extraction.py +303 -0
- kiln_ai/datamodel/json_schema.py +25 -10
- kiln_ai/datamodel/project.py +40 -1
- kiln_ai/datamodel/rag.py +79 -0
- kiln_ai/datamodel/registry.py +0 -15
- kiln_ai/datamodel/run_config.py +62 -0
- kiln_ai/datamodel/task.py +2 -77
- kiln_ai/datamodel/task_output.py +6 -1
- kiln_ai/datamodel/task_run.py +41 -0
- kiln_ai/datamodel/test_attachment.py +649 -0
- kiln_ai/datamodel/test_basemodel.py +4 -4
- kiln_ai/datamodel/test_chunk_models.py +317 -0
- kiln_ai/datamodel/test_dataset_split.py +1 -1
- kiln_ai/datamodel/test_embedding_models.py +448 -0
- kiln_ai/datamodel/test_eval_model.py +6 -6
- kiln_ai/datamodel/test_example_models.py +175 -0
- kiln_ai/datamodel/test_external_tool_server.py +691 -0
- kiln_ai/datamodel/test_extraction_chunk.py +206 -0
- kiln_ai/datamodel/test_extraction_model.py +470 -0
- kiln_ai/datamodel/test_rag.py +641 -0
- kiln_ai/datamodel/test_registry.py +8 -3
- kiln_ai/datamodel/test_task.py +15 -47
- kiln_ai/datamodel/test_tool_id.py +320 -0
- kiln_ai/datamodel/test_vector_store.py +320 -0
- kiln_ai/datamodel/tool_id.py +105 -0
- kiln_ai/datamodel/vector_store.py +141 -0
- kiln_ai/tools/__init__.py +8 -0
- kiln_ai/tools/base_tool.py +82 -0
- kiln_ai/tools/built_in_tools/__init__.py +13 -0
- kiln_ai/tools/built_in_tools/math_tools.py +124 -0
- kiln_ai/tools/built_in_tools/test_math_tools.py +204 -0
- kiln_ai/tools/mcp_server_tool.py +95 -0
- kiln_ai/tools/mcp_session_manager.py +246 -0
- kiln_ai/tools/rag_tools.py +157 -0
- kiln_ai/tools/test_base_tools.py +199 -0
- kiln_ai/tools/test_mcp_server_tool.py +457 -0
- kiln_ai/tools/test_mcp_session_manager.py +1585 -0
- kiln_ai/tools/test_rag_tools.py +848 -0
- kiln_ai/tools/test_tool_registry.py +562 -0
- kiln_ai/tools/tool_registry.py +85 -0
- kiln_ai/utils/__init__.py +3 -0
- kiln_ai/utils/async_job_runner.py +62 -17
- kiln_ai/utils/config.py +24 -2
- kiln_ai/utils/env.py +15 -0
- kiln_ai/utils/filesystem.py +14 -0
- kiln_ai/utils/filesystem_cache.py +60 -0
- kiln_ai/utils/litellm.py +94 -0
- kiln_ai/utils/lock.py +100 -0
- kiln_ai/utils/mime_type.py +38 -0
- kiln_ai/utils/open_ai_types.py +94 -0
- kiln_ai/utils/pdf_utils.py +38 -0
- kiln_ai/utils/project_utils.py +17 -0
- kiln_ai/utils/test_async_job_runner.py +151 -35
- kiln_ai/utils/test_config.py +138 -1
- kiln_ai/utils/test_env.py +142 -0
- kiln_ai/utils/test_filesystem_cache.py +316 -0
- kiln_ai/utils/test_litellm.py +206 -0
- kiln_ai/utils/test_lock.py +185 -0
- kiln_ai/utils/test_mime_type.py +66 -0
- kiln_ai/utils/test_open_ai_types.py +131 -0
- kiln_ai/utils/test_pdf_utils.py +73 -0
- kiln_ai/utils/test_uuid.py +111 -0
- kiln_ai/utils/test_validation.py +524 -0
- kiln_ai/utils/uuid.py +9 -0
- kiln_ai/utils/validation.py +90 -0
- {kiln_ai-0.19.0.dist-info → kiln_ai-0.21.0.dist-info}/METADATA +12 -5
- kiln_ai-0.21.0.dist-info/RECORD +211 -0
- kiln_ai-0.19.0.dist-info/RECORD +0 -115
- {kiln_ai-0.19.0.dist-info → kiln_ai-0.21.0.dist-info}/WHEEL +0 -0
- {kiln_ai-0.19.0.dist-info → kiln_ai-0.21.0.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from kiln_ai.utils.validation import (
|
|
4
|
+
tool_name_validator,
|
|
5
|
+
validate_return_dict_prop,
|
|
6
|
+
validate_return_dict_prop_optional,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestValidateReturnDictProp:
|
|
11
|
+
"""Test cases for validate_return_dict_prop function."""
|
|
12
|
+
|
|
13
|
+
def test_valid_string_property(self):
|
|
14
|
+
"""Test validation succeeds for valid string property."""
|
|
15
|
+
test_dict = {"name": "test_value"}
|
|
16
|
+
result = validate_return_dict_prop(test_dict, "name", str, "prefix")
|
|
17
|
+
assert result == "test_value"
|
|
18
|
+
|
|
19
|
+
def test_valid_int_property(self):
|
|
20
|
+
"""Test validation succeeds for valid integer property."""
|
|
21
|
+
test_dict = {"count": 42}
|
|
22
|
+
result = validate_return_dict_prop(test_dict, "count", int, "prefix")
|
|
23
|
+
assert result == 42
|
|
24
|
+
|
|
25
|
+
def test_valid_bool_property(self):
|
|
26
|
+
"""Test validation succeeds for valid boolean property."""
|
|
27
|
+
test_dict = {"enabled": True}
|
|
28
|
+
result = validate_return_dict_prop(test_dict, "enabled", bool, "prefix")
|
|
29
|
+
assert result is True
|
|
30
|
+
|
|
31
|
+
def test_valid_list_property(self):
|
|
32
|
+
"""Test validation succeeds for valid list property."""
|
|
33
|
+
test_dict = {"items": [1, 2, 3]}
|
|
34
|
+
result = validate_return_dict_prop(test_dict, "items", list, "prefix")
|
|
35
|
+
assert result == [1, 2, 3]
|
|
36
|
+
|
|
37
|
+
def test_valid_dict_property(self):
|
|
38
|
+
"""Test validation succeeds for valid dict property."""
|
|
39
|
+
test_dict = {"config": {"key": "value"}}
|
|
40
|
+
result = validate_return_dict_prop(test_dict, "config", dict, "prefix")
|
|
41
|
+
assert result == {"key": "value"}
|
|
42
|
+
|
|
43
|
+
def test_missing_key_raises_error(self):
|
|
44
|
+
"""Test that missing key raises ValueError with appropriate message."""
|
|
45
|
+
test_dict = {"other_key": "value"}
|
|
46
|
+
with pytest.raises(ValueError) as exc_info:
|
|
47
|
+
validate_return_dict_prop(test_dict, "missing_key", str, "prefix")
|
|
48
|
+
|
|
49
|
+
expected_msg = "prefix missing_key is a required property"
|
|
50
|
+
assert str(exc_info.value) == expected_msg
|
|
51
|
+
|
|
52
|
+
def test_wrong_type_raises_error(self):
|
|
53
|
+
"""Test that wrong type raises ValueError with appropriate message."""
|
|
54
|
+
test_dict = {"count": "not_a_number"}
|
|
55
|
+
with pytest.raises(ValueError) as exc_info:
|
|
56
|
+
validate_return_dict_prop(test_dict, "count", int, "prefix")
|
|
57
|
+
|
|
58
|
+
expected_msg = "prefix count must be of type <class 'int'>"
|
|
59
|
+
assert str(exc_info.value) == expected_msg
|
|
60
|
+
|
|
61
|
+
def test_none_value_with_none_type(self):
|
|
62
|
+
"""Test that None value validates correctly when expecting NoneType."""
|
|
63
|
+
test_dict = {"value": None}
|
|
64
|
+
result = validate_return_dict_prop(test_dict, "value", type(None), "prefix")
|
|
65
|
+
assert result is None
|
|
66
|
+
|
|
67
|
+
def test_none_value_with_string_type_raises_error(self):
|
|
68
|
+
"""Test that None value raises error when expecting string."""
|
|
69
|
+
test_dict = {"value": None}
|
|
70
|
+
with pytest.raises(ValueError) as exc_info:
|
|
71
|
+
validate_return_dict_prop(test_dict, "value", str, "prefix")
|
|
72
|
+
|
|
73
|
+
expected_msg = "prefix value must be of type <class 'str'>"
|
|
74
|
+
assert str(exc_info.value) == expected_msg
|
|
75
|
+
|
|
76
|
+
@pytest.mark.parametrize(
|
|
77
|
+
"test_value,expected_type",
|
|
78
|
+
[
|
|
79
|
+
("string", str),
|
|
80
|
+
(123, int),
|
|
81
|
+
(3.14, float),
|
|
82
|
+
(True, bool),
|
|
83
|
+
([1, 2, 3], list),
|
|
84
|
+
({"k": "v"}, dict),
|
|
85
|
+
((1, 2), tuple),
|
|
86
|
+
({1, 2, 3}, set),
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
def test_various_types_succeed(self, test_value, expected_type):
|
|
90
|
+
"""Test validation succeeds for various types."""
|
|
91
|
+
test_dict = {"value": test_value}
|
|
92
|
+
result = validate_return_dict_prop(test_dict, "value", expected_type, "prefix")
|
|
93
|
+
assert result == test_value
|
|
94
|
+
assert isinstance(result, expected_type)
|
|
95
|
+
|
|
96
|
+
@pytest.mark.parametrize(
|
|
97
|
+
"test_value,wrong_type",
|
|
98
|
+
[
|
|
99
|
+
("string", int),
|
|
100
|
+
(123, str),
|
|
101
|
+
(3.14, int),
|
|
102
|
+
(True, str),
|
|
103
|
+
([1, 2, 3], dict),
|
|
104
|
+
({"k": "v"}, list),
|
|
105
|
+
((1, 2), list),
|
|
106
|
+
({1, 2, 3}, list),
|
|
107
|
+
],
|
|
108
|
+
)
|
|
109
|
+
def test_various_types_fail(self, test_value, wrong_type):
|
|
110
|
+
"""Test validation fails for wrong types."""
|
|
111
|
+
test_dict = {"value": test_value}
|
|
112
|
+
with pytest.raises(ValueError):
|
|
113
|
+
validate_return_dict_prop(test_dict, "value", wrong_type, "prefix")
|
|
114
|
+
|
|
115
|
+
def test_empty_dict_raises_error(self):
|
|
116
|
+
"""Test that empty dictionary raises error for any key."""
|
|
117
|
+
test_dict = {}
|
|
118
|
+
with pytest.raises(ValueError) as exc_info:
|
|
119
|
+
validate_return_dict_prop(test_dict, "any_key", str, "prefix")
|
|
120
|
+
|
|
121
|
+
expected_msg = "prefix any_key is a required property"
|
|
122
|
+
assert str(exc_info.value) == expected_msg
|
|
123
|
+
|
|
124
|
+
def test_empty_string_key(self):
|
|
125
|
+
"""Test validation with empty string as key."""
|
|
126
|
+
test_dict = {"": "empty_key_value"}
|
|
127
|
+
result = validate_return_dict_prop(test_dict, "", str, "prefix")
|
|
128
|
+
assert result == "empty_key_value"
|
|
129
|
+
|
|
130
|
+
def test_numeric_values_and_inheritance(self):
|
|
131
|
+
"""Test that isinstance works correctly with numeric inheritance."""
|
|
132
|
+
# bool is a subclass of int in Python, so True/False are valid ints
|
|
133
|
+
test_dict = {"flag": True}
|
|
134
|
+
result = validate_return_dict_prop(test_dict, "flag", int, "prefix")
|
|
135
|
+
assert result is True
|
|
136
|
+
assert isinstance(result, int) # This should pass since bool inherits from int
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class TestValidateReturnDictPropOptional:
|
|
140
|
+
"""Test cases for validate_return_dict_prop_optional function."""
|
|
141
|
+
|
|
142
|
+
def test_valid_string_property(self):
|
|
143
|
+
"""Test validation succeeds for valid string property."""
|
|
144
|
+
test_dict = {"name": "test_value"}
|
|
145
|
+
result = validate_return_dict_prop_optional(test_dict, "name", str, "prefix")
|
|
146
|
+
assert result == "test_value"
|
|
147
|
+
|
|
148
|
+
def test_valid_int_property(self):
|
|
149
|
+
"""Test validation succeeds for valid integer property."""
|
|
150
|
+
test_dict = {"count": 42}
|
|
151
|
+
result = validate_return_dict_prop_optional(test_dict, "count", int, "prefix")
|
|
152
|
+
assert result == 42
|
|
153
|
+
|
|
154
|
+
def test_missing_key_returns_none(self):
|
|
155
|
+
"""Test that missing key returns None instead of raising error."""
|
|
156
|
+
test_dict = {"other_key": "value"}
|
|
157
|
+
result = validate_return_dict_prop_optional(
|
|
158
|
+
test_dict, "missing_key", str, "prefix"
|
|
159
|
+
)
|
|
160
|
+
assert result is None
|
|
161
|
+
|
|
162
|
+
def test_none_value_returns_none(self):
|
|
163
|
+
"""Test that None value returns None."""
|
|
164
|
+
test_dict = {"value": None}
|
|
165
|
+
result = validate_return_dict_prop_optional(test_dict, "value", str, "prefix")
|
|
166
|
+
assert result is None
|
|
167
|
+
|
|
168
|
+
def test_empty_dict_returns_none(self):
|
|
169
|
+
"""Test that empty dictionary returns None for any key."""
|
|
170
|
+
test_dict = {}
|
|
171
|
+
result = validate_return_dict_prop_optional(test_dict, "any_key", str, "prefix")
|
|
172
|
+
assert result is None
|
|
173
|
+
|
|
174
|
+
def test_wrong_type_raises_error(self):
|
|
175
|
+
"""Test that wrong type still raises ValueError (delegates to required function)."""
|
|
176
|
+
test_dict = {"count": "not_a_number"}
|
|
177
|
+
with pytest.raises(ValueError) as exc_info:
|
|
178
|
+
validate_return_dict_prop_optional(test_dict, "count", int, "prefix")
|
|
179
|
+
|
|
180
|
+
expected_msg = "prefix count must be of type <class 'int'>"
|
|
181
|
+
assert str(exc_info.value) == expected_msg
|
|
182
|
+
|
|
183
|
+
def test_explicit_none_vs_missing_key(self):
|
|
184
|
+
"""Test that explicit None value and missing key both return None."""
|
|
185
|
+
# Missing key
|
|
186
|
+
test_dict_missing = {"other": "value"}
|
|
187
|
+
result_missing = validate_return_dict_prop_optional(
|
|
188
|
+
test_dict_missing, "target", str, "prefix"
|
|
189
|
+
)
|
|
190
|
+
assert result_missing is None
|
|
191
|
+
|
|
192
|
+
# Explicit None
|
|
193
|
+
test_dict_none = {"target": None}
|
|
194
|
+
result_none = validate_return_dict_prop_optional(
|
|
195
|
+
test_dict_none, "target", str, "prefix"
|
|
196
|
+
)
|
|
197
|
+
assert result_none is None
|
|
198
|
+
|
|
199
|
+
@pytest.mark.parametrize(
|
|
200
|
+
"test_value,expected_type",
|
|
201
|
+
[
|
|
202
|
+
("string", str),
|
|
203
|
+
(123, int),
|
|
204
|
+
(3.14, float),
|
|
205
|
+
(True, bool),
|
|
206
|
+
([1, 2, 3], list),
|
|
207
|
+
({"k": "v"}, dict),
|
|
208
|
+
((1, 2), tuple),
|
|
209
|
+
({1, 2, 3}, set),
|
|
210
|
+
],
|
|
211
|
+
)
|
|
212
|
+
def test_various_types_succeed(self, test_value, expected_type):
|
|
213
|
+
"""Test validation succeeds for various types."""
|
|
214
|
+
test_dict = {"value": test_value}
|
|
215
|
+
result = validate_return_dict_prop_optional(
|
|
216
|
+
test_dict, "value", expected_type, "prefix"
|
|
217
|
+
)
|
|
218
|
+
assert result == test_value
|
|
219
|
+
assert isinstance(result, expected_type)
|
|
220
|
+
|
|
221
|
+
@pytest.mark.parametrize(
|
|
222
|
+
"test_value,wrong_type",
|
|
223
|
+
[
|
|
224
|
+
("string", int),
|
|
225
|
+
(123, str),
|
|
226
|
+
(3.14, int),
|
|
227
|
+
(True, str),
|
|
228
|
+
([1, 2, 3], dict),
|
|
229
|
+
({"k": "v"}, list),
|
|
230
|
+
((1, 2), list),
|
|
231
|
+
({1, 2, 3}, list),
|
|
232
|
+
],
|
|
233
|
+
)
|
|
234
|
+
def test_various_types_fail(self, test_value, wrong_type):
|
|
235
|
+
"""Test validation fails for wrong types (delegates to required function)."""
|
|
236
|
+
test_dict = {"value": test_value}
|
|
237
|
+
with pytest.raises(ValueError):
|
|
238
|
+
validate_return_dict_prop_optional(test_dict, "value", wrong_type, "prefix")
|
|
239
|
+
|
|
240
|
+
def test_empty_string_key_with_value(self):
|
|
241
|
+
"""Test validation with empty string as key when value exists."""
|
|
242
|
+
test_dict = {"": "empty_key_value"}
|
|
243
|
+
result = validate_return_dict_prop_optional(test_dict, "", str, "prefix")
|
|
244
|
+
assert result == "empty_key_value"
|
|
245
|
+
|
|
246
|
+
def test_empty_string_key_missing(self):
|
|
247
|
+
"""Test validation with empty string as key when key is missing."""
|
|
248
|
+
test_dict = {"other": "value"}
|
|
249
|
+
result = validate_return_dict_prop_optional(test_dict, "", str, "prefix")
|
|
250
|
+
assert result is None
|
|
251
|
+
|
|
252
|
+
def test_numeric_inheritance_behavior(self):
|
|
253
|
+
"""Test that isinstance works correctly with numeric inheritance."""
|
|
254
|
+
# bool is a subclass of int in Python, so True/False are valid ints
|
|
255
|
+
test_dict = {"flag": True}
|
|
256
|
+
result = validate_return_dict_prop_optional(test_dict, "flag", int, "prefix")
|
|
257
|
+
assert result is True
|
|
258
|
+
assert isinstance(result, int)
|
|
259
|
+
|
|
260
|
+
def test_optional_with_zero_values(self):
|
|
261
|
+
"""Test that zero-like values (0, False, [], {}) are not treated as None."""
|
|
262
|
+
test_cases = [
|
|
263
|
+
({"count": 0}, "count", int, 0),
|
|
264
|
+
({"flag": False}, "flag", bool, False),
|
|
265
|
+
({"items": []}, "items", list, []),
|
|
266
|
+
({"config": {}}, "config", dict, {}),
|
|
267
|
+
({"text": ""}, "text", str, ""),
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
for test_dict, key, expected_type, expected_value in test_cases:
|
|
271
|
+
result = validate_return_dict_prop_optional(
|
|
272
|
+
test_dict, key, expected_type, "prefix"
|
|
273
|
+
)
|
|
274
|
+
assert result == expected_value
|
|
275
|
+
assert result is not None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class TestToolNameValidator:
|
|
279
|
+
"""Test cases for tool_name_validator function."""
|
|
280
|
+
|
|
281
|
+
def test_valid_simple_name(self):
|
|
282
|
+
"""Test validation succeeds for simple valid tool names."""
|
|
283
|
+
valid_names = [
|
|
284
|
+
"tool",
|
|
285
|
+
"my_tool",
|
|
286
|
+
"data_processor",
|
|
287
|
+
"test123",
|
|
288
|
+
"a",
|
|
289
|
+
"tool_v2",
|
|
290
|
+
"get_weather",
|
|
291
|
+
]
|
|
292
|
+
for name in valid_names:
|
|
293
|
+
result = tool_name_validator(name)
|
|
294
|
+
assert result == name
|
|
295
|
+
|
|
296
|
+
def test_valid_name_with_numbers(self):
|
|
297
|
+
"""Test validation succeeds for tool names with numbers."""
|
|
298
|
+
valid_names = [
|
|
299
|
+
"tool1",
|
|
300
|
+
"my_tool_v2",
|
|
301
|
+
"data_processor_3000",
|
|
302
|
+
"test_123_abc",
|
|
303
|
+
"version2",
|
|
304
|
+
]
|
|
305
|
+
for name in valid_names:
|
|
306
|
+
result = tool_name_validator(name)
|
|
307
|
+
assert result == name
|
|
308
|
+
|
|
309
|
+
def test_none_name_raises_error(self):
|
|
310
|
+
"""Test that None name raises ValueError."""
|
|
311
|
+
with pytest.raises(ValueError) as exc_info:
|
|
312
|
+
tool_name_validator(None)
|
|
313
|
+
assert str(exc_info.value) == "Tool name cannot be empty"
|
|
314
|
+
|
|
315
|
+
def test_empty_string_raises_error(self):
|
|
316
|
+
"""Test that empty string raises ValueError."""
|
|
317
|
+
with pytest.raises(ValueError) as exc_info:
|
|
318
|
+
tool_name_validator("")
|
|
319
|
+
assert str(exc_info.value) == "Tool name cannot be empty"
|
|
320
|
+
|
|
321
|
+
def test_whitespace_only_raises_error(self):
|
|
322
|
+
"""Test that whitespace-only string raises ValueError."""
|
|
323
|
+
whitespace_names = [" ", " ", "\t", "\n", " \t "]
|
|
324
|
+
for name in whitespace_names:
|
|
325
|
+
with pytest.raises(ValueError) as exc_info:
|
|
326
|
+
tool_name_validator(name)
|
|
327
|
+
assert str(exc_info.value) == "Tool name cannot be empty"
|
|
328
|
+
|
|
329
|
+
def test_non_string_raises_error(self):
|
|
330
|
+
"""Test that non-string input raises ValueError."""
|
|
331
|
+
non_string_inputs = [123, [], {}, True, 3.14]
|
|
332
|
+
for input_val in non_string_inputs:
|
|
333
|
+
with pytest.raises(ValueError) as exc_info:
|
|
334
|
+
tool_name_validator(input_val)
|
|
335
|
+
assert str(exc_info.value) == "Tool name must be a string"
|
|
336
|
+
|
|
337
|
+
def test_uppercase_letters_raise_error(self):
|
|
338
|
+
"""Test that uppercase letters raise ValueError."""
|
|
339
|
+
invalid_names = [
|
|
340
|
+
"Tool",
|
|
341
|
+
"MY_TOOL",
|
|
342
|
+
"myTool",
|
|
343
|
+
"tool_Name",
|
|
344
|
+
"TOOL",
|
|
345
|
+
"Test123",
|
|
346
|
+
]
|
|
347
|
+
for name in invalid_names:
|
|
348
|
+
with pytest.raises(ValueError) as exc_info:
|
|
349
|
+
tool_name_validator(name)
|
|
350
|
+
assert "Tool name must be in snake_case" in str(exc_info.value)
|
|
351
|
+
|
|
352
|
+
def test_special_characters_raise_error(self):
|
|
353
|
+
"""Test that special characters raise ValueError."""
|
|
354
|
+
invalid_names = [
|
|
355
|
+
"tool-name",
|
|
356
|
+
"tool.name",
|
|
357
|
+
"tool@name",
|
|
358
|
+
"tool#name",
|
|
359
|
+
"tool$name",
|
|
360
|
+
"tool%name",
|
|
361
|
+
"tool&name",
|
|
362
|
+
"tool*name",
|
|
363
|
+
"tool+name",
|
|
364
|
+
"tool=name",
|
|
365
|
+
"tool!name",
|
|
366
|
+
"tool?name",
|
|
367
|
+
"tool name", # space
|
|
368
|
+
"tool,name",
|
|
369
|
+
"tool;name",
|
|
370
|
+
"tool:name",
|
|
371
|
+
"tool'name",
|
|
372
|
+
'tool"name',
|
|
373
|
+
"tool(name)",
|
|
374
|
+
"tool[name]",
|
|
375
|
+
"tool{name}",
|
|
376
|
+
"tool/name",
|
|
377
|
+
"tool\\name",
|
|
378
|
+
]
|
|
379
|
+
for name in invalid_names:
|
|
380
|
+
with pytest.raises(ValueError) as exc_info:
|
|
381
|
+
tool_name_validator(name)
|
|
382
|
+
assert "Tool name must be in snake_case" in str(exc_info.value)
|
|
383
|
+
|
|
384
|
+
def test_starts_with_underscore_raises_error(self):
|
|
385
|
+
"""Test that names starting with underscore raise ValueError."""
|
|
386
|
+
invalid_names = ["_tool", "_my_tool", "_", "_123"]
|
|
387
|
+
for name in invalid_names:
|
|
388
|
+
with pytest.raises(ValueError) as exc_info:
|
|
389
|
+
tool_name_validator(name)
|
|
390
|
+
assert (
|
|
391
|
+
str(exc_info.value)
|
|
392
|
+
== "Tool name cannot start or end with an underscore"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def test_ends_with_underscore_raises_error(self):
|
|
396
|
+
"""Test that names ending with underscore raise ValueError."""
|
|
397
|
+
invalid_names = ["tool_", "my_tool_", "test_"]
|
|
398
|
+
for name in invalid_names:
|
|
399
|
+
with pytest.raises(ValueError) as exc_info:
|
|
400
|
+
tool_name_validator(name)
|
|
401
|
+
assert (
|
|
402
|
+
str(exc_info.value)
|
|
403
|
+
== "Tool name cannot start or end with an underscore"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def test_consecutive_underscores_raise_error(self):
|
|
407
|
+
"""Test that consecutive underscores raise ValueError."""
|
|
408
|
+
invalid_names = [
|
|
409
|
+
"tool__name",
|
|
410
|
+
"my__tool",
|
|
411
|
+
"test___name",
|
|
412
|
+
"a__b__c",
|
|
413
|
+
"tool____name",
|
|
414
|
+
]
|
|
415
|
+
for name in invalid_names:
|
|
416
|
+
with pytest.raises(ValueError) as exc_info:
|
|
417
|
+
tool_name_validator(name)
|
|
418
|
+
assert (
|
|
419
|
+
str(exc_info.value)
|
|
420
|
+
== "Tool name cannot contain consecutive underscores"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def test_starts_with_number_raises_error(self):
|
|
424
|
+
"""Test that names starting with number raise ValueError."""
|
|
425
|
+
invalid_names = ["1tool", "2_tool", "123abc", "9test"]
|
|
426
|
+
for name in invalid_names:
|
|
427
|
+
with pytest.raises(ValueError) as exc_info:
|
|
428
|
+
tool_name_validator(name)
|
|
429
|
+
assert str(exc_info.value) == "Tool name must start with a lowercase letter"
|
|
430
|
+
|
|
431
|
+
def test_starts_with_underscore_number_raises_error(self):
|
|
432
|
+
"""Test that names starting with underscore followed by number raise ValueError."""
|
|
433
|
+
invalid_names = ["_1tool", "_2tool"]
|
|
434
|
+
for name in invalid_names:
|
|
435
|
+
with pytest.raises(ValueError) as exc_info:
|
|
436
|
+
tool_name_validator(name)
|
|
437
|
+
# This should fail on the underscore check first
|
|
438
|
+
assert (
|
|
439
|
+
str(exc_info.value)
|
|
440
|
+
== "Tool name cannot start or end with an underscore"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
def test_long_name_raises_error(self):
|
|
444
|
+
"""Test that names longer than 64 characters raise ValueError."""
|
|
445
|
+
# Create a 65-character name
|
|
446
|
+
long_name = "a" * 65
|
|
447
|
+
with pytest.raises(ValueError) as exc_info:
|
|
448
|
+
tool_name_validator(long_name)
|
|
449
|
+
assert str(exc_info.value) == "Tool name must be less than 64 characters long"
|
|
450
|
+
|
|
451
|
+
def test_exactly_64_characters_succeeds(self):
|
|
452
|
+
"""Test that names with exactly 64 characters succeed."""
|
|
453
|
+
# Create a 64-character name
|
|
454
|
+
max_length_name = "a" * 64
|
|
455
|
+
result = tool_name_validator(max_length_name)
|
|
456
|
+
assert result == max_length_name
|
|
457
|
+
|
|
458
|
+
def test_boundary_length_cases(self):
|
|
459
|
+
"""Test various boundary cases for name length."""
|
|
460
|
+
# Test lengths around the limit
|
|
461
|
+
test_cases = [
|
|
462
|
+
("a", 1), # minimum valid length
|
|
463
|
+
("ab", 2),
|
|
464
|
+
("a" * 63, 63), # just under limit
|
|
465
|
+
("a" * 64, 64), # exactly at limit
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
for name, expected_length in test_cases:
|
|
469
|
+
result = tool_name_validator(name)
|
|
470
|
+
assert result == name
|
|
471
|
+
assert len(result) == expected_length
|
|
472
|
+
|
|
473
|
+
def test_complex_valid_names(self):
|
|
474
|
+
"""Test complex but valid tool names."""
|
|
475
|
+
valid_names = [
|
|
476
|
+
"get_user_data",
|
|
477
|
+
"process_payment_info",
|
|
478
|
+
"validate_email_address",
|
|
479
|
+
"send_notification_v2",
|
|
480
|
+
"calculate_tax_amount",
|
|
481
|
+
"fetch_weather_data_for_city",
|
|
482
|
+
"convert_currency_usd_to_eur",
|
|
483
|
+
"a1b2c3d4e5f6g7h8i9j0",
|
|
484
|
+
"tool_with_many_underscores_and_numbers_123",
|
|
485
|
+
]
|
|
486
|
+
for name in valid_names:
|
|
487
|
+
result = tool_name_validator(name)
|
|
488
|
+
assert result == name
|
|
489
|
+
|
|
490
|
+
@pytest.mark.parametrize(
|
|
491
|
+
"invalid_name,expected_error",
|
|
492
|
+
[
|
|
493
|
+
(None, "Tool name cannot be empty"),
|
|
494
|
+
("", "Tool name cannot be empty"),
|
|
495
|
+
(" ", "Tool name cannot be empty"),
|
|
496
|
+
(123, "Tool name must be a string"),
|
|
497
|
+
("Tool", "Tool name must be in snake_case"),
|
|
498
|
+
("tool-name", "Tool name must be in snake_case"),
|
|
499
|
+
("_tool", "Tool name cannot start or end with an underscore"),
|
|
500
|
+
("tool_", "Tool name cannot start or end with an underscore"),
|
|
501
|
+
("tool__name", "Tool name cannot contain consecutive underscores"),
|
|
502
|
+
("1tool", "Tool name must start with a lowercase letter"),
|
|
503
|
+
("a" * 65, "Tool name must be less than 64 characters long"),
|
|
504
|
+
],
|
|
505
|
+
)
|
|
506
|
+
def test_parametrized_invalid_cases(self, invalid_name, expected_error):
|
|
507
|
+
"""Test various invalid cases with parameterized inputs."""
|
|
508
|
+
with pytest.raises(ValueError) as exc_info:
|
|
509
|
+
tool_name_validator(invalid_name)
|
|
510
|
+
assert expected_error in str(exc_info.value)
|
|
511
|
+
|
|
512
|
+
def test_edge_case_single_character_names(self):
|
|
513
|
+
"""Test single character names (valid and invalid)."""
|
|
514
|
+
# Valid single characters
|
|
515
|
+
valid_chars = "abcdefghijklmnopqrstuvwxyz"
|
|
516
|
+
for char in valid_chars:
|
|
517
|
+
result = tool_name_validator(char)
|
|
518
|
+
assert result == char
|
|
519
|
+
|
|
520
|
+
# Invalid single characters
|
|
521
|
+
invalid_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+="
|
|
522
|
+
for char in invalid_chars:
|
|
523
|
+
with pytest.raises(ValueError):
|
|
524
|
+
tool_name_validator(char)
|
kiln_ai/utils/uuid.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
# do not change this, or this will break backwards compatibility with existing UUIDs
|
|
4
|
+
KILN_UUID_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_DNS, "kiln.tech")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def string_to_uuid(s: str) -> uuid.UUID:
|
|
8
|
+
"""Return a deterministic UUIDv5 for the input string."""
|
|
9
|
+
return uuid.uuid5(KILN_UUID_NAMESPACE, s)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Annotated, Any, TypeVar, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import BeforeValidator
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_return_dict_prop(
|
|
10
|
+
dict: dict[str, Any], key: str, type: type[T], error_msg_prefix: str
|
|
11
|
+
) -> T:
|
|
12
|
+
"""
|
|
13
|
+
Validate that a property exists in a dictionary and is of a specified type.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
dict: The dictionary to validate.
|
|
17
|
+
key: The key of the property to validate.
|
|
18
|
+
type: The type of the property to validate.
|
|
19
|
+
error_msg_prefix: The prefix of the error message.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The value of the property.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If the property is not found or is not of the specified type.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> validate_return_dict_prop({"key": "value"}, "key", str, "LanceDB vector store configs properties:")
|
|
29
|
+
"value"
|
|
30
|
+
"""
|
|
31
|
+
if key not in dict:
|
|
32
|
+
raise ValueError(f"{error_msg_prefix} {key} is a required property")
|
|
33
|
+
if not isinstance(dict[key], type):
|
|
34
|
+
raise ValueError(f"{error_msg_prefix} {key} must be of type {type}")
|
|
35
|
+
return dict[key]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def validate_return_dict_prop_optional(
|
|
39
|
+
dict: dict[str, Any], key: str, type: type[T], error_msg_prefix: str
|
|
40
|
+
) -> Union[T, None]:
|
|
41
|
+
"""
|
|
42
|
+
Validate that a property exists in a dictionary and is of a specified type.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
dict: The dictionary to validate.
|
|
46
|
+
key: The key of the property to validate.
|
|
47
|
+
type: The type of the property to validate.
|
|
48
|
+
error_msg_prefix: The prefix of the error message.
|
|
49
|
+
"""
|
|
50
|
+
if key not in dict or dict[key] is None:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
return validate_return_dict_prop(dict, key, type, error_msg_prefix)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def tool_name_validator(name: str) -> str:
|
|
57
|
+
# Check if name is None or empty
|
|
58
|
+
if name is None or (isinstance(name, str) and len(name.strip()) == 0):
|
|
59
|
+
raise ValueError("Tool name cannot be empty")
|
|
60
|
+
|
|
61
|
+
if not isinstance(name, str):
|
|
62
|
+
raise ValueError("Tool name must be a string")
|
|
63
|
+
|
|
64
|
+
# Check if name contains only lowercase letters, numbers, and underscores
|
|
65
|
+
snake_case_regex = re.compile(r"^[a-z0-9_]+$")
|
|
66
|
+
if not snake_case_regex.match(name):
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"Tool name must be in snake_case: containing only lowercase letters (a-z), numbers (0-9), and underscores"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Check that it doesn't start or end with underscore
|
|
72
|
+
if name.startswith("_") or name.endswith("_"):
|
|
73
|
+
raise ValueError("Tool name cannot start or end with an underscore")
|
|
74
|
+
|
|
75
|
+
# Check that it doesn't have consecutive underscores
|
|
76
|
+
if "__" in name:
|
|
77
|
+
raise ValueError("Tool name cannot contain consecutive underscores")
|
|
78
|
+
|
|
79
|
+
# Check that it starts with a letter (good snake_case practice)
|
|
80
|
+
if not re.match(r"^[a-z]", name):
|
|
81
|
+
raise ValueError("Tool name must start with a lowercase letter")
|
|
82
|
+
|
|
83
|
+
# Check length
|
|
84
|
+
if len(name) > 64:
|
|
85
|
+
raise ValueError("Tool name must be less than 64 characters long")
|
|
86
|
+
|
|
87
|
+
return name
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
ToolNameString = Annotated[str, BeforeValidator(tool_name_validator)]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kiln-ai
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.0
|
|
4
4
|
Summary: Kiln AI
|
|
5
|
-
Project-URL: Homepage, https://
|
|
5
|
+
Project-URL: Homepage, https://kiln.tech
|
|
6
6
|
Project-URL: Repository, https://github.com/Kiln-AI/kiln
|
|
7
7
|
Project-URL: Documentation, https://kiln-ai.github.io/Kiln/kiln_core_docs/kiln_ai.html
|
|
8
8
|
Project-URL: Issues, https://github.com/Kiln-AI/kiln/issues
|
|
@@ -15,14 +15,21 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
17
|
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: anyio>=4.10.0
|
|
18
19
|
Requires-Dist: boto3>=1.37.10
|
|
19
20
|
Requires-Dist: coverage>=7.6.4
|
|
21
|
+
Requires-Dist: exceptiongroup>=1.0.0; python_version < '3.11'
|
|
20
22
|
Requires-Dist: google-cloud-aiplatform>=1.84.0
|
|
23
|
+
Requires-Dist: google-genai>=1.21.1
|
|
21
24
|
Requires-Dist: jsonschema>=4.23.0
|
|
25
|
+
Requires-Dist: lancedb>=0.24.2
|
|
22
26
|
Requires-Dist: litellm>=1.72.6
|
|
27
|
+
Requires-Dist: llama-index-vector-stores-lancedb>=0.3.3
|
|
28
|
+
Requires-Dist: llama-index>=0.13.3
|
|
23
29
|
Requires-Dist: openai>=1.53.0
|
|
24
30
|
Requires-Dist: pdoc>=15.0.0
|
|
25
31
|
Requires-Dist: pydantic>=2.9.2
|
|
32
|
+
Requires-Dist: pypdf>=6.0.0
|
|
26
33
|
Requires-Dist: pytest-benchmark>=5.1.0
|
|
27
34
|
Requires-Dist: pytest-cov>=6.0.0
|
|
28
35
|
Requires-Dist: pyyaml>=6.0.2
|
|
@@ -53,7 +60,7 @@ pip install kiln_ai
|
|
|
53
60
|
|
|
54
61
|
## About
|
|
55
62
|
|
|
56
|
-
This package is the Kiln AI core library. There is also a separate desktop application and server package. Learn more about Kiln AI at [
|
|
63
|
+
This package is the Kiln AI core library. There is also a separate desktop application and server package. Learn more about Kiln AI at [kiln.tech](https://kiln.tech) and on Github: [github.com/Kiln-AI/kiln](https://github.com/Kiln-AI/kiln).
|
|
57
64
|
|
|
58
65
|
# Guide: Using the Kiln Python Library
|
|
59
66
|
|
|
@@ -306,8 +313,8 @@ You can add additional AI models and providers to Kiln.
|
|
|
306
313
|
|
|
307
314
|
See our docs for more information, including how to add these from the UI:
|
|
308
315
|
|
|
309
|
-
- [Custom Models From Existing Providers](https://docs.
|
|
310
|
-
- [Custom OpenAI Compatible Servers](https://docs.
|
|
316
|
+
- [Custom Models From Existing Providers](https://docs.kiln.tech/docs/models-and-ai-providers#custom-models-from-existing-providers)
|
|
317
|
+
- [Custom OpenAI Compatible Servers](https://docs.kiln.tech/docs/models-and-ai-providers#custom-openai-compatible-servers)
|
|
311
318
|
|
|
312
319
|
You can also add these from code. The kiln_ai.utils.Config class helps you manage the Kiln config file (stored at `~/.kiln_settings/config.yaml`):
|
|
313
320
|
|