soprano-sdk 0.1.94__py3-none-any.whl → 0.1.96__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.
@@ -0,0 +1,10 @@
1
+ from .core.engine import WorkflowEngine, load_workflow
2
+ from .tools import WorkflowTool
3
+
4
+ __version__ = "0.1.0"
5
+
6
+ __all__ = [
7
+ "WorkflowEngine",
8
+ "load_workflow",
9
+ "WorkflowTool",
10
+ ]
@@ -0,0 +1,30 @@
1
+ """Agent module for managing different agent frameworks."""
2
+
3
+ from .factory import (
4
+ AgentAdapter,
5
+ AgentCreator,
6
+ AgentFactory,
7
+ LangGraphAgentAdapter,
8
+ LangGraphAgentCreator,
9
+ CrewAIAgentAdapter,
10
+ CrewAIAgentCreator,
11
+ AgnoAgentAdapter,
12
+ AgnoAgentCreator,
13
+ PydanticAIAgentAdapter,
14
+ PydanticAIAgentCreator,
15
+ )
16
+
17
+ __all__ = [
18
+ "AgentAdapter",
19
+ "AgentCreator",
20
+ "AgentFactory",
21
+ "LangGraphAgentAdapter",
22
+ "LangGraphAgentCreator",
23
+ "CrewAIAgentAdapter",
24
+ "CrewAIAgentCreator",
25
+ "AgnoAgentAdapter",
26
+ "AgnoAgentCreator",
27
+ "PydanticAIAgentAdapter",
28
+ "PydanticAIAgentCreator",
29
+ ]
30
+
@@ -0,0 +1,91 @@
1
+ import json
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, Dict, List
4
+ from langgraph.graph.state import CompiledStateGraph
5
+ from agno.agent import Agent as AgnoAgent
6
+ from pydantic import BaseModel
7
+ from pydantic_ai.agent import Agent as PydanticAIAgent
8
+ from crewai.agent import Agent as CrewAIAgent
9
+ from ..utils.logger import logger
10
+
11
+ class AgentAdapter(ABC):
12
+
13
+ @abstractmethod
14
+ def invoke(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
15
+ pass
16
+
17
+
18
+ class LangGraphAgentAdapter(AgentAdapter):
19
+
20
+ def __init__(self, agent: CompiledStateGraph):
21
+ self.agent = agent
22
+
23
+ def invoke(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
24
+ logger.info("Invoking LangGraph agent with messages")
25
+ response = self.agent.invoke({"messages": messages})
26
+
27
+ if structured_response := response.get('structured_response'):
28
+ return structured_response.model_dump()
29
+
30
+ if not response or "messages" not in response:
31
+ raise ValueError("Agent response missing 'messages'")
32
+
33
+ response_messages = response.get("messages")
34
+ if not response_messages:
35
+ raise ValueError("Agent response 'messages' list is empty")
36
+
37
+ return response_messages[-1].content
38
+
39
+
40
+ class CrewAIAgentAdapter(AgentAdapter):
41
+
42
+ def __init__(self, agent: CrewAIAgent, output_schema: BaseModel):
43
+ self.agent = agent
44
+ self.output_schema=output_schema
45
+
46
+ def invoke(self, messages: List[Dict[str, str]]) -> Any:
47
+ try:
48
+ logger.info("Invoking LangGraph agent with messages")
49
+ result = self.agent.kickoff(messages, response_format=self.output_schema)
50
+
51
+ if structured_response := getattr(result, 'pydantic', None) :
52
+ return structured_response.model_dump()
53
+
54
+ if agent_response := getattr(result, 'raw', None) :
55
+ return agent_response
56
+
57
+ return str(result)
58
+ except Exception as e:
59
+ raise RuntimeError(f"CrewAI agent invocation failed: {e}")
60
+
61
+
62
+ class AgnoAgentAdapter(AgentAdapter):
63
+
64
+ def __init__(self, agent: AgnoAgent):
65
+ self.agent = agent
66
+
67
+ def invoke(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
68
+ try:
69
+ logger.info("Invoking LangGraph agent with messages")
70
+ response = self.agent.run(messages)
71
+ agent_response = response.content if hasattr(response, 'content') else str(response)
72
+
73
+ return agent_response
74
+ except Exception as e:
75
+ raise RuntimeError(f"Agno agent invocation failed: {e}")
76
+
77
+
78
+ class PydanticAIAgentAdapter(AgentAdapter):
79
+
80
+ def __init__(self, agent: PydanticAIAgent):
81
+ self.agent = agent
82
+
83
+ def invoke(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
84
+ try:
85
+ logger.info("Invoking LangGraph agent with messages")
86
+ result = self.agent.run_sync(messages)
87
+ agent_response = result.output if hasattr(result, 'output') else str(result)
88
+
89
+ return agent_response
90
+ except Exception as e:
91
+ raise RuntimeError(f"Pydantic-AI agent invocation failed: {e}")
@@ -0,0 +1,228 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict, List, Literal, Tuple, Callable
3
+
4
+ from agno.models.openai import OpenAIChat
5
+ from crewai import LLM
6
+ from langchain_openai import ChatOpenAI
7
+ from pydantic import SecretStr, BaseModel
8
+ from typing import Optional
9
+
10
+ from .adaptor import (
11
+ AgentAdapter,
12
+ LangGraphAgentAdapter,
13
+ CrewAIAgentAdapter,
14
+ AgnoAgentAdapter,
15
+ PydanticAIAgentAdapter
16
+ )
17
+
18
+
19
+ def get_model(config: Dict[str, Any], framework: Literal['langgraph', 'crewai', 'agno', 'pydantic-ai'] = "langgraph", output_schema: Optional[BaseModel] = None, tools: Optional[List] = None):
20
+ errors = []
21
+
22
+ model_name: str = config.get("model_name", "")
23
+ if not model_name:
24
+ errors.append("Model name is required in model_config")
25
+
26
+ base_url = config.get("base_url")
27
+ if not base_url:
28
+ errors.append("Base url for model is required in model_config")
29
+
30
+ api_key = config.get("api_key", "")
31
+ if not api_key:
32
+ if auth_callback := config.get("auth_callback"):
33
+ api_key = auth_callback()
34
+ if not api_key:
35
+ errors.append("API key/Auth callback for model is required in model_config")
36
+
37
+ if errors:
38
+ raise ValueError("; ".join(errors))
39
+
40
+ if framework == "agno" :
41
+ return OpenAIChat(
42
+ id=model_name,
43
+ api_key=api_key,
44
+ base_url=base_url
45
+ )
46
+
47
+ if framework == "crewai" :
48
+ return LLM(
49
+ api_key=api_key,
50
+ model=f"openai/{model_name}",
51
+ base_url=base_url,
52
+ temperature=0.1,
53
+ top_p=0.7
54
+ )
55
+
56
+ llm = ChatOpenAI(
57
+ model=model_name,
58
+ api_key=SecretStr(api_key),
59
+ base_url=base_url,
60
+ )
61
+
62
+ if output_schema:
63
+ return llm.with_structured_output(output_schema)
64
+
65
+ if tools:
66
+ llm = llm.bind_tools(tools)
67
+
68
+ return llm
69
+
70
+
71
+ class AgentCreator(ABC):
72
+ @abstractmethod
73
+ def create_agent(
74
+ self,
75
+ name: str,
76
+ model_config: Dict[str, Any],
77
+ tools: List[Any],
78
+ system_prompt: str,
79
+ structured_output_model: Any = None
80
+ ) -> AgentAdapter:
81
+ pass
82
+
83
+
84
+ class LangGraphAgentCreator(AgentCreator):
85
+ def create_agent(
86
+ self,
87
+ name: str,
88
+ model_config: Dict[str, Any],
89
+ tools: List[Any],
90
+ system_prompt: str,
91
+ structured_output_model: Any = None,
92
+ ) -> LangGraphAgentAdapter:
93
+ from langchain.agents import create_agent
94
+ from langchain.tools import tool
95
+ from langchain.agents.structured_output import ProviderStrategy
96
+
97
+ tools = [tool(tool_name, description=description)(tool_callable) for tool_name, description, tool_callable in tools]
98
+
99
+ output_parser = None
100
+ if structured_output_model:
101
+ output_parser = ProviderStrategy(structured_output_model)
102
+
103
+ agent = create_agent(
104
+ name=name,
105
+ model=get_model(model_config, 'langgraph', tools=tools),
106
+ tools=tools,
107
+ system_prompt=system_prompt,
108
+ response_format=output_parser
109
+ )
110
+ return LangGraphAgentAdapter(agent)
111
+
112
+
113
+ class CrewAIAgentCreator(AgentCreator):
114
+ def create_agent(
115
+ self,
116
+ name: str,
117
+ model_config: Dict[str, Any],
118
+ tools: List[Any],
119
+ system_prompt: str,
120
+ structured_output_model: Any = None
121
+ ) -> CrewAIAgentAdapter:
122
+ from crewai.agent import Agent
123
+ from crewai.tools import tool
124
+
125
+ def create_crewai_tool(tool_name: str, description: str, tool_callable: Callable) -> Any:
126
+ tool_callable.__doc__ = description
127
+ return tool(tool_name)(tool_callable)
128
+
129
+ tools = [create_crewai_tool(tn, desc, tc) for tn, desc, tc in tools]
130
+
131
+ agent = Agent(
132
+ role=name,
133
+ backstory=system_prompt,
134
+ goal="Collect the required data from user messages using the available tools.",
135
+ tools=tools,
136
+ llm=get_model(model_config, 'crewai'),
137
+ max_retry_limit=2
138
+ )
139
+
140
+ return CrewAIAgentAdapter(agent, output_schema=structured_output_model)
141
+
142
+
143
+ class AgnoAgentCreator(AgentCreator):
144
+ def create_agent(
145
+ self,
146
+ name: str,
147
+ model_config: Dict[str, Any],
148
+ tools: List[Any],
149
+ system_prompt: str,
150
+ structured_output_model: Any = None
151
+ ) -> AgnoAgentAdapter:
152
+ from agno.agent import Agent
153
+ from agno.tools import tool
154
+
155
+ tools = [tool(name=tool_name, description=description)(tool_callable) for tool_name, description, tool_callable in tools]
156
+
157
+ agent = Agent(
158
+ name=name,
159
+ model=get_model(model_config, 'agno'),
160
+ tools=tools,
161
+ instructions=[system_prompt]
162
+ )
163
+
164
+ return AgnoAgentAdapter(agent)
165
+
166
+
167
+ class PydanticAIAgentCreator(AgentCreator):
168
+ def create_agent(
169
+ self,
170
+ name: str,
171
+ model_config: Dict[str, Any],
172
+ tools: List[Tuple[str, str, Callable]],
173
+ system_prompt: str,
174
+ structured_output_model: Any = None
175
+ ) -> PydanticAIAgentAdapter:
176
+ from pydantic_ai import Agent
177
+
178
+ agent = Agent(
179
+ model=get_model(model_config, 'pydantic-ai'),
180
+ system_prompt=system_prompt,
181
+ )
182
+
183
+ for tool_name, description, tool_callable in tools:
184
+ agent.tool(name=tool_name, description=description)(tool_callable)
185
+
186
+ return PydanticAIAgentAdapter(agent)
187
+
188
+
189
+ class AgentFactory:
190
+ _CREATORS = {
191
+ "langgraph": LangGraphAgentCreator,
192
+ "crewai": CrewAIAgentCreator,
193
+ "agno": AgnoAgentCreator,
194
+ "pydantic-ai": PydanticAIAgentCreator,
195
+ }
196
+
197
+ @classmethod
198
+ def get_creator(cls, framework: str) -> AgentCreator:
199
+ framework_lower = framework.lower()
200
+
201
+ if framework_lower not in cls._CREATORS:
202
+ supported = ", ".join(cls._CREATORS.keys())
203
+ raise ValueError(
204
+ f"Unsupported agent framework: '{framework}'. "
205
+ f"Supported frameworks: {supported}"
206
+ )
207
+
208
+ creator_class = cls._CREATORS[framework_lower]
209
+ return creator_class()
210
+
211
+ @classmethod
212
+ def create_agent(
213
+ cls,
214
+ framework: str,
215
+ name: str,
216
+ model_config: Dict[str, Any],
217
+ tools: List[Any],
218
+ system_prompt: str,
219
+ structured_output_model: Any
220
+ ) -> Any:
221
+ creator = cls.get_creator(framework)
222
+ return creator.create_agent(
223
+ name=name,
224
+ model_config=model_config,
225
+ tools=tools,
226
+ system_prompt=system_prompt,
227
+ structured_output_model=structured_output_model
228
+ )
@@ -0,0 +1,97 @@
1
+ from typing import Any, Dict, List, Optional, Type
2
+ from pydantic import BaseModel, Field, create_model
3
+
4
+
5
+ TYPE_MAP = {
6
+ "text": str,
7
+ "number": int,
8
+ "double": float,
9
+ "boolean": bool,
10
+ "list": list,
11
+ "dict": dict,
12
+ }
13
+
14
+
15
+ def create_structured_output_model(
16
+ fields: List[Dict[str, Any]],
17
+ model_name: str = "StructuredOutput",
18
+ needs_intent_change: bool = False,
19
+ ) -> Type[BaseModel]:
20
+ if not fields:
21
+ raise ValueError("At least one field definition is required")
22
+
23
+ field_definitions = {"bot_response": (Optional[str], Field(None, description="bot response for the user query"))}
24
+
25
+ if needs_intent_change:
26
+ field_definitions["intent_change"] = (Optional[str], Field(None, description="node name for handling new intent"))
27
+
28
+ for field_def in fields:
29
+ field_name = field_def.get("name")
30
+ field_type = field_def.get("type")
31
+ field_description = field_def.get("description", "")
32
+ is_required = field_def.get("required", True)
33
+
34
+ if not field_name:
35
+ raise ValueError("Field definition missing 'name'")
36
+
37
+ if not field_type:
38
+ raise ValueError(f"Field '{field_name}' missing 'type'")
39
+
40
+ python_type = TYPE_MAP.get(field_type)
41
+ if python_type is None:
42
+ raise ValueError(
43
+ f"Unknown type '{field_type}' for field '{field_name}'. "
44
+ f"Supported types: {list(TYPE_MAP.keys())}"
45
+ )
46
+
47
+ if is_required:
48
+ field_definitions[field_name] = (
49
+ python_type,
50
+ Field(..., description=field_description)
51
+ )
52
+ else:
53
+ python_type = Optional[python_type]
54
+ field_definitions[field_name] = (
55
+ python_type,
56
+ Field(default=None, description=field_description)
57
+ )
58
+
59
+ try:
60
+ model = create_model(model_name, **field_definitions)
61
+ return model
62
+ except Exception as e:
63
+ raise ValueError(f"Failed to create model '{model_name}': {e}")
64
+
65
+
66
+ def validate_field_definitions(fields: List[Dict[str, Any]]) -> bool:
67
+ if not isinstance(fields, list):
68
+ raise ValueError("Field definitions must be a list")
69
+
70
+ if not fields:
71
+ raise ValueError("At least one field definition is required")
72
+
73
+ field_names = set()
74
+
75
+ for i, field_def in enumerate(fields):
76
+ if not isinstance(field_def, dict):
77
+ raise ValueError(f"Field definition at index {i} must be a dictionary")
78
+
79
+ required_keys = ["name", "type", "description"]
80
+ for key in required_keys:
81
+ if key not in field_def:
82
+ raise ValueError(f"Field definition at index {i} missing required key '{key}'")
83
+
84
+ field_name = field_def["name"]
85
+
86
+ if field_name in field_names:
87
+ raise ValueError(f"Duplicate field name '{field_name}' found")
88
+ field_names.add(field_name)
89
+
90
+ field_type = field_def["type"]
91
+ if field_type not in TYPE_MAP:
92
+ raise ValueError(
93
+ f"Invalid type '{field_type}' for field '{field_name}'. "
94
+ f"Supported types: {list(TYPE_MAP.keys())}"
95
+ )
96
+
97
+ return True
File without changes
@@ -0,0 +1,59 @@
1
+ from enum import Enum
2
+
3
+
4
+ class WorkflowKeys:
5
+ STEP_ID = '_step_id'
6
+ STATUS = '_status'
7
+ OUTCOME_ID = '_outcome_id'
8
+ MESSAGES = '_messages'
9
+ CONVERSATIONS = '_conversations'
10
+ STATE_HISTORY = '_state_history'
11
+ COLLECTOR_NODES = '_collector_nodes'
12
+ ATTEMPT_COUNTS = '_attempt_counts'
13
+ NODE_EXECUTION_ORDER = '_node_execution_order'
14
+ NODE_FIELD_MAP = '_node_field_map'
15
+ COMPUTED_FIELDS = '_computed_fields'
16
+ ERROR = 'error'
17
+
18
+
19
+ class ActionType(Enum):
20
+ COLLECT_INPUT_WITH_AGENT = 'collect_input_with_agent'
21
+ CALL_FUNCTION = 'call_function'
22
+
23
+
24
+ class DataType(Enum):
25
+ TEXT = 'text'
26
+ NUMBER = 'number'
27
+ DOUBLE = 'double'
28
+ BOOLEAN = 'boolean'
29
+ LIST = 'list'
30
+ DICT = 'dict'
31
+ ANY = "any"
32
+
33
+
34
+ class OutcomeType(Enum):
35
+ SUCCESS = 'success'
36
+ FAILURE = 'failure'
37
+
38
+
39
+ class StatusPattern:
40
+ COLLECTING = '{step_id}_collecting'
41
+ MAX_ATTEMPTS = '{step_id}_max_attempts'
42
+ NEXT_STEP = '{step_id}_{next_step}'
43
+ SUCCESS = '{step_id}_success'
44
+ FAILED = '{step_id}_failed'
45
+ INTENT_CHANGE = '{step_id}_{target_node}'
46
+
47
+
48
+ class TransitionPattern:
49
+ CAPTURED = '{field}_CAPTURED:'
50
+ FAILED = '{field}_FAILED:'
51
+ INTENT_CHANGE = 'INTENT_CHANGE:'
52
+
53
+
54
+ DEFAULT_MAX_ATTEMPTS = 3
55
+ DEFAULT_MODEL = 'gpt-4o-mini'
56
+ DEFAULT_TIMEOUT = 300
57
+
58
+ MAX_ATTEMPTS_MESSAGE = "I'm having trouble understanding your {field}. Please contact customer service for assistance."
59
+ WORKFLOW_COMPLETE_MESSAGE = "Workflow completed."