Skip to content

Commit 1c81a46

Browse files
πŸ› Bugfix:fix aidp search tool params' save error#3296 (#3297)
* πŸ› Bugfix: Update HTTP client settings to increase timeout and disable SSL verification in aidp_service and aidp_search_tool (#3280) * πŸ› Bugfix: Fix page show * πŸ› Bugfix: Prevent saving null values in tool parameters across backend and frontend components. Ensure only defined values are used when merging and updating tool configurations. * πŸ› Bugfix: Ensure `useSaveGuard` returns true upon successful save and update unit tests to reflect changes in return type for tool instance creation and update.
1 parent 28c2ed3 commit 1c81a46

6 files changed

Lines changed: 59 additions & 12 deletions

File tree

β€Žbackend/database/tool_db.pyβ€Ž

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ def create_or_update_tool_by_tool_info(tool_info, tenant_id: str, user_id: str,
4747
tool_info_dict = tool_info.__dict__ | {
4848
"tenant_id": tenant_id, "user_id": user_id, "version_no": version_no}
4949

50+
# Filter out null values from params to avoid saving nulls to database
51+
if 'params' in tool_info_dict and tool_info_dict['params'] is not None:
52+
tool_info_dict['params'] = {
53+
k: v for k, v in tool_info_dict['params'].items()
54+
if v is not None
55+
}
56+
5057
with get_db_session() as session:
5158
# Query if there is an existing ToolInstance
5259
# Note: Do not filter by user_id to avoid creating duplicate instances
@@ -71,7 +78,7 @@ def create_or_update_tool_by_tool_info(tool_info, tenant_id: str, user_id: str,
7178
session.add(new_tool_instance)
7279
session.flush() # Flush to get the ID
7380
tool_instance = new_tool_instance
74-
return tool_instance
81+
return as_dict(tool_instance)
7582

7683

7784
def query_all_tools(tenant_id: str):
@@ -258,7 +265,11 @@ def add_tool_field(tool_info):
258265
tool_params = tool.params
259266
for ele in tool_params:
260267
param_name = ele["name"]
261-
ele["default"] = tool_info["params"].get(param_name)
268+
instance_value = tool_info["params"].get(param_name)
269+
# Only set default if instance value is not None
270+
# This prevents null values from being saved to database and returned as defaults
271+
if instance_value is not None:
272+
ele["default"] = instance_value
262273
tool_dict = as_dict(tool)
263274
tool_dict["params"] = tool_params
264275

β€Žbackend/services/agent_service.pyβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,9 @@ async def update_agent_info_impl(request: AgentInfoRequest, authorization: str =
12411241
if inst.get("tool_id") == tool_id),
12421242
None
12431243
)
1244-
params = (existing_instance or {}).get("params", {})
1244+
# Safely get params, default to empty dict if None or not present
1245+
raw_params = (existing_instance or {}).get("params")
1246+
params = raw_params if raw_params is not None else {}
12451247
create_or_update_tool_by_tool_info(
12461248
tool_info=ToolInstanceInfoRequest(
12471249
tool_id=tool_id,

β€Žfrontend/app/[locale]/agents/components/agentConfig/ToolManagement.tsxβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,15 @@ export default function ToolManagement({
143143

144144
if (tooInstance.success && tooInstance.data) {
145145
// Merge instance params with default params
146+
// Only use instance value if it exists and is not null/undefined
146147
const mergedParams =
147148
defaultTool.initParams?.map((param: ToolParam) => {
148149
const instanceValue = tooInstance.data?.params?.[param.name];
150+
// Use instance value only if it's not null or undefined
151+
const hasValidInstanceValue = instanceValue !== null && instanceValue !== undefined;
149152
return {
150153
...param,
151-
value:
152-
instanceValue !== undefined ? instanceValue : param.value,
154+
value: hasValidInstanceValue ? instanceValue : param.value,
153155
};
154156
}) ||
155157
defaultTool.initParams ||

β€Žfrontend/app/[locale]/agents/components/agentConfig/tool/ToolConfigModal.tsxβ€Ž

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,8 +1313,18 @@ export default function ToolConfigModal({
13131313
return;
13141314
}
13151315

1316-
// Convert params to backend format (use the synced params)
1317-
const paramsObj = currentParams.reduce(
1316+
// Convert params to backend format - use latestFormValues directly to avoid async state issues
1317+
// This ensures we capture the most recent form values without relying on async setState
1318+
const syncedParams = [...currentParams];
1319+
if (latestFormValues) {
1320+
Object.entries(latestFormValues).forEach(([fieldName, value]) => {
1321+
const index = parseInt(fieldName.replace("param_", ""));
1322+
if (!isNaN(index) && syncedParams[index]) {
1323+
syncedParams[index] = { ...syncedParams[index], value };
1324+
}
1325+
});
1326+
}
1327+
const paramsObj = syncedParams.reduce(
13181328
(acc, param) => {
13191329
acc[param.name] = param.value;
13201330
return acc;
@@ -1326,7 +1336,7 @@ export default function ToolConfigModal({
13261336
// Include display_names for knowledge base tools to pass to prompt generation
13271337
const updatedTool: typeof toolToSave = {
13281338
...toolToSave,
1329-
initParams: currentParams,
1339+
initParams: syncedParams,
13301340
// Store knowledge base display names for prompt generation
13311341
...(toolRequiresKbSelection && selectedKbDisplayNames.length > 0
13321342
? { display_names: selectedKbDisplayNames }

β€Žfrontend/hooks/agent/useSaveGuard.tsβ€Ž

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,12 @@ async function batchUpdateToolConfigs(
4040
for (const tool of currentTools) {
4141
const toolId = parseInt(tool.id);
4242
const isEnabled = true; // Selected tools are always enabled
43+
// Only include params that have a defined value (not undefined or null)
44+
// This ensures we don't save null values from form defaults or stale data
4345
const params = tool.initParams?.reduce((acc: Record<string, any>, param: any) => {
44-
acc[param.name] = param.value;
46+
if (param.value !== undefined && param.value !== null) {
47+
acc[param.name] = param.value;
48+
}
4549
return acc;
4650
}, {} as Record<string, any>) || {};
4751

@@ -192,14 +196,24 @@ export const useSaveGuard = () => {
192196
const baselineTools = useAgentConfigStore.getState().baselineAgent?.tools || [];
193197
await batchUpdateToolConfigs(finalAgentId, currentEditedAgent.tools || [], baselineTools);
194198

195-
// Common logic for both creation and update: refresh cache and update store
199+
// Refresh cache
196200
await queryClient.invalidateQueries({
197201
queryKey: ["agentInfo", finalAgentId]
198202
});
199203
await queryClient.refetchQueries({
200204
queryKey: ["agentInfo", finalAgentId]
201205
});
202206

207+
// CRITICAL: Update store with the latest data from cache after saving tool configs
208+
// This ensures that on subsequent saves, the tool initParams reflect the latest
209+
// values that were saved (including any defaults merged by the backend)
210+
const latestAgentData = queryClient.getQueryData(["agentInfo", finalAgentId]);
211+
if (latestAgentData && typeof latestAgentData === 'object' && 'tools' in latestAgentData) {
212+
const latestTools = (latestAgentData as any).tools || [];
213+
// Update editedAgent with the latest tools from cache
214+
useAgentConfigStore.getState().updateTools(latestTools);
215+
}
216+
203217
// Refresh skill instances after save
204218
await queryClient.invalidateQueries({
205219
queryKey: ["agentSkillInstances", finalAgentId]
@@ -208,6 +222,8 @@ export const useSaveGuard = () => {
208222
// Also invalidate the agents list cache to ensure the list reflects any changes
209223
queryClient.invalidateQueries({ queryKey: ["agents"] });
210224

225+
// Mark as saved (this will sync editedAgent to baselineAgent)
226+
useAgentConfigStore.getState().markAsSaved();
211227
return true;
212228
} else {
213229
message.error(result.message || t("businessLogic.config.error.saveFailed") );

β€Žtest/backend/database/test_tool_db.pyβ€Ž

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,16 @@ def test_create_or_update_tool_by_tool_info_update_existing(monkeypatch, mock_se
215215
mock_ctx.__exit__.return_value = None
216216
monkeypatch.setattr(
217217
"backend.database.tool_db.get_db_session", lambda: mock_ctx)
218+
monkeypatch.setattr("backend.database.tool_db.as_dict",
219+
lambda obj: obj.__dict__ if hasattr(obj, '__dict__') else obj)
218220

219221
tool_info = MagicMock()
220222
tool_info.__dict__ = {"agent_id": 1, "tool_id": 1}
221223

222224
result = create_or_update_tool_by_tool_info(tool_info, "tenant1", "user1")
223225

224-
assert result == mock_tool_instance
226+
# Result is now as_dict() of the tool_instance
227+
assert isinstance(result, dict)
225228

226229

227230
def test_create_or_update_tool_by_tool_info_create_new(monkeypatch, mock_session):
@@ -263,6 +266,8 @@ def __init__(self, **kwargs):
263266

264267
monkeypatch.setattr(
265268
"backend.database.tool_db.ToolInstance", MockToolInstanceClass)
269+
monkeypatch.setattr("backend.database.tool_db.as_dict",
270+
lambda obj: obj.__dict__ if hasattr(obj, '__dict__') else obj)
266271

267272
session.add = MagicMock()
268273
session.flush = MagicMock()
@@ -272,7 +277,8 @@ def __init__(self, **kwargs):
272277

273278
result = create_or_update_tool_by_tool_info(tool_info, "tenant1", "user1")
274279

275-
assert isinstance(result, MockToolInstanceClass)
280+
# Result is now as_dict() of the tool_instance (a dict)
281+
assert isinstance(result, dict)
276282
session.add.assert_called_once()
277283
session.flush.assert_called_once()
278284

0 commit comments

Comments
Β (0)