kiln-ai 0.20.1__py3-none-any.whl → 0.22.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 (133) hide show
  1. kiln_ai/adapters/__init__.py +6 -0
  2. kiln_ai/adapters/adapter_registry.py +43 -226
  3. kiln_ai/adapters/chunkers/__init__.py +13 -0
  4. kiln_ai/adapters/chunkers/base_chunker.py +42 -0
  5. kiln_ai/adapters/chunkers/chunker_registry.py +16 -0
  6. kiln_ai/adapters/chunkers/fixed_window_chunker.py +39 -0
  7. kiln_ai/adapters/chunkers/helpers.py +23 -0
  8. kiln_ai/adapters/chunkers/test_base_chunker.py +63 -0
  9. kiln_ai/adapters/chunkers/test_chunker_registry.py +28 -0
  10. kiln_ai/adapters/chunkers/test_fixed_window_chunker.py +346 -0
  11. kiln_ai/adapters/chunkers/test_helpers.py +75 -0
  12. kiln_ai/adapters/data_gen/test_data_gen_task.py +9 -3
  13. kiln_ai/adapters/embedding/__init__.py +0 -0
  14. kiln_ai/adapters/embedding/base_embedding_adapter.py +44 -0
  15. kiln_ai/adapters/embedding/embedding_registry.py +32 -0
  16. kiln_ai/adapters/embedding/litellm_embedding_adapter.py +199 -0
  17. kiln_ai/adapters/embedding/test_base_embedding_adapter.py +283 -0
  18. kiln_ai/adapters/embedding/test_embedding_registry.py +166 -0
  19. kiln_ai/adapters/embedding/test_litellm_embedding_adapter.py +1149 -0
  20. kiln_ai/adapters/eval/eval_runner.py +6 -2
  21. kiln_ai/adapters/eval/test_base_eval.py +1 -3
  22. kiln_ai/adapters/eval/test_g_eval.py +1 -1
  23. kiln_ai/adapters/extractors/__init__.py +18 -0
  24. kiln_ai/adapters/extractors/base_extractor.py +72 -0
  25. kiln_ai/adapters/extractors/encoding.py +20 -0
  26. kiln_ai/adapters/extractors/extractor_registry.py +44 -0
  27. kiln_ai/adapters/extractors/extractor_runner.py +112 -0
  28. kiln_ai/adapters/extractors/litellm_extractor.py +406 -0
  29. kiln_ai/adapters/extractors/test_base_extractor.py +244 -0
  30. kiln_ai/adapters/extractors/test_encoding.py +54 -0
  31. kiln_ai/adapters/extractors/test_extractor_registry.py +181 -0
  32. kiln_ai/adapters/extractors/test_extractor_runner.py +181 -0
  33. kiln_ai/adapters/extractors/test_litellm_extractor.py +1290 -0
  34. kiln_ai/adapters/fine_tune/test_dataset_formatter.py +2 -2
  35. kiln_ai/adapters/fine_tune/test_fireworks_tinetune.py +2 -6
  36. kiln_ai/adapters/fine_tune/test_together_finetune.py +2 -6
  37. kiln_ai/adapters/ml_embedding_model_list.py +494 -0
  38. kiln_ai/adapters/ml_model_list.py +876 -18
  39. kiln_ai/adapters/model_adapters/litellm_adapter.py +40 -75
  40. kiln_ai/adapters/model_adapters/test_litellm_adapter.py +79 -1
  41. kiln_ai/adapters/model_adapters/test_litellm_adapter_tools.py +119 -5
  42. kiln_ai/adapters/model_adapters/test_saving_adapter_results.py +9 -3
  43. kiln_ai/adapters/model_adapters/test_structured_output.py +9 -10
  44. kiln_ai/adapters/ollama_tools.py +69 -12
  45. kiln_ai/adapters/provider_tools.py +190 -46
  46. kiln_ai/adapters/rag/deduplication.py +49 -0
  47. kiln_ai/adapters/rag/progress.py +252 -0
  48. kiln_ai/adapters/rag/rag_runners.py +844 -0
  49. kiln_ai/adapters/rag/test_deduplication.py +195 -0
  50. kiln_ai/adapters/rag/test_progress.py +785 -0
  51. kiln_ai/adapters/rag/test_rag_runners.py +2376 -0
  52. kiln_ai/adapters/remote_config.py +80 -8
  53. kiln_ai/adapters/test_adapter_registry.py +579 -86
  54. kiln_ai/adapters/test_ml_embedding_model_list.py +239 -0
  55. kiln_ai/adapters/test_ml_model_list.py +202 -0
  56. kiln_ai/adapters/test_ollama_tools.py +340 -1
  57. kiln_ai/adapters/test_prompt_builders.py +1 -1
  58. kiln_ai/adapters/test_provider_tools.py +199 -8
  59. kiln_ai/adapters/test_remote_config.py +551 -56
  60. kiln_ai/adapters/vector_store/__init__.py +1 -0
  61. kiln_ai/adapters/vector_store/base_vector_store_adapter.py +83 -0
  62. kiln_ai/adapters/vector_store/lancedb_adapter.py +389 -0
  63. kiln_ai/adapters/vector_store/test_base_vector_store.py +160 -0
  64. kiln_ai/adapters/vector_store/test_lancedb_adapter.py +1841 -0
  65. kiln_ai/adapters/vector_store/test_vector_store_registry.py +199 -0
  66. kiln_ai/adapters/vector_store/vector_store_registry.py +33 -0
  67. kiln_ai/datamodel/__init__.py +16 -13
  68. kiln_ai/datamodel/basemodel.py +201 -4
  69. kiln_ai/datamodel/chunk.py +158 -0
  70. kiln_ai/datamodel/datamodel_enums.py +27 -0
  71. kiln_ai/datamodel/embedding.py +64 -0
  72. kiln_ai/datamodel/external_tool_server.py +206 -54
  73. kiln_ai/datamodel/extraction.py +317 -0
  74. kiln_ai/datamodel/project.py +33 -1
  75. kiln_ai/datamodel/rag.py +79 -0
  76. kiln_ai/datamodel/task.py +5 -0
  77. kiln_ai/datamodel/task_output.py +41 -11
  78. kiln_ai/datamodel/test_attachment.py +649 -0
  79. kiln_ai/datamodel/test_basemodel.py +270 -14
  80. kiln_ai/datamodel/test_chunk_models.py +317 -0
  81. kiln_ai/datamodel/test_dataset_split.py +1 -1
  82. kiln_ai/datamodel/test_datasource.py +50 -0
  83. kiln_ai/datamodel/test_embedding_models.py +448 -0
  84. kiln_ai/datamodel/test_eval_model.py +6 -6
  85. kiln_ai/datamodel/test_external_tool_server.py +534 -152
  86. kiln_ai/datamodel/test_extraction_chunk.py +206 -0
  87. kiln_ai/datamodel/test_extraction_model.py +501 -0
  88. kiln_ai/datamodel/test_rag.py +641 -0
  89. kiln_ai/datamodel/test_task.py +35 -1
  90. kiln_ai/datamodel/test_tool_id.py +187 -1
  91. kiln_ai/datamodel/test_vector_store.py +320 -0
  92. kiln_ai/datamodel/tool_id.py +58 -0
  93. kiln_ai/datamodel/vector_store.py +141 -0
  94. kiln_ai/tools/base_tool.py +12 -3
  95. kiln_ai/tools/built_in_tools/math_tools.py +12 -4
  96. kiln_ai/tools/kiln_task_tool.py +158 -0
  97. kiln_ai/tools/mcp_server_tool.py +2 -2
  98. kiln_ai/tools/mcp_session_manager.py +51 -22
  99. kiln_ai/tools/rag_tools.py +164 -0
  100. kiln_ai/tools/test_kiln_task_tool.py +527 -0
  101. kiln_ai/tools/test_mcp_server_tool.py +4 -15
  102. kiln_ai/tools/test_mcp_session_manager.py +187 -227
  103. kiln_ai/tools/test_rag_tools.py +929 -0
  104. kiln_ai/tools/test_tool_registry.py +290 -7
  105. kiln_ai/tools/tool_registry.py +69 -16
  106. kiln_ai/utils/__init__.py +3 -0
  107. kiln_ai/utils/async_job_runner.py +62 -17
  108. kiln_ai/utils/config.py +2 -2
  109. kiln_ai/utils/env.py +15 -0
  110. kiln_ai/utils/filesystem.py +14 -0
  111. kiln_ai/utils/filesystem_cache.py +60 -0
  112. kiln_ai/utils/litellm.py +94 -0
  113. kiln_ai/utils/lock.py +100 -0
  114. kiln_ai/utils/mime_type.py +38 -0
  115. kiln_ai/utils/open_ai_types.py +19 -2
  116. kiln_ai/utils/pdf_utils.py +59 -0
  117. kiln_ai/utils/test_async_job_runner.py +151 -35
  118. kiln_ai/utils/test_env.py +142 -0
  119. kiln_ai/utils/test_filesystem_cache.py +316 -0
  120. kiln_ai/utils/test_litellm.py +206 -0
  121. kiln_ai/utils/test_lock.py +185 -0
  122. kiln_ai/utils/test_mime_type.py +66 -0
  123. kiln_ai/utils/test_open_ai_types.py +88 -12
  124. kiln_ai/utils/test_pdf_utils.py +86 -0
  125. kiln_ai/utils/test_uuid.py +111 -0
  126. kiln_ai/utils/test_validation.py +524 -0
  127. kiln_ai/utils/uuid.py +9 -0
  128. kiln_ai/utils/validation.py +90 -0
  129. {kiln_ai-0.20.1.dist-info → kiln_ai-0.22.0.dist-info}/METADATA +9 -1
  130. kiln_ai-0.22.0.dist-info/RECORD +213 -0
  131. kiln_ai-0.20.1.dist-info/RECORD +0 -138
  132. {kiln_ai-0.20.1.dist-info → kiln_ai-0.22.0.dist-info}/WHEEL +0 -0
  133. {kiln_ai-0.20.1.dist-info → kiln_ai-0.22.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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiln-ai
3
- Version: 0.20.1
3
+ Version: 0.22.0
4
4
  Summary: Kiln AI
5
5
  Project-URL: Homepage, https://kiln.tech
6
6
  Project-URL: Repository, https://github.com/Kiln-AI/kiln
@@ -15,15 +15,23 @@ 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
20
21
  Requires-Dist: exceptiongroup>=1.0.0; python_version < '3.11'
21
22
  Requires-Dist: google-cloud-aiplatform>=1.84.0
23
+ Requires-Dist: google-genai>=1.21.1
22
24
  Requires-Dist: jsonschema>=4.23.0
25
+ Requires-Dist: lancedb>=0.24.2
23
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
24
29
  Requires-Dist: openai>=1.53.0
25
30
  Requires-Dist: pdoc>=15.0.0
31
+ Requires-Dist: pillow>=11.1.0
26
32
  Requires-Dist: pydantic>=2.9.2
33
+ Requires-Dist: pypdf>=6.0.0
34
+ Requires-Dist: pypdfium2>=4.30.0
27
35
  Requires-Dist: pytest-benchmark>=5.1.0
28
36
  Requires-Dist: pytest-cov>=6.0.0
29
37
  Requires-Dist: pyyaml>=6.0.2