soprano-sdk 0.2.11__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.
@@ -20,7 +20,7 @@ class LangGraphAgentAdapter(AgentAdapter):
20
20
  self.agent = agent
21
21
 
22
22
  def invoke(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
23
- logger.info("Invoking LangGraph agent with messages")
23
+ logger.info("Invoking LangGraphAgentAdapter agent with messages")
24
24
  response = self.agent.invoke({"messages": messages})
25
25
 
26
26
  if structured_response := response.get('structured_response'):
@@ -44,7 +44,7 @@ class CrewAIAgentAdapter(AgentAdapter):
44
44
 
45
45
  def invoke(self, messages: List[Dict[str, str]]) -> Any:
46
46
  try:
47
- logger.info("Invoking LangGraph agent with messages")
47
+ logger.info("Invoking CrewAIAgentAdapter agent with messages")
48
48
  result = self.agent.kickoff(messages, response_format=self.output_schema)
49
49
 
50
50
  if structured_response := getattr(result, 'pydantic', None) :
@@ -65,7 +65,7 @@ class AgnoAgentAdapter(AgentAdapter):
65
65
 
66
66
  def invoke(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
67
67
  try:
68
- logger.info("Invoking LangGraph agent with messages")
68
+ logger.info("Invoking AgnoAgentAdapter agent with messages")
69
69
  response = self.agent.run(messages)
70
70
  agent_response = response.content if hasattr(response, 'content') else str(response)
71
71
 
@@ -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
@@ -195,6 +195,7 @@ class CollectInputStrategy(ActionStrategy):
195
195
  context_value = self.engine_context.get_context_value(self.field)
196
196
  if context_value is None:
197
197
  return
198
+
198
199
  logger.info(f"Using context value for '{self.field}': {context_value}")
199
200
  state[self.field] = context_value
200
201
  span.add_event("context.value_used", {"field": self.field, "value": str(context_value)})
@@ -375,7 +376,9 @@ class CollectInputStrategy(ActionStrategy):
375
376
  if self.is_structured_output:
376
377
  try:
377
378
  response_dict = json.loads(agent_response) if isinstance(agent_response, str) else agent_response
378
- prompt = response_dict.get("bot_response", "")
379
+ bot_response = response_dict.get("bot_response", None)
380
+ # Treat empty or whitespace-only bot_response as None
381
+ prompt = bot_response if (bot_response and bot_response.strip()) else None
379
382
  except (json.JSONDecodeError, TypeError, ValueError) as e:
380
383
  logger.error(f"Error When Converting Structured Output {agent_response} to JSON {e}")
381
384
  prompt = agent_response
soprano_sdk/tools.py CHANGED
@@ -83,21 +83,35 @@ class WorkflowTool:
83
83
  callback_handler = CallbackHandler()
84
84
  config = {"configurable": {"thread_id": thread_id}, "callbacks": [callback_handler]}
85
85
 
86
- self.engine.update_context(initial_context)
87
- span.add_event("context.updated", {"fields": list(initial_context.keys())})
88
-
89
86
  state = self.graph.get_state(config)
90
87
 
88
+ # Intelligently update context based on workflow state
91
89
  if state.next:
90
+ # Workflow is resuming - only update fields that haven't been collected yet
92
91
  span.set_attribute("workflow.resumed", True)
93
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
+
94
102
  result = self.graph.invoke(
95
- Command(resume=user_message or "", update=initial_context),
103
+ Command(resume=user_message or "", update=filtered_context),
96
104
  config=config
97
105
  )
106
+
98
107
  else:
108
+ # Fresh start - update all fields from initial_context
99
109
  span.set_attribute("workflow.resumed", False)
100
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
+
101
115
  result = self.graph.invoke(initial_context, config=config)
102
116
 
103
117
  final_state = self.graph.get_state(config)
@@ -124,6 +138,51 @@ class WorkflowTool:
124
138
  span.set_attribute("workflow.status", "completed")
125
139
  return self.engine.get_outcome_message(result)
126
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
+
127
186
  def resume(
128
187
  self,
129
188
  thread_id: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soprano-sdk
3
- Version: 0.2.11
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
@@ -29,6 +29,7 @@ Requires-Dist: pyyaml>=6.0
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: gradio>=5.46.0; extra == 'dev'
31
31
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
32
+ Requires-Dist: ruff==0.14.13; extra == 'dev'
32
33
  Provides-Extra: persistence
33
34
  Requires-Dist: langgraph-checkpoint-mongodb>=0.2.0; extra == 'persistence'
34
35
  Requires-Dist: pymongo>=4.0.0; extra == 'persistence'
@@ -1,22 +1,22 @@
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=xsJbY1hZzhXbNRTAVj9M_2saU0oa7J8O9DaRGGuPf30,8832
3
+ soprano_sdk/tools.py,sha256=dmJ0OZ7Bj3rvjBQvLzgWlYRFVtNJOyMO2jLqaS13cAc,10971
4
4
  soprano_sdk/agents/__init__.py,sha256=Yzbtv6iP_ABRgZo0IUjy9vDofEvLFbOjuABw758176A,636
5
- soprano_sdk/agents/adaptor.py,sha256=Cm02YKFclrESu-Qq4CTknCgU7KaA7Z_2FspnQDkEVfU,3214
5
+ soprano_sdk/agents/adaptor.py,sha256=dRk4pU1UgUhsKRotOzILjeXk0Zzoj7GzB-SKyBgKbNs,3242
6
6
  soprano_sdk/agents/factory.py,sha256=Aucfz4rZVKCXMAQtbGAqp1JR8aYwa66mokRmKkKGhYA,6699
7
7
  soprano_sdk/agents/structured_output.py,sha256=7DSVzfMPsZAqBwI3v6XL15qG5Gh4jJ-qddcVPaa3gdc,3326
8
8
  soprano_sdk/authenticators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
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=Hxf9asv4-5Su5FLewzEajUjtYqzadwETnktAVaTKwZc,24384
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.11.dist-info/METADATA,sha256=5YwqTD9lTaDhDwqNWEfH4UXJOPdpXEPZYI12QjC6NHs,11298
33
- soprano_sdk-0.2.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
- soprano_sdk-0.2.11.dist-info/licenses/LICENSE,sha256=A1aBauSjPNtVehOXJe3WuvdU2xvM9H8XmigFMm6665s,1073
35
- soprano_sdk-0.2.11.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,,