hyperforge-smart 1.0.0.post129__tar.gz → 1.0.0.post136__tar.gz
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.
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/PKG-INFO +1 -1
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/pyproject.toml +1 -1
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart/agent.py +119 -25
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart/config.py +8 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart/prompts.py +5 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart.egg-info/PKG-INFO +1 -1
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/tests/test_smart.py +325 -3
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/README.md +0 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/setup.cfg +0 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart/__init__.py +0 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart/py.typed +0 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart.egg-info/SOURCES.txt +0 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart.egg-info/dependency_links.txt +0 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart.egg-info/requires.txt +0 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart.egg-info/top_level.txt +0 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/tests/test_contexts.py +0 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/tests/test_result_payload.py +0 -0
- {hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/tests/test_smart_mcp_perplexity.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hyperforge_smart"
|
|
7
|
-
version = "1.0.0.
|
|
7
|
+
version = "1.0.0.post136"
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
description = "Conditional Hyperforge agent"
|
|
10
10
|
authors = [{ name = "Nuclia", email = "nucliadb@nuclia.com" }]
|
{hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart/agent.py
RENAMED
|
@@ -95,12 +95,16 @@ USER_FEEDBACK_TOOL = Tool(
|
|
|
95
95
|
parameters={
|
|
96
96
|
"type": "object",
|
|
97
97
|
"properties": {
|
|
98
|
+
"decision_id": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "Stable identifier for the underlying decision. Reuse it if the question is rephrased.",
|
|
101
|
+
},
|
|
98
102
|
"question": {
|
|
99
103
|
"type": "string",
|
|
100
104
|
"description": "The question to ask the user",
|
|
101
105
|
},
|
|
102
106
|
},
|
|
103
|
-
"required": ["question"],
|
|
107
|
+
"required": ["decision_id", "question"],
|
|
104
108
|
},
|
|
105
109
|
)
|
|
106
110
|
|
|
@@ -706,28 +710,45 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
706
710
|
messages: List[Message],
|
|
707
711
|
tool_calls: List[Tuple[str, Any]],
|
|
708
712
|
turn_label: str,
|
|
713
|
+
resolved_feedback: Optional[Dict[str, str]] = None,
|
|
709
714
|
collected_contexts: Optional[List[Context]] = None,
|
|
710
715
|
attempted_tool_calls: Optional[Dict[str, ToolAttempt]] = None,
|
|
711
716
|
) -> List[Tuple[str, Any]]:
|
|
712
717
|
"""Handle one turn of tool calls.
|
|
713
718
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
can ``continue`` to the next iteration without executing other tools.
|
|
717
|
-
|
|
718
|
-
Otherwise executes all tool calls in parallel, records an execution step and
|
|
719
|
-
returns (results, False).
|
|
719
|
+
New feedback pauses other tool execution for the turn. Already-resolved
|
|
720
|
+
feedback becomes a synthetic result, and any other requested tools continue.
|
|
720
721
|
"""
|
|
721
722
|
agent_path = f"/context/{self.config.id or 'default'}"
|
|
723
|
+
synthetic_results: List[Tuple[str, Any]] = []
|
|
722
724
|
|
|
723
725
|
# --- user_feedback path ---
|
|
724
726
|
if any(name == "user_feedback" for name, _ in tool_calls):
|
|
725
727
|
for name, args in tool_calls:
|
|
726
728
|
if name == "user_feedback":
|
|
729
|
+
decision_id: Optional[str] = (
|
|
730
|
+
args.get("decision_id") if args else None
|
|
731
|
+
)
|
|
727
732
|
feedback_question: Optional[str] = (
|
|
728
733
|
args.get("question") if args else None
|
|
729
734
|
)
|
|
730
735
|
if feedback_question:
|
|
736
|
+
decision_id = decision_id or (
|
|
737
|
+
f"legacy:{feedback_question.strip().lower()}"
|
|
738
|
+
)
|
|
739
|
+
if (
|
|
740
|
+
resolved_feedback is not None
|
|
741
|
+
and decision_id in resolved_feedback
|
|
742
|
+
):
|
|
743
|
+
cached_feedback_result = (
|
|
744
|
+
"user_feedback",
|
|
745
|
+
"Feedback resolved. Do not request this feedback again. "
|
|
746
|
+
f"Decision ID: {decision_id}\n"
|
|
747
|
+
f"Question: {feedback_question}\n"
|
|
748
|
+
f"Response: {resolved_feedback[decision_id]}",
|
|
749
|
+
)
|
|
750
|
+
synthetic_results.append(cached_feedback_result)
|
|
751
|
+
continue
|
|
731
752
|
feedback = Feedback(
|
|
732
753
|
request_id=memory.get_session_id(),
|
|
733
754
|
question=feedback_question,
|
|
@@ -744,10 +765,7 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
744
765
|
answer = await memory.send_feedback(feedback)
|
|
745
766
|
feedback_text = (
|
|
746
767
|
answer.response
|
|
747
|
-
if
|
|
748
|
-
answer is not None
|
|
749
|
-
and answer.request_id == memory.get_session_id()
|
|
750
|
-
)
|
|
768
|
+
if answer is not None
|
|
751
769
|
else "(No response received)"
|
|
752
770
|
)
|
|
753
771
|
messages.append(Message(author=Author.USER, text=feedback_text))
|
|
@@ -762,10 +780,16 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
762
780
|
)
|
|
763
781
|
feedback_result: Tuple[str, Any] = (
|
|
764
782
|
"user_feedback",
|
|
765
|
-
|
|
783
|
+
"Feedback resolved. Do not request this feedback again. "
|
|
784
|
+
f"Decision ID: {decision_id}\n"
|
|
785
|
+
f"Question: {feedback_question}\n"
|
|
786
|
+
f"Response: {feedback_text}",
|
|
766
787
|
)
|
|
788
|
+
if resolved_feedback is not None:
|
|
789
|
+
resolved_feedback[decision_id] = feedback_text
|
|
790
|
+
feedback_results = [*synthetic_results, feedback_result]
|
|
767
791
|
result_texts = self._process_results(
|
|
768
|
-
|
|
792
|
+
feedback_results, collected_contexts=collected_contexts
|
|
769
793
|
)
|
|
770
794
|
if result_texts:
|
|
771
795
|
messages.append(
|
|
@@ -774,8 +798,20 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
774
798
|
text="\n\n".join(result_texts),
|
|
775
799
|
)
|
|
776
800
|
)
|
|
777
|
-
return
|
|
778
|
-
|
|
801
|
+
return feedback_results
|
|
802
|
+
|
|
803
|
+
tool_calls = [
|
|
804
|
+
(name, args) for name, args in tool_calls if name != "user_feedback"
|
|
805
|
+
]
|
|
806
|
+
if not tool_calls:
|
|
807
|
+
result_texts = self._process_results(
|
|
808
|
+
synthetic_results, collected_contexts=collected_contexts
|
|
809
|
+
)
|
|
810
|
+
if result_texts:
|
|
811
|
+
messages.append(
|
|
812
|
+
Message(author=Author.NUCLIA, text="\n\n".join(result_texts))
|
|
813
|
+
)
|
|
814
|
+
return synthetic_results
|
|
779
815
|
|
|
780
816
|
# --- normal tool execution path ---
|
|
781
817
|
attempted_tool_calls = (
|
|
@@ -818,16 +854,34 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
818
854
|
)
|
|
819
855
|
calls_to_execute.append((name, tool_arguments))
|
|
820
856
|
|
|
857
|
+
async def execute_with_delegation(
|
|
858
|
+
tool_name: str, tool_arguments: Dict[str, Any]
|
|
859
|
+
) -> Tuple[str, Any]:
|
|
860
|
+
await memory.add_step(
|
|
861
|
+
step_module=self.config.module,
|
|
862
|
+
step_title=self.step_title(f"Delegating to {tool_name}"),
|
|
863
|
+
step_reason="Starting selected tool",
|
|
864
|
+
step_agent_path=agent_path,
|
|
865
|
+
step_value=tool_name,
|
|
866
|
+
timeit=0,
|
|
867
|
+
)
|
|
868
|
+
return await self.execute_tool_call(
|
|
869
|
+
memory, manager, tool_name, tool_arguments
|
|
870
|
+
)
|
|
871
|
+
|
|
821
872
|
executed_results = []
|
|
822
|
-
if calls_to_execute:
|
|
873
|
+
if calls_to_execute and self.config.parallel_tool_calls:
|
|
823
874
|
executed_results = await asyncio.gather(
|
|
824
875
|
*[
|
|
825
|
-
|
|
876
|
+
execute_with_delegation(name, args)
|
|
826
877
|
for name, args in calls_to_execute
|
|
827
878
|
]
|
|
828
879
|
)
|
|
880
|
+
elif calls_to_execute:
|
|
881
|
+
for name, args in calls_to_execute:
|
|
882
|
+
executed_results.append(await execute_with_delegation(name, args))
|
|
829
883
|
|
|
830
|
-
results = [*executed_results, *skipped_results]
|
|
884
|
+
results = [*synthetic_results, *executed_results, *skipped_results]
|
|
831
885
|
for (tool_name, tool_arguments), (_, result) in zip(
|
|
832
886
|
calls_to_execute, executed_results
|
|
833
887
|
):
|
|
@@ -867,6 +921,20 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
867
921
|
)
|
|
868
922
|
return list(results)
|
|
869
923
|
|
|
924
|
+
@staticmethod
|
|
925
|
+
def _has_unresolved_feedback(
|
|
926
|
+
tool_calls: List[Tuple[str, Any]], resolved_feedback: Dict[str, str]
|
|
927
|
+
) -> bool:
|
|
928
|
+
for name, args in tool_calls:
|
|
929
|
+
if name != "user_feedback" or not args or not args.get("question"):
|
|
930
|
+
continue
|
|
931
|
+
decision_id = args.get("decision_id") or (
|
|
932
|
+
f"legacy:{args['question'].strip().lower()}"
|
|
933
|
+
)
|
|
934
|
+
if decision_id not in resolved_feedback:
|
|
935
|
+
return True
|
|
936
|
+
return False
|
|
937
|
+
|
|
870
938
|
async def _reactive_loop(
|
|
871
939
|
self,
|
|
872
940
|
question: str,
|
|
@@ -899,6 +967,7 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
899
967
|
finished = False
|
|
900
968
|
total_input_tokens = 0.0
|
|
901
969
|
total_output_tokens = 0.0
|
|
970
|
+
resolved_feedback: Dict[str, str] = {}
|
|
902
971
|
attempted_tool_calls: Dict[str, ToolAttempt] = {}
|
|
903
972
|
while not finished and iteration < self.config.max_iterations:
|
|
904
973
|
iteration += 1
|
|
@@ -937,20 +1006,28 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
937
1006
|
output_nuclia_tokens=output_tokens,
|
|
938
1007
|
)
|
|
939
1008
|
|
|
940
|
-
if not tool_calls
|
|
1009
|
+
if not tool_calls:
|
|
941
1010
|
finished = True
|
|
942
1011
|
break
|
|
943
1012
|
|
|
944
1013
|
# Execute tool calls (handles user_feedback and normal tool calls)
|
|
1014
|
+
requested_new_feedback = SmartAgent._has_unresolved_feedback(
|
|
1015
|
+
tool_calls, resolved_feedback
|
|
1016
|
+
)
|
|
945
1017
|
_ = await self._execute_tool_calls_turn(
|
|
946
1018
|
memory=memory,
|
|
947
1019
|
manager=manager,
|
|
948
1020
|
messages=messages,
|
|
949
1021
|
tool_calls=tool_calls,
|
|
950
1022
|
turn_label=f"iteration {iteration}/{self.config.max_iterations}",
|
|
1023
|
+
resolved_feedback=resolved_feedback,
|
|
951
1024
|
collected_contexts=contexts,
|
|
952
1025
|
attempted_tool_calls=attempted_tool_calls,
|
|
953
1026
|
)
|
|
1027
|
+
if not requested_new_feedback and any(
|
|
1028
|
+
name == "task_complete" for name, _ in tool_calls
|
|
1029
|
+
):
|
|
1030
|
+
finished = True
|
|
954
1031
|
|
|
955
1032
|
reason = (
|
|
956
1033
|
"Task complete signal received"
|
|
@@ -1026,6 +1103,8 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
1026
1103
|
steps: List[Dict[str, Any]],
|
|
1027
1104
|
summary: str,
|
|
1028
1105
|
tools: List[Tool],
|
|
1106
|
+
messages: List[Message],
|
|
1107
|
+
resolved_feedback: Dict[str, str],
|
|
1029
1108
|
) -> Tuple[List[Tuple[str, Any]], float, float, List[ToolAttempt]]:
|
|
1030
1109
|
"""Run the executor LLM turn: call tools guided by the current plan steps."""
|
|
1031
1110
|
system = PLAN_EXECUTE_EXECUTOR_SYSTEM_PROMPT_TEMPLATE.render(
|
|
@@ -1034,10 +1113,6 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
1034
1113
|
summary=summary,
|
|
1035
1114
|
extra_instructions=self.config.extra_prompt or "",
|
|
1036
1115
|
)
|
|
1037
|
-
messages: List[Message] = [
|
|
1038
|
-
Message(author=Author.USER, text=question),
|
|
1039
|
-
]
|
|
1040
|
-
|
|
1041
1116
|
all_results: List[Tuple[str, Any]] = []
|
|
1042
1117
|
total_input_tokens = 0.0
|
|
1043
1118
|
total_output_tokens = 0.0
|
|
@@ -1060,19 +1135,34 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
1060
1135
|
|
|
1061
1136
|
tool_calls = list(iterate_tools_resp(resp))
|
|
1062
1137
|
|
|
1063
|
-
if not tool_calls
|
|
1138
|
+
if not tool_calls:
|
|
1139
|
+
finished = True
|
|
1140
|
+
break
|
|
1141
|
+
|
|
1142
|
+
executable_calls = [
|
|
1143
|
+
(name, args) for name, args in tool_calls if name != "task_complete"
|
|
1144
|
+
]
|
|
1145
|
+
if not executable_calls:
|
|
1064
1146
|
finished = True
|
|
1065
1147
|
break
|
|
1066
1148
|
|
|
1149
|
+
requested_new_feedback = SmartAgent._has_unresolved_feedback(
|
|
1150
|
+
executable_calls, resolved_feedback
|
|
1151
|
+
)
|
|
1067
1152
|
results = await self._execute_tool_calls_turn(
|
|
1068
1153
|
memory=memory,
|
|
1069
1154
|
manager=manager,
|
|
1070
1155
|
messages=messages,
|
|
1071
|
-
tool_calls=
|
|
1156
|
+
tool_calls=executable_calls,
|
|
1072
1157
|
turn_label=f"executor turn {executor_turn}/{max_executor_turns}",
|
|
1158
|
+
resolved_feedback=resolved_feedback,
|
|
1073
1159
|
attempted_tool_calls=attempted_tool_calls,
|
|
1074
1160
|
)
|
|
1075
1161
|
all_results.extend(results)
|
|
1162
|
+
if not requested_new_feedback and any(
|
|
1163
|
+
name == "task_complete" for name, _ in tool_calls
|
|
1164
|
+
):
|
|
1165
|
+
finished = True
|
|
1076
1166
|
|
|
1077
1167
|
return (
|
|
1078
1168
|
all_results,
|
|
@@ -1100,6 +1190,8 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
1100
1190
|
iteration = 0
|
|
1101
1191
|
total_input_tokens = 0.0
|
|
1102
1192
|
total_output_tokens = 0.0
|
|
1193
|
+
executor_messages = [Message(author=Author.USER, text=question)]
|
|
1194
|
+
resolved_feedback: Dict[str, str] = {}
|
|
1103
1195
|
status = "done"
|
|
1104
1196
|
steps: List[Dict[str, Any]] = []
|
|
1105
1197
|
|
|
@@ -1172,6 +1264,8 @@ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
|
|
|
1172
1264
|
steps=steps,
|
|
1173
1265
|
summary=summary,
|
|
1174
1266
|
tools=tools,
|
|
1267
|
+
messages=executor_messages,
|
|
1268
|
+
resolved_feedback=resolved_feedback,
|
|
1175
1269
|
)
|
|
1176
1270
|
total_input_tokens += exec_in_tokens
|
|
1177
1271
|
total_output_tokens += exec_out_tokens
|
{hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart/config.py
RENAMED
|
@@ -121,6 +121,14 @@ class SmartAgentConfig(ContextAgentConfig):
|
|
|
121
121
|
title="Max iterations",
|
|
122
122
|
description="Maximum number of planning and execution iterations before stopping",
|
|
123
123
|
)
|
|
124
|
+
parallel_tool_calls: bool = Field(
|
|
125
|
+
default=True,
|
|
126
|
+
title="Parallel tool calls",
|
|
127
|
+
description=(
|
|
128
|
+
"Execute tool calls selected in the same turn concurrently. Disable for "
|
|
129
|
+
"interactive or stateful tools that must complete in order."
|
|
130
|
+
),
|
|
131
|
+
)
|
|
124
132
|
extra_prompt: Optional[str] = Field(
|
|
125
133
|
None,
|
|
126
134
|
title="Extra prompt",
|
{hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart/prompts.py
RENAMED
|
@@ -11,6 +11,9 @@ REACTIVE_SYSTEM_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(
|
|
|
11
11
|
You are a smart assistant that selects tools to gather information needed to answer a user's question.
|
|
12
12
|
Choose the best tool or tools for the task. You may call multiple tools in one turn.
|
|
13
13
|
Call task_complete when you have enough information to answer the question.
|
|
14
|
+
When a "Feedback resolved" result exists for a decision ID, that decision is
|
|
15
|
+
answered. Reuse the same decision ID if wording changes and continue with the
|
|
16
|
+
recorded response.
|
|
14
17
|
Guidelines:
|
|
15
18
|
- Prefer finishing as soon as the gathered information is sufficient to answer the question.
|
|
16
19
|
- Do not repeat the exact same tool call with the same arguments unless there is a clear reason that new information will be produced.
|
|
@@ -151,6 +154,8 @@ described in the retrieval plan below.
|
|
|
151
154
|
|
|
152
155
|
Call the tools needed to fulfil this plan. You may call multiple tools.
|
|
153
156
|
Call task_complete once you have executed all planned retrieval steps.
|
|
157
|
+
When a tool result says "Feedback resolved", its decision ID is already answered.
|
|
158
|
+
Reuse that ID if wording changes and continue using the recorded response.
|
|
154
159
|
{% if extra_instructions %}
|
|
155
160
|
Extra instructions: {{ extra_instructions }}
|
|
156
161
|
{% endif %}"""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from copy import deepcopy
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from types import SimpleNamespace
|
|
4
|
+
from types import MethodType, SimpleNamespace
|
|
5
5
|
from typing import Any, ClassVar, cast
|
|
6
6
|
from unittest.mock import patch
|
|
7
7
|
|
|
@@ -97,6 +97,319 @@ CONFIG = {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
@pytest.mark.asyncio
|
|
101
|
+
async def test_plan_executor_preserves_messages_between_iterations():
|
|
102
|
+
observed_messages = []
|
|
103
|
+
|
|
104
|
+
async def choose_tools(_self, *, messages, **_kwargs):
|
|
105
|
+
observed_messages.append(messages)
|
|
106
|
+
return SimpleNamespace(tools=None), 0, 0
|
|
107
|
+
|
|
108
|
+
smart: Any = SimpleNamespace(
|
|
109
|
+
config=SimpleNamespace(extra_prompt=None, max_iterations=1),
|
|
110
|
+
choose_tools=MethodType(choose_tools, object()),
|
|
111
|
+
_planner_attempts_from_registry=SmartAgent._planner_attempts_from_registry,
|
|
112
|
+
)
|
|
113
|
+
memory: Any = SimpleNamespace(get_tracking_info=lambda: None)
|
|
114
|
+
manager: Any = None
|
|
115
|
+
messages = [
|
|
116
|
+
Message(author=Author.USER, text="Coordinate the incident"),
|
|
117
|
+
Message(author=Author.NUCLIA, text="Tour Manager approved the change"),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
await SmartAgent._call_executor(
|
|
121
|
+
smart,
|
|
122
|
+
memory=memory,
|
|
123
|
+
manager=manager,
|
|
124
|
+
question="Coordinate the incident",
|
|
125
|
+
steps=[{"description": "Continue coordination", "reason": "Approval received"}],
|
|
126
|
+
summary="The Tour Manager approved the change",
|
|
127
|
+
tools=[],
|
|
128
|
+
messages=messages,
|
|
129
|
+
resolved_feedback={},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
assert observed_messages == [messages]
|
|
133
|
+
assert observed_messages[0] is messages
|
|
134
|
+
assert observed_messages[0][-1].text == "Tour Manager approved the change"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@pytest.mark.asyncio
|
|
138
|
+
async def test_feedback_decision_id_survives_rephrasing():
|
|
139
|
+
class Memory:
|
|
140
|
+
calls = 0
|
|
141
|
+
|
|
142
|
+
def get_session_id(self):
|
|
143
|
+
return "session-1"
|
|
144
|
+
|
|
145
|
+
async def send_feedback(self, feedback):
|
|
146
|
+
self.calls += 1
|
|
147
|
+
return UserToAgentInteraction(
|
|
148
|
+
request_id=feedback.request_id,
|
|
149
|
+
response="Approved",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
async def add_step(self, **_kwargs):
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
memory: Any = Memory()
|
|
156
|
+
manager: Any = None
|
|
157
|
+
smart: Any = SimpleNamespace(
|
|
158
|
+
config=SimpleNamespace(
|
|
159
|
+
id="operations",
|
|
160
|
+
module="smart",
|
|
161
|
+
feedback_timeout=5_000,
|
|
162
|
+
max_tool_result_bytes=None,
|
|
163
|
+
max_tool_result_item_bytes=None,
|
|
164
|
+
),
|
|
165
|
+
step_title=lambda title: title,
|
|
166
|
+
)
|
|
167
|
+
smart._process_results = MethodType(SmartAgent._process_results, smart)
|
|
168
|
+
smart._inspect_result = MethodType(SmartAgent._inspect_result, smart)
|
|
169
|
+
messages: list[Message] = []
|
|
170
|
+
resolved_feedback: dict[str, str] = {}
|
|
171
|
+
|
|
172
|
+
for question in (
|
|
173
|
+
"Approve the shorter set?",
|
|
174
|
+
"Can we reduce the set to 60 minutes?",
|
|
175
|
+
):
|
|
176
|
+
await SmartAgent._execute_tool_calls_turn(
|
|
177
|
+
smart,
|
|
178
|
+
memory=memory,
|
|
179
|
+
manager=manager,
|
|
180
|
+
messages=messages,
|
|
181
|
+
tool_calls=[
|
|
182
|
+
(
|
|
183
|
+
"user_feedback",
|
|
184
|
+
{"decision_id": "set-duration", "question": question},
|
|
185
|
+
)
|
|
186
|
+
],
|
|
187
|
+
turn_label="test",
|
|
188
|
+
resolved_feedback=resolved_feedback,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
assert memory.calls == 1
|
|
192
|
+
assert resolved_feedback == {"set-duration": "Approved"}
|
|
193
|
+
assert "Decision ID: set-duration" in messages[-1].text
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@pytest.mark.asyncio
|
|
197
|
+
async def test_different_feedback_decisions_are_both_requested():
|
|
198
|
+
class Memory:
|
|
199
|
+
calls = 0
|
|
200
|
+
|
|
201
|
+
def get_session_id(self):
|
|
202
|
+
return "session-1"
|
|
203
|
+
|
|
204
|
+
async def send_feedback(self, feedback):
|
|
205
|
+
self.calls += 1
|
|
206
|
+
return UserToAgentInteraction(
|
|
207
|
+
request_id=feedback.request_id,
|
|
208
|
+
response="Approved",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
async def add_step(self, **_kwargs):
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
memory: Any = Memory()
|
|
215
|
+
manager: Any = None
|
|
216
|
+
smart: Any = SimpleNamespace(
|
|
217
|
+
config=SimpleNamespace(
|
|
218
|
+
id="operations",
|
|
219
|
+
module="smart",
|
|
220
|
+
feedback_timeout=5_000,
|
|
221
|
+
max_tool_result_bytes=None,
|
|
222
|
+
max_tool_result_item_bytes=None,
|
|
223
|
+
),
|
|
224
|
+
step_title=lambda title: title,
|
|
225
|
+
)
|
|
226
|
+
smart._process_results = MethodType(SmartAgent._process_results, smart)
|
|
227
|
+
smart._inspect_result = MethodType(SmartAgent._inspect_result, smart)
|
|
228
|
+
messages: list[Message] = []
|
|
229
|
+
resolved_feedback: dict[str, str] = {}
|
|
230
|
+
|
|
231
|
+
for decision_id in ("set-duration", "gate-activation"):
|
|
232
|
+
await SmartAgent._execute_tool_calls_turn(
|
|
233
|
+
smart,
|
|
234
|
+
memory=memory,
|
|
235
|
+
manager=manager,
|
|
236
|
+
messages=messages,
|
|
237
|
+
tool_calls=[
|
|
238
|
+
(
|
|
239
|
+
"user_feedback",
|
|
240
|
+
{"decision_id": decision_id, "question": f"Approve {decision_id}?"},
|
|
241
|
+
)
|
|
242
|
+
],
|
|
243
|
+
turn_label="test",
|
|
244
|
+
resolved_feedback=resolved_feedback,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
assert memory.calls == 2
|
|
248
|
+
assert resolved_feedback == {
|
|
249
|
+
"set-duration": "Approved",
|
|
250
|
+
"gate-activation": "Approved",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@pytest.mark.asyncio
|
|
255
|
+
async def test_feedback_response_with_upstream_request_id_is_cached():
|
|
256
|
+
class Memory:
|
|
257
|
+
def get_session_id(self):
|
|
258
|
+
return "session-1"
|
|
259
|
+
|
|
260
|
+
async def send_feedback(self, _feedback):
|
|
261
|
+
return UserToAgentInteraction(
|
|
262
|
+
request_id="upstream-a2a-request", response="Approved"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
async def add_step(self, **_kwargs):
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
smart: Any = SimpleNamespace(
|
|
269
|
+
config=SimpleNamespace(
|
|
270
|
+
id="operations",
|
|
271
|
+
module="smart",
|
|
272
|
+
feedback_timeout=5_000,
|
|
273
|
+
max_tool_result_bytes=None,
|
|
274
|
+
max_tool_result_item_bytes=None,
|
|
275
|
+
),
|
|
276
|
+
step_title=lambda title: title,
|
|
277
|
+
)
|
|
278
|
+
smart._process_results = MethodType(SmartAgent._process_results, smart)
|
|
279
|
+
smart._inspect_result = MethodType(SmartAgent._inspect_result, smart)
|
|
280
|
+
resolved_feedback: dict[str, str] = {}
|
|
281
|
+
memory: Any = Memory()
|
|
282
|
+
manager: Any = None
|
|
283
|
+
|
|
284
|
+
await SmartAgent._execute_tool_calls_turn(
|
|
285
|
+
smart,
|
|
286
|
+
memory=memory,
|
|
287
|
+
manager=manager,
|
|
288
|
+
messages=[],
|
|
289
|
+
tool_calls=[
|
|
290
|
+
(
|
|
291
|
+
"user_feedback",
|
|
292
|
+
{"decision_id": "set-duration", "question": "Approve?"},
|
|
293
|
+
)
|
|
294
|
+
],
|
|
295
|
+
turn_label="test",
|
|
296
|
+
resolved_feedback=resolved_feedback,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
assert resolved_feedback == {"set-duration": "Approved"}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@pytest.mark.asyncio
|
|
303
|
+
async def test_cached_feedback_does_not_skip_other_tools_in_same_turn():
|
|
304
|
+
class Memory:
|
|
305
|
+
async def add_step(self, **_kwargs):
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
async def execute_tool_call(_memory, _manager, name, _args):
|
|
309
|
+
return name, "tool result"
|
|
310
|
+
|
|
311
|
+
smart: Any = SimpleNamespace(
|
|
312
|
+
config=SimpleNamespace(
|
|
313
|
+
id="operations",
|
|
314
|
+
module="smart",
|
|
315
|
+
parallel_tool_calls=False,
|
|
316
|
+
max_tool_result_bytes=None,
|
|
317
|
+
max_tool_result_item_bytes=None,
|
|
318
|
+
),
|
|
319
|
+
step_title=lambda title: title,
|
|
320
|
+
execute_tool_call=execute_tool_call,
|
|
321
|
+
)
|
|
322
|
+
smart._process_results = MethodType(SmartAgent._process_results, smart)
|
|
323
|
+
smart._inspect_result = MethodType(SmartAgent._inspect_result, smart)
|
|
324
|
+
smart._tool_call_key = SmartAgent._tool_call_key
|
|
325
|
+
smart._classify_tool_result = SmartAgent._classify_tool_result
|
|
326
|
+
memory: Any = Memory()
|
|
327
|
+
manager: Any = None
|
|
328
|
+
|
|
329
|
+
results = await SmartAgent._execute_tool_calls_turn(
|
|
330
|
+
smart,
|
|
331
|
+
memory=memory,
|
|
332
|
+
manager=manager,
|
|
333
|
+
messages=[],
|
|
334
|
+
tool_calls=[
|
|
335
|
+
(
|
|
336
|
+
"user_feedback",
|
|
337
|
+
{"decision_id": "set-duration", "question": "Reduce the set?"},
|
|
338
|
+
),
|
|
339
|
+
("venue_lookup", {"gate": "C"}),
|
|
340
|
+
],
|
|
341
|
+
turn_label="test",
|
|
342
|
+
resolved_feedback={"set-duration": "Approved"},
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
assert [name for name, _ in results] == ["user_feedback", "venue_lookup"]
|
|
346
|
+
assert results[1] == ("venue_lookup", "tool result")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@pytest.mark.asyncio
|
|
350
|
+
async def test_executor_runs_tools_returned_with_task_complete():
|
|
351
|
+
smart: Any = SimpleNamespace(
|
|
352
|
+
config=SimpleNamespace(max_iterations=1, extra_prompt=None),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
async def choose_tools(**_kwargs):
|
|
356
|
+
return (
|
|
357
|
+
SimpleNamespace(
|
|
358
|
+
tools={
|
|
359
|
+
"calls": [
|
|
360
|
+
SimpleNamespace(
|
|
361
|
+
function=SimpleNamespace(
|
|
362
|
+
name="a2a_query_of_logistics", arguments={}
|
|
363
|
+
)
|
|
364
|
+
),
|
|
365
|
+
SimpleNamespace(
|
|
366
|
+
function=SimpleNamespace(name="task_complete", arguments={})
|
|
367
|
+
),
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
),
|
|
371
|
+
0,
|
|
372
|
+
0,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
executed_calls = []
|
|
376
|
+
|
|
377
|
+
async def execute_turn(**kwargs):
|
|
378
|
+
executed_calls.extend(kwargs["tool_calls"])
|
|
379
|
+
return [("a2a_query of logistics", "result")]
|
|
380
|
+
|
|
381
|
+
smart.choose_tools = choose_tools
|
|
382
|
+
smart._execute_tool_calls_turn = execute_turn
|
|
383
|
+
smart._planner_attempts_from_registry = SmartAgent._planner_attempts_from_registry
|
|
384
|
+
memory: Any = SimpleNamespace(get_tracking_info=lambda: None)
|
|
385
|
+
manager: Any = None
|
|
386
|
+
|
|
387
|
+
results, _, _, _ = await SmartAgent._call_executor(
|
|
388
|
+
smart,
|
|
389
|
+
memory=memory,
|
|
390
|
+
manager=manager,
|
|
391
|
+
question="question",
|
|
392
|
+
steps=[],
|
|
393
|
+
summary="",
|
|
394
|
+
tools=[],
|
|
395
|
+
messages=[],
|
|
396
|
+
resolved_feedback={},
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
assert executed_calls == [("a2a_query_of_logistics", {})]
|
|
400
|
+
assert results == [("a2a_query of logistics", "result")]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_new_feedback_prevents_same_turn_completion():
|
|
404
|
+
calls = [
|
|
405
|
+
("user_feedback", {"decision_id": "approval", "question": "Approve?"}),
|
|
406
|
+
("task_complete", {}),
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
assert SmartAgent._has_unresolved_feedback(calls, {})
|
|
410
|
+
assert not SmartAgent._has_unresolved_feedback(calls, {"approval": "yes"})
|
|
411
|
+
|
|
412
|
+
|
|
100
413
|
@pytest.mark.asyncio
|
|
101
414
|
async def test_choose_tools_uses_auto_tool_choice():
|
|
102
415
|
smart_agent = SmartAgent(
|
|
@@ -367,7 +680,7 @@ async def test_smart_with_user_feedback():
|
|
|
367
680
|
|
|
368
681
|
async def mock_send_feedback(feedback):
|
|
369
682
|
return UserToAgentInteraction(
|
|
370
|
-
request_id=
|
|
683
|
+
request_id="upstream-a2a-request",
|
|
371
684
|
response="Snoopy the pet",
|
|
372
685
|
)
|
|
373
686
|
|
|
@@ -391,7 +704,16 @@ async def test_smart_with_user_feedback():
|
|
|
391
704
|
)
|
|
392
705
|
|
|
393
706
|
assert question_memory.final_answer
|
|
394
|
-
|
|
707
|
+
feedback_steps = [
|
|
708
|
+
step
|
|
709
|
+
for step in question_memory.steps
|
|
710
|
+
if step.module == "smart" and "user feedback" in step.title.lower()
|
|
711
|
+
]
|
|
712
|
+
assert feedback_steps, "Expected a user feedback step"
|
|
713
|
+
assert any(
|
|
714
|
+
"Feedback response: Snoopy the pet" in (step.value or "")
|
|
715
|
+
for step in feedback_steps
|
|
716
|
+
)
|
|
395
717
|
assert any(
|
|
396
718
|
keyword.lower() in question_memory.final_answer.lower()
|
|
397
719
|
for keyword in ["Snoopy", "dog", "Carmen"]
|
|
File without changes
|
|
File without changes
|
{hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart/__init__.py
RENAMED
|
File without changes
|
{hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/src/hyperforge_smart/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/tests/test_result_payload.py
RENAMED
|
File without changes
|
{hyperforge_smart-1.0.0.post129 → hyperforge_smart-1.0.0.post136}/tests/test_smart_mcp_perplexity.py
RENAMED
|
File without changes
|