soprano-sdk 0.2.20__py3-none-any.whl → 0.2.21__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.
- soprano_sdk/core/constants.py +43 -0
- soprano_sdk/core/engine.py +182 -5
- soprano_sdk/nodes/collect_input.py +7 -2
- soprano_sdk/nodes/follow_up.py +6 -1
- soprano_sdk/tools.py +37 -8
- soprano_sdk/validation/schema.py +47 -0
- {soprano_sdk-0.2.20.dist-info → soprano_sdk-0.2.21.dist-info}/METADATA +145 -2
- {soprano_sdk-0.2.20.dist-info → soprano_sdk-0.2.21.dist-info}/RECORD +10 -10
- {soprano_sdk-0.2.20.dist-info → soprano_sdk-0.2.21.dist-info}/WHEEL +0 -0
- {soprano_sdk-0.2.20.dist-info → soprano_sdk-0.2.21.dist-info}/licenses/LICENSE +0 -0
soprano_sdk/core/constants.py
CHANGED
|
@@ -17,6 +17,8 @@ class WorkflowKeys:
|
|
|
17
17
|
NODE_FIELD_MAP = '_node_field_map'
|
|
18
18
|
COMPUTED_FIELDS = '_computed_fields'
|
|
19
19
|
ERROR = 'error'
|
|
20
|
+
TARGET_LANGUAGE = '_target_language'
|
|
21
|
+
TARGET_SCRIPT = '_target_script'
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class ActionType(Enum):
|
|
@@ -70,6 +72,47 @@ DEFAULT_TIMEOUT = 300
|
|
|
70
72
|
MAX_ATTEMPTS_MESSAGE = "I'm having trouble understanding your {field}. Please contact customer service for assistance."
|
|
71
73
|
WORKFLOW_COMPLETE_MESSAGE = "Workflow completed."
|
|
72
74
|
|
|
75
|
+
# Humanization defaults
|
|
76
|
+
DEFAULT_HUMANIZATION_ENABLED = True
|
|
77
|
+
DEFAULT_HUMANIZATION_SYSTEM_PROMPT = """You are a helpful assistant that transforms template-based messages into natural, conversational responses.
|
|
78
|
+
|
|
79
|
+
Your task:
|
|
80
|
+
1. Take the reference message provided and rewrite it naturally
|
|
81
|
+
2. Maintain ALL factual information and important details from the reference
|
|
82
|
+
3. Use the conversation history for context and tone matching
|
|
83
|
+
4. Be warm, professional, and helpful
|
|
84
|
+
5. Keep the response concise but complete
|
|
85
|
+
|
|
86
|
+
Reference message to humanize:
|
|
87
|
+
{reference_message}
|
|
88
|
+
|
|
89
|
+
Respond with ONLY the humanized message. Do not add explanations or meta-commentary."""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class HumanizationKeys:
|
|
93
|
+
"""Keys for humanization agent configuration"""
|
|
94
|
+
ENABLED = 'enabled'
|
|
95
|
+
MODEL = 'model'
|
|
96
|
+
BASE_URL = 'base_url'
|
|
97
|
+
INSTRUCTIONS = 'instructions'
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Localization defaults
|
|
101
|
+
DEFAULT_LOCALIZATION_INSTRUCTIONS = """LANGUAGE REQUIREMENT:
|
|
102
|
+
You MUST respond in {language} using {script} script.
|
|
103
|
+
- All your responses must be in {language}
|
|
104
|
+
- Use the {script} writing system
|
|
105
|
+
- Maintain the same meaning and tone as you would in English
|
|
106
|
+
- Do not mix languages unless quoting the user
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class LocalizationKeys:
|
|
111
|
+
"""Keys for localization configuration"""
|
|
112
|
+
LANGUAGE = 'language'
|
|
113
|
+
SCRIPT = 'script'
|
|
114
|
+
INSTRUCTIONS = 'instructions'
|
|
115
|
+
|
|
73
116
|
|
|
74
117
|
class MFAConfig(BaseSettings):
|
|
75
118
|
"""
|
soprano_sdk/core/engine.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Optional, Dict, Any, Tuple
|
|
1
|
+
from typing import Optional, Dict, Any, Tuple, List
|
|
2
2
|
|
|
3
3
|
import yaml
|
|
4
4
|
from jinja2 import Environment
|
|
@@ -7,7 +7,16 @@ from langgraph.constants import START
|
|
|
7
7
|
from langgraph.graph import StateGraph
|
|
8
8
|
from langgraph.graph.state import CompiledStateGraph
|
|
9
9
|
|
|
10
|
-
from .constants import
|
|
10
|
+
from .constants import (
|
|
11
|
+
WorkflowKeys,
|
|
12
|
+
MFAConfig,
|
|
13
|
+
DEFAULT_HUMANIZATION_ENABLED,
|
|
14
|
+
DEFAULT_HUMANIZATION_SYSTEM_PROMPT,
|
|
15
|
+
HumanizationKeys,
|
|
16
|
+
DEFAULT_LOCALIZATION_INSTRUCTIONS,
|
|
17
|
+
LocalizationKeys
|
|
18
|
+
)
|
|
19
|
+
from ..agents.factory import AgentFactory
|
|
11
20
|
from .state import create_state_model
|
|
12
21
|
from ..nodes.factory import NodeFactory
|
|
13
22
|
from ..routing.router import WorkflowRouter
|
|
@@ -148,6 +157,167 @@ class WorkflowEngine:
|
|
|
148
157
|
except Exception as e:
|
|
149
158
|
raise RuntimeError(f"Failed to build workflow graph: {e}")
|
|
150
159
|
|
|
160
|
+
def _aggregate_conversation_history(self, state: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
161
|
+
"""Aggregate all conversations from collector nodes in execution order."""
|
|
162
|
+
conversations = state.get(WorkflowKeys.CONVERSATIONS, {})
|
|
163
|
+
node_order = state.get(WorkflowKeys.NODE_EXECUTION_ORDER, [])
|
|
164
|
+
node_field_map = state.get(WorkflowKeys.NODE_FIELD_MAP, {})
|
|
165
|
+
|
|
166
|
+
aggregated = []
|
|
167
|
+
for node_id in node_order:
|
|
168
|
+
field = node_field_map.get(node_id)
|
|
169
|
+
if field:
|
|
170
|
+
conv_key = f"{field}_conversation"
|
|
171
|
+
if conv_messages := conversations.get(conv_key):
|
|
172
|
+
aggregated.extend(conv_messages)
|
|
173
|
+
|
|
174
|
+
return aggregated
|
|
175
|
+
|
|
176
|
+
def _get_humanization_config(self) -> Dict[str, Any]:
|
|
177
|
+
"""Get humanization agent configuration from workflow config."""
|
|
178
|
+
return self.config.get('humanization_agent', {})
|
|
179
|
+
|
|
180
|
+
def _should_humanize_outcome(self, outcome: Dict[str, Any]) -> bool:
|
|
181
|
+
"""Determine if an outcome should be humanized."""
|
|
182
|
+
# Check workflow-level setting (default: enabled)
|
|
183
|
+
workflow_humanization = self._get_humanization_config()
|
|
184
|
+
workflow_enabled = workflow_humanization.get(
|
|
185
|
+
HumanizationKeys.ENABLED,
|
|
186
|
+
DEFAULT_HUMANIZATION_ENABLED
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if not workflow_enabled:
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
# Check per-outcome setting (default: True, inherit from workflow)
|
|
193
|
+
return outcome.get('humanize', True)
|
|
194
|
+
|
|
195
|
+
def _get_humanization_model_config(self) -> Optional[Dict[str, Any]]:
|
|
196
|
+
"""Get model config for humanization, with overrides applied."""
|
|
197
|
+
model_config = self.get_config_value('model_config')
|
|
198
|
+
if not model_config:
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
humanization_config = self._get_humanization_config()
|
|
202
|
+
model_config = model_config.copy() # Don't mutate original
|
|
203
|
+
|
|
204
|
+
# Apply overrides from humanization_agent config
|
|
205
|
+
if model := humanization_config.get(HumanizationKeys.MODEL):
|
|
206
|
+
model_config['model_name'] = model
|
|
207
|
+
if base_url := humanization_config.get(HumanizationKeys.BASE_URL):
|
|
208
|
+
model_config['base_url'] = base_url
|
|
209
|
+
|
|
210
|
+
return model_config
|
|
211
|
+
|
|
212
|
+
def _get_localization_config(self) -> Dict[str, Any]:
|
|
213
|
+
"""Get localization configuration from workflow config."""
|
|
214
|
+
return self.config.get('localization', {})
|
|
215
|
+
|
|
216
|
+
def get_localization_instructions(self, state: Dict[str, Any]) -> str:
|
|
217
|
+
"""Get localization instructions based on state (per-turn) or YAML defaults.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
state: Current workflow state containing per-turn language/script values
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Localization instructions string to prepend to agent prompts, or empty string if no localization
|
|
224
|
+
"""
|
|
225
|
+
# First check state for per-turn values
|
|
226
|
+
language = state.get(WorkflowKeys.TARGET_LANGUAGE)
|
|
227
|
+
script = state.get(WorkflowKeys.TARGET_SCRIPT)
|
|
228
|
+
|
|
229
|
+
# Fall back to YAML defaults if not in state
|
|
230
|
+
yaml_config = self._get_localization_config()
|
|
231
|
+
if not language:
|
|
232
|
+
language = yaml_config.get(LocalizationKeys.LANGUAGE)
|
|
233
|
+
if not script:
|
|
234
|
+
script = yaml_config.get(LocalizationKeys.SCRIPT)
|
|
235
|
+
|
|
236
|
+
# No localization if neither specified
|
|
237
|
+
if not language and not script:
|
|
238
|
+
return ""
|
|
239
|
+
|
|
240
|
+
# Use custom instructions if provided in YAML
|
|
241
|
+
custom_instructions = yaml_config.get(LocalizationKeys.INSTRUCTIONS)
|
|
242
|
+
if custom_instructions:
|
|
243
|
+
return custom_instructions.format(
|
|
244
|
+
language=language or "the target language",
|
|
245
|
+
script=script or "the appropriate script"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return DEFAULT_LOCALIZATION_INSTRUCTIONS.format(
|
|
249
|
+
language=language or "the target language",
|
|
250
|
+
script=script or "the appropriate script"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def _humanize_message(self, reference_message: str, state: Dict[str, Any]) -> str:
|
|
254
|
+
"""Use LLM to humanize the reference message using conversation context."""
|
|
255
|
+
try:
|
|
256
|
+
model_config = self._get_humanization_model_config()
|
|
257
|
+
if not model_config:
|
|
258
|
+
logger.warning("No model_config found, skipping humanization")
|
|
259
|
+
return reference_message
|
|
260
|
+
|
|
261
|
+
humanization_config = self._get_humanization_config()
|
|
262
|
+
|
|
263
|
+
# Build system prompt
|
|
264
|
+
custom_instructions = humanization_config.get(HumanizationKeys.INSTRUCTIONS)
|
|
265
|
+
if custom_instructions:
|
|
266
|
+
system_prompt = f"{custom_instructions}\n\nReference message to humanize:\n{reference_message}"
|
|
267
|
+
else:
|
|
268
|
+
system_prompt = DEFAULT_HUMANIZATION_SYSTEM_PROMPT.format(
|
|
269
|
+
reference_message=reference_message
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Inject localization instructions if specified
|
|
273
|
+
localization_instructions = self.get_localization_instructions(state)
|
|
274
|
+
if localization_instructions:
|
|
275
|
+
system_prompt = f"{localization_instructions}\n\n{system_prompt}"
|
|
276
|
+
|
|
277
|
+
# Aggregate conversation history
|
|
278
|
+
conversation_history = self._aggregate_conversation_history(state)
|
|
279
|
+
|
|
280
|
+
# Create agent for humanization
|
|
281
|
+
framework = self.get_config_value('agent_framework', 'langgraph')
|
|
282
|
+
agent = AgentFactory.create_agent(
|
|
283
|
+
framework=framework,
|
|
284
|
+
name="HumanizationAgent",
|
|
285
|
+
model_config=model_config,
|
|
286
|
+
tools=[],
|
|
287
|
+
system_prompt=system_prompt,
|
|
288
|
+
structured_output_model=None
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Invoke agent with conversation history
|
|
292
|
+
if conversation_history:
|
|
293
|
+
messages = conversation_history + [
|
|
294
|
+
{"role": "user", "content": "Please humanize the reference message based on our conversation."}
|
|
295
|
+
]
|
|
296
|
+
else:
|
|
297
|
+
messages = [
|
|
298
|
+
{"role": "user", "content": "Please humanize the reference message."}
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
humanized_response = agent.invoke(messages)
|
|
302
|
+
|
|
303
|
+
# Handle different response types
|
|
304
|
+
if isinstance(humanized_response, dict):
|
|
305
|
+
humanized_message = humanized_response.get('content', str(humanized_response))
|
|
306
|
+
else:
|
|
307
|
+
humanized_message = str(humanized_response)
|
|
308
|
+
|
|
309
|
+
# Validate we got a meaningful response
|
|
310
|
+
if not humanized_message or humanized_message.strip() == '':
|
|
311
|
+
logger.warning("Humanization returned empty response, using original message")
|
|
312
|
+
return reference_message
|
|
313
|
+
|
|
314
|
+
logger.info(f"Message humanized successfully: {humanized_message[:100]}...")
|
|
315
|
+
return humanized_message
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.warning(f"Humanization failed, using original message: {e}")
|
|
319
|
+
return reference_message
|
|
320
|
+
|
|
151
321
|
def get_outcome_message(self, state: Dict[str, Any]) -> str:
|
|
152
322
|
outcome_id = state.get(WorkflowKeys.OUTCOME_ID)
|
|
153
323
|
step_id = state.get(WorkflowKeys.STEP_ID)
|
|
@@ -156,9 +326,16 @@ class WorkflowEngine:
|
|
|
156
326
|
if outcome and 'message' in outcome:
|
|
157
327
|
message = outcome['message']
|
|
158
328
|
template_loader = self.get_config_value("template_loader", Environment())
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
329
|
+
rendered_message = template_loader.from_string(message).render(state)
|
|
330
|
+
|
|
331
|
+
# Apply humanization if enabled for this outcome
|
|
332
|
+
if self._should_humanize_outcome(outcome):
|
|
333
|
+
final_message = self._humanize_message(rendered_message, state)
|
|
334
|
+
else:
|
|
335
|
+
final_message = rendered_message
|
|
336
|
+
|
|
337
|
+
logger.info(f"Outcome message generated in step {step_id}: {final_message}")
|
|
338
|
+
return final_message
|
|
162
339
|
|
|
163
340
|
if error := state.get("error"):
|
|
164
341
|
logger.info(f"Outcome error found in step {step_id}: {error}")
|
|
@@ -122,8 +122,8 @@ class CollectInputStrategy(ActionStrategy):
|
|
|
122
122
|
self.next_step = self.step_config.get("next", None)
|
|
123
123
|
self.is_structured_output = self.agent_config.get("structured_output", {}).get("enabled", False)
|
|
124
124
|
|
|
125
|
-
# Out-of-scope detection configuration (
|
|
126
|
-
self.enable_out_of_scope = self.agent_config.get("detect_out_of_scope",
|
|
125
|
+
# Out-of-scope detection configuration (disabled by default)
|
|
126
|
+
self.enable_out_of_scope = self.agent_config.get("detect_out_of_scope", False)
|
|
127
127
|
self.scope_description = self.agent_config.get(
|
|
128
128
|
"scope_description",
|
|
129
129
|
self.agent_config.get("description", f"collecting {self.field}")
|
|
@@ -348,6 +348,11 @@ class CollectInputStrategy(ActionStrategy):
|
|
|
348
348
|
|
|
349
349
|
instructions = self._render_template_string(instructions, state)
|
|
350
350
|
|
|
351
|
+
# Inject localization instructions at the start (per-turn)
|
|
352
|
+
localization_instructions = self.engine_context.get_localization_instructions(state)
|
|
353
|
+
if localization_instructions:
|
|
354
|
+
instructions = f"{localization_instructions}\n\n{instructions}"
|
|
355
|
+
|
|
351
356
|
if collector_nodes:
|
|
352
357
|
collector_nodes_for_intent_change = {
|
|
353
358
|
node_id: node_desc for node_id, node_desc in collector_nodes.items()
|
soprano_sdk/nodes/follow_up.py
CHANGED
|
@@ -103,7 +103,7 @@ class FollowUpStrategy(ActionStrategy):
|
|
|
103
103
|
'closure_patterns',
|
|
104
104
|
DEFAULT_CLOSURE_PATTERNS
|
|
105
105
|
)
|
|
106
|
-
self.enable_out_of_scope = self.agent_config.get('detect_out_of_scope',
|
|
106
|
+
self.enable_out_of_scope = self.agent_config.get('detect_out_of_scope', False)
|
|
107
107
|
self.scope_description = self.agent_config.get(
|
|
108
108
|
'scope_description',
|
|
109
109
|
self.agent_config.get('description', 'answering follow-up questions')
|
|
@@ -318,6 +318,11 @@ class FollowUpStrategy(ActionStrategy):
|
|
|
318
318
|
scope_description=self.scope_description
|
|
319
319
|
)
|
|
320
320
|
|
|
321
|
+
# Inject localization instructions at the start (per-turn)
|
|
322
|
+
localization_instructions = self.engine_context.get_localization_instructions(state)
|
|
323
|
+
if localization_instructions:
|
|
324
|
+
instructions = f"{localization_instructions}\n\n{instructions}"
|
|
325
|
+
|
|
321
326
|
framework = self.engine_context.get_config_value('agent_framework', 'langgraph')
|
|
322
327
|
|
|
323
328
|
return AgentFactory.create_agent(
|
soprano_sdk/tools.py
CHANGED
|
@@ -10,7 +10,7 @@ from .utils.logger import logger
|
|
|
10
10
|
from langfuse.langchain import CallbackHandler
|
|
11
11
|
|
|
12
12
|
from .core.engine import load_workflow
|
|
13
|
-
from .core.constants import MFAConfig, InterruptType
|
|
13
|
+
from .core.constants import MFAConfig, InterruptType, WorkflowKeys
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class WorkflowTool:
|
|
@@ -54,7 +54,9 @@ class WorkflowTool:
|
|
|
54
54
|
self,
|
|
55
55
|
thread_id: Optional[str] = None,
|
|
56
56
|
user_message: Optional[str] = None,
|
|
57
|
-
initial_context: Optional[Dict[str, Any]] = None
|
|
57
|
+
initial_context: Optional[Dict[str, Any]] = None,
|
|
58
|
+
target_language: Optional[str] = None,
|
|
59
|
+
target_script: Optional[str] = None
|
|
58
60
|
) -> str:
|
|
59
61
|
"""Execute the workflow with automatic state detection
|
|
60
62
|
|
|
@@ -65,16 +67,25 @@ class WorkflowTool:
|
|
|
65
67
|
thread_id: Thread ID for state tracking
|
|
66
68
|
user_message: User's message (used for resume if workflow is interrupted)
|
|
67
69
|
initial_context: Context to inject for fresh starts (e.g., {"order_id": "123"})
|
|
70
|
+
target_language: Target language for this turn (e.g., "Tamil", "Hindi")
|
|
71
|
+
target_script: Target script for this turn (e.g., "Tamil", "Devanagari")
|
|
68
72
|
|
|
69
73
|
Returns:
|
|
70
74
|
Final outcome message or interrupt prompt
|
|
71
75
|
"""
|
|
72
76
|
from langgraph.types import Command
|
|
73
77
|
from soprano_sdk.utils.tracing import trace_workflow_execution
|
|
74
|
-
|
|
78
|
+
|
|
75
79
|
if thread_id is None:
|
|
76
80
|
thread_id = str(uuid.uuid4())
|
|
77
|
-
|
|
81
|
+
|
|
82
|
+
# Build localization state for this turn
|
|
83
|
+
localization_state = {}
|
|
84
|
+
if target_language:
|
|
85
|
+
localization_state[WorkflowKeys.TARGET_LANGUAGE] = target_language
|
|
86
|
+
if target_script:
|
|
87
|
+
localization_state[WorkflowKeys.TARGET_SCRIPT] = target_script
|
|
88
|
+
|
|
78
89
|
with trace_workflow_execution(
|
|
79
90
|
workflow_name=self.engine.workflow_name,
|
|
80
91
|
thread_id=thread_id,
|
|
@@ -99,8 +110,10 @@ class WorkflowTool:
|
|
|
99
110
|
"filtered_out": list(set(initial_context.keys()) - set(filtered_context.keys()))
|
|
100
111
|
})
|
|
101
112
|
|
|
113
|
+
# Merge localization state with filtered context
|
|
114
|
+
update_state = {**filtered_context, **localization_state}
|
|
102
115
|
result = self.graph.invoke(
|
|
103
|
-
Command(resume=user_message or "", update=
|
|
116
|
+
Command(resume=user_message or "", update=update_state),
|
|
104
117
|
config=config
|
|
105
118
|
)
|
|
106
119
|
|
|
@@ -112,7 +125,9 @@ class WorkflowTool:
|
|
|
112
125
|
self.engine.update_context(initial_context)
|
|
113
126
|
span.add_event("context.updated", {"fields": list(initial_context.keys())})
|
|
114
127
|
|
|
115
|
-
|
|
128
|
+
# Merge localization state with initial context
|
|
129
|
+
invoke_state = {**(initial_context or {}), **localization_state}
|
|
130
|
+
result = self.graph.invoke(invoke_state, config=config)
|
|
116
131
|
|
|
117
132
|
final_state = self.graph.get_state(config)
|
|
118
133
|
if not final_state.next and self.checkpointer:
|
|
@@ -196,21 +211,35 @@ class WorkflowTool:
|
|
|
196
211
|
def resume(
|
|
197
212
|
self,
|
|
198
213
|
thread_id: str,
|
|
199
|
-
resume_value: Union[str, Dict[str, Any]]
|
|
214
|
+
resume_value: Union[str, Dict[str, Any]],
|
|
215
|
+
target_language: Optional[str] = None,
|
|
216
|
+
target_script: Optional[str] = None
|
|
200
217
|
) -> str:
|
|
201
218
|
"""Resume an interrupted workflow with user input or async result
|
|
202
219
|
|
|
203
220
|
Args:
|
|
204
221
|
thread_id: Thread ID of the interrupted workflow
|
|
205
222
|
resume_value: User's response (str) or async operation result (dict)
|
|
223
|
+
target_language: Target language for this turn (e.g., "Tamil", "Hindi")
|
|
224
|
+
target_script: Target script for this turn (e.g., "Tamil", "Devanagari")
|
|
206
225
|
|
|
207
226
|
Returns:
|
|
208
227
|
Either another interrupt prompt/async metadata or final outcome message
|
|
209
228
|
"""
|
|
210
229
|
from langgraph.types import Command
|
|
211
230
|
|
|
231
|
+
# Build localization state for this turn
|
|
232
|
+
localization_state = {}
|
|
233
|
+
if target_language:
|
|
234
|
+
localization_state[WorkflowKeys.TARGET_LANGUAGE] = target_language
|
|
235
|
+
if target_script:
|
|
236
|
+
localization_state[WorkflowKeys.TARGET_SCRIPT] = target_script
|
|
237
|
+
|
|
212
238
|
config = {"configurable": {"thread_id": thread_id}}
|
|
213
|
-
|
|
239
|
+
if localization_state:
|
|
240
|
+
result = self.graph.invoke(Command(resume=resume_value, update=localization_state), config=config)
|
|
241
|
+
else:
|
|
242
|
+
result = self.graph.invoke(Command(resume=resume_value), config=config)
|
|
214
243
|
|
|
215
244
|
# Check if workflow needs more input or has another async operation
|
|
216
245
|
if "__interrupt__" in result and result["__interrupt__"]:
|
soprano_sdk/validation/schema.py
CHANGED
|
@@ -22,6 +22,48 @@ WORKFLOW_SCHEMA = {
|
|
|
22
22
|
"default": "langgraph",
|
|
23
23
|
"description": "Agent framework to use for all agents in this workflow (default: langgraph)"
|
|
24
24
|
},
|
|
25
|
+
"humanization_agent": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"description": "Configuration for LLM-powered message humanization (enabled by default)",
|
|
28
|
+
"properties": {
|
|
29
|
+
"enabled": {
|
|
30
|
+
"type": "boolean",
|
|
31
|
+
"default": True,
|
|
32
|
+
"description": "Whether to enable LLM humanization for outcome messages (default: true)"
|
|
33
|
+
},
|
|
34
|
+
"model": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Model to use for humanization (overrides model_config.model_name)"
|
|
37
|
+
},
|
|
38
|
+
"base_url": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"format": "uri",
|
|
41
|
+
"description": "Base URL for humanization model (overrides model_config.base_url)"
|
|
42
|
+
},
|
|
43
|
+
"instructions": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Custom system prompt for the humanization agent"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"localization": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"description": "Default localization configuration (can be overridden per-turn via execute() parameters)",
|
|
52
|
+
"properties": {
|
|
53
|
+
"language": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Default target language (e.g., Tamil, Hindi, Spanish)"
|
|
56
|
+
},
|
|
57
|
+
"script": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Default target script/writing system (e.g., Tamil, Devanagari, Latin)"
|
|
60
|
+
},
|
|
61
|
+
"instructions": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "Custom instructions for language/script requirements"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
25
67
|
"data": {
|
|
26
68
|
"type": "array",
|
|
27
69
|
"description": "Data fields used in the workflow",
|
|
@@ -290,6 +332,11 @@ WORKFLOW_SCHEMA = {
|
|
|
290
332
|
"message": {
|
|
291
333
|
"type": "string",
|
|
292
334
|
"description": "Outcome message (supports {field} placeholders)"
|
|
335
|
+
},
|
|
336
|
+
"humanize": {
|
|
337
|
+
"type": "boolean",
|
|
338
|
+
"default": True,
|
|
339
|
+
"description": "Whether to apply LLM humanization to this outcome's message (default: true)"
|
|
293
340
|
}
|
|
294
341
|
}
|
|
295
342
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soprano-sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.21
|
|
4
4
|
Summary: YAML-driven workflow engine with AI agent integration for building conversational SOPs
|
|
5
5
|
Author: Arvind Thangamani
|
|
6
6
|
License: MIT
|
|
@@ -54,6 +54,8 @@ A YAML-driven workflow engine with AI agent integration for building conversatio
|
|
|
54
54
|
- **Follow-up Conversations**: Handle user follow-up questions with full workflow context
|
|
55
55
|
- **Intent Detection**: Route users between collector nodes based on detected intent
|
|
56
56
|
- **Out-of-Scope Detection**: Signal when user queries are unrelated to the current workflow
|
|
57
|
+
- **Outcome Humanization**: LLM-powered transformation of outcome messages into natural, context-aware responses
|
|
58
|
+
- **Per-Turn Localization**: Dynamic language and script switching for multi-language support
|
|
57
59
|
|
|
58
60
|
## Installation
|
|
59
61
|
|
|
@@ -364,7 +366,7 @@ Data collector and follow-up nodes can detect when user queries are unrelated to
|
|
|
364
366
|
**Configuration:**
|
|
365
367
|
```yaml
|
|
366
368
|
agent:
|
|
367
|
-
detect_out_of_scope: true #
|
|
369
|
+
detect_out_of_scope: true # Disabled by default, set to true to enable
|
|
368
370
|
scope_description: "collecting order information for returns" # Optional
|
|
369
371
|
```
|
|
370
372
|
|
|
@@ -373,6 +375,145 @@ agent:
|
|
|
373
375
|
__OUT_OF_SCOPE_INTERRUPT__|{thread_id}|{workflow_name}|{"reason":"...","user_message":"..."}
|
|
374
376
|
```
|
|
375
377
|
|
|
378
|
+
## Outcome Humanization
|
|
379
|
+
|
|
380
|
+
Outcome messages can be automatically humanized using an LLM to transform template-based messages into natural, context-aware responses. This feature uses the full conversation history to generate responses that match the tone and context of the interaction.
|
|
381
|
+
|
|
382
|
+
### How It Works
|
|
383
|
+
|
|
384
|
+
1. **Template rendering**: The outcome message template is first rendered with state values (e.g., `{{order_id}}` → `1234`)
|
|
385
|
+
2. **LLM humanization**: The rendered message is passed to an LLM along with the conversation history
|
|
386
|
+
3. **Natural response**: The LLM generates a warm, conversational response while preserving all factual details
|
|
387
|
+
|
|
388
|
+
### Configuration
|
|
389
|
+
|
|
390
|
+
Humanization is **enabled by default**. Configure it at the workflow level:
|
|
391
|
+
|
|
392
|
+
```yaml
|
|
393
|
+
name: "Return Processing Workflow"
|
|
394
|
+
version: "1.0"
|
|
395
|
+
|
|
396
|
+
# Humanization configuration (optional - enabled by default)
|
|
397
|
+
humanization_agent:
|
|
398
|
+
model: "gpt-4o" # Override model for humanization (optional)
|
|
399
|
+
base_url: "https://custom-api.com/v1" # Override base URL (optional)
|
|
400
|
+
instructions: | # Custom instructions (optional)
|
|
401
|
+
You are a friendly customer service representative.
|
|
402
|
+
Rewrite the message to be warm and empathetic.
|
|
403
|
+
Always thank the customer for their patience.
|
|
404
|
+
|
|
405
|
+
outcomes:
|
|
406
|
+
- id: success
|
|
407
|
+
type: success
|
|
408
|
+
message: "Return approved for order {{order_id}}. Reason: {{return_reason}}."
|
|
409
|
+
|
|
410
|
+
- id: technical_error
|
|
411
|
+
type: failure
|
|
412
|
+
humanize: false # Disable humanization for this specific outcome
|
|
413
|
+
message: "Error code: {{error_code}}. Contact support."
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Example Transformation
|
|
417
|
+
|
|
418
|
+
| Template Message | Humanized Response |
|
|
419
|
+
|-----------------|-------------------|
|
|
420
|
+
| `"Return approved for order 1234. Reason: damaged item."` | `"Great news! I've approved the return for your order #1234. I completely understand about the damaged item - that's so frustrating. You'll receive an email shortly with return instructions. Is there anything else I can help you with?"` |
|
|
421
|
+
|
|
422
|
+
### Disabling Humanization
|
|
423
|
+
|
|
424
|
+
**Globally** (for entire workflow):
|
|
425
|
+
```yaml
|
|
426
|
+
humanization_agent:
|
|
427
|
+
enabled: false
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**Per-outcome**:
|
|
431
|
+
```yaml
|
|
432
|
+
outcomes:
|
|
433
|
+
- id: error_code
|
|
434
|
+
type: failure
|
|
435
|
+
humanize: false # Keep exact message for debugging/logging
|
|
436
|
+
message: "Error: {{error_code}}"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Model Configuration
|
|
440
|
+
|
|
441
|
+
The humanization agent inherits the workflow's runtime `model_config`. You can override specific settings:
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
config = {
|
|
445
|
+
"model_config": {
|
|
446
|
+
"model_name": "gpt-4o-mini", # Base model for all agents
|
|
447
|
+
"api_key": os.getenv("OPENAI_API_KEY"),
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
# In YAML, humanization_agent.model overrides model_name for humanization only
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Per-Turn Localization
|
|
455
|
+
|
|
456
|
+
The framework supports per-turn localization, allowing dynamic language and script switching during workflow execution. Each call to `execute()` can specify a different target language/script.
|
|
457
|
+
|
|
458
|
+
### How It Works
|
|
459
|
+
|
|
460
|
+
1. **Per-turn parameters**: Pass `target_language` and `target_script` to `execute()`
|
|
461
|
+
2. **Instruction injection**: Localization instructions are prepended to agent system prompts
|
|
462
|
+
3. **No extra LLM calls**: The same agent that generates the response handles localization
|
|
463
|
+
|
|
464
|
+
### Usage
|
|
465
|
+
|
|
466
|
+
**Per-turn language switching:**
|
|
467
|
+
```python
|
|
468
|
+
from soprano_sdk import WorkflowTool
|
|
469
|
+
|
|
470
|
+
tool = WorkflowTool(
|
|
471
|
+
yaml_path="return_workflow.yaml",
|
|
472
|
+
name="return_processor",
|
|
473
|
+
description="Process returns",
|
|
474
|
+
checkpointer=checkpointer,
|
|
475
|
+
config=config
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Turn 1: English (no localization)
|
|
479
|
+
result = tool.execute(thread_id="123", user_message="hi")
|
|
480
|
+
|
|
481
|
+
# Turn 2: Switch to Tamil
|
|
482
|
+
result = tool.execute(
|
|
483
|
+
thread_id="123",
|
|
484
|
+
user_message="my order id is 1234",
|
|
485
|
+
target_language="Tamil",
|
|
486
|
+
target_script="Tamil"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Turn 3: Back to English (no localization params)
|
|
490
|
+
result = tool.execute(thread_id="123", user_message="yes")
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### YAML Defaults (Optional)
|
|
494
|
+
|
|
495
|
+
You can set default localization in the workflow YAML. These are used when `target_language`/`target_script` are not passed to `execute()`:
|
|
496
|
+
|
|
497
|
+
```yaml
|
|
498
|
+
name: "Return Workflow"
|
|
499
|
+
version: "1.0"
|
|
500
|
+
|
|
501
|
+
localization:
|
|
502
|
+
language: "Tamil"
|
|
503
|
+
script: "Tamil"
|
|
504
|
+
instructions: | # Optional: custom instructions
|
|
505
|
+
Use formal Tamil suitable for customer service.
|
|
506
|
+
Always be polite and respectful.
|
|
507
|
+
|
|
508
|
+
# ... rest of workflow
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Key Points
|
|
512
|
+
|
|
513
|
+
- **Localization affects**: Data collector prompts, follow-up responses, and humanized outcome messages
|
|
514
|
+
- **Outcome messages require humanization**: If `humanize: false`, outcome messages stay in English (template output)
|
|
515
|
+
- **Per-turn override**: Runtime parameters always override YAML defaults
|
|
516
|
+
|
|
376
517
|
## Examples
|
|
377
518
|
|
|
378
519
|
See the `examples/` directory for complete workflow examples:
|
|
@@ -518,6 +659,8 @@ Contributions are welcome! Please open an issue or submit a pull request.
|
|
|
518
659
|
- ✅ Thread ID strategies and examples
|
|
519
660
|
- ✅ Follow-up node for conversational Q&A
|
|
520
661
|
- ✅ Out-of-scope detection for multi-workflow routing
|
|
662
|
+
- ✅ Outcome humanization with LLM
|
|
663
|
+
- ✅ Per-turn localization for multi-language support
|
|
521
664
|
- Additional action types (webhook, conditional branching, parallel execution)
|
|
522
665
|
- More workflow examples (customer onboarding, support ticketing, approval flows)
|
|
523
666
|
- Workflow testing utilities
|
|
@@ -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=
|
|
3
|
+
soprano_sdk/tools.py,sha256=D2EGT513OGjAA5h0RSy79OA34jgM30S8d49AG4JdL4M,13653
|
|
4
4
|
soprano_sdk/agents/__init__.py,sha256=Yzbtv6iP_ABRgZo0IUjy9vDofEvLFbOjuABw758176A,636
|
|
5
5
|
soprano_sdk/agents/adaptor.py,sha256=5y9e2F_ZILOPrkunvv0GhjVdniAnOOTaD4j3Ig-xd3c,5041
|
|
6
6
|
soprano_sdk/agents/factory.py,sha256=CvXhpvtjf_Hb4Ce8WKAa0FzyY5Jm6lssvIIfSXu7jPY,9788
|
|
@@ -8,17 +8,17 @@ soprano_sdk/agents/structured_output.py,sha256=VnSRmgarbgsozQSdCr9fVSjclwi04GJ6N
|
|
|
8
8
|
soprano_sdk/authenticators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
soprano_sdk/authenticators/mfa.py,sha256=xnur1lxbx4r_pUaNPD8vtQiNccQTjbPnqgi_vqdRZaQ,7336
|
|
10
10
|
soprano_sdk/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
soprano_sdk/core/constants.py,sha256=
|
|
12
|
-
soprano_sdk/core/engine.py,sha256=
|
|
11
|
+
soprano_sdk/core/constants.py,sha256=gByXYM7Lz7I7Sm2-tnn8alpaxuyuU_jfBmCpkigJHy4,4973
|
|
12
|
+
soprano_sdk/core/engine.py,sha256=VvqV4N6mDKHmsw8NBXkegyWdwWOivjQVe1R2nv3zBTw,19704
|
|
13
13
|
soprano_sdk/core/rollback_strategies.py,sha256=NjDTtBCZlqyDql5PSwI9SMDLK7_BNlTxbW_cq_5gV0g,7783
|
|
14
14
|
soprano_sdk/core/state.py,sha256=BZWL5W9zecjNe-IFg_4zv0lSLPqRST67mN01Zi8H2mk,2976
|
|
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=gjgAe5dqNk5ItOETLhwF6is7l9BJtrAouLXFOiDKA3E,2601
|
|
18
18
|
soprano_sdk/nodes/call_function.py,sha256=afYBmj5Aditbkvb_7gD3CsXBEEUohcsC1_cdHfcOunE,5847
|
|
19
|
-
soprano_sdk/nodes/collect_input.py,sha256=
|
|
19
|
+
soprano_sdk/nodes/collect_input.py,sha256=h4fLJPvLPRz2k6l-KfyeJLOwWAr3ALjtzyRWmJgAyiQ,28021
|
|
20
20
|
soprano_sdk/nodes/factory.py,sha256=i37whA0ugMSwbmWk6H798Suhq6J108a5VenuFqxCVu8,1863
|
|
21
|
-
soprano_sdk/nodes/follow_up.py,sha256=
|
|
21
|
+
soprano_sdk/nodes/follow_up.py,sha256=iVCKCCuqgU-kkLBi4iYBa_bB7j5QI8U2KNGHpgYPmRA,14165
|
|
22
22
|
soprano_sdk/routing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
soprano_sdk/routing/router.py,sha256=ECZn7NIVrVQ5l5eZr8sfVNT0MShixnQEsqh4YO6XwRs,3882
|
|
24
24
|
soprano_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -28,9 +28,9 @@ soprano_sdk/utils/template.py,sha256=MG_B9TMx1ShpnSGo7s7TO-VfQzuFByuRNhJTvZ668kM
|
|
|
28
28
|
soprano_sdk/utils/tool.py,sha256=hWN826HIKmLdswLCTURLH8hWlb2WU0MB8nIUErbpB-8,1877
|
|
29
29
|
soprano_sdk/utils/tracing.py,sha256=gSHeBDLe-MbAZ9rkzpCoGFveeMdR9KLaA6tteB0IWjk,1991
|
|
30
30
|
soprano_sdk/validation/__init__.py,sha256=ImChmO86jYHU90xzTttto2-LmOUOmvY_ibOQaLRz5BA,262
|
|
31
|
-
soprano_sdk/validation/schema.py,sha256=
|
|
31
|
+
soprano_sdk/validation/schema.py,sha256=NGnxP3TKcB-cylVcdOkxYhTPBbHnbmKTG_EamfA1VdY,17682
|
|
32
32
|
soprano_sdk/validation/validator.py,sha256=f-e2MMRL70asOIXr_0Fsd5CgGKVRiQp7AaYsHA45Km0,8792
|
|
33
|
-
soprano_sdk-0.2.
|
|
34
|
-
soprano_sdk-0.2.
|
|
35
|
-
soprano_sdk-0.2.
|
|
36
|
-
soprano_sdk-0.2.
|
|
33
|
+
soprano_sdk-0.2.21.dist-info/METADATA,sha256=YalPq6Je3QAYK_JW5cJrDnVkF9iBgqYcctM93OZXMaE,20410
|
|
34
|
+
soprano_sdk-0.2.21.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
35
|
+
soprano_sdk-0.2.21.dist-info/licenses/LICENSE,sha256=A1aBauSjPNtVehOXJe3WuvdU2xvM9H8XmigFMm6665s,1073
|
|
36
|
+
soprano_sdk-0.2.21.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|