soprano-sdk 0.1.96__py3-none-any.whl → 0.1.98__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.
@@ -1,4 +1,3 @@
1
- import json
2
1
  from abc import ABC, abstractmethod
3
2
  from typing import Any, Dict, List
4
3
  from langgraph.graph.state import CompiledStateGraph
@@ -20,7 +20,7 @@ def create_structured_output_model(
20
20
  if not fields:
21
21
  raise ValueError("At least one field definition is required")
22
22
 
23
- field_definitions = {"bot_response": (Optional[str], Field(None, description="bot response for the user query"))}
23
+ field_definitions = {"bot_response": (Optional[str], Field(None, description="bot response for the user query, only use this for clarification or asking for more information"))}
24
24
 
25
25
  if needs_intent_change:
26
26
  field_definitions["intent_change"] = (Optional[str], Field(None, description="node name for handling new intent"))
@@ -20,7 +20,7 @@ from ..validation import validate_workflow
20
20
  class WorkflowEngine:
21
21
  def __init__(self, yaml_path: str, configs: dict):
22
22
  self.yaml_path = yaml_path
23
- self.configs = configs
23
+ self.configs = configs or {}
24
24
  logger.info(f"Loading workflow from: {yaml_path}")
25
25
 
26
26
  try:
@@ -1,5 +1,4 @@
1
1
  import copy
2
- import logging
3
2
  import uuid
4
3
  from abc import ABC, abstractmethod
5
4
  from datetime import datetime
soprano_sdk/core/state.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import types
2
- from typing import Annotated, Optional, Dict, List, Any, Type
2
+ from typing import Annotated, Optional, Dict, List, Any
3
3
 
4
4
  from typing_extensions import TypedDict
5
5
 
soprano_sdk/nodes/base.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import Dict, Any, Callable, List
3
- import logging
4
3
 
5
4
  from langgraph.errors import GraphInterrupt
6
5
 
@@ -77,10 +77,14 @@ class CallFunctionStrategy(ActionStrategy):
77
77
  ) -> Dict[str, Any]:
78
78
  for transition in self.transitions:
79
79
  check_value = result
80
- if 'path' in transition:
81
- check_value = get_nested_value(result, transition['path'])
82
-
83
- if check_value != transition['condition']:
80
+ if 'ref' in transition:
81
+ check_value = get_nested_value(result, transition['ref'])
82
+
83
+ condition = transition['condition']
84
+ if isinstance(condition, list):
85
+ if check_value not in condition:
86
+ continue
87
+ elif check_value != condition:
84
88
  continue
85
89
 
86
90
  next_dest = transition['next']
@@ -61,6 +61,12 @@ IF the user's query continues with the SAME intent OR does not match any intent
61
61
  - Proceed with your normal response
62
62
  - Do NOT mention intent detection
63
63
  - Answer the user's question as configured
64
+
65
+ BOT RESPONSE RULES:
66
+ - If the user is asking a question or needs information, provide a helpful and concise response
67
+ - If the user input is unclear or does not provide enough information, ask for clarification or more details
68
+ - { "populate bot_response field to respond back to the user" if with_structured_output else ""}
69
+ - Do not respond or use bot_response if the user provides a valid input
64
70
  """
65
71
 
66
72
  def _create_rollback_strategy(strategy_name: str) -> RollbackStrategy:
@@ -178,7 +184,8 @@ class CollectInputStrategy(ActionStrategy):
178
184
  return template_loader.from_string(template_str).render(state)
179
185
 
180
186
  def _apply_context_value(self, state: Dict[str, Any], span) -> None:
181
- if not (context_value := self.engine_context.get_context_value(self.field)):
187
+ context_value = self.engine_context.get_context_value(self.field)
188
+ if context_value is None:
182
189
  return
183
190
  logger.info(f"Using context value for '{self.field}': {context_value}")
184
191
  state[self.field] = context_value
@@ -223,18 +230,16 @@ class CollectInputStrategy(ActionStrategy):
223
230
  def _validate_collected_input(self, state) -> Tuple[bool, Optional[str]]:
224
231
  if not self.validator:
225
232
  return True, None
226
- result = self.validator(**state)
227
- if isinstance(result, tuple):
228
- return result
229
- return result, None
233
+ return self.validator(**state)
230
234
 
231
235
  def _handle_pre_populated_field(self, state: Dict[str, Any], conversation: List) -> Dict[str, Any]:
232
236
  logger.info(f"Field '{self.field}' is populated, skipping collection")
233
237
 
234
- is_valid_input, _ = self._validate_collected_input(state)
238
+ is_valid_input, validator_error_message = self._validate_collected_input(state)
235
239
  if not is_valid_input:
236
240
  self._set_status(state, "collecting")
237
- return self._handle_validation_failure(state, conversation, message=f"{state[self.field]}", role="user")
241
+ conversation.append({"role": "user", "content": f"{state[self.field]}"})
242
+ return self._handle_validation_failure(state, conversation, message=validator_error_message)
238
243
 
239
244
  if self.transitions:
240
245
  first_transition = self.transitions[0]
@@ -344,16 +349,18 @@ class CollectInputStrategy(ActionStrategy):
344
349
  conversation: List[Dict[str, str]],
345
350
  state: Dict[str, Any]
346
351
  ) -> str:
347
- if len(conversation) == 0:
348
- if not (prompt := self.agent_config.get('initial_message')):
349
- prompt = agent.invoke([{"role": "user", "content": ""}])
352
+ last_assistant_message = next((msg['content'] for msg in reversed(conversation) if msg['role'] == 'assistant'), None)
353
+
354
+ if last_assistant_message is not None:
355
+ return last_assistant_message
350
356
 
351
- prompt = self._render_template_string(prompt, state)
352
- conversation.append({"role": "assistant", "content": prompt})
357
+ if not (prompt := self.agent_config.get('initial_message')):
358
+ prompt = agent.invoke([{"role": "user", "content": ""}])
353
359
 
354
- return prompt
360
+ prompt = self._render_template_string(prompt, state)
361
+ conversation.append({"role": "assistant", "content": prompt})
355
362
 
356
- return conversation[-1]['content']
363
+ return prompt
357
364
 
358
365
  def _update_conversation(self, state: Dict[str, Any], conversation: List[Dict[str, str]]):
359
366
  state[WorkflowKeys.CONVERSATIONS][self._conversation_key] = conversation
@@ -391,6 +398,11 @@ class CollectInputStrategy(ActionStrategy):
391
398
  workflow_steps=workflow_steps
392
399
  )
393
400
 
401
+ for key, value in restored_state.items():
402
+ context_value = self.engine_context.get_context_value(key)
403
+ if context_value is not None:
404
+ restored_state[key] = context_value
405
+
394
406
  if not restored_state:
395
407
  logger.warning(f"Rollback strategy returned empty state for node '{target_node}'")
396
408
  return {}
@@ -409,8 +421,17 @@ class CollectInputStrategy(ActionStrategy):
409
421
  self._set_status(state, 'collecting')
410
422
 
411
423
  for transition in self.transitions:
412
- pattern = transition['pattern']
413
- if pattern not in agent_response:
424
+ patterns = transition['pattern']
425
+ if isinstance(patterns, str):
426
+ patterns = [patterns]
427
+
428
+ matched_pattern = None
429
+ for pattern in patterns:
430
+ if pattern in agent_response:
431
+ matched_pattern = pattern
432
+ break
433
+
434
+ if not matched_pattern:
414
435
  continue
415
436
 
416
437
  matched = True
@@ -418,12 +439,12 @@ class CollectInputStrategy(ActionStrategy):
418
439
 
419
440
  logger.info(f"Matched transition: {transition}")
420
441
 
421
- value = agent_response.split(pattern)[1].strip()
442
+ value = agent_response.split(matched_pattern)[1].strip()
422
443
  if value:
423
444
  self._store_field_value(state, value)
424
- is_valid_input, message = self._validate_collected_input(state)
445
+ is_valid_input, validation_error_message = self._validate_collected_input(state)
425
446
  if not is_valid_input:
426
- return self._handle_validation_failure(state, conversation, message=message)
447
+ return self._handle_validation_failure(state, conversation, message=validation_error_message)
427
448
  state[WorkflowKeys.MESSAGES] = [f"✓ {self._formatted_field_name} collected: {value}" ]
428
449
  else:
429
450
  state[WorkflowKeys.MESSAGES] = []
@@ -472,25 +493,20 @@ class CollectInputStrategy(ActionStrategy):
472
493
  return state
473
494
 
474
495
  def _find_matching_transition(self, agent_response: Any) -> Optional[str]:
475
- is_structured_output = isinstance(agent_response, dict)
476
-
477
496
  for transition in self.transitions:
478
- if is_structured_output:
479
- next_node = transition.get("next")
480
- match_value = transition.get("match")
481
- ref_field = transition.get("ref")
497
+ next_node = transition.get("next")
498
+ match_value = transition.get("match")
499
+ ref_field = transition.get("ref")
482
500
 
483
- if not all([next_node, match_value, ref_field]):
484
- raise RuntimeError(f"Transition in step '{self.step_id}' missing required properties for structured output routing")
501
+ if not next_node or not ref_field or match_value is None:
502
+ raise RuntimeError(f"Transition in step '{self.step_id}' missing required properties for structured output routing")
485
503
 
486
- if field_value := agent_response.get(ref_field):
487
- if field_value == match_value:
488
- return next_node
489
- else:
490
- next_node = transition.get("next")
491
- pattern = transition.get("pattern")
492
- if pattern in agent_response:
504
+ field_value = agent_response.get(ref_field)
505
+ if isinstance(match_value, list):
506
+ if field_value in match_value:
493
507
  return next_node
508
+ elif field_value == match_value:
509
+ return next_node
494
510
 
495
511
  return None
496
512
 
soprano_sdk/tools.py CHANGED
@@ -5,7 +5,6 @@ Workflow Tools - Wraps workflows as callable tools for agent frameworks
5
5
  import uuid
6
6
  from typing import Optional, Dict, Any
7
7
 
8
- from langgraph.graph.state import CompiledStateGraph
9
8
 
10
9
  from .utils.logger import logger
11
10
 
@@ -78,35 +77,23 @@ class WorkflowTool:
78
77
  ) as span:
79
78
  callback_handler = CallbackHandler()
80
79
  config = {"configurable": {"thread_id": thread_id}, "callbacks": [callback_handler]}
81
-
82
- update_context = {}
83
- engine_context_data = {}
84
- for key, value in initial_context.items():
85
- if key in self.engine.collect_input_fields:
86
- engine_context_data[key] = value
87
- continue
88
- if value:
89
- update_context[key] = value
90
-
91
- if engine_context_data:
92
- self.engine.update_context(engine_context_data)
93
- span.add_event("context.updated", {"fields": list(engine_context_data.keys())})
80
+
81
+ self.engine.update_context(initial_context)
82
+ span.add_event("context.updated", {"fields": list(initial_context.keys())})
94
83
 
95
84
  state = self.graph.get_state(config)
96
85
 
97
86
  if state.next:
98
- # Workflow is interrupted and waiting for input
99
87
  span.set_attribute("workflow.resumed", True)
100
88
  logger.info(f"[WorkflowTool] Resuming interrupted workflow {self.name} (thread: {thread_id})")
101
89
  result = self.graph.invoke(
102
- Command(resume=user_message or "", update=update_context),
90
+ Command(resume=user_message or "", update=initial_context),
103
91
  config=config
104
92
  )
105
93
  else:
106
- # Workflow is fresh or completed, start/restart
107
94
  span.set_attribute("workflow.resumed", False)
108
95
  logger.info(f"[WorkflowTool] Starting fresh workflow {self.name} (thread: {thread_id})")
109
- result = self.graph.invoke(update_context, config=config)
96
+ result = self.graph.invoke(initial_context, config=config)
110
97
 
111
98
  final_state = self.graph.get_state(config)
112
99
  if not final_state.next and self.checkpointer:
@@ -7,14 +7,14 @@ def get_nested_value(data: Any, path: str) -> Any:
7
7
  if not path:
8
8
  return data
9
9
 
10
- if not path.strip().startswith('{{'):
11
- template_str = f'{{{{ {path} }}}}'
12
- else:
13
- template_str = path
10
+ template_str = f"{{{{ {path} }}}}"
14
11
 
15
12
  try:
16
13
  template = Template(template_str)
17
- result = template.render(result=data)
14
+ if isinstance(data, dict):
15
+ result = template.render(**data)
16
+ else:
17
+ result = template.render(data)
18
18
 
19
19
  if not result or result == '':
20
20
  return None
@@ -1,10 +1,8 @@
1
1
  from opentelemetry import trace
2
- from typing import Optional, Dict, Any
2
+ from typing import Any
3
3
  from contextlib import contextmanager
4
- import logging
5
4
 
6
5
  from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
7
- from opentelemetry.trace import Link
8
6
 
9
7
  from ..utils.logger import logger
10
8
 
@@ -164,10 +164,6 @@ WORKFLOW_SCHEMA = {
164
164
  "pattern": "^[a-zA-Z_][a-zA-Z0-9_.]*\\.[a-zA-Z_][a-zA-Z0-9_]*$",
165
165
  "description": "Function path (for call_function, format: module.function)"
166
166
  },
167
- "inputs": {
168
- "type": "object",
169
- "description": "Input mapping for function"
170
- },
171
167
  "output": {
172
168
  "type": "string",
173
169
  "description": "Output field name"
@@ -183,8 +179,11 @@ WORKFLOW_SCHEMA = {
183
179
  "type": "object",
184
180
  "properties": {
185
181
  "pattern": {
186
- "type": "string",
187
- "description": "Pattern to match in response (for non-structured output)"
182
+ "anyOf": [
183
+ {"type": "string"},
184
+ {"type": "array", "items": {"type": "string"}}
185
+ ],
186
+ "description": "Pattern(s) to match in response (for non-structured output)"
188
187
  },
189
188
  "match": {
190
189
  "description": "Value to match against a field (for structured output)"
@@ -196,10 +195,6 @@ WORKFLOW_SCHEMA = {
196
195
  "condition": {
197
196
  "description": "Condition to evaluate (for call_function)"
198
197
  },
199
- "path": {
200
- "type": "string",
201
- "description": "Dot-notation path to the value for evaluation (optional)"
202
- },
203
198
  "next": {
204
199
  "type": "string",
205
200
  "description": "Next step or outcome ID"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soprano-sdk
3
- Version: 0.1.96
3
+ Version: 0.1.98
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,32 +1,32 @@
1
1
  soprano_sdk/__init__.py,sha256=y3c4i7Q7SAPS2Tee7V0TzWdhgMxBWfDJJ98eqD1HxGI,188
2
2
  soprano_sdk/engine.py,sha256=EFK91iTHjp72otLN6Kg-yeLye2J3CAKN0QH4FI2taL8,14838
3
- soprano_sdk/tools.py,sha256=A0qFEwn208GCYc-_7ZbZKR7N3HG5-6TA3Ma1RPo6YnM,8029
3
+ soprano_sdk/tools.py,sha256=cYEto0cmQ5GZ4zsH_YPfbaaDbzl5aO3rAOm7KlzTFVk,7444
4
4
  soprano_sdk/agents/__init__.py,sha256=Yzbtv6iP_ABRgZo0IUjy9vDofEvLFbOjuABw758176A,636
5
- soprano_sdk/agents/adaptor.py,sha256=IMMgo9_KLI82i1eenOaojw7UE0jjx9vjm8mjfsodKSM,3226
5
+ soprano_sdk/agents/adaptor.py,sha256=Cm02YKFclrESu-Qq4CTknCgU7KaA7Z_2FspnQDkEVfU,3214
6
6
  soprano_sdk/agents/factory.py,sha256=Aucfz4rZVKCXMAQtbGAqp1JR8aYwa66mokRmKkKGhYA,6699
7
- soprano_sdk/agents/structured_output.py,sha256=LDBWCMJFclOvcFB3OJpu37tO0Ct_M-L2PIH5MCYSjLI,3262
7
+ soprano_sdk/agents/structured_output.py,sha256=7DSVzfMPsZAqBwI3v6XL15qG5Gh4jJ-qddcVPaa3gdc,3326
8
8
  soprano_sdk/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  soprano_sdk/core/constants.py,sha256=pEwW_NeHhxs7aG457uiBCs65czAapozY6r9JAegc01Y,1451
10
- soprano_sdk/core/engine.py,sha256=WEyqGaBasGnSqlBAkFSQIZXJb7T6OEsN-DWznIBAzNs,8341
11
- soprano_sdk/core/rollback_strategies.py,sha256=UZyDBRmbi4kS7D9cGJT_mzSZzfqgbUoeTnOGcW1WOc0,7798
12
- soprano_sdk/core/state.py,sha256=h0Uo4uCwBAGTWrmzpDbcTwH6lI97-fXU9ek0tc3p2bM,2617
10
+ soprano_sdk/core/engine.py,sha256=aN805CtG7TDBkBbIJSbAbLo_5tKqrfve394L_DzZq8s,8347
11
+ soprano_sdk/core/rollback_strategies.py,sha256=NjDTtBCZlqyDql5PSwI9SMDLK7_BNlTxbW_cq_5gV0g,7783
12
+ soprano_sdk/core/state.py,sha256=ICyFhio1VESQxzYNwKrS-gk3QDc0OrwJLQtVnZ9c9LU,2611
13
13
  soprano_sdk/nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- soprano_sdk/nodes/base.py,sha256=H6wvvN3kUeXluZAt5Hf3iocO9NjTElAH4fTb2-1JKr0,2233
15
- soprano_sdk/nodes/call_function.py,sha256=UM-JpaG6DutuaFhOrXxEcT_fnK85aH6sFCVkfeYIfwc,4350
16
- soprano_sdk/nodes/collect_input.py,sha256=LhDsqtlBKK-uIz90QIODDoR5PZbnv4rWyj-hvTR64do,21987
14
+ soprano_sdk/nodes/base.py,sha256=bD8P_SLW65yQ1QaCQ5aN8eQCe50sdHh4wdcQzdY6wGA,2218
15
+ soprano_sdk/nodes/call_function.py,sha256=23Q9wUY1F9dLMW2qSAApA8Sw5SyvtS70RBCMhZxvRu0,4506
16
+ soprano_sdk/nodes/collect_input.py,sha256=2Z5XztS_mBF43asds60-ZlSUUu2bFDEIPdWQY6R1c0k,22827
17
17
  soprano_sdk/nodes/factory.py,sha256=l-Gysfgnao-o2dphhnbjjxcH3ojZanZNYN3CBH9dDbA,1624
18
18
  soprano_sdk/routing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  soprano_sdk/routing/router.py,sha256=SrNciTIXXdC9bAbbO5bX7PN9mlRbITjr4RZdNm4jEVA,3450
20
20
  soprano_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  soprano_sdk/utils/function.py,sha256=yqkY4MlHOenv-Q3NciiovK1lamyrGQljpy6Q41wviy8,1216
22
22
  soprano_sdk/utils/logger.py,sha256=hMYaNHt5syGOXRkglTUKzkgfSbWerix_pHQntcYyep8,157
23
- soprano_sdk/utils/template.py,sha256=EssXvRDWdyITVWp52weYNIwnqj9ilJaoQotWFIg4rGQ,678
23
+ soprano_sdk/utils/template.py,sha256=MG_B9TMx1ShpnSGo7s7TO-VfQzuFByuRNhJTvZ668kM,685
24
24
  soprano_sdk/utils/tool.py,sha256=hWN826HIKmLdswLCTURLH8hWlb2WU0MB8nIUErbpB-8,1877
25
- soprano_sdk/utils/tracing.py,sha256=iSJlTAaiGzgBvZhLISCGAd9_7F2HRzhcIUNHuaFv_Zc,2059
25
+ soprano_sdk/utils/tracing.py,sha256=gSHeBDLe-MbAZ9rkzpCoGFveeMdR9KLaA6tteB0IWjk,1991
26
26
  soprano_sdk/validation/__init__.py,sha256=ImChmO86jYHU90xzTttto2-LmOUOmvY_ibOQaLRz5BA,262
27
- soprano_sdk/validation/schema.py,sha256=uJJZRDgwzWT2W8amd_W8mUAULvDnHJhiMEl-5so1ZK0,13559
27
+ soprano_sdk/validation/schema.py,sha256=HzqRp-5rl3GzX5KtFckAbBh8S7zklvQmRERIyfByS2w,13335
28
28
  soprano_sdk/validation/validator.py,sha256=l2P24wiCWBNTZ9-dRbgWwK48BGaR1xIdnBxzSCu0RPM,6498
29
- soprano_sdk-0.1.96.dist-info/METADATA,sha256=eLSUnBX8Gg_37Okmm3g82Jm4UZp6j4pA64Y3cSd0YbQ,11269
30
- soprano_sdk-0.1.96.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
- soprano_sdk-0.1.96.dist-info/licenses/LICENSE,sha256=A1aBauSjPNtVehOXJe3WuvdU2xvM9H8XmigFMm6665s,1073
32
- soprano_sdk-0.1.96.dist-info/RECORD,,
29
+ soprano_sdk-0.1.98.dist-info/METADATA,sha256=yQkfqKo19uFrYSjqaFddjivMmAlU6IdMjHF3-NpkPkc,11269
30
+ soprano_sdk-0.1.98.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
+ soprano_sdk-0.1.98.dist-info/licenses/LICENSE,sha256=A1aBauSjPNtVehOXJe3WuvdU2xvM9H8XmigFMm6665s,1073
32
+ soprano_sdk-0.1.98.dist-info/RECORD,,