Skip to content

Commit 4e1557e

Browse files
majdyzclaude
andauthored
fix(backend): Add dynamic input pin support for Smart Decision Maker Block (#11082)
## Summary - Centralize dynamic field delimiters and helpers in backend/data/dynamic_fields.py. - Refactor SmartDecisionMaker: build function signatures with dynamic-field mapping and re-map tool outputs back to original dynamic names. - Deterministic retry loop with retry-only feedback to avoid polluting final conversation history. - Update executor/utils.py and data/graph.py to use centralized utilities. - Update and extend tests: dynamic-field E2E flow, mapping verification, output yielding, and retry validation; switch mocked llm_call to AsyncMock; align tool-name expectations. - Add a single-tool fallback in schema lookup to support mocked scenarios. ## Validation - Full backend test suite: 1125 passed, 88 skipped, 53 warnings (local). - Backend lint/format pass. ## Scope - Minimal and localized to SmartDecisionMaker and dynamic-field utilities; unrelated pyright warnings remain unchanged. ## Risks/Mitigations - Behavior is backward-compatible; dynamic-field constants are centralized and reused. - Output re-mapping only affects SmartDecisionMaker tool outputs and matches existing link naming conventions. ## Checklist - [x] Formatted and linted - [x] All updated tests pass locally - [x] No secrets introduced --------- Co-authored-by: Claude <[email protected]>
1 parent 7f8cf36 commit 4e1557e

File tree

11 files changed

+1422
-359
lines changed

11 files changed

+1422
-359
lines changed

autogpt_platform/backend/backend/blocks/smart_decision_maker.py

Lines changed: 200 additions & 148 deletions
Large diffs are not rendered by default.

autogpt_platform/backend/backend/blocks/test/test_smart_decision_maker.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,17 @@ async def test_smart_decision_maker_tracks_llm_stats():
216216
}
217217

218218
# Mock the _create_function_signature method to avoid database calls
219-
with patch("backend.blocks.llm.llm_call", return_value=mock_response), patch.object(
220-
SmartDecisionMakerBlock, "_create_function_signature", return_value=[]
219+
from unittest.mock import AsyncMock
220+
221+
with patch(
222+
"backend.blocks.llm.llm_call",
223+
new_callable=AsyncMock,
224+
return_value=mock_response,
225+
), patch.object(
226+
SmartDecisionMakerBlock,
227+
"_create_function_signature",
228+
new_callable=AsyncMock,
229+
return_value=[],
221230
):
222231

223232
# Create test input
@@ -301,11 +310,16 @@ async def test_smart_decision_maker_parameter_validation():
301310
mock_response_with_typo.reasoning = None
302311
mock_response_with_typo.raw_response = {"role": "assistant", "content": None}
303312

313+
from unittest.mock import AsyncMock
314+
304315
with patch(
305-
"backend.blocks.llm.llm_call", return_value=mock_response_with_typo
316+
"backend.blocks.llm.llm_call",
317+
new_callable=AsyncMock,
318+
return_value=mock_response_with_typo,
306319
) as mock_llm_call, patch.object(
307320
SmartDecisionMakerBlock,
308321
"_create_function_signature",
322+
new_callable=AsyncMock,
309323
return_value=mock_tool_functions,
310324
):
311325

@@ -332,7 +346,7 @@ async def test_smart_decision_maker_parameter_validation():
332346

333347
# Verify error message contains details about the typo
334348
error_msg = str(exc_info.value)
335-
assert "Tool call validation failed" in error_msg
349+
assert "Tool call 'search_keywords' has parameter errors" in error_msg
336350
assert "Unknown parameters: ['maximum_keyword_difficulty']" in error_msg
337351

338352
# Verify that LLM was called the expected number of times (retries)
@@ -353,11 +367,16 @@ async def test_smart_decision_maker_parameter_validation():
353367
mock_response_missing_required.reasoning = None
354368
mock_response_missing_required.raw_response = {"role": "assistant", "content": None}
355369

370+
from unittest.mock import AsyncMock
371+
356372
with patch(
357-
"backend.blocks.llm.llm_call", return_value=mock_response_missing_required
373+
"backend.blocks.llm.llm_call",
374+
new_callable=AsyncMock,
375+
return_value=mock_response_missing_required,
358376
), patch.object(
359377
SmartDecisionMakerBlock,
360378
"_create_function_signature",
379+
new_callable=AsyncMock,
361380
return_value=mock_tool_functions,
362381
):
363382

@@ -398,11 +417,16 @@ async def test_smart_decision_maker_parameter_validation():
398417
mock_response_valid.reasoning = None
399418
mock_response_valid.raw_response = {"role": "assistant", "content": None}
400419

420+
from unittest.mock import AsyncMock
421+
401422
with patch(
402-
"backend.blocks.llm.llm_call", return_value=mock_response_valid
423+
"backend.blocks.llm.llm_call",
424+
new_callable=AsyncMock,
425+
return_value=mock_response_valid,
403426
), patch.object(
404427
SmartDecisionMakerBlock,
405428
"_create_function_signature",
429+
new_callable=AsyncMock,
406430
return_value=mock_tool_functions,
407431
):
408432

@@ -447,11 +471,16 @@ async def test_smart_decision_maker_parameter_validation():
447471
mock_response_all_params.reasoning = None
448472
mock_response_all_params.raw_response = {"role": "assistant", "content": None}
449473

474+
from unittest.mock import AsyncMock
475+
450476
with patch(
451-
"backend.blocks.llm.llm_call", return_value=mock_response_all_params
477+
"backend.blocks.llm.llm_call",
478+
new_callable=AsyncMock,
479+
return_value=mock_response_all_params,
452480
), patch.object(
453481
SmartDecisionMakerBlock,
454482
"_create_function_signature",
483+
new_callable=AsyncMock,
455484
return_value=mock_tool_functions,
456485
):
457486

@@ -553,9 +582,14 @@ def __init__(self, role, content, tool_calls=None):
553582
)
554583

555584
# Mock llm_call to return different responses on different calls
556-
with patch("backend.blocks.llm.llm_call") as mock_llm_call, patch.object(
585+
from unittest.mock import AsyncMock
586+
587+
with patch(
588+
"backend.blocks.llm.llm_call", new_callable=AsyncMock
589+
) as mock_llm_call, patch.object(
557590
SmartDecisionMakerBlock,
558591
"_create_function_signature",
592+
new_callable=AsyncMock,
559593
return_value=mock_tool_functions,
560594
):
561595
# First call returns response that will trigger retry due to validation error
@@ -614,11 +648,16 @@ def __init__(self, role, content, tool_calls=None):
614648
"I'll help you with that." # Ollama returns string
615649
)
616650

651+
from unittest.mock import AsyncMock
652+
617653
with patch(
618-
"backend.blocks.llm.llm_call", return_value=mock_response_ollama
654+
"backend.blocks.llm.llm_call",
655+
new_callable=AsyncMock,
656+
return_value=mock_response_ollama,
619657
), patch.object(
620658
SmartDecisionMakerBlock,
621659
"_create_function_signature",
660+
new_callable=AsyncMock,
622661
return_value=[], # No tools for this test
623662
):
624663
input_data = SmartDecisionMakerBlock.Input(
@@ -655,11 +694,16 @@ def __init__(self, role, content, tool_calls=None):
655694
"content": "Test response",
656695
} # Dict format
657696

697+
from unittest.mock import AsyncMock
698+
658699
with patch(
659-
"backend.blocks.llm.llm_call", return_value=mock_response_dict
700+
"backend.blocks.llm.llm_call",
701+
new_callable=AsyncMock,
702+
return_value=mock_response_dict,
660703
), patch.object(
661704
SmartDecisionMakerBlock,
662705
"_create_function_signature",
706+
new_callable=AsyncMock,
663707
return_value=[],
664708
):
665709
input_data = SmartDecisionMakerBlock.Input(

autogpt_platform/backend/backend/blocks/test/test_smart_decision_maker_dict.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,24 @@ async def test_smart_decision_maker_handles_dynamic_dict_fields():
4848
assert "parameters" in signature["function"]
4949
assert "properties" in signature["function"]["parameters"]
5050

51-
# Check that dynamic fields are handled
51+
# Check that dynamic fields are handled with original names
5252
properties = signature["function"]["parameters"]["properties"]
5353
assert len(properties) == 3 # Should have all three fields
5454

55-
# Each dynamic field should have proper schema
56-
for prop_value in properties.values():
55+
# Check that field names are cleaned (for Anthropic API compatibility)
56+
assert "values___name" in properties
57+
assert "values___age" in properties
58+
assert "values___city" in properties
59+
60+
# Each dynamic field should have proper schema with descriptive text
61+
for field_name, prop_value in properties.items():
5762
assert "type" in prop_value
5863
assert prop_value["type"] == "string" # Dynamic fields get string type
5964
assert "description" in prop_value
60-
assert "Dynamic value for" in prop_value["description"]
65+
# Check that descriptions properly explain the dynamic field
66+
if field_name == "values___name":
67+
assert "Dictionary field 'name'" in prop_value["description"]
68+
assert "values['name']" in prop_value["description"]
6169

6270

6371
@pytest.mark.asyncio
@@ -96,10 +104,18 @@ async def test_smart_decision_maker_handles_dynamic_list_fields():
96104
properties = signature["function"]["parameters"]["properties"]
97105
assert len(properties) == 2 # Should have both list items
98106

99-
# Each dynamic field should have proper schema
100-
for prop_value in properties.values():
107+
# Check that field names are cleaned (for Anthropic API compatibility)
108+
assert "entries___0" in properties
109+
assert "entries___1" in properties
110+
111+
# Each dynamic field should have proper schema with descriptive text
112+
for field_name, prop_value in properties.items():
101113
assert prop_value["type"] == "string"
102-
assert "Dynamic value for" in prop_value["description"]
114+
assert "description" in prop_value
115+
# Check that descriptions properly explain the list field
116+
if field_name == "entries___0":
117+
assert "List item 0" in prop_value["description"]
118+
assert "entries[0]" in prop_value["description"]
103119

104120

105121
@pytest.mark.asyncio

0 commit comments

Comments
 (0)