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.
- kiln_ai/adapters/__init__.py +6 -0
- kiln_ai/adapters/adapter_registry.py +43 -226
- 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/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/eval_runner.py +6 -2
- kiln_ai/adapters/eval/test_base_eval.py +1 -3
- kiln_ai/adapters/eval/test_g_eval.py +1 -1
- 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 +406 -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 +1290 -0
- 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_together_finetune.py +2 -6
- kiln_ai/adapters/ml_embedding_model_list.py +494 -0
- kiln_ai/adapters/ml_model_list.py +876 -18
- kiln_ai/adapters/model_adapters/litellm_adapter.py +40 -75
- kiln_ai/adapters/model_adapters/test_litellm_adapter.py +79 -1
- kiln_ai/adapters/model_adapters/test_litellm_adapter_tools.py +119 -5
- kiln_ai/adapters/model_adapters/test_saving_adapter_results.py +9 -3
- kiln_ai/adapters/model_adapters/test_structured_output.py +9 -10
- kiln_ai/adapters/ollama_tools.py +69 -12
- kiln_ai/adapters/provider_tools.py +190 -46
- 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/test_adapter_registry.py +579 -86
- kiln_ai/adapters/test_ml_embedding_model_list.py +239 -0
- kiln_ai/adapters/test_ml_model_list.py +202 -0
- kiln_ai/adapters/test_ollama_tools.py +340 -1
- kiln_ai/adapters/test_prompt_builders.py +1 -1
- kiln_ai/adapters/test_provider_tools.py +199 -8
- kiln_ai/adapters/test_remote_config.py +551 -56
- 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 +16 -13
- kiln_ai/datamodel/basemodel.py +201 -4
- kiln_ai/datamodel/chunk.py +158 -0
- kiln_ai/datamodel/datamodel_enums.py +27 -0
- kiln_ai/datamodel/embedding.py +64 -0
- kiln_ai/datamodel/external_tool_server.py +206 -54
- kiln_ai/datamodel/extraction.py +317 -0
- kiln_ai/datamodel/project.py +33 -1
- kiln_ai/datamodel/rag.py +79 -0
- kiln_ai/datamodel/task.py +5 -0
- kiln_ai/datamodel/task_output.py +41 -11
- kiln_ai/datamodel/test_attachment.py +649 -0
- kiln_ai/datamodel/test_basemodel.py +270 -14
- kiln_ai/datamodel/test_chunk_models.py +317 -0
- kiln_ai/datamodel/test_dataset_split.py +1 -1
- kiln_ai/datamodel/test_datasource.py +50 -0
- kiln_ai/datamodel/test_embedding_models.py +448 -0
- kiln_ai/datamodel/test_eval_model.py +6 -6
- kiln_ai/datamodel/test_external_tool_server.py +534 -152
- kiln_ai/datamodel/test_extraction_chunk.py +206 -0
- kiln_ai/datamodel/test_extraction_model.py +501 -0
- kiln_ai/datamodel/test_rag.py +641 -0
- kiln_ai/datamodel/test_task.py +35 -1
- kiln_ai/datamodel/test_tool_id.py +187 -1
- kiln_ai/datamodel/test_vector_store.py +320 -0
- kiln_ai/datamodel/tool_id.py +58 -0
- kiln_ai/datamodel/vector_store.py +141 -0
- kiln_ai/tools/base_tool.py +12 -3
- kiln_ai/tools/built_in_tools/math_tools.py +12 -4
- kiln_ai/tools/kiln_task_tool.py +158 -0
- kiln_ai/tools/mcp_server_tool.py +2 -2
- kiln_ai/tools/mcp_session_manager.py +51 -22
- kiln_ai/tools/rag_tools.py +164 -0
- kiln_ai/tools/test_kiln_task_tool.py +527 -0
- kiln_ai/tools/test_mcp_server_tool.py +4 -15
- kiln_ai/tools/test_mcp_session_manager.py +187 -227
- kiln_ai/tools/test_rag_tools.py +929 -0
- kiln_ai/tools/test_tool_registry.py +290 -7
- kiln_ai/tools/tool_registry.py +69 -16
- kiln_ai/utils/__init__.py +3 -0
- kiln_ai/utils/async_job_runner.py +62 -17
- kiln_ai/utils/config.py +2 -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 +19 -2
- kiln_ai/utils/pdf_utils.py +59 -0
- kiln_ai/utils/test_async_job_runner.py +151 -35
- 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 +88 -12
- kiln_ai/utils/test_pdf_utils.py +86 -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.20.1.dist-info → kiln_ai-0.22.0.dist-info}/METADATA +9 -1
- kiln_ai-0.22.0.dist-info/RECORD +213 -0
- kiln_ai-0.20.1.dist-info/RECORD +0 -138
- {kiln_ai-0.20.1.dist-info → kiln_ai-0.22.0.dist-info}/WHEEL +0 -0
- {kiln_ai-0.20.1.dist-info → kiln_ai-0.22.0.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from kiln_ai.datamodel import Task
|
|
6
|
+
from kiln_ai.datamodel.datamodel_enums import ModelProviderName, StructuredOutputMode
|
|
7
|
+
from kiln_ai.datamodel.external_tool_server import ExternalToolServer, ToolServerType
|
|
8
|
+
from kiln_ai.datamodel.run_config import RunConfigProperties
|
|
9
|
+
from kiln_ai.datamodel.task import TaskRunConfig
|
|
10
|
+
from kiln_ai.datamodel.task_output import DataSource, DataSourceType
|
|
11
|
+
from kiln_ai.tools.base_tool import ToolCallContext
|
|
12
|
+
from kiln_ai.tools.kiln_task_tool import KilnTaskTool, KilnTaskToolResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestKilnTaskToolResult:
|
|
16
|
+
"""Test the KilnTaskToolResult class."""
|
|
17
|
+
|
|
18
|
+
def test_init(self):
|
|
19
|
+
"""Test KilnTaskToolResult initialization."""
|
|
20
|
+
output = "test output"
|
|
21
|
+
kiln_task_tool_data = "project_id:::tool_id:::task_id:::run_id"
|
|
22
|
+
|
|
23
|
+
result = KilnTaskToolResult(output, kiln_task_tool_data)
|
|
24
|
+
|
|
25
|
+
assert result.output == output
|
|
26
|
+
assert result.kiln_task_tool_data == kiln_task_tool_data
|
|
27
|
+
|
|
28
|
+
def test_init_with_empty_strings(self):
|
|
29
|
+
"""Test KilnTaskToolResult initialization with empty strings."""
|
|
30
|
+
result = KilnTaskToolResult("", "")
|
|
31
|
+
|
|
32
|
+
assert result.output == ""
|
|
33
|
+
assert result.kiln_task_tool_data == ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestKilnTaskTool:
|
|
37
|
+
"""Test the KilnTaskTool class."""
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def mock_external_tool_server(self):
|
|
41
|
+
"""Create a mock ExternalToolServer for testing."""
|
|
42
|
+
return ExternalToolServer(
|
|
43
|
+
name="test_tool",
|
|
44
|
+
type=ToolServerType.kiln_task,
|
|
45
|
+
description="Test Kiln task tool",
|
|
46
|
+
properties={
|
|
47
|
+
"name": "test_task_tool",
|
|
48
|
+
"description": "A test task tool",
|
|
49
|
+
"task_id": "test_task_123",
|
|
50
|
+
"run_config_id": "test_config_456",
|
|
51
|
+
"is_archived": False,
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@pytest.fixture
|
|
56
|
+
def mock_task(self):
|
|
57
|
+
"""Create a mock Task for testing."""
|
|
58
|
+
task = MagicMock(spec=Task)
|
|
59
|
+
task.id = "test_task_123"
|
|
60
|
+
task.input_json_schema = None
|
|
61
|
+
task.input_schema.return_value = None
|
|
62
|
+
task.run_configs.return_value = []
|
|
63
|
+
return task
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def mock_run_config(self):
|
|
67
|
+
"""Create a mock TaskRunConfig for testing."""
|
|
68
|
+
run_config = MagicMock(spec=TaskRunConfig)
|
|
69
|
+
run_config.id = "test_config_456"
|
|
70
|
+
run_config.run_config_properties = {
|
|
71
|
+
"model_name": "gpt-4",
|
|
72
|
+
"model_provider_name": "openai",
|
|
73
|
+
"prompt_id": "simple_prompt_builder",
|
|
74
|
+
"structured_output_mode": "default",
|
|
75
|
+
}
|
|
76
|
+
return run_config
|
|
77
|
+
|
|
78
|
+
@pytest.fixture
|
|
79
|
+
def mock_context(self):
|
|
80
|
+
"""Create a mock ToolCallContext for testing."""
|
|
81
|
+
context = MagicMock(spec=ToolCallContext)
|
|
82
|
+
context.allow_saving = True
|
|
83
|
+
return context
|
|
84
|
+
|
|
85
|
+
@pytest.fixture
|
|
86
|
+
def kiln_task_tool(self, mock_external_tool_server):
|
|
87
|
+
"""Create a KilnTaskTool instance for testing."""
|
|
88
|
+
return KilnTaskTool(
|
|
89
|
+
project_id="test_project",
|
|
90
|
+
tool_id="test_tool_id",
|
|
91
|
+
data_model=mock_external_tool_server,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@pytest.mark.asyncio
|
|
95
|
+
async def test_init(self, mock_external_tool_server):
|
|
96
|
+
"""Test KilnTaskTool initialization."""
|
|
97
|
+
tool = KilnTaskTool(
|
|
98
|
+
project_id="test_project",
|
|
99
|
+
tool_id="test_tool_id",
|
|
100
|
+
data_model=mock_external_tool_server,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
assert tool._project_id == "test_project"
|
|
104
|
+
assert tool._tool_id == "test_tool_id"
|
|
105
|
+
assert tool._tool_server_model == mock_external_tool_server
|
|
106
|
+
assert tool._name == "test_task_tool"
|
|
107
|
+
assert tool._description == "A test task tool"
|
|
108
|
+
assert tool._task_id == "test_task_123"
|
|
109
|
+
assert tool._run_config_id == "test_config_456"
|
|
110
|
+
|
|
111
|
+
@pytest.mark.asyncio
|
|
112
|
+
async def test_init_with_missing_properties(self):
|
|
113
|
+
"""Test KilnTaskTool initialization with missing properties."""
|
|
114
|
+
# Create a server with minimal required properties
|
|
115
|
+
server = ExternalToolServer(
|
|
116
|
+
name="test_tool",
|
|
117
|
+
type=ToolServerType.kiln_task,
|
|
118
|
+
description="Test tool",
|
|
119
|
+
properties={
|
|
120
|
+
"name": "minimal_tool",
|
|
121
|
+
"description": "",
|
|
122
|
+
"task_id": "",
|
|
123
|
+
"run_config_id": "",
|
|
124
|
+
"is_archived": False,
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
tool = KilnTaskTool(
|
|
129
|
+
project_id="test_project",
|
|
130
|
+
tool_id="test_tool_id",
|
|
131
|
+
data_model=server,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
assert tool._name == "minimal_tool"
|
|
135
|
+
assert tool._description == ""
|
|
136
|
+
assert tool._task_id == ""
|
|
137
|
+
assert tool._run_config_id == ""
|
|
138
|
+
|
|
139
|
+
@pytest.mark.asyncio
|
|
140
|
+
async def test_id(self, kiln_task_tool):
|
|
141
|
+
"""Test the id method."""
|
|
142
|
+
result = await kiln_task_tool.id()
|
|
143
|
+
assert result == "test_tool_id"
|
|
144
|
+
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_name(self, kiln_task_tool):
|
|
147
|
+
"""Test the name method."""
|
|
148
|
+
result = await kiln_task_tool.name()
|
|
149
|
+
assert result == "test_task_tool"
|
|
150
|
+
|
|
151
|
+
@pytest.mark.asyncio
|
|
152
|
+
async def test_description(self, kiln_task_tool):
|
|
153
|
+
"""Test the description method."""
|
|
154
|
+
result = await kiln_task_tool.description()
|
|
155
|
+
assert result == "A test task tool"
|
|
156
|
+
|
|
157
|
+
@pytest.mark.asyncio
|
|
158
|
+
async def test_toolcall_definition(self, kiln_task_tool):
|
|
159
|
+
"""Test the toolcall_definition method."""
|
|
160
|
+
# Mock the parameters_schema property directly
|
|
161
|
+
kiln_task_tool.parameters_schema = {"type": "object"}
|
|
162
|
+
|
|
163
|
+
definition = await kiln_task_tool.toolcall_definition()
|
|
164
|
+
|
|
165
|
+
assert definition["type"] == "function"
|
|
166
|
+
assert definition["function"]["name"] == "test_task_tool"
|
|
167
|
+
assert definition["function"]["description"] == "A test task tool"
|
|
168
|
+
assert definition["function"]["parameters"] == {"type": "object"}
|
|
169
|
+
|
|
170
|
+
@pytest.mark.asyncio
|
|
171
|
+
async def test_run_with_plaintext_input(
|
|
172
|
+
self, kiln_task_tool, mock_context, mock_task, mock_run_config
|
|
173
|
+
):
|
|
174
|
+
"""Test the run method with plaintext input."""
|
|
175
|
+
# Setup mocks
|
|
176
|
+
kiln_task_tool._task = mock_task
|
|
177
|
+
kiln_task_tool._run_config = mock_run_config
|
|
178
|
+
|
|
179
|
+
with (
|
|
180
|
+
patch(
|
|
181
|
+
"kiln_ai.adapters.adapter_registry.adapter_for_task"
|
|
182
|
+
) as mock_adapter_for_task,
|
|
183
|
+
patch(
|
|
184
|
+
"kiln_ai.adapters.model_adapters.base_adapter.AdapterConfig"
|
|
185
|
+
) as mock_adapter_config,
|
|
186
|
+
):
|
|
187
|
+
# Mock adapter and task run
|
|
188
|
+
mock_adapter = AsyncMock()
|
|
189
|
+
mock_adapter_for_task.return_value = mock_adapter
|
|
190
|
+
|
|
191
|
+
mock_task_run = MagicMock()
|
|
192
|
+
mock_task_run.id = "run_789"
|
|
193
|
+
mock_task_run.output.output = "Task completed successfully"
|
|
194
|
+
mock_adapter.invoke.return_value = mock_task_run
|
|
195
|
+
|
|
196
|
+
# Test with plaintext input
|
|
197
|
+
result = await kiln_task_tool.run(context=mock_context, input="test input")
|
|
198
|
+
|
|
199
|
+
# Verify adapter was created correctly
|
|
200
|
+
mock_adapter_for_task.assert_called_once_with(
|
|
201
|
+
mock_task,
|
|
202
|
+
run_config_properties={
|
|
203
|
+
"model_name": "gpt-4",
|
|
204
|
+
"model_provider_name": "openai",
|
|
205
|
+
"prompt_id": "simple_prompt_builder",
|
|
206
|
+
"structured_output_mode": "default",
|
|
207
|
+
},
|
|
208
|
+
base_adapter_config=mock_adapter_config.return_value,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Verify adapter config
|
|
212
|
+
mock_adapter_config.assert_called_once_with(
|
|
213
|
+
allow_saving=True,
|
|
214
|
+
default_tags=["tool_call"],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Verify adapter invoke was called
|
|
218
|
+
mock_adapter.invoke.assert_called_once_with(
|
|
219
|
+
"test input",
|
|
220
|
+
input_source=DataSource(
|
|
221
|
+
type=DataSourceType.tool_call,
|
|
222
|
+
run_config=RunConfigProperties(
|
|
223
|
+
model_name="gpt-4",
|
|
224
|
+
model_provider_name=ModelProviderName.openai,
|
|
225
|
+
prompt_id="simple_prompt_builder",
|
|
226
|
+
structured_output_mode=StructuredOutputMode.default,
|
|
227
|
+
),
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Verify result
|
|
232
|
+
assert isinstance(result, KilnTaskToolResult)
|
|
233
|
+
assert result.output == "Task completed successfully"
|
|
234
|
+
assert (
|
|
235
|
+
result.kiln_task_tool_data
|
|
236
|
+
== "test_project:::test_tool_id:::test_task_123:::run_789"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
@pytest.mark.asyncio
|
|
240
|
+
async def test_run_with_structured_input(
|
|
241
|
+
self, kiln_task_tool, mock_context, mock_task, mock_run_config
|
|
242
|
+
):
|
|
243
|
+
"""Test the run method with structured input."""
|
|
244
|
+
# Setup task with JSON schema
|
|
245
|
+
mock_task.input_json_schema = {
|
|
246
|
+
"type": "object",
|
|
247
|
+
"properties": {"param1": {"type": "string"}},
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Setup mocks
|
|
251
|
+
kiln_task_tool._task = mock_task
|
|
252
|
+
kiln_task_tool._run_config = mock_run_config
|
|
253
|
+
|
|
254
|
+
with patch(
|
|
255
|
+
"kiln_ai.adapters.adapter_registry.adapter_for_task"
|
|
256
|
+
) as mock_adapter_for_task:
|
|
257
|
+
# Mock adapter and task run
|
|
258
|
+
mock_adapter = AsyncMock()
|
|
259
|
+
mock_adapter_for_task.return_value = mock_adapter
|
|
260
|
+
|
|
261
|
+
mock_task_run = MagicMock()
|
|
262
|
+
mock_task_run.id = "run_789"
|
|
263
|
+
mock_task_run.output.output = "Structured task completed"
|
|
264
|
+
mock_adapter.invoke.return_value = mock_task_run
|
|
265
|
+
|
|
266
|
+
# Test with structured input
|
|
267
|
+
result = await kiln_task_tool.run(
|
|
268
|
+
context=mock_context, param1="value1", param2="value2"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Verify adapter invoke was called with kwargs
|
|
272
|
+
mock_adapter.invoke.assert_called_once_with(
|
|
273
|
+
{"param1": "value1", "param2": "value2"},
|
|
274
|
+
input_source=DataSource(
|
|
275
|
+
type=DataSourceType.tool_call,
|
|
276
|
+
run_config=RunConfigProperties(
|
|
277
|
+
model_name="gpt-4",
|
|
278
|
+
model_provider_name=ModelProviderName.openai,
|
|
279
|
+
prompt_id="simple_prompt_builder",
|
|
280
|
+
structured_output_mode=StructuredOutputMode.default,
|
|
281
|
+
),
|
|
282
|
+
),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Verify result
|
|
286
|
+
assert result.output == "Structured task completed"
|
|
287
|
+
|
|
288
|
+
@pytest.mark.asyncio
|
|
289
|
+
async def test_run_without_context(self, kiln_task_tool):
|
|
290
|
+
"""Test the run method without context raises ValueError."""
|
|
291
|
+
with pytest.raises(
|
|
292
|
+
ValueError, match="Context is required for running a KilnTaskTool"
|
|
293
|
+
):
|
|
294
|
+
await kiln_task_tool.run(input="test input")
|
|
295
|
+
|
|
296
|
+
@pytest.mark.asyncio
|
|
297
|
+
async def test_run_plaintext_missing_input(
|
|
298
|
+
self, kiln_task_tool, mock_context, mock_task
|
|
299
|
+
):
|
|
300
|
+
"""Test the run method with plaintext task but missing input parameter."""
|
|
301
|
+
# Setup mocks
|
|
302
|
+
kiln_task_tool._task = mock_task
|
|
303
|
+
|
|
304
|
+
with pytest.raises(ValueError, match="Input not found in kwargs"):
|
|
305
|
+
await kiln_task_tool.run(context=mock_context, wrong_param="value")
|
|
306
|
+
|
|
307
|
+
@pytest.mark.asyncio
|
|
308
|
+
async def test_task_property_project_not_found(self, kiln_task_tool):
|
|
309
|
+
"""Test _task property when project is not found."""
|
|
310
|
+
with patch("kiln_ai.tools.kiln_task_tool.project_from_id", return_value=None):
|
|
311
|
+
with pytest.raises(ValueError, match="Project not found: test_project"):
|
|
312
|
+
_ = kiln_task_tool._task
|
|
313
|
+
|
|
314
|
+
@pytest.mark.asyncio
|
|
315
|
+
async def test_task_property_task_not_found(self, kiln_task_tool):
|
|
316
|
+
"""Test _task property when task is not found."""
|
|
317
|
+
mock_project = MagicMock()
|
|
318
|
+
mock_project.path = "/test/path"
|
|
319
|
+
|
|
320
|
+
with (
|
|
321
|
+
patch(
|
|
322
|
+
"kiln_ai.tools.kiln_task_tool.project_from_id",
|
|
323
|
+
return_value=mock_project,
|
|
324
|
+
),
|
|
325
|
+
patch(
|
|
326
|
+
"kiln_ai.tools.kiln_task_tool.Task.from_id_and_parent_path",
|
|
327
|
+
return_value=None,
|
|
328
|
+
),
|
|
329
|
+
):
|
|
330
|
+
with pytest.raises(
|
|
331
|
+
ValueError,
|
|
332
|
+
match="Task not found: test_task_123 in project test_project",
|
|
333
|
+
):
|
|
334
|
+
_ = kiln_task_tool._task
|
|
335
|
+
|
|
336
|
+
@pytest.mark.asyncio
|
|
337
|
+
async def test_task_property_success(self, kiln_task_tool, mock_task):
|
|
338
|
+
"""Test _task property when task is found successfully."""
|
|
339
|
+
mock_project = MagicMock()
|
|
340
|
+
mock_project.path = "/test/path"
|
|
341
|
+
|
|
342
|
+
with (
|
|
343
|
+
patch(
|
|
344
|
+
"kiln_ai.tools.kiln_task_tool.project_from_id",
|
|
345
|
+
return_value=mock_project,
|
|
346
|
+
),
|
|
347
|
+
patch(
|
|
348
|
+
"kiln_ai.tools.kiln_task_tool.Task.from_id_and_parent_path",
|
|
349
|
+
return_value=mock_task,
|
|
350
|
+
),
|
|
351
|
+
):
|
|
352
|
+
result = kiln_task_tool._task
|
|
353
|
+
assert result == mock_task
|
|
354
|
+
|
|
355
|
+
@pytest.mark.asyncio
|
|
356
|
+
async def test_run_config_property_not_found(self, kiln_task_tool, mock_task):
|
|
357
|
+
"""Test _run_config property when run config is not found."""
|
|
358
|
+
mock_task.run_configs.return_value = []
|
|
359
|
+
|
|
360
|
+
# Setup mocks
|
|
361
|
+
kiln_task_tool._task = mock_task
|
|
362
|
+
|
|
363
|
+
with pytest.raises(
|
|
364
|
+
ValueError,
|
|
365
|
+
match="Task run config not found: test_config_456 for task test_task_123 in project test_project",
|
|
366
|
+
):
|
|
367
|
+
_ = kiln_task_tool._run_config
|
|
368
|
+
|
|
369
|
+
@pytest.mark.asyncio
|
|
370
|
+
async def test_run_config_property_success(
|
|
371
|
+
self, kiln_task_tool, mock_task, mock_run_config
|
|
372
|
+
):
|
|
373
|
+
"""Test _run_config property when run config is found successfully."""
|
|
374
|
+
mock_task.run_configs.return_value = [mock_run_config]
|
|
375
|
+
|
|
376
|
+
# Setup mocks
|
|
377
|
+
kiln_task_tool._task = mock_task
|
|
378
|
+
|
|
379
|
+
result = kiln_task_tool._run_config
|
|
380
|
+
assert result == mock_run_config
|
|
381
|
+
|
|
382
|
+
@pytest.mark.asyncio
|
|
383
|
+
async def test_parameters_schema_with_json_schema(self, kiln_task_tool, mock_task):
|
|
384
|
+
"""Test parameters_schema property with JSON schema."""
|
|
385
|
+
expected_schema = {
|
|
386
|
+
"type": "object",
|
|
387
|
+
"properties": {"param": {"type": "string"}},
|
|
388
|
+
}
|
|
389
|
+
mock_task.input_json_schema = expected_schema
|
|
390
|
+
mock_task.input_schema.return_value = expected_schema
|
|
391
|
+
|
|
392
|
+
# Setup mocks
|
|
393
|
+
kiln_task_tool._task = mock_task
|
|
394
|
+
|
|
395
|
+
result = kiln_task_tool.parameters_schema
|
|
396
|
+
assert result == expected_schema
|
|
397
|
+
|
|
398
|
+
@pytest.mark.asyncio
|
|
399
|
+
async def test_parameters_schema_plaintext(self, kiln_task_tool, mock_task):
|
|
400
|
+
"""Test parameters_schema property for plaintext task."""
|
|
401
|
+
mock_task.input_json_schema = None
|
|
402
|
+
|
|
403
|
+
# Setup mocks
|
|
404
|
+
kiln_task_tool._task = mock_task
|
|
405
|
+
|
|
406
|
+
result = kiln_task_tool.parameters_schema
|
|
407
|
+
|
|
408
|
+
expected = {
|
|
409
|
+
"type": "object",
|
|
410
|
+
"properties": {
|
|
411
|
+
"input": {
|
|
412
|
+
"type": "string",
|
|
413
|
+
"description": "Plaintext input for the tool.",
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
"required": ["input"],
|
|
417
|
+
}
|
|
418
|
+
assert result == expected
|
|
419
|
+
|
|
420
|
+
@pytest.mark.asyncio
|
|
421
|
+
async def test_parameters_schema_none_raises_error(self, kiln_task_tool, mock_task):
|
|
422
|
+
"""Test parameters_schema property when schema is None raises ValueError."""
|
|
423
|
+
# Set up a task with JSON schema but input_schema returns None
|
|
424
|
+
mock_task.input_json_schema = {
|
|
425
|
+
"type": "object",
|
|
426
|
+
"properties": {"param": {"type": "string"}},
|
|
427
|
+
}
|
|
428
|
+
mock_task.input_schema.return_value = None
|
|
429
|
+
|
|
430
|
+
# Setup mocks - directly assign the task to bypass cached property
|
|
431
|
+
kiln_task_tool._task = mock_task
|
|
432
|
+
|
|
433
|
+
with pytest.raises(
|
|
434
|
+
ValueError,
|
|
435
|
+
match="Failed to create parameters schema for tool_id test_tool_id",
|
|
436
|
+
):
|
|
437
|
+
_ = kiln_task_tool.parameters_schema
|
|
438
|
+
|
|
439
|
+
@pytest.mark.asyncio
|
|
440
|
+
async def test_cached_properties(self, kiln_task_tool, mock_task, mock_run_config):
|
|
441
|
+
"""Test that cached properties work correctly."""
|
|
442
|
+
mock_project = MagicMock()
|
|
443
|
+
mock_project.path = "/test/path"
|
|
444
|
+
|
|
445
|
+
with (
|
|
446
|
+
patch(
|
|
447
|
+
"kiln_ai.tools.kiln_task_tool.project_from_id",
|
|
448
|
+
return_value=mock_project,
|
|
449
|
+
),
|
|
450
|
+
patch(
|
|
451
|
+
"kiln_ai.tools.kiln_task_tool.Task.from_id_and_parent_path",
|
|
452
|
+
return_value=mock_task,
|
|
453
|
+
),
|
|
454
|
+
):
|
|
455
|
+
# First access should call the methods
|
|
456
|
+
task1 = kiln_task_tool._task
|
|
457
|
+
task2 = kiln_task_tool._task
|
|
458
|
+
|
|
459
|
+
# Should be the same object (cached)
|
|
460
|
+
assert task1 is task2
|
|
461
|
+
|
|
462
|
+
# Verify the methods were called only once
|
|
463
|
+
assert mock_project is not None # project_from_id was called
|
|
464
|
+
# Task.from_id_and_parent_path should have been called once
|
|
465
|
+
with patch(
|
|
466
|
+
"kiln_ai.tools.kiln_task_tool.Task.from_id_and_parent_path"
|
|
467
|
+
) as mock_from_id:
|
|
468
|
+
mock_from_id.return_value = mock_task
|
|
469
|
+
_ = kiln_task_tool._task
|
|
470
|
+
# Should not be called again due to caching
|
|
471
|
+
mock_from_id.assert_not_called()
|
|
472
|
+
|
|
473
|
+
@pytest.mark.asyncio
|
|
474
|
+
async def test_run_with_adapter_exception(
|
|
475
|
+
self, kiln_task_tool, mock_context, mock_task, mock_run_config
|
|
476
|
+
):
|
|
477
|
+
"""Test the run method when adapter raises an exception."""
|
|
478
|
+
# Setup mocks
|
|
479
|
+
kiln_task_tool._task = mock_task
|
|
480
|
+
kiln_task_tool._run_config = mock_run_config
|
|
481
|
+
|
|
482
|
+
with patch(
|
|
483
|
+
"kiln_ai.adapters.adapter_registry.adapter_for_task"
|
|
484
|
+
) as mock_adapter_for_task:
|
|
485
|
+
# Mock adapter to raise an exception
|
|
486
|
+
mock_adapter = AsyncMock()
|
|
487
|
+
mock_adapter.invoke.side_effect = Exception("Adapter failed")
|
|
488
|
+
mock_adapter_for_task.return_value = mock_adapter
|
|
489
|
+
|
|
490
|
+
with pytest.raises(Exception, match="Adapter failed"):
|
|
491
|
+
await kiln_task_tool.run(context=mock_context, input="test input")
|
|
492
|
+
|
|
493
|
+
@pytest.mark.asyncio
|
|
494
|
+
async def test_run_with_different_allow_saving(
|
|
495
|
+
self, kiln_task_tool, mock_task, mock_run_config
|
|
496
|
+
):
|
|
497
|
+
"""Test the run method with different allow_saving values."""
|
|
498
|
+
mock_context_false = MagicMock(spec=ToolCallContext)
|
|
499
|
+
mock_context_false.allow_saving = False
|
|
500
|
+
|
|
501
|
+
# Setup mocks
|
|
502
|
+
kiln_task_tool._task = mock_task
|
|
503
|
+
kiln_task_tool._run_config = mock_run_config
|
|
504
|
+
|
|
505
|
+
with (
|
|
506
|
+
patch(
|
|
507
|
+
"kiln_ai.adapters.adapter_registry.adapter_for_task"
|
|
508
|
+
) as mock_adapter_for_task,
|
|
509
|
+
patch(
|
|
510
|
+
"kiln_ai.adapters.model_adapters.base_adapter.AdapterConfig"
|
|
511
|
+
) as mock_adapter_config,
|
|
512
|
+
):
|
|
513
|
+
mock_adapter = AsyncMock()
|
|
514
|
+
mock_adapter_for_task.return_value = mock_adapter
|
|
515
|
+
|
|
516
|
+
mock_task_run = MagicMock()
|
|
517
|
+
mock_task_run.id = "run_789"
|
|
518
|
+
mock_task_run.output.output = "Task completed"
|
|
519
|
+
mock_adapter.invoke.return_value = mock_task_run
|
|
520
|
+
|
|
521
|
+
await kiln_task_tool.run(context=mock_context_false, input="test input")
|
|
522
|
+
|
|
523
|
+
# Verify adapter config was called with allow_saving=False
|
|
524
|
+
mock_adapter_config.assert_called_once_with(
|
|
525
|
+
allow_saving=False,
|
|
526
|
+
default_tags=["tool_call"],
|
|
527
|
+
)
|
|
@@ -10,7 +10,10 @@ from mcp.types import (
|
|
|
10
10
|
Tool,
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
-
from kiln_ai.datamodel.external_tool_server import
|
|
13
|
+
from kiln_ai.datamodel.external_tool_server import (
|
|
14
|
+
ExternalToolServer,
|
|
15
|
+
ToolServerType,
|
|
16
|
+
)
|
|
14
17
|
from kiln_ai.datamodel.tool_id import MCP_REMOTE_TOOL_ID_PREFIX
|
|
15
18
|
from kiln_ai.tools.mcp_server_tool import MCPServerTool
|
|
16
19
|
|
|
@@ -27,7 +30,6 @@ class TestMCPServerTool:
|
|
|
27
30
|
description="Test server",
|
|
28
31
|
properties={
|
|
29
32
|
"server_url": "https://example.com",
|
|
30
|
-
"headers": {},
|
|
31
33
|
},
|
|
32
34
|
)
|
|
33
35
|
|
|
@@ -60,7 +62,6 @@ class TestMCPServerTool:
|
|
|
60
62
|
type=ToolServerType.remote_mcp,
|
|
61
63
|
properties={
|
|
62
64
|
"server_url": "https://example.com",
|
|
63
|
-
"headers": {},
|
|
64
65
|
},
|
|
65
66
|
)
|
|
66
67
|
tool = MCPServerTool(server, "test_tool")
|
|
@@ -90,7 +91,6 @@ class TestMCPServerTool:
|
|
|
90
91
|
type=ToolServerType.remote_mcp,
|
|
91
92
|
properties={
|
|
92
93
|
"server_url": "https://example.com",
|
|
93
|
-
"headers": {},
|
|
94
94
|
},
|
|
95
95
|
)
|
|
96
96
|
tool = MCPServerTool(server, "test_tool")
|
|
@@ -116,7 +116,6 @@ class TestMCPServerTool:
|
|
|
116
116
|
type=ToolServerType.remote_mcp,
|
|
117
117
|
properties={
|
|
118
118
|
"server_url": "https://example.com",
|
|
119
|
-
"headers": {},
|
|
120
119
|
},
|
|
121
120
|
)
|
|
122
121
|
tool = MCPServerTool(server, "test_tool")
|
|
@@ -143,7 +142,6 @@ class TestMCPServerTool:
|
|
|
143
142
|
type=ToolServerType.remote_mcp,
|
|
144
143
|
properties={
|
|
145
144
|
"server_url": "https://example.com",
|
|
146
|
-
"headers": {},
|
|
147
145
|
},
|
|
148
146
|
)
|
|
149
147
|
tool = MCPServerTool(server, "test_tool")
|
|
@@ -170,7 +168,6 @@ class TestMCPServerTool:
|
|
|
170
168
|
type=ToolServerType.remote_mcp,
|
|
171
169
|
properties={
|
|
172
170
|
"server_url": "https://example.com",
|
|
173
|
-
"headers": {},
|
|
174
171
|
},
|
|
175
172
|
)
|
|
176
173
|
tool = MCPServerTool(server, "test_tool")
|
|
@@ -196,7 +193,6 @@ class TestMCPServerTool:
|
|
|
196
193
|
type=ToolServerType.remote_mcp,
|
|
197
194
|
properties={
|
|
198
195
|
"server_url": "https://example.com",
|
|
199
|
-
"headers": {},
|
|
200
196
|
},
|
|
201
197
|
)
|
|
202
198
|
tool = MCPServerTool(server, "test_tool")
|
|
@@ -231,7 +227,6 @@ class TestMCPServerTool:
|
|
|
231
227
|
type=ToolServerType.remote_mcp,
|
|
232
228
|
properties={
|
|
233
229
|
"server_url": "https://example.com",
|
|
234
|
-
"headers": {},
|
|
235
230
|
},
|
|
236
231
|
)
|
|
237
232
|
tool = MCPServerTool(server, "target_tool")
|
|
@@ -258,7 +253,6 @@ class TestMCPServerTool:
|
|
|
258
253
|
type=ToolServerType.remote_mcp,
|
|
259
254
|
properties={
|
|
260
255
|
"server_url": "https://example.com",
|
|
261
|
-
"headers": {},
|
|
262
256
|
},
|
|
263
257
|
)
|
|
264
258
|
tool = MCPServerTool(server, "missing_tool")
|
|
@@ -287,7 +281,6 @@ class TestMCPServerTool:
|
|
|
287
281
|
type=ToolServerType.remote_mcp,
|
|
288
282
|
properties={
|
|
289
283
|
"server_url": "https://example.com",
|
|
290
|
-
"headers": {},
|
|
291
284
|
},
|
|
292
285
|
)
|
|
293
286
|
tool = MCPServerTool(server, "test_tool")
|
|
@@ -321,7 +314,6 @@ class TestMCPServerTool:
|
|
|
321
314
|
type=ToolServerType.remote_mcp,
|
|
322
315
|
properties={
|
|
323
316
|
"server_url": "https://example.com",
|
|
324
|
-
"headers": {},
|
|
325
317
|
},
|
|
326
318
|
)
|
|
327
319
|
tool = MCPServerTool(server, "test_tool")
|
|
@@ -347,7 +339,6 @@ class TestMCPServerTool:
|
|
|
347
339
|
type=ToolServerType.remote_mcp,
|
|
348
340
|
properties={
|
|
349
341
|
"server_url": "https://example.com",
|
|
350
|
-
"headers": {},
|
|
351
342
|
},
|
|
352
343
|
)
|
|
353
344
|
tool = MCPServerTool(server, "test_tool")
|
|
@@ -365,7 +356,6 @@ class TestMCPServerTool:
|
|
|
365
356
|
type=ToolServerType.remote_mcp,
|
|
366
357
|
properties={
|
|
367
358
|
"server_url": "https://example.com",
|
|
368
|
-
"headers": {},
|
|
369
359
|
},
|
|
370
360
|
)
|
|
371
361
|
tool = MCPServerTool(server, "test_tool")
|
|
@@ -415,7 +405,6 @@ class TestMCPServerToolIntegration:
|
|
|
415
405
|
description="Postman Echo MCP Server for testing",
|
|
416
406
|
properties={
|
|
417
407
|
"server_url": "https://postman-echo-mcp.fly.dev/",
|
|
418
|
-
"headers": {},
|
|
419
408
|
},
|
|
420
409
|
)
|
|
421
410
|
|