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.

Files changed (158) hide show
  1. kiln_ai/adapters/__init__.py +8 -2
  2. kiln_ai/adapters/adapter_registry.py +43 -208
  3. kiln_ai/adapters/chat/chat_formatter.py +8 -12
  4. kiln_ai/adapters/chat/test_chat_formatter.py +6 -2
  5. kiln_ai/adapters/chunkers/__init__.py +13 -0
  6. kiln_ai/adapters/chunkers/base_chunker.py +42 -0
  7. kiln_ai/adapters/chunkers/chunker_registry.py +16 -0
  8. kiln_ai/adapters/chunkers/fixed_window_chunker.py +39 -0
  9. kiln_ai/adapters/chunkers/helpers.py +23 -0
  10. kiln_ai/adapters/chunkers/test_base_chunker.py +63 -0
  11. kiln_ai/adapters/chunkers/test_chunker_registry.py +28 -0
  12. kiln_ai/adapters/chunkers/test_fixed_window_chunker.py +346 -0
  13. kiln_ai/adapters/chunkers/test_helpers.py +75 -0
  14. kiln_ai/adapters/data_gen/test_data_gen_task.py +9 -3
  15. kiln_ai/adapters/docker_model_runner_tools.py +119 -0
  16. kiln_ai/adapters/embedding/__init__.py +0 -0
  17. kiln_ai/adapters/embedding/base_embedding_adapter.py +44 -0
  18. kiln_ai/adapters/embedding/embedding_registry.py +32 -0
  19. kiln_ai/adapters/embedding/litellm_embedding_adapter.py +199 -0
  20. kiln_ai/adapters/embedding/test_base_embedding_adapter.py +283 -0
  21. kiln_ai/adapters/embedding/test_embedding_registry.py +166 -0
  22. kiln_ai/adapters/embedding/test_litellm_embedding_adapter.py +1149 -0
  23. kiln_ai/adapters/eval/base_eval.py +2 -2
  24. kiln_ai/adapters/eval/eval_runner.py +9 -3
  25. kiln_ai/adapters/eval/g_eval.py +2 -2
  26. kiln_ai/adapters/eval/test_base_eval.py +2 -4
  27. kiln_ai/adapters/eval/test_g_eval.py +4 -5
  28. kiln_ai/adapters/extractors/__init__.py +18 -0
  29. kiln_ai/adapters/extractors/base_extractor.py +72 -0
  30. kiln_ai/adapters/extractors/encoding.py +20 -0
  31. kiln_ai/adapters/extractors/extractor_registry.py +44 -0
  32. kiln_ai/adapters/extractors/extractor_runner.py +112 -0
  33. kiln_ai/adapters/extractors/litellm_extractor.py +386 -0
  34. kiln_ai/adapters/extractors/test_base_extractor.py +244 -0
  35. kiln_ai/adapters/extractors/test_encoding.py +54 -0
  36. kiln_ai/adapters/extractors/test_extractor_registry.py +181 -0
  37. kiln_ai/adapters/extractors/test_extractor_runner.py +181 -0
  38. kiln_ai/adapters/extractors/test_litellm_extractor.py +1192 -0
  39. kiln_ai/adapters/fine_tune/__init__.py +1 -1
  40. kiln_ai/adapters/fine_tune/openai_finetune.py +14 -4
  41. kiln_ai/adapters/fine_tune/test_dataset_formatter.py +2 -2
  42. kiln_ai/adapters/fine_tune/test_fireworks_tinetune.py +2 -6
  43. kiln_ai/adapters/fine_tune/test_openai_finetune.py +108 -111
  44. kiln_ai/adapters/fine_tune/test_together_finetune.py +2 -6
  45. kiln_ai/adapters/ml_embedding_model_list.py +192 -0
  46. kiln_ai/adapters/ml_model_list.py +761 -37
  47. kiln_ai/adapters/model_adapters/base_adapter.py +51 -21
  48. kiln_ai/adapters/model_adapters/litellm_adapter.py +380 -138
  49. kiln_ai/adapters/model_adapters/test_base_adapter.py +193 -17
  50. kiln_ai/adapters/model_adapters/test_litellm_adapter.py +407 -2
  51. kiln_ai/adapters/model_adapters/test_litellm_adapter_tools.py +1103 -0
  52. kiln_ai/adapters/model_adapters/test_saving_adapter_results.py +5 -5
  53. kiln_ai/adapters/model_adapters/test_structured_output.py +113 -5
  54. kiln_ai/adapters/ollama_tools.py +69 -12
  55. kiln_ai/adapters/parsers/__init__.py +1 -1
  56. kiln_ai/adapters/provider_tools.py +205 -47
  57. kiln_ai/adapters/rag/deduplication.py +49 -0
  58. kiln_ai/adapters/rag/progress.py +252 -0
  59. kiln_ai/adapters/rag/rag_runners.py +844 -0
  60. kiln_ai/adapters/rag/test_deduplication.py +195 -0
  61. kiln_ai/adapters/rag/test_progress.py +785 -0
  62. kiln_ai/adapters/rag/test_rag_runners.py +2376 -0
  63. kiln_ai/adapters/remote_config.py +80 -8
  64. kiln_ai/adapters/repair/test_repair_task.py +12 -9
  65. kiln_ai/adapters/run_output.py +3 -0
  66. kiln_ai/adapters/test_adapter_registry.py +657 -85
  67. kiln_ai/adapters/test_docker_model_runner_tools.py +305 -0
  68. kiln_ai/adapters/test_ml_embedding_model_list.py +429 -0
  69. kiln_ai/adapters/test_ml_model_list.py +251 -1
  70. kiln_ai/adapters/test_ollama_tools.py +340 -1
  71. kiln_ai/adapters/test_prompt_adaptors.py +13 -6
  72. kiln_ai/adapters/test_prompt_builders.py +1 -1
  73. kiln_ai/adapters/test_provider_tools.py +254 -8
  74. kiln_ai/adapters/test_remote_config.py +651 -58
  75. kiln_ai/adapters/vector_store/__init__.py +1 -0
  76. kiln_ai/adapters/vector_store/base_vector_store_adapter.py +83 -0
  77. kiln_ai/adapters/vector_store/lancedb_adapter.py +389 -0
  78. kiln_ai/adapters/vector_store/test_base_vector_store.py +160 -0
  79. kiln_ai/adapters/vector_store/test_lancedb_adapter.py +1841 -0
  80. kiln_ai/adapters/vector_store/test_vector_store_registry.py +199 -0
  81. kiln_ai/adapters/vector_store/vector_store_registry.py +33 -0
  82. kiln_ai/datamodel/__init__.py +39 -34
  83. kiln_ai/datamodel/basemodel.py +170 -1
  84. kiln_ai/datamodel/chunk.py +158 -0
  85. kiln_ai/datamodel/datamodel_enums.py +28 -0
  86. kiln_ai/datamodel/embedding.py +64 -0
  87. kiln_ai/datamodel/eval.py +1 -1
  88. kiln_ai/datamodel/external_tool_server.py +298 -0
  89. kiln_ai/datamodel/extraction.py +303 -0
  90. kiln_ai/datamodel/json_schema.py +25 -10
  91. kiln_ai/datamodel/project.py +40 -1
  92. kiln_ai/datamodel/rag.py +79 -0
  93. kiln_ai/datamodel/registry.py +0 -15
  94. kiln_ai/datamodel/run_config.py +62 -0
  95. kiln_ai/datamodel/task.py +2 -77
  96. kiln_ai/datamodel/task_output.py +6 -1
  97. kiln_ai/datamodel/task_run.py +41 -0
  98. kiln_ai/datamodel/test_attachment.py +649 -0
  99. kiln_ai/datamodel/test_basemodel.py +4 -4
  100. kiln_ai/datamodel/test_chunk_models.py +317 -0
  101. kiln_ai/datamodel/test_dataset_split.py +1 -1
  102. kiln_ai/datamodel/test_embedding_models.py +448 -0
  103. kiln_ai/datamodel/test_eval_model.py +6 -6
  104. kiln_ai/datamodel/test_example_models.py +175 -0
  105. kiln_ai/datamodel/test_external_tool_server.py +691 -0
  106. kiln_ai/datamodel/test_extraction_chunk.py +206 -0
  107. kiln_ai/datamodel/test_extraction_model.py +470 -0
  108. kiln_ai/datamodel/test_rag.py +641 -0
  109. kiln_ai/datamodel/test_registry.py +8 -3
  110. kiln_ai/datamodel/test_task.py +15 -47
  111. kiln_ai/datamodel/test_tool_id.py +320 -0
  112. kiln_ai/datamodel/test_vector_store.py +320 -0
  113. kiln_ai/datamodel/tool_id.py +105 -0
  114. kiln_ai/datamodel/vector_store.py +141 -0
  115. kiln_ai/tools/__init__.py +8 -0
  116. kiln_ai/tools/base_tool.py +82 -0
  117. kiln_ai/tools/built_in_tools/__init__.py +13 -0
  118. kiln_ai/tools/built_in_tools/math_tools.py +124 -0
  119. kiln_ai/tools/built_in_tools/test_math_tools.py +204 -0
  120. kiln_ai/tools/mcp_server_tool.py +95 -0
  121. kiln_ai/tools/mcp_session_manager.py +246 -0
  122. kiln_ai/tools/rag_tools.py +157 -0
  123. kiln_ai/tools/test_base_tools.py +199 -0
  124. kiln_ai/tools/test_mcp_server_tool.py +457 -0
  125. kiln_ai/tools/test_mcp_session_manager.py +1585 -0
  126. kiln_ai/tools/test_rag_tools.py +848 -0
  127. kiln_ai/tools/test_tool_registry.py +562 -0
  128. kiln_ai/tools/tool_registry.py +85 -0
  129. kiln_ai/utils/__init__.py +3 -0
  130. kiln_ai/utils/async_job_runner.py +62 -17
  131. kiln_ai/utils/config.py +24 -2
  132. kiln_ai/utils/env.py +15 -0
  133. kiln_ai/utils/filesystem.py +14 -0
  134. kiln_ai/utils/filesystem_cache.py +60 -0
  135. kiln_ai/utils/litellm.py +94 -0
  136. kiln_ai/utils/lock.py +100 -0
  137. kiln_ai/utils/mime_type.py +38 -0
  138. kiln_ai/utils/open_ai_types.py +94 -0
  139. kiln_ai/utils/pdf_utils.py +38 -0
  140. kiln_ai/utils/project_utils.py +17 -0
  141. kiln_ai/utils/test_async_job_runner.py +151 -35
  142. kiln_ai/utils/test_config.py +138 -1
  143. kiln_ai/utils/test_env.py +142 -0
  144. kiln_ai/utils/test_filesystem_cache.py +316 -0
  145. kiln_ai/utils/test_litellm.py +206 -0
  146. kiln_ai/utils/test_lock.py +185 -0
  147. kiln_ai/utils/test_mime_type.py +66 -0
  148. kiln_ai/utils/test_open_ai_types.py +131 -0
  149. kiln_ai/utils/test_pdf_utils.py +73 -0
  150. kiln_ai/utils/test_uuid.py +111 -0
  151. kiln_ai/utils/test_validation.py +524 -0
  152. kiln_ai/utils/uuid.py +9 -0
  153. kiln_ai/utils/validation.py +90 -0
  154. {kiln_ai-0.19.0.dist-info → kiln_ai-0.21.0.dist-info}/METADATA +12 -5
  155. kiln_ai-0.21.0.dist-info/RECORD +211 -0
  156. kiln_ai-0.19.0.dist-info/RECORD +0 -115
  157. {kiln_ai-0.19.0.dist-info → kiln_ai-0.21.0.dist-info}/WHEEL +0 -0
  158. {kiln_ai-0.19.0.dist-info → kiln_ai-0.21.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -3,22 +3,18 @@ from pydantic import ValidationError
3
3
 
4
4
  from kiln_ai.datamodel.datamodel_enums import StructuredOutputMode, TaskOutputRatingType
5
5
  from kiln_ai.datamodel.prompt_id import PromptGenerators
6
- from kiln_ai.datamodel.task import RunConfig, RunConfigProperties, Task, TaskRunConfig
6
+ from kiln_ai.datamodel.task import RunConfigProperties, Task, TaskRunConfig
7
7
  from kiln_ai.datamodel.task_output import normalize_rating
8
8
 
9
9
 
10
10
  def test_runconfig_valid_creation():
11
- task = Task(id="task1", name="Test Task", instruction="Do something")
12
-
13
- config = RunConfig(
14
- task=task,
11
+ config = RunConfigProperties(
15
12
  model_name="gpt-4",
16
13
  model_provider_name="openai",
17
14
  prompt_id=PromptGenerators.SIMPLE,
18
15
  structured_output_mode="json_schema",
19
16
  )
20
17
 
21
- assert config.task == task
22
18
  assert config.model_name == "gpt-4"
23
19
  assert config.model_provider_name == "openai"
24
20
  assert config.prompt_id == PromptGenerators.SIMPLE # Check default value
@@ -26,13 +22,12 @@ def test_runconfig_valid_creation():
26
22
 
27
23
  def test_runconfig_missing_required_fields():
28
24
  with pytest.raises(ValidationError) as exc_info:
29
- RunConfig()
25
+ RunConfigProperties() # type: ignore
30
26
 
31
27
  errors = exc_info.value.errors()
32
28
  assert (
33
- len(errors) == 5
29
+ len(errors) == 4
34
30
  ) # task, model_name, model_provider_name, and prompt_id are required
35
- assert any(error["loc"][0] == "task" for error in errors)
36
31
  assert any(error["loc"][0] == "model_name" for error in errors)
37
32
  assert any(error["loc"][0] == "model_provider_name" for error in errors)
38
33
  assert any(error["loc"][0] == "prompt_id" for error in errors)
@@ -40,10 +35,7 @@ def test_runconfig_missing_required_fields():
40
35
 
41
36
 
42
37
  def test_runconfig_custom_prompt_id():
43
- task = Task(id="task1", name="Test Task", instruction="Do something")
44
-
45
- config = RunConfig(
46
- task=task,
38
+ config = RunConfigProperties(
47
39
  model_name="gpt-4",
48
40
  model_provider_name="openai",
49
41
  prompt_id=PromptGenerators.SIMPLE_CHAIN_OF_THOUGHT,
@@ -100,30 +92,18 @@ def test_task_run_config_missing_required_fields(sample_task):
100
92
  with pytest.raises(ValidationError) as exc_info:
101
93
  TaskRunConfig(
102
94
  run_config_properties=RunConfigProperties(
103
- task=sample_task, model_name="gpt-4", model_provider_name="openai"
104
- ),
95
+ model_name="gpt-4", model_provider_name="openai"
96
+ ), # type: ignore
105
97
  parent=sample_task,
106
- )
98
+ ) # type: ignore
107
99
  assert "Field required" in str(exc_info.value)
108
100
 
109
101
  # Test missing run_config
110
102
  with pytest.raises(ValidationError) as exc_info:
111
- TaskRunConfig(name="Test Config", parent=sample_task)
103
+ TaskRunConfig(name="Test Config", parent=sample_task) # type: ignore
112
104
  assert "Field required" in str(exc_info.value)
113
105
 
114
106
 
115
- def test_task_run_config_missing_task_in_run_config(sample_task):
116
- with pytest.raises(
117
- ValidationError, match="Input should be a valid dictionary or instance of Task"
118
- ):
119
- # Create a run config without a task
120
- RunConfig(
121
- model_name="gpt-4",
122
- model_provider_name="openai",
123
- task=None, # type: ignore
124
- )
125
-
126
-
127
107
  @pytest.mark.parametrize(
128
108
  "rating_type,rating,expected",
129
109
  [
@@ -165,10 +145,8 @@ def test_normalize_rating_errors(rating_type, rating):
165
145
 
166
146
  def test_run_config_defaults():
167
147
  """RunConfig should require top_p, temperature, and structured_output_mode to be set."""
168
- task = Task(id="task1", name="Test Task", instruction="Do something")
169
148
 
170
- config = RunConfig(
171
- task=task,
149
+ config = RunConfigProperties(
172
150
  model_name="gpt-4",
173
151
  model_provider_name="openai",
174
152
  prompt_id=PromptGenerators.SIMPLE,
@@ -180,11 +158,9 @@ def test_run_config_defaults():
180
158
 
181
159
  def test_run_config_valid_ranges():
182
160
  """RunConfig should accept valid ranges for top_p and temperature."""
183
- task = Task(id="task1", name="Test Task", instruction="Do something")
184
161
 
185
162
  # Test valid values
186
- config = RunConfig(
187
- task=task,
163
+ config = RunConfigProperties(
188
164
  model_name="gpt-4",
189
165
  model_provider_name="openai",
190
166
  prompt_id=PromptGenerators.SIMPLE,
@@ -201,10 +177,8 @@ def test_run_config_valid_ranges():
201
177
  @pytest.mark.parametrize("top_p", [0.0, 0.5, 1.0])
202
178
  def test_run_config_valid_top_p(top_p):
203
179
  """Test that RunConfig accepts valid top_p values (0-1)."""
204
- task = Task(id="task1", name="Test Task", instruction="Do something")
205
180
 
206
- config = RunConfig(
207
- task=task,
181
+ config = RunConfigProperties(
208
182
  model_name="gpt-4",
209
183
  model_provider_name="openai",
210
184
  prompt_id=PromptGenerators.SIMPLE,
@@ -219,11 +193,9 @@ def test_run_config_valid_top_p(top_p):
219
193
  @pytest.mark.parametrize("top_p", [-0.1, 1.1, 2.0])
220
194
  def test_run_config_invalid_top_p(top_p):
221
195
  """Test that RunConfig rejects invalid top_p values."""
222
- task = Task(id="task1", name="Test Task", instruction="Do something")
223
196
 
224
197
  with pytest.raises(ValueError, match="top_p must be between 0 and 1"):
225
- RunConfig(
226
- task=task,
198
+ RunConfigProperties(
227
199
  model_name="gpt-4",
228
200
  model_provider_name="openai",
229
201
  prompt_id=PromptGenerators.SIMPLE,
@@ -236,10 +208,8 @@ def test_run_config_invalid_top_p(top_p):
236
208
  @pytest.mark.parametrize("temperature", [0.0, 1.0, 2.0])
237
209
  def test_run_config_valid_temperature(temperature):
238
210
  """Test that RunConfig accepts valid temperature values (0-2)."""
239
- task = Task(id="task1", name="Test Task", instruction="Do something")
240
211
 
241
- config = RunConfig(
242
- task=task,
212
+ config = RunConfigProperties(
243
213
  model_name="gpt-4",
244
214
  model_provider_name="openai",
245
215
  prompt_id=PromptGenerators.SIMPLE,
@@ -254,11 +224,9 @@ def test_run_config_valid_temperature(temperature):
254
224
  @pytest.mark.parametrize("temperature", [-0.1, 2.1, 3.0])
255
225
  def test_run_config_invalid_temperature(temperature):
256
226
  """Test that RunConfig rejects invalid temperature values."""
257
- task = Task(id="task1", name="Test Task", instruction="Do something")
258
227
 
259
228
  with pytest.raises(ValueError, match="temperature must be between 0 and 2"):
260
- RunConfig(
261
- task=task,
229
+ RunConfigProperties(
262
230
  model_name="gpt-4",
263
231
  model_provider_name="openai",
264
232
  prompt_id=PromptGenerators.SIMPLE,
@@ -0,0 +1,320 @@
1
+ import pytest
2
+ from pydantic import BaseModel, ValidationError
3
+
4
+ from kiln_ai.datamodel.tool_id import (
5
+ MCP_LOCAL_TOOL_ID_PREFIX,
6
+ MCP_REMOTE_TOOL_ID_PREFIX,
7
+ RAG_TOOL_ID_PREFIX,
8
+ KilnBuiltInToolId,
9
+ ToolId,
10
+ _check_tool_id,
11
+ mcp_server_and_tool_name_from_id,
12
+ rag_config_id_from_id,
13
+ )
14
+
15
+
16
+ class TestKilnBuiltInToolId:
17
+ """Test the KilnBuiltInToolId enum."""
18
+
19
+ def test_enum_values(self):
20
+ """Test that enum has expected values."""
21
+ assert KilnBuiltInToolId.ADD_NUMBERS == "kiln_tool::add_numbers"
22
+ assert KilnBuiltInToolId.SUBTRACT_NUMBERS == "kiln_tool::subtract_numbers"
23
+ assert KilnBuiltInToolId.MULTIPLY_NUMBERS == "kiln_tool::multiply_numbers"
24
+ assert KilnBuiltInToolId.DIVIDE_NUMBERS == "kiln_tool::divide_numbers"
25
+ for enum_value in KilnBuiltInToolId.__members__.values():
26
+ assert _check_tool_id(enum_value) == enum_value
27
+
28
+ def test_enum_membership(self):
29
+ """Test enum membership checks."""
30
+ assert "kiln_tool::add_numbers" in KilnBuiltInToolId.__members__.values()
31
+ assert "invalid_tool" not in KilnBuiltInToolId.__members__.values()
32
+
33
+
34
+ class TestCheckToolId:
35
+ """Test the _check_tool_id validation function."""
36
+
37
+ def test_valid_builtin_tools(self):
38
+ """Test validation of valid built-in tools."""
39
+ for tool_id in KilnBuiltInToolId:
40
+ result = _check_tool_id(tool_id.value)
41
+ assert result == tool_id.value
42
+
43
+ def test_valid_mcp_remote_tools(self):
44
+ """Test validation of valid MCP remote tools."""
45
+ valid_ids = [
46
+ "mcp::remote::server1::tool1",
47
+ "mcp::remote::my_server::my_tool",
48
+ "mcp::remote::test::function_name",
49
+ ]
50
+ for tool_id in valid_ids:
51
+ result = _check_tool_id(tool_id)
52
+ assert result == tool_id
53
+
54
+ def test_valid_mcp_local_tools(self):
55
+ """Test validation of valid MCP local tools."""
56
+ valid_ids = [
57
+ "mcp::local::server1::tool1",
58
+ "mcp::local::my_server::my_tool",
59
+ "mcp::local::test::function_name",
60
+ ]
61
+ for tool_id in valid_ids:
62
+ result = _check_tool_id(tool_id)
63
+ assert result == tool_id
64
+
65
+ def test_invalid_empty_or_none(self):
66
+ """Test validation fails for empty or None values."""
67
+ with pytest.raises(ValueError, match="Invalid tool ID"):
68
+ _check_tool_id("")
69
+
70
+ with pytest.raises(ValueError, match="Invalid tool ID"):
71
+ _check_tool_id(None) # type: ignore
72
+
73
+ def test_invalid_non_string(self):
74
+ """Test validation fails for non-string values."""
75
+ with pytest.raises(ValueError, match="Invalid tool ID"):
76
+ _check_tool_id(123) # type: ignore
77
+
78
+ with pytest.raises(ValueError, match="Invalid tool ID"):
79
+ _check_tool_id(["tool"]) # type: ignore
80
+
81
+ def test_invalid_unknown_tool(self):
82
+ """Test validation fails for unknown tool IDs."""
83
+ with pytest.raises(ValueError, match="Invalid tool ID: unknown_tool"):
84
+ _check_tool_id("unknown_tool")
85
+
86
+ def test_invalid_mcp_format(self):
87
+ """Test validation fails for invalid MCP tool formats."""
88
+ # These IDs start with the MCP remote prefix but have invalid formats
89
+ mcp_remote_invalid_ids = [
90
+ "mcp::remote::", # Missing server and tool
91
+ "mcp::remote::server", # Missing tool
92
+ "mcp::remote::server::", # Empty tool name
93
+ "mcp::remote::::tool", # Empty server name
94
+ "mcp::remote::server::tool::extra", # Too many parts
95
+ ]
96
+
97
+ for invalid_id in mcp_remote_invalid_ids:
98
+ with pytest.raises(ValueError, match="Invalid remote MCP tool ID"):
99
+ _check_tool_id(invalid_id)
100
+
101
+ # These IDs start with the MCP local prefix but have invalid formats
102
+ mcp_local_invalid_ids = [
103
+ "mcp::local::", # Missing server and tool
104
+ "mcp::local::server", # Missing tool
105
+ "mcp::local::server::", # Empty tool name
106
+ "mcp::local::::tool", # Empty server name
107
+ "mcp::local::server::tool::extra", # Too many parts
108
+ ]
109
+
110
+ for invalid_id in mcp_local_invalid_ids:
111
+ with pytest.raises(ValueError, match="Invalid local MCP tool ID"):
112
+ _check_tool_id(invalid_id)
113
+
114
+ # This ID doesn't start with MCP prefix so gets generic error
115
+ with pytest.raises(ValueError, match="Invalid tool ID"):
116
+ _check_tool_id("mcp::wrong::server::tool")
117
+
118
+ def test_valid_rag_tools(self):
119
+ """Test validation of valid RAG tools."""
120
+ valid_ids = [
121
+ "kiln_tool::rag::config1",
122
+ "kiln_tool::rag::my_rag_config",
123
+ "kiln_tool::rag::test_config_123",
124
+ ]
125
+ for tool_id in valid_ids:
126
+ result = _check_tool_id(tool_id)
127
+ assert result == tool_id
128
+
129
+ def test_invalid_rag_format(self):
130
+ """Test validation fails for invalid RAG tool formats."""
131
+ # These IDs start with the RAG prefix but have invalid formats
132
+ rag_invalid_ids = [
133
+ "kiln_tool::rag::", # Missing config ID
134
+ "kiln_tool::rag::config::extra", # Too many parts
135
+ ]
136
+
137
+ for invalid_id in rag_invalid_ids:
138
+ with pytest.raises(ValueError, match="Invalid RAG tool ID"):
139
+ _check_tool_id(invalid_id)
140
+
141
+ def test_rag_tool_empty_config_id(self):
142
+ """Test that RAG tool with empty config ID is handled properly."""
143
+ # This tests the case where rag_config_id_from_id returns empty string
144
+ # which should trigger line 66 in the source
145
+ with pytest.raises(ValueError, match="Invalid RAG tool ID"):
146
+ _check_tool_id("kiln_tool::rag::")
147
+
148
+
149
+ class TestMcpServerAndToolNameFromId:
150
+ """Test the mcp_server_and_tool_name_from_id function."""
151
+
152
+ def test_valid_mcp_ids(self):
153
+ """Test parsing valid MCP tool IDs."""
154
+ test_cases = [
155
+ # Remote MCP tools
156
+ ("mcp::remote::server1::tool1", ("server1", "tool1")),
157
+ ("mcp::remote::my_server::my_tool", ("my_server", "my_tool")),
158
+ ("mcp::remote::test::function_name", ("test", "function_name")),
159
+ # Local MCP tools
160
+ ("mcp::local::server1::tool1", ("server1", "tool1")),
161
+ ("mcp::local::my_server::my_tool", ("my_server", "my_tool")),
162
+ ("mcp::local::test::function_name", ("test", "function_name")),
163
+ ]
164
+
165
+ for tool_id, expected in test_cases:
166
+ result = mcp_server_and_tool_name_from_id(tool_id)
167
+ assert result == expected
168
+
169
+ def test_invalid_mcp_ids(self):
170
+ """Test parsing fails for invalid MCP tool IDs."""
171
+ # Test remote MCP tool ID errors
172
+ remote_invalid_ids = [
173
+ "mcp::remote::", # Only 3 parts
174
+ "mcp::remote::server", # Only 3 parts
175
+ "mcp::remote::server::tool::extra", # 5 parts
176
+ ]
177
+
178
+ for invalid_id in remote_invalid_ids:
179
+ with pytest.raises(ValueError, match="Invalid remote MCP tool ID"):
180
+ mcp_server_and_tool_name_from_id(invalid_id)
181
+
182
+ # Test local MCP tool ID errors
183
+ local_invalid_ids = [
184
+ "mcp::local::", # Only 3 parts
185
+ "mcp::local::server", # Only 3 parts
186
+ "mcp::local::server::tool::extra", # 5 parts
187
+ ]
188
+
189
+ for invalid_id in local_invalid_ids:
190
+ with pytest.raises(ValueError, match="Invalid local MCP tool ID"):
191
+ mcp_server_and_tool_name_from_id(invalid_id)
192
+
193
+ # Test generic MCP tool ID errors (not remote or local)
194
+ generic_invalid_ids = [
195
+ "not_mcp_format", # Only 1 part
196
+ "single_part", # Only 1 part
197
+ "", # Empty string
198
+ ]
199
+
200
+ for invalid_id in generic_invalid_ids:
201
+ with pytest.raises(ValueError, match="Invalid MCP tool ID"):
202
+ mcp_server_and_tool_name_from_id(invalid_id)
203
+
204
+ def test_mcp_ids_with_wrong_prefix_still_parse(self):
205
+ """Test that IDs with wrong prefix but correct structure still parse (validation happens elsewhere)."""
206
+ # This function only checks structure (4 parts), not content
207
+ result = mcp_server_and_tool_name_from_id("mcp::wrong::server::tool")
208
+ assert result == ("server", "tool")
209
+
210
+
211
+ class TestToolIdPydanticType:
212
+ """Test the ToolId pydantic type annotation."""
213
+
214
+ class _ModelWithToolId(BaseModel):
215
+ tool_id: ToolId
216
+
217
+ def test_valid_builtin_tools(self):
218
+ """Test ToolId validates built-in tools."""
219
+ for tool_id in KilnBuiltInToolId:
220
+ model = self._ModelWithToolId(tool_id=tool_id.value)
221
+ assert model.tool_id == tool_id.value
222
+
223
+ def test_valid_mcp_tools(self):
224
+ """Test ToolId validates MCP remote and local tools."""
225
+ valid_ids = [
226
+ # Remote MCP tools
227
+ "mcp::remote::server1::tool1",
228
+ "mcp::remote::my_server::my_tool",
229
+ # Local MCP tools
230
+ "mcp::local::server1::tool1",
231
+ "mcp::local::my_server::my_tool",
232
+ # RAG tools
233
+ "kiln_tool::rag::config1",
234
+ "kiln_tool::rag::my_rag_config",
235
+ ]
236
+
237
+ for tool_id in valid_ids:
238
+ model = self._ModelWithToolId(tool_id=tool_id)
239
+ assert model.tool_id == tool_id
240
+
241
+ def test_invalid_tools_raise_validation_error(self):
242
+ """Test ToolId raises ValidationError for invalid tools."""
243
+ invalid_ids = [
244
+ "",
245
+ "unknown_tool",
246
+ "mcp::remote::",
247
+ "mcp::remote::server",
248
+ "mcp::local::",
249
+ "mcp::local::server",
250
+ "kiln_tool::rag::",
251
+ "kiln_tool::rag::config::extra",
252
+ ]
253
+
254
+ for invalid_id in invalid_ids:
255
+ with pytest.raises(ValidationError):
256
+ self._ModelWithToolId(tool_id=invalid_id)
257
+
258
+ def test_non_string_raises_validation_error(self):
259
+ """Test ToolId raises ValidationError for non-string values."""
260
+ with pytest.raises(ValidationError):
261
+ self._ModelWithToolId(tool_id=123) # type: ignore
262
+
263
+ with pytest.raises(ValidationError):
264
+ self._ModelWithToolId(tool_id=None) # type: ignore
265
+
266
+
267
+ class TestConstants:
268
+ """Test module constants."""
269
+
270
+ def test_mcp_remote_tool_id_prefix(self):
271
+ """Test the MCP remote tool ID prefix constant."""
272
+ assert MCP_REMOTE_TOOL_ID_PREFIX == "mcp::remote::"
273
+
274
+ def test_mcp_local_tool_id_prefix(self):
275
+ """Test the MCP local tool ID prefix constant."""
276
+ assert MCP_LOCAL_TOOL_ID_PREFIX == "mcp::local::"
277
+
278
+ def test_rag_tool_id_prefix(self):
279
+ """Test the RAG tool ID prefix constant."""
280
+ assert RAG_TOOL_ID_PREFIX == "kiln_tool::rag::"
281
+
282
+
283
+ class TestRagConfigIdFromId:
284
+ """Test the rag_config_id_from_id function."""
285
+
286
+ def test_valid_rag_ids(self):
287
+ """Test parsing valid RAG tool IDs."""
288
+ test_cases = [
289
+ ("kiln_tool::rag::config1", "config1"),
290
+ ("kiln_tool::rag::my_rag_config", "my_rag_config"),
291
+ ("kiln_tool::rag::test_config_123", "test_config_123"),
292
+ ("kiln_tool::rag::a", "a"), # Minimal valid case
293
+ ]
294
+
295
+ for tool_id, expected in test_cases:
296
+ result = rag_config_id_from_id(tool_id)
297
+ assert result == expected
298
+
299
+ def test_invalid_rag_ids(self):
300
+ """Test parsing fails for invalid RAG tool IDs."""
301
+ # Test various invalid formats that should trigger line 104
302
+ invalid_ids = [
303
+ "kiln_tool::rag::config::extra", # Too many parts (4 parts)
304
+ "wrong::rag::config", # Wrong prefix
305
+ "kiln_tool::wrong::config", # Wrong middle part
306
+ "rag::config", # Too few parts (2 parts)
307
+ "", # Empty string
308
+ "single_part", # Only 1 part
309
+ ]
310
+
311
+ for invalid_id in invalid_ids:
312
+ with pytest.raises(ValueError, match="Invalid RAG tool ID"):
313
+ rag_config_id_from_id(invalid_id)
314
+
315
+ def test_rag_id_with_empty_config_id(self):
316
+ """Test that RAG tool ID with empty config ID returns empty string."""
317
+ # This is actually valid according to the parser - it returns empty string
318
+ # The validation for empty config ID happens in _check_tool_id
319
+ result = rag_config_id_from_id("kiln_tool::rag::")
320
+ assert result == ""