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.
- soprano_sdk/agents/adaptor.py +0 -1
- soprano_sdk/agents/structured_output.py +1 -1
- soprano_sdk/core/engine.py +1 -1
- soprano_sdk/core/rollback_strategies.py +0 -1
- soprano_sdk/core/state.py +1 -1
- soprano_sdk/nodes/base.py +0 -1
- soprano_sdk/nodes/call_function.py +8 -4
- soprano_sdk/nodes/collect_input.py +50 -34
- soprano_sdk/tools.py +5 -18
- soprano_sdk/utils/template.py +5 -5
- soprano_sdk/utils/tracing.py +1 -3
- soprano_sdk/validation/schema.py +5 -10
- {soprano_sdk-0.1.96.dist-info → soprano_sdk-0.1.98.dist-info}/METADATA +1 -1
- {soprano_sdk-0.1.96.dist-info → soprano_sdk-0.1.98.dist-info}/RECORD +16 -16
- {soprano_sdk-0.1.96.dist-info → soprano_sdk-0.1.98.dist-info}/WHEEL +0 -0
- {soprano_sdk-0.1.96.dist-info → soprano_sdk-0.1.98.dist-info}/licenses/LICENSE +0 -0
soprano_sdk/agents/adaptor.py
CHANGED
|
@@ -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"))
|
soprano_sdk/core/engine.py
CHANGED
|
@@ -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:
|
soprano_sdk/core/state.py
CHANGED
soprano_sdk/nodes/base.py
CHANGED
|
@@ -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 '
|
|
81
|
-
check_value = get_nested_value(result, transition['
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
352
|
-
|
|
357
|
+
if not (prompt := self.agent_config.get('initial_message')):
|
|
358
|
+
prompt = agent.invoke([{"role": "user", "content": ""}])
|
|
353
359
|
|
|
354
|
-
|
|
360
|
+
prompt = self._render_template_string(prompt, state)
|
|
361
|
+
conversation.append({"role": "assistant", "content": prompt})
|
|
355
362
|
|
|
356
|
-
return
|
|
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
|
-
|
|
413
|
-
if
|
|
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(
|
|
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,
|
|
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=
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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=
|
|
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(
|
|
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:
|
soprano_sdk/utils/template.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
soprano_sdk/utils/tracing.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
from opentelemetry import trace
|
|
2
|
-
from typing import
|
|
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
|
|
soprano_sdk/validation/schema.py
CHANGED
|
@@ -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
|
-
"
|
|
187
|
-
|
|
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,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=
|
|
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=
|
|
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=
|
|
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=
|
|
11
|
-
soprano_sdk/core/rollback_strategies.py,sha256=
|
|
12
|
-
soprano_sdk/core/state.py,sha256=
|
|
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=
|
|
15
|
-
soprano_sdk/nodes/call_function.py,sha256=
|
|
16
|
-
soprano_sdk/nodes/collect_input.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
30
|
-
soprano_sdk-0.1.
|
|
31
|
-
soprano_sdk-0.1.
|
|
32
|
-
soprano_sdk-0.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|