Skip to content

Commit a28385d

Browse files
authored
🐛 Record the settings of tools
2 parents 3498c46 + 71fd073 commit a28385d

File tree

11 files changed

+395
-20
lines changed

11 files changed

+395
-20
lines changed

backend/apps/tool_config_app.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from services.tool_configuration_service import (
1111
search_tool_info_impl,
1212
update_tool_info_impl,
13-
update_tool_list, list_all_tools,
13+
update_tool_list,
14+
list_all_tools,
15+
load_last_tool_config_impl,
1416
)
1517
from utils.auth_utils import get_current_user_id
1618

@@ -78,3 +80,21 @@ async def scan_and_update_tool(
7880
logger.error(f"Failed to update tool: {e}")
7981
raise HTTPException(
8082
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to update tool")
83+
84+
@router.get("/load_config/{tool_id}")
85+
async def load_last_tool_config(tool_id: int, authorization: Optional[str] = Header(None)):
86+
try:
87+
user_id, tenant_id = get_current_user_id(authorization)
88+
tool_params = load_last_tool_config_impl(tool_id, tenant_id, user_id)
89+
return JSONResponse(
90+
status_code=HTTPStatus.OK,
91+
content={"message": tool_params, "status": "success"}
92+
)
93+
except ValueError:
94+
logger.error(f"Tool configuration not found for tool ID: {tool_id}")
95+
raise HTTPException(
96+
status_code=HTTPStatus.NOT_FOUND, detail="Tool configuration not found")
97+
except Exception as e:
98+
logger.error(f"Failed to load tool config: {e}")
99+
raise HTTPException(
100+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to load tool config")

backend/database/tool_db.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,14 @@ def delete_tools_by_agent_id(agent_id, tenant_id, user_id):
213213
).update({
214214
ToolInstance.delete_flag: 'Y', 'updated_by': user_id
215215
})
216+
217+
def search_last_tool_instance_by_tool_id(tool_id: int, tenant_id: str, user_id: str):
218+
with get_db_session() as session:
219+
query = session.query(ToolInstance).filter(
220+
ToolInstance.tool_id == tool_id,
221+
ToolInstance.tenant_id == tenant_id,
222+
ToolInstance.user_id == user_id,
223+
ToolInstance.delete_flag != 'Y'
224+
).order_by(ToolInstance.update_time.desc())
225+
tool_instance = query.first()
226+
return as_dict(tool_instance) if tool_instance else None

backend/services/tool_configuration_service.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
create_or_update_tool_by_tool_info,
2020
query_all_tools,
2121
query_tool_instances_by_id,
22-
update_tool_table_from_scan_tool_list
22+
update_tool_table_from_scan_tool_list,
23+
search_last_tool_instance_by_tool_id
2324
)
2425
from database.user_tenant_db import get_all_tenant_ids
2526

@@ -413,4 +414,13 @@ async def initialize_tools_on_startup():
413414

414415
except Exception as e:
415416
logger.error(f"❌ Tool initialization failed: {str(e)}")
416-
raise
417+
raise
418+
419+
def load_last_tool_config_impl(tool_id: int, tenant_id: str, user_id: str):
420+
"""
421+
Load the last tool configuration for a given tool ID
422+
"""
423+
tool_instance = search_last_tool_instance_by_tool_id(tool_id, tenant_id, user_id)
424+
if tool_instance is None:
425+
raise ValueError(f"Tool configuration not found for tool ID: {tool_id}")
426+
return tool_instance.get("params", {})

frontend/app/[locale]/setup/agents/components/tool/ToolConfigModal.tsx

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ToolParam, ToolConfigModalProps } from "@/types/agentConfig";
99
import {
1010
updateToolConfig,
1111
searchToolConfig,
12+
loadLastToolConfig,
1213
} from "@/services/agentConfigService";
1314
import log from "@/lib/logger";
1415

@@ -122,6 +123,36 @@ export default function ToolConfigModal({
122123
setCurrentParams(newParams);
123124
};
124125

126+
// load last tool config
127+
const handleLoadLastConfig = async () => {
128+
if (!tool) return;
129+
130+
try {
131+
const result = await loadLastToolConfig(parseInt(tool.id));
132+
if (result.success && result.data) {
133+
// Parse the last config data
134+
const lastConfig = result.data;
135+
136+
// Update current params with last config values
137+
const updatedParams = currentParams.map((param) => {
138+
const lastValue = lastConfig[param.name];
139+
return {
140+
...param,
141+
value: lastValue !== undefined ? lastValue : param.value,
142+
};
143+
});
144+
145+
setCurrentParams(updatedParams);
146+
message.success(t("toolConfig.message.loadLastConfigSuccess"));
147+
} else {
148+
message.warning(t("toolConfig.message.loadLastConfigNotFound"));
149+
}
150+
} catch (error) {
151+
log.error(t("toolConfig.message.loadLastConfigFailed"), error);
152+
message.error(t("toolConfig.message.loadLastConfigFailed"));
153+
}
154+
};
155+
125156
const handleSave = async () => {
126157
if (!tool || !checkRequiredFields()) return;
127158

@@ -257,26 +288,36 @@ export default function ToolConfigModal({
257288
title={
258289
<div className="flex justify-between items-center w-full pr-8">
259290
<span>{`${tool?.name}`}</span>
260-
<Tag
261-
color={
262-
tool?.source === "mcp"
263-
? "blue"
291+
<div className="flex items-center gap-2">
292+
<button
293+
onClick={handleLoadLastConfig}
294+
className="px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
295+
>
296+
{t("toolConfig.message.loadLastConfig")}
297+
</button>
298+
<Tag
299+
color={
300+
tool?.source === "mcp"
301+
? "blue"
302+
: tool?.source === "langchain"
303+
? "orange"
304+
: "green"
305+
}
306+
>
307+
{tool?.source === "mcp"
308+
? t("toolPool.tag.mcp")
264309
: tool?.source === "langchain"
265-
? "orange"
266-
: "green"
267-
}
268-
>
269-
{tool?.source === "mcp"
270-
? t("toolPool.tag.mcp")
271-
: tool?.source === "langchain"
272-
? t("toolPool.tag.langchain")
273-
: t("toolPool.tag.local")}
274-
</Tag>
310+
? t("toolPool.tag.langchain")
311+
: t("toolPool.tag.local")}
312+
</Tag>
313+
</div>
275314
</div>
276315
}
277316
open={isOpen}
278317
onCancel={onCancel}
279318
onOk={handleSave}
319+
okText={t("common.button.save")}
320+
cancelText={t("common.button.cancel")}
280321
width={600}
281322
confirmLoading={isLoading}
282323
>

frontend/public/locales/en/common.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,10 @@
351351
"toolConfig.message.saveError": "Save failed",
352352
"toolConfig.message.saveFailed": "Save failed, please try again later",
353353
"toolConfig.message.requiredFields": "The following required fields are not filled: ",
354+
"toolConfig.message.loadLastConfig": "Load Last Configuration",
355+
"toolConfig.message.loadLastConfigSuccess": "Last configuration loaded successfully",
356+
"toolConfig.message.loadLastConfigFailed": "Failed to load last configuration",
357+
"toolConfig.message.loadLastConfigNotFound": "No last configuration found",
354358
"toolConfig.input.model.placeholder": "Please select model",
355359
"toolConfig.input.string.placeholder": "Please enter {{name}}",
356360
"toolConfig.input.array.placeholder": "Please enter JSON array",

frontend/public/locales/zh/common.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,10 @@
352352
"toolConfig.message.saveError": "保存失败",
353353
"toolConfig.message.saveFailed": "保存失败,请稍后重试",
354354
"toolConfig.message.requiredFields": "以下必填字段未填写: ",
355+
"toolConfig.message.loadLastConfig": "加载上一次配置",
356+
"toolConfig.message.loadLastConfigSuccess": "上一次配置加载成功",
357+
"toolConfig.message.loadLastConfigFailed": "加载上一次配置失败",
358+
"toolConfig.message.loadLastConfigNotFound": "未找到上一次配置",
355359
"toolConfig.input.model.placeholder": "请选择模型",
356360
"toolConfig.input.string.placeholder": "请输入{{name}}",
357361
"toolConfig.input.array.placeholder": "请输入JSON数组",

frontend/services/agentConfigService.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,38 @@ export const searchToolConfig = async (toolId: number, agentId: number) => {
223223
}
224224
};
225225

226+
/**
227+
* load last tool config
228+
* @param toolId tool id
229+
* @returns last tool config info
230+
*/
231+
export const loadLastToolConfig = async (toolId: number) => {
232+
try {
233+
const response = await fetch(API_ENDPOINTS.tool.loadConfig(toolId), {
234+
method: "GET",
235+
headers: getAuthHeaders(),
236+
});
237+
238+
if (!response.ok) {
239+
throw new Error(`Request failed: ${response.status}`);
240+
}
241+
242+
const data = await response.json();
243+
return {
244+
success: true,
245+
data: data.message, // Backend returns config in message field
246+
message: "",
247+
};
248+
} catch (error) {
249+
log.error("Failed to load last tool configuration:", error);
250+
return {
251+
success: false,
252+
data: null,
253+
message: "Failed to load last tool configuration, please try again later",
254+
};
255+
}
256+
};
257+
226258
/**
227259
* Update Agent information
228260
* @param agentId agent id

frontend/services/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const API_ENDPOINTS = {
4747
update: `${API_BASE_URL}/tool/update`,
4848
search: `${API_BASE_URL}/tool/search`,
4949
updateTool: `${API_BASE_URL}/tool/scan_tool`,
50+
loadConfig: (toolId: number) => `${API_BASE_URL}/tool/load_config/${toolId}`,
5051
},
5152
prompt: {
5253
generate: `${API_BASE_URL}/prompt/generate`,

test/backend/app/test_tool_config_app.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,85 @@ def test_scan_tool_auth_failure(self, mock_get_user_id):
401401
assert "Failed to update tool" in data["detail"]
402402

403403

404+
class TestLoadLastToolConfigAPI:
405+
"""Test endpoint for loading last tool configuration"""
406+
407+
@patch('apps.tool_config_app.get_current_user_id')
408+
@patch('apps.tool_config_app.load_last_tool_config_impl')
409+
def test_load_last_tool_config_success(self, mock_load_config, mock_get_user_id):
410+
"""Test successful loading of last tool configuration"""
411+
mock_get_user_id.return_value = ("user123", "tenant456")
412+
mock_load_config.return_value = {"param1": "value1", "param2": "value2"}
413+
414+
response = client.get("/tool/load_config/123")
415+
416+
assert response.status_code == HTTPStatus.OK
417+
data = response.json()
418+
assert data["status"] == "success"
419+
assert data["message"] == {"param1": "value1", "param2": "value2"}
420+
421+
mock_get_user_id.assert_called_once_with(None)
422+
mock_load_config.assert_called_once_with(123, "tenant456", "user123")
423+
424+
@patch('apps.tool_config_app.get_current_user_id')
425+
@patch('apps.tool_config_app.load_last_tool_config_impl')
426+
def test_load_last_tool_config_not_found(self, mock_load_config, mock_get_user_id):
427+
"""Test loading tool config when not found"""
428+
mock_get_user_id.return_value = ("user123", "tenant456")
429+
mock_load_config.side_effect = ValueError("Tool configuration not found for tool ID: 123")
430+
431+
response = client.get("/tool/load_config/123")
432+
433+
assert response.status_code == HTTPStatus.NOT_FOUND
434+
data = response.json()
435+
assert "Tool configuration not found" in data["detail"]
436+
437+
mock_get_user_id.assert_called_once_with(None)
438+
mock_load_config.assert_called_once_with(123, "tenant456", "user123")
439+
440+
@patch('apps.tool_config_app.get_current_user_id')
441+
@patch('apps.tool_config_app.load_last_tool_config_impl')
442+
def test_load_last_tool_config_service_error(self, mock_load_config, mock_get_user_id):
443+
"""Test service error when loading tool config"""
444+
mock_get_user_id.return_value = ("user123", "tenant456")
445+
mock_load_config.side_effect = Exception("Database error")
446+
447+
response = client.get("/tool/load_config/123")
448+
449+
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
450+
data = response.json()
451+
assert "Failed to load tool config" in data["detail"]
452+
453+
mock_get_user_id.assert_called_once_with(None)
454+
mock_load_config.assert_called_once_with(123, "tenant456", "user123")
455+
456+
@patch('apps.tool_config_app.get_current_user_id')
457+
def test_load_last_tool_config_auth_error(self, mock_get_user_id):
458+
"""Test authentication error when loading tool config"""
459+
mock_get_user_id.side_effect = Exception("Auth error")
460+
461+
response = client.get("/tool/load_config/123")
462+
463+
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
464+
data = response.json()
465+
assert "Failed to load tool config" in data["detail"]
466+
467+
@patch('apps.tool_config_app.get_current_user_id')
468+
@patch('apps.tool_config_app.load_last_tool_config_impl')
469+
def test_load_last_tool_config_with_authorization_header(self, mock_load_config, mock_get_user_id):
470+
"""Test loading tool config with authorization header"""
471+
mock_get_user_id.return_value = ("user123", "tenant456")
472+
mock_load_config.return_value = {"param1": "value1"}
473+
474+
response = client.get(
475+
"/tool/load_config/123",
476+
headers={"Authorization": "Bearer test_token"}
477+
)
478+
479+
assert response.status_code == HTTPStatus.OK
480+
mock_get_user_id.assert_called_with("Bearer test_token")
481+
482+
404483
class TestDataValidation:
405484
"""Data validation tests"""
406485

0 commit comments

Comments
 (0)