soprano-sdk 0.2.12__py3-none-any.whl → 0.2.13__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.
@@ -56,7 +56,8 @@ class WorkflowEngine:
56
56
 
57
57
  logger.info(
58
58
  f"Workflow loaded: {self.workflow_name} v{self.workflow_version} "
59
- f"({len(self.steps)} steps, {len(self.outcomes)} outcomes)"
59
+ f"({len(self.steps)} steps, {len(self.outcomes)} outcomes, "
60
+ f"{len(self.collector_node_field_map)} collector nodes)"
60
61
  )
61
62
 
62
63
  except Exception as e:
@@ -202,6 +203,7 @@ class WorkflowEngine:
202
203
  def load_steps(self):
203
204
  prepared_steps: list = []
204
205
  mfa_redirects: Dict[str, str] = {}
206
+ self.collector_node_field_map: Dict[str, str] = {} # Map of node_id -> field
205
207
 
206
208
  for step in self.config['steps']:
207
209
  step_id = step['id']
@@ -228,6 +230,14 @@ class WorkflowEngine:
228
230
 
229
231
  prepared_steps.append(step)
230
232
 
233
+ # Build collector node -> field map
234
+ for step in prepared_steps:
235
+ if step.get('action') == 'collect_input':
236
+ node_id = step.get('id')
237
+ field = step.get('field')
238
+ if node_id and field:
239
+ self.collector_node_field_map[node_id] = field
240
+
231
241
  for step in prepared_steps:
232
242
  if step['id'] in self.mfa_validator_steps: # MFA Validator
233
243
  continue
@@ -196,17 +196,6 @@ class CollectInputStrategy(ActionStrategy):
196
196
  if context_value is None:
197
197
  return
198
198
 
199
- # Check if this node has already executed - if so, don't overwrite the collected value
200
- execution_order = state.get(WorkflowKeys.NODE_EXECUTION_ORDER, [])
201
- if self.step_id in execution_order:
202
- logger.info(f"Skipping context value for '{self.field}' - node '{self.step_id}' already executed")
203
- span.add_event("context.value_skipped", {
204
- "field": self.field,
205
- "reason": "node_already_executed",
206
- "existing_value": str(state.get(self.field))
207
- })
208
- return
209
-
210
199
  logger.info(f"Using context value for '{self.field}': {context_value}")
211
200
  state[self.field] = context_value
212
201
  span.add_event("context.value_used", {"field": self.field, "value": str(context_value)})
soprano_sdk/tools.py CHANGED
@@ -83,26 +83,35 @@ class WorkflowTool:
83
83
  callback_handler = CallbackHandler()
84
84
  config = {"configurable": {"thread_id": thread_id}, "callbacks": [callback_handler]}
85
85
 
86
- span.add_event("context.updated", {"fields": list(initial_context.keys())})
87
-
88
86
  state = self.graph.get_state(config)
89
87
 
90
- # Update engine context on both resume and fresh start
91
- # Note: collect_input nodes will check NODE_EXECUTION_ORDER to avoid
92
- # overwriting already-collected values
93
- self.engine.update_context(initial_context)
94
-
88
+ # Intelligently update context based on workflow state
95
89
  if state.next:
90
+ # Workflow is resuming - only update fields that haven't been collected yet
96
91
  span.set_attribute("workflow.resumed", True)
97
92
  logger.info(f"[WorkflowTool] Resuming interrupted workflow {self.name} (thread: {thread_id})")
93
+
94
+ filtered_context = self._filter_already_collected_fields(state.values, initial_context)
95
+ self.engine.update_context(filtered_context)
96
+
97
+ span.add_event("context.updated", {
98
+ "fields": list(filtered_context.keys()),
99
+ "filtered_out": list(set(initial_context.keys()) - set(filtered_context.keys()))
100
+ })
101
+
98
102
  result = self.graph.invoke(
99
- Command(resume=user_message or "", update=initial_context),
103
+ Command(resume=user_message or "", update=filtered_context),
100
104
  config=config
101
105
  )
102
106
 
103
107
  else:
108
+ # Fresh start - update all fields from initial_context
104
109
  span.set_attribute("workflow.resumed", False)
105
110
  logger.info(f"[WorkflowTool] Starting fresh workflow {self.name} (thread: {thread_id})")
111
+
112
+ self.engine.update_context(initial_context)
113
+ span.add_event("context.updated", {"fields": list(initial_context.keys())})
114
+
106
115
  result = self.graph.invoke(initial_context, config=config)
107
116
 
108
117
  final_state = self.graph.get_state(config)
@@ -129,6 +138,51 @@ class WorkflowTool:
129
138
  span.set_attribute("workflow.status", "completed")
130
139
  return self.engine.get_outcome_message(result)
131
140
 
141
+ def _filter_already_collected_fields(
142
+ self,
143
+ current_state: Dict[str, Any],
144
+ initial_context: Optional[Dict[str, Any]]
145
+ ) -> Dict[str, Any]:
146
+ """
147
+ Filter initial_context to exclude fields that have already been collected.
148
+
149
+ Args:
150
+ current_state: Current workflow state
151
+ initial_context: Context to filter
152
+
153
+ Returns:
154
+ Filtered context with only uncollected fields
155
+ """
156
+ if not initial_context:
157
+ return {}
158
+
159
+ from .core.constants import WorkflowKeys
160
+
161
+ execution_order = current_state.get(WorkflowKeys.NODE_EXECUTION_ORDER, [])
162
+
163
+ node_to_field_map = self.engine.collector_node_field_map
164
+
165
+ # Determine which fields have already been collected
166
+ collected_fields = set()
167
+ for executed_node_id in execution_order:
168
+ if executed_node_id in node_to_field_map:
169
+ collected_fields.add(node_to_field_map[executed_node_id])
170
+
171
+ # Filter initial_context to exclude already-collected fields
172
+ filtered_context = {
173
+ field: value
174
+ for field, value in initial_context.items()
175
+ if field not in collected_fields
176
+ }
177
+
178
+ if collected_fields:
179
+ logger.info(
180
+ f"[WorkflowTool] Filtered out already-collected fields: {collected_fields}. "
181
+ f"Updating context with: {list(filtered_context.keys())}"
182
+ )
183
+
184
+ return filtered_context
185
+
132
186
  def resume(
133
187
  self,
134
188
  thread_id: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soprano-sdk
3
- Version: 0.2.12
3
+ Version: 0.2.13
4
4
  Summary: YAML-driven workflow engine with AI agent integration for building conversational SOPs
5
5
  Author: Arvind Thangamani
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  soprano_sdk/__init__.py,sha256=YZVl_SwQ0C-E_5_f1AwUe_hPcbgCt8k7k4_WAHM8vjE,243
2
2
  soprano_sdk/engine.py,sha256=EFK91iTHjp72otLN6Kg-yeLye2J3CAKN0QH4FI2taL8,14838
3
- soprano_sdk/tools.py,sha256=0zIMCQ0voPGXFHgQCN4jKaMPBXCbnia4Ly5hssPI3oU,9033
3
+ soprano_sdk/tools.py,sha256=dmJ0OZ7Bj3rvjBQvLzgWlYRFVtNJOyMO2jLqaS13cAc,10971
4
4
  soprano_sdk/agents/__init__.py,sha256=Yzbtv6iP_ABRgZo0IUjy9vDofEvLFbOjuABw758176A,636
5
5
  soprano_sdk/agents/adaptor.py,sha256=dRk4pU1UgUhsKRotOzILjeXk0Zzoj7GzB-SKyBgKbNs,3242
6
6
  soprano_sdk/agents/factory.py,sha256=Aucfz4rZVKCXMAQtbGAqp1JR8aYwa66mokRmKkKGhYA,6699
@@ -9,14 +9,14 @@ soprano_sdk/authenticators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
9
9
  soprano_sdk/authenticators/mfa.py,sha256=Vew9Nb8pIRTw9hKbEZTH3YScY-fZ_TLq4ZuCzc-wbr8,7387
10
10
  soprano_sdk/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  soprano_sdk/core/constants.py,sha256=UPXlRbF7gsOUNOV0Lm0jvgFfgZX7JrsV6n9I5csMfns,3508
12
- soprano_sdk/core/engine.py,sha256=vM2-nAvDc6Oam-q_BegabBT4uKVMV8DVV10SyJJPgVw,11762
12
+ soprano_sdk/core/engine.py,sha256=HKYoqwDm541pWSWwEKHxLlL3PX90Ux_5l_-HqihgL-g,12245
13
13
  soprano_sdk/core/rollback_strategies.py,sha256=NjDTtBCZlqyDql5PSwI9SMDLK7_BNlTxbW_cq_5gV0g,7783
14
14
  soprano_sdk/core/state.py,sha256=k8ojLfWgjES3p9XWMeGU5s4UK-Xa5T8mS4VtZzTrcDw,2961
15
15
  soprano_sdk/nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  soprano_sdk/nodes/async_function.py,sha256=v6WujLKm8NXX2iAkJ7Gz_QIVCtWFrpC6nnPyyfuCxXs,9354
17
17
  soprano_sdk/nodes/base.py,sha256=idFyOGGPnjsASYnrOF_NIh7eFcSuJqw61EoVN_WCTaU,2360
18
18
  soprano_sdk/nodes/call_function.py,sha256=afYBmj5Aditbkvb_7gD3CsXBEEUohcsC1_cdHfcOunE,5847
19
- soprano_sdk/nodes/collect_input.py,sha256=IHDzhfh97-AxtaWfRbSFXgaaxnw4BLgumfPt7rbqA0s,25125
19
+ soprano_sdk/nodes/collect_input.py,sha256=0M_-orYlOFvEBAiW1dDnPef1i77n41CjdV8ZKpWqLBE,24562
20
20
  soprano_sdk/nodes/factory.py,sha256=IbBzT4FKBnYw5PuSo7uDONV3HSFtoyqjBQQtXtUY2IY,1756
21
21
  soprano_sdk/routing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  soprano_sdk/routing/router.py,sha256=Z218r4BMbmlL9282ombutAoKsIs1WHZ2d5YHnbCeet8,3698
@@ -29,7 +29,7 @@ soprano_sdk/utils/tracing.py,sha256=gSHeBDLe-MbAZ9rkzpCoGFveeMdR9KLaA6tteB0IWjk,
29
29
  soprano_sdk/validation/__init__.py,sha256=ImChmO86jYHU90xzTttto2-LmOUOmvY_ibOQaLRz5BA,262
30
30
  soprano_sdk/validation/schema.py,sha256=SlC4sq-ueEg0p_8Uox_cgPj9S-0AEEiOOlA1Vsu0DsE,15443
31
31
  soprano_sdk/validation/validator.py,sha256=GaCvHvjwVe88Z8yatQsueiPnqtq1oo5uN75gogzpQT0,8940
32
- soprano_sdk-0.2.12.dist-info/METADATA,sha256=t7Kq8Jrr6wYLa17TuLFyCBS6ACpX6NL7CXqPf2-QAH8,11343
33
- soprano_sdk-0.2.12.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
- soprano_sdk-0.2.12.dist-info/licenses/LICENSE,sha256=A1aBauSjPNtVehOXJe3WuvdU2xvM9H8XmigFMm6665s,1073
35
- soprano_sdk-0.2.12.dist-info/RECORD,,
32
+ soprano_sdk-0.2.13.dist-info/METADATA,sha256=UcRp0Owv8akIqBA-y5fQmQBgIFlFkelf9CfQ0iXIV4g,11343
33
+ soprano_sdk-0.2.13.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
+ soprano_sdk-0.2.13.dist-info/licenses/LICENSE,sha256=A1aBauSjPNtVehOXJe3WuvdU2xvM9H8XmigFMm6665s,1073
35
+ soprano_sdk-0.2.13.dist-info/RECORD,,