alita-sdk 0.3.182__py3-none-any.whl → 0.3.183__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.
@@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
14
14
 
15
15
  ai_icon = b'<plain_txt_msg:img>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<!plain_txt_msg>'
16
16
  user_icon = b'<plain_txt_msg:img>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<!plain_txt_msg>'
17
- agent_types = ["pipeline", "react", "xml", "openai"]
17
+ agent_types = ["pipeline", "react", "xml", "openai", "predict"]
18
18
 
19
19
  def img_to_txt(filename):
20
20
  msg = b"<plain_txt_msg:img>"
@@ -1034,6 +1034,7 @@ def run_streamlit(st, ai_icon=None, user_icon=None):
1034
1034
  st.session_state.messages.append({"role": "user", "content": prompt})
1035
1035
  with st.chat_message("assistant", avatar=ai_icon):
1036
1036
  st_cb = AlitaStreamlitCallback(st)
1037
+ logger.info(st.session_state.messages)
1037
1038
  response = st.session_state.agent_executor.invoke(
1038
1039
  {"input": prompt, "chat_history": st.session_state.messages[:-1]},
1039
1040
  { 'callbacks': [st_cb], 'configurable': {"thread_id": st.session_state.thread_id}}
@@ -79,6 +79,7 @@ _safe_import_tool('pptx', 'pptx', 'get_tools', 'PPTXToolkit')
79
79
  _safe_import_tool('postman', 'postman', 'get_tools', 'PostmanToolkit')
80
80
  _safe_import_tool('memory', 'memory', 'get_tools', 'MemoryToolkit')
81
81
  _safe_import_tool('zephyr_squad', 'zephyr_squad', 'get_tools', 'ZephyrSquadToolkit')
82
+ _safe_import_tool('slack', 'slack', 'get_tools', 'SlackToolkit')
82
83
 
83
84
  # Log import summary
84
85
  available_count = len(AVAILABLE_TOOLS)
@@ -10,7 +10,9 @@ from langchain_core.tools import ToolException
10
10
  from msrest.authentication import BasicAuthentication
11
11
  from pydantic import create_model, PrivateAttr, model_validator, SecretStr
12
12
  from pydantic.fields import FieldInfo as Field
13
+ import xml.etree.ElementTree as ET
13
14
 
15
+ from ..work_item import AzureDevOpsApiWrapper
14
16
  from ...elitea_base import BaseToolApiWrapper
15
17
 
16
18
  logger = logging.getLogger(__name__)
@@ -101,6 +103,16 @@ TestCaseAddModel = create_model(
101
103
  suite_id=(int, Field(description="ID of the test suite to which test cases are to be added"))
102
104
  )
103
105
 
106
+ TestCaseCreateModel = create_model(
107
+ "TestCaseCreateModel",
108
+ project=(str, Field(description="Project ID or project name")),
109
+ plan_id=(int, Field(description="ID of the test plan to which test cases are to be added")),
110
+ suite_id=(int, Field(description="ID of the test suite to which test cases are to be added")),
111
+ title=(str, Field(description="Test case title")),
112
+ description=(str, Field(description="Test case description")),
113
+ test_steps=(str, Field(description="""Json array with test steps. Example: [{"action": "Some action", "expectedResult": "Some expectation"},...]""")),
114
+ )
115
+
104
116
  TestCaseGetModel = create_model(
105
117
  "TestCaseGetModel",
106
118
  project=(str, Field(description="Project ID or project name")),
@@ -202,17 +214,51 @@ class TestPlanApiWrapper(BaseToolApiWrapper):
202
214
  logger.error(f"Error getting test suite(s): {e}")
203
215
  return ToolException(f"Error getting test suite(s): {e}")
204
216
 
205
- def add_test_case(self, suite_test_case_create_update_parameters: str, project: str, plan_id: int, suite_id: int):
217
+ def add_test_case(self, suite_test_case_create_update_parameters, project: str, plan_id: int, suite_id: int):
206
218
  """Add a test case to a suite in Azure DevOps."""
207
219
  try:
208
- params = json.loads(suite_test_case_create_update_parameters)
209
- suite_test_case_create_update_params_obj = [SuiteTestCaseCreateUpdateParameters(**param) for param in params]
220
+ if isinstance(suite_test_case_create_update_parameters, str):
221
+ suite_test_case_create_update_parameters = json.loads(suite_test_case_create_update_parameters)
222
+ suite_test_case_create_update_params_obj = [SuiteTestCaseCreateUpdateParameters(**param) for param in suite_test_case_create_update_parameters]
210
223
  test_cases = self._client.add_test_cases_to_suite(suite_test_case_create_update_params_obj, project, plan_id, suite_id)
211
224
  return [test_case.as_dict() for test_case in test_cases]
212
225
  except Exception as e:
213
226
  logger.error(f"Error adding test case: {e}")
214
227
  return ToolException(f"Error adding test case: {e}")
215
228
 
229
+ def create_test_case(self, project: str, plan_id: int, suite_id: int, title: str, description: str, test_steps: str):
230
+ """Creates a new test case in specified suite in Azure DevOps."""
231
+ work_item_wrapper = AzureDevOpsApiWrapper(organization_url=self.organization_url, token=self.token.get_secret_value(), project=project)
232
+ work_item_json = self.build_ado_test_case(title, description, json.loads(test_steps))
233
+ created_work_item_id = work_item_wrapper.create_work_item(work_item_json=json.dumps(work_item_json), wi_type="Test Case")['id']
234
+ return self.add_test_case([{"work_item":{"id":created_work_item_id}}], project, plan_id, suite_id)
235
+
236
+ def build_ado_test_case(self, title, description, steps):
237
+ """
238
+ :param title: test title
239
+ :param description: test description
240
+ :param steps: steps [(action, expected result), ...]
241
+ :return: JSON with ADO fields
242
+ """
243
+ steps_elem = ET.Element("steps")
244
+
245
+ for idx, step in enumerate(steps, start=1):
246
+ step_elem = ET.SubElement(steps_elem, "step", id=str(idx), type="Action")
247
+ action_elem = ET.SubElement(step_elem, "parameterizedString", isformatted="true")
248
+ action_elem.text = step["action"]
249
+ expected_elem = ET.SubElement(step_elem, "parameterizedString", isformatted="true")
250
+ expected_elem.text = step["expectedResult"]
251
+
252
+ steps_xml = ET.tostring(steps_elem, encoding="unicode")
253
+
254
+ return {
255
+ "fields": {
256
+ "System.Title": title,
257
+ "System.Description": description,
258
+ "Microsoft.VSTS.TCM.Steps": steps_xml
259
+ }
260
+ }
261
+
216
262
  def get_test_case(self, project: str, plan_id: int, suite_id: int, test_case_id: str):
217
263
  """Get a test case from a suite in Azure DevOps."""
218
264
  try:
@@ -280,6 +326,12 @@ class TestPlanApiWrapper(BaseToolApiWrapper):
280
326
  "args_schema": TestCaseAddModel,
281
327
  "ref": self.add_test_case,
282
328
  },
329
+ {
330
+ "name": "create_test_case",
331
+ "description": self.create_test_case.__doc__,
332
+ "args_schema": TestCaseCreateModel,
333
+ "ref": self.create_test_case,
334
+ },
283
335
  {
284
336
  "name": "get_test_case",
285
337
  "description": self.get_test_case.__doc__,
@@ -150,14 +150,15 @@ class AzureDevOpsApiWrapper(BaseToolApiWrapper):
150
150
 
151
151
  return parsed_items
152
152
 
153
- def _transform_work_item(self, work_item_json: str):
153
+ def _transform_work_item(self, work_item_json):
154
154
  try:
155
155
  # Convert the input JSON to a Python dictionary
156
- params = json.loads(work_item_json)
156
+ if isinstance(work_item_json, str):
157
+ work_item_json = json.loads(work_item_json)
157
158
  except (json.JSONDecodeError, ValueError) as e:
158
159
  raise ToolException(f"Issues during attempt to parse work_item_json: {e}")
159
160
 
160
- if 'fields' not in params:
161
+ if 'fields' not in work_item_json:
161
162
  raise ToolException("The 'fields' property is missing from the work_item_json.")
162
163
 
163
164
  # Transform the dictionary into a list of JsonPatchOperation objects
@@ -167,11 +168,11 @@ class AzureDevOpsApiWrapper(BaseToolApiWrapper):
167
168
  "path": f"/fields/{field}",
168
169
  "value": value
169
170
  }
170
- for field, value in params["fields"].items()
171
+ for field, value in work_item_json["fields"].items()
171
172
  ]
172
173
  return patch_document
173
174
 
174
- def create_work_item(self, work_item_json: str, wi_type="Task"):
175
+ def create_work_item(self, work_item_json, wi_type="Task"):
175
176
  """Create a work item in Azure DevOps."""
176
177
  try:
177
178
  patch_document = self._transform_work_item(work_item_json)
@@ -185,7 +186,10 @@ class AzureDevOpsApiWrapper(BaseToolApiWrapper):
185
186
  project=self.project,
186
187
  type=wi_type
187
188
  )
188
- return f"Work item {work_item.id} created successfully. View it at {work_item.url}."
189
+ return {
190
+ "id": work_item.id,
191
+ "message": f"Work item {work_item.id} created successfully. View it at {work_item.url}."
192
+ }
189
193
  except Exception as e:
190
194
  if "unknown value" in str(e):
191
195
  logger.error(f"Unable to create work item due to incorrect assignee: {e}")
@@ -70,6 +70,9 @@ class CarrierAPIWrapper(BaseModel):
70
70
  def get_integrations(self, name: str):
71
71
  return self._client.get_integrations(name)
72
72
 
73
+ def get_available_locations(self):
74
+ return self._client.get_available_locations()
75
+
73
76
  def run_test(self, test_id: str, json_body):
74
77
  return self._client.run_test(test_id, json_body)
75
78
 
@@ -79,8 +79,8 @@ class RunTestByIDTool(BaseTool):
79
79
  description: str = "Execute test plan from the Carrier platform."
80
80
  args_schema: Type[BaseModel] = create_model(
81
81
  "RunTestByIdInput",
82
- test_id=(str, Field(default=None, description="Test id to execute")),
83
- name=(str, Field(default=None, description="Test name to execute")),
82
+ test_id=(int, Field(default=None, description="Test id to execute. Use test_id if user provide id in int format")),
83
+ name=(str, Field(default=None, description="Test name to execute. Use name if user provide name in str format")),
84
84
  test_parameters=(list, Field(
85
85
  default=None,
86
86
  description=(
@@ -89,9 +89,26 @@ class RunTestByIDTool(BaseTool):
89
89
  "contain parameter names and their values."
90
90
  )
91
91
  )),
92
+ location=(str, Field(
93
+ default=None,
94
+ description=(
95
+ "Location to execute the test. Choose from public_regions, project_regions, "
96
+ "or cloud_regions. For cloud_regions, additional parameters may be required."
97
+ )
98
+ )),
99
+ cloud_settings=(dict, Field(
100
+ default={},
101
+ description=(
102
+ "Additional parameters for cloud_regions. Provide as a dictionary, "
103
+ "e.g., {'region_name': 'us-west-1', 'instance_type': 't2.large'}. "
104
+ "Don't provide this parameter as string! It should be a dictionary!"
105
+ "If no changes are needed, respond with 'use default'."
106
+ "Ensure these settings are passed as a valid dictionary not string"
107
+ )
108
+ )),
92
109
  )
93
110
 
94
- def _run(self, test_id=None, name=None, test_parameters=None):
111
+ def _run(self, test_id=None, name=None, test_parameters=None, location=None, cloud_settings=None):
95
112
  try:
96
113
  if not test_id and not name:
97
114
  return {"message": "Please provide test id or test name to start"}
@@ -115,9 +132,18 @@ class RunTestByIDTool(BaseTool):
115
132
  # If no test_parameters are provided, return the default ones for confirmation
116
133
  if test_parameters is None:
117
134
  return {
118
- "message": "Please confirm or override the following test parameters to proceed with the test execution.",
135
+ "message": "The test requires confirmation or customization of the following parameters before execution.",
119
136
  "default_test_parameters": default_test_parameters,
120
- "instruction": "To override parameters, provide a list of dictionaries for 'test_parameters', e.g., [{'vUsers': '5', 'duration': '120'}].",
137
+ "instruction": (
138
+ "If the user has already indicated that default parameters should be used, "
139
+ "pass 'default_test_parameters' as 'test_parameters' and invoke the '_run' method again without prompting the user.\n"
140
+ "If the user wants to proceed with default parameters, respond with 'use default'.\n"
141
+ "In this case, the agent should pass 'default_test_parameters' as 'test_parameters' to the tool.\n"
142
+ "If the user provides specific overrides, parse them into a list of dictionaries in the following format:\n"
143
+ "[{'vUsers': '5', 'duration': '120'}].\n"
144
+ "Each dictionary should contain the parameter name and its desired value.\n"
145
+ "Ensure that you correctly parse and validate the user's input before invoking the '_run' method."
146
+ ),
121
147
  }
122
148
 
123
149
  # Normalize test_parameters if provided in an incorrect format
@@ -126,6 +152,55 @@ class RunTestByIDTool(BaseTool):
126
152
  # Apply user-provided test parameters
127
153
  updated_test_parameters = self._apply_test_parameters(default_test_parameters, test_parameters)
128
154
 
155
+ # Fetch available locations
156
+ available_locations = self.api_wrapper.get_available_locations()
157
+ # If location is not provided, prompt the user with available options
158
+ if not location:
159
+ return {
160
+ "message": "Please select a location to execute the test.",
161
+ "available_locations": {
162
+ "public_regions": available_locations["public_regions"],
163
+ "project_regions": available_locations["project_regions"],
164
+ "cloud_regions": [region["name"] for region in available_locations["cloud_regions"]],
165
+ },
166
+ "instruction": (
167
+ "For public_regions and project_regions, provide the region name. "
168
+ "For cloud_regions, provide the region name and optionally override cloud_settings."
169
+ )
170
+ }
171
+
172
+ # Handle cloud_regions with additional parameters
173
+ selected_cloud_region = next(
174
+ (region for region in available_locations["cloud_regions"] if region["name"] == location),
175
+ None
176
+ )
177
+
178
+ if selected_cloud_region:
179
+ # Extract available cloud_settings from the selected cloud region
180
+ available_cloud_settings = selected_cloud_region["cloud_settings"]
181
+
182
+ # Add default values for instance_type and ec2_instance_type
183
+ available_cloud_settings["instance_type"] = "spot"
184
+ available_cloud_settings["ec2_instance_type"] = "t2.medium"
185
+
186
+ # If cloud_settings are not provided, prompt the user with available parameters
187
+ if not cloud_settings:
188
+ return {
189
+ "message": f"Please confirm or override the following cloud settings for the selected location: {location}",
190
+ "available_cloud_settings": available_cloud_settings,
191
+ "instruction": (
192
+ "Provide a dictionary to override cloud settings, e.g., "
193
+ "{'region_name': 'us-west-1', 'instance_type': 't2.large'}. "
194
+ "Don't provide this parameter as string! It should be a dictionary! "
195
+ "Ensure these settings are passed to the 'cloud_settings' argument, not 'test_parameters'."
196
+ "Ensure these settings are passed as a valid dictionary not string"
197
+ )
198
+ }
199
+
200
+ # Validate and merge user-provided cloud_settings with available parameters
201
+ cloud_settings = self._merge_cloud_settings(available_cloud_settings, cloud_settings)
202
+
203
+
129
204
  # Build common_params dictionary
130
205
  common_params = {
131
206
  param["name"]: param
@@ -136,7 +211,8 @@ class RunTestByIDTool(BaseTool):
136
211
  # Add env_vars, parallel_runners, and location to common_params
137
212
  common_params["env_vars"] = test_data.get("env_vars", {})
138
213
  common_params["parallel_runners"] = test_data.get("parallel_runners")
139
- common_params["location"] = test_data.get("location")
214
+ common_params["location"] = location
215
+ common_params["env_vars"]["cloud_settings"] = cloud_settings or {}
140
216
 
141
217
  # Build the JSON body
142
218
  json_body = {
@@ -146,7 +222,7 @@ class RunTestByIDTool(BaseTool):
146
222
  }
147
223
 
148
224
  # Execute the test
149
- report_id = self.api_wrapper.run_test(test_id, json_body)
225
+ report_id = self.api_wrapper.run_test(test_data.get("id"), json_body)
150
226
  return f"Test started. Report id: {report_id}. Link to report:" \
151
227
  f"{self.api_wrapper.url.rstrip('/')}/-/performance/backend/results?result_id={report_id}"
152
228
 
@@ -207,6 +283,24 @@ class RunTestByIDTool(BaseTool):
207
283
  })
208
284
  return updated_parameters
209
285
 
286
+ def _merge_cloud_settings(self, available_cloud_settings, user_cloud_settings):
287
+ """
288
+ Merge user-provided cloud settings with available cloud settings.
289
+ Ensure that user-provided values override the defaults.
290
+ """
291
+ if not user_cloud_settings:
292
+ return available_cloud_settings
293
+
294
+ # Validate user-provided keys against available keys
295
+ invalid_keys = [key for key in user_cloud_settings if key not in available_cloud_settings]
296
+ if invalid_keys:
297
+ raise ValueError(
298
+ f"Invalid keys in cloud settings: {invalid_keys}. Allowed keys: {list(available_cloud_settings.keys())}")
299
+
300
+ # Merge the settings
301
+ merged_settings = {**available_cloud_settings, **user_cloud_settings}
302
+ return merged_settings
303
+
210
304
 
211
305
  class CreateBackendTestInput(BaseModel):
212
306
  test_name: str = Field(..., description="Test name")
@@ -103,6 +103,10 @@ class CarrierClient(BaseModel):
103
103
  endpoint = f"api/v1/integrations/integrations/{self.credentials.project_id}?name={name}"
104
104
  return self.request('get', endpoint)
105
105
 
106
+ def get_available_locations(self):
107
+ endpoint = f"api/v1/shared/locations/default/{self.credentials.project_id}"
108
+ return self.request('get', endpoint)
109
+
106
110
  def run_ui_test(self, test_id: str, json_body):
107
111
  """Run a UI test with the given test ID and JSON body."""
108
112
  endpoint = f"api/v1/ui_performance/test/{self.credentials.project_id}/{test_id}"
@@ -0,0 +1,57 @@
1
+ from typing import List, Optional
2
+
3
+ from langchain_core.tools import BaseToolkit, BaseTool
4
+ from pydantic import create_model, BaseModel, Field, SecretStr
5
+ from ..base.tool import BaseAction
6
+
7
+ from .api_wrapper import SlackApiWrapper
8
+ from ..utils import TOOLKIT_SPLITTER, clean_string, get_max_toolkit_length
9
+
10
+ name = "slack"
11
+
12
+ def get_tools(tool):
13
+ return SlackToolkit().get_toolkit(
14
+ selected_tools=tool['settings'].get('selected_tools', []),
15
+ slack_token=tool['settings'].get('slack_token'),
16
+ channel_id=tool['settings'].get('channel_id'),
17
+ toolkit_name=tool.get('toolkit_name')
18
+ ).get_tools()
19
+
20
+ class SlackToolkit(BaseToolkit):
21
+ tools: List[BaseTool] = []
22
+
23
+ @staticmethod
24
+ def toolkit_config_schema() -> BaseModel:
25
+ selected_tools = {x['name']: x['args_schema'].schema() for x in SlackApiWrapper.model_construct().get_available_tools()}
26
+ SlackToolkit.toolkit_max_length = get_max_toolkit_length(selected_tools)
27
+ return create_model(
28
+ name,
29
+ slack_token=(SecretStr, Field( description="Slack Bot/User OAuth Token like XOXB-*****-*****-*****-*****")),
30
+ channel_id=(str, Field(title="Channel ID", description="Channel ID, user ID, or conversation ID to send the message to. (like C12345678 for public channels, D12345678 for DMs)")),
31
+ selected_tools=(list[str], Field(title="Selected Tools", description="List of tools to enable", default=[])),
32
+ __config__={'json_schema_extra': {'metadata': {"label": "slack", "icon_url": None, "hidden": True}}}
33
+ )
34
+
35
+ @classmethod
36
+ def get_toolkit(cls, selected_tools: Optional[List[str]] = None, toolkit_name: Optional[str] = None, **kwargs):
37
+ if selected_tools is None:
38
+ selected_tools = []
39
+ slack_api_wrapper = SlackApiWrapper(**kwargs)
40
+ prefix = clean_string(toolkit_name, cls.toolkit_max_length) + TOOLKIT_SPLITTER if toolkit_name else ''
41
+ available_tools = slack_api_wrapper.get_available_tools()
42
+ tools = []
43
+ for tool in available_tools:
44
+ if selected_tools and tool["name"] not in selected_tools:
45
+ continue
46
+ tools.append(BaseAction(
47
+ api_wrapper=slack_api_wrapper,
48
+ name=prefix + tool["name"],
49
+ description=tool["description"],
50
+ args_schema=tool["args_schema"],
51
+ func=tool["ref"]
52
+ ))
53
+ return cls(tools=tools)
54
+
55
+ def get_tools(self) -> List[BaseTool]:
56
+ return self.tools
57
+
@@ -0,0 +1,190 @@
1
+ import logging
2
+ from typing import Optional
3
+ from pydantic import BaseModel, Field, SecretStr, create_model, model_validator
4
+ from slack_sdk import WebClient
5
+ from slack_sdk.errors import SlackApiError
6
+
7
+ from alita_sdk.tools.elitea_base import BaseToolApiWrapper
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ SendMessageModel = create_model(
13
+ "SendMessageModel",
14
+ message=(str, Field(description="The message text to send."))
15
+ )
16
+
17
+ ReadMessagesModel = create_model(
18
+ "ReadMessagesModel",
19
+ limit=(int, Field(default=10, description="The number of messages to fetch (default is 10)."))
20
+ )
21
+
22
+ CreateChannelModel = create_model(
23
+ "CreateChannelModel",
24
+ channel_name=(str, Field(description="Channel ID, user ID, or conversation ID to send the message to. (like C12345678 for public channels, D12345678 for DMs)")),
25
+ is_private=(bool, Field(default=False, description="Whether to make the channel private (default: False)."))
26
+ )
27
+
28
+ ListUsersModel = create_model(
29
+ "ListUsersModel"
30
+ )
31
+
32
+
33
+ class SlackApiWrapper(BaseToolApiWrapper):
34
+
35
+ """
36
+ Slack API wrapper for interacting with Slack channels and messages.
37
+ """
38
+ slack_token: Optional[SecretStr] = Field(default=None,description="Slack Bot/User OAuth Token like XOXB-*****-*****-*****-*****")
39
+ channel_id: Optional[str] = Field(default=None, description="Channel ID, user ID, or conversation ID to send the message to. (like C12345678 for public channels, D12345678 for DMs)")
40
+
41
+ @model_validator(mode="after")
42
+ @classmethod
43
+ def validate_toolkit(cls, values):
44
+ token = values.slack_token.get_secret_value() if values.slack_token else None
45
+ if not token:
46
+ logging.error("Slack token is required.")
47
+ raise ValueError("Slack token is required.")
48
+ try:
49
+ cls._client = WebClient(token=token)
50
+ logging.info("Authenticated with Slack token.")
51
+ except Exception as e:
52
+ logging.error(f"Failed to authenticate with Slack: {str(e)}")
53
+ raise ValueError(f"Failed to authenticate with Slack: {str(e)}")
54
+ return values
55
+
56
+ def _get_client(self):
57
+ if not self._client:
58
+ self._client = WebClient(token=self.slack_token.get_secret_value())
59
+ return self._client
60
+
61
+
62
+ def send_message(self, message: str):
63
+ """
64
+ Sends a message to a specified Slack channel, user, or conversation.
65
+ """
66
+
67
+ try:
68
+
69
+ client = self._get_client()
70
+ response = client.chat_postMessage(channel=self.channel_id, text=message)
71
+ logger.info(f"Message sent to {self.channel_id}: {message}")
72
+ return f"Message sent successfully to {self.channel_id}."
73
+
74
+ except SlackApiError as e:
75
+ logger.error(f"Failed to send message to {self.channel_id}: {e.response['error']}")
76
+ return f"Received the error : {e.response['error']}"
77
+
78
+ def read_messages(self, limit=10):
79
+ """
80
+ Reads the latest messages from a Slack channel or conversation.
81
+
82
+ :param limit: int: The number of messages to fetch (default is 10)
83
+ :return: list: Returns a list of messages with metadata.
84
+ """
85
+ try:
86
+
87
+ client = self._get_client()
88
+ # Fetch conversation history
89
+ response = client.conversations_history(
90
+ channel=self.channel_id,
91
+ limit=limit )
92
+
93
+ # Extract messages from the response
94
+ messages = self.extract_slack_messages(response.get('messages', []))
95
+
96
+ return messages
97
+
98
+ except SlackApiError as e:
99
+ # Handle errors from the Slack API
100
+ logger.error(f"Failed to read message from {self.channel_id}: {e.response['error']}")
101
+ return f"Received the error : {e.response['error']}"
102
+
103
+ def create_slack_channel(self, channel_name: str, is_private=False):
104
+ """
105
+ Creates a new Slack channel.
106
+
107
+ :param channel_name: str: Desired name for the channel (e.g., "my-new-channel").
108
+ :param is_private: bool: Whether to make the channel private (default: False).
109
+ :return: dict: Slack API response or error message.
110
+ """
111
+
112
+ try:
113
+ client = self._get_client()
114
+ response = client.conversations_create(
115
+ name=channel_name,
116
+ is_private=is_private
117
+ )
118
+ channel_id = response["channel"]["id"]
119
+ print(f"Channel '{channel_name}' created successfully! Channel ID: {channel_id}")
120
+ return {"success": True, "channel_id": channel_id}
121
+ except SlackApiError as e:
122
+ error_message = e.response.get("error", "unknown_error")
123
+ print(f"Failed to create channel '{channel_name}': {error_message}")
124
+ return {"success": False, "error": error_message}
125
+
126
+ def list_users(self):
127
+ """
128
+ Lists all users in the Slack workspace.
129
+
130
+ :return: list: List of users with their IDs and names.
131
+ """
132
+
133
+
134
+ try:
135
+ client = self._get_client()
136
+ print(client.auth_test())
137
+ response = client.users_list()
138
+ users = response["members"]
139
+ return [{"id": user["id"], "name": user["name"]} for user in users if not user["is_bot"]]
140
+
141
+ except SlackApiError as e:
142
+ logger.error(f"Failed to list users: {e.response['error']}")
143
+ return f"Received the error : {e.response['error']}"
144
+
145
+ def extract_slack_messages(self, data):
146
+ extracted_info = []
147
+
148
+ for item in data:
149
+ # Extract 'user' and 'text'
150
+ user = item.get("user", "Undefined User")
151
+ message = item.get("text", "No message")
152
+
153
+ # Extract 'app name'
154
+ app_name = item.get("bot_profile", {}).get("name", "No App Name")
155
+
156
+ # Append to result
157
+ extracted_info.append({"user": user, "message": message, "app_name": app_name})
158
+
159
+ return extracted_info
160
+
161
+
162
+ def get_available_tools(self):
163
+ return [
164
+ {
165
+ "name": "send_message",
166
+ "description": self.send_message.__doc__ or "Send a message to a Slack channel, user, or conversation.",
167
+ "args_schema": SendMessageModel,
168
+ "ref": self.send_message
169
+
170
+ },
171
+ {
172
+ "name": "read_messages",
173
+ "description": self.read_messages.__doc__ or "Send a message to a Slack channel, user, or conversation.",
174
+ "args_schema": ReadMessagesModel,
175
+ "ref": self.read_messages
176
+ },
177
+ {
178
+ "name": "create_channel",
179
+ "description": self.create_slack_channel.__doc__ or "Send a message to a Slack channel, user, or conversation.",
180
+ "args_schema": CreateChannelModel,
181
+ "ref": self.create_slack_channel
182
+ },
183
+ {
184
+ "name": "list_users",
185
+ "description": self.list_users.__doc__ or "List all users in the Slack workspace.",
186
+ "args_schema": ListUsersModel,
187
+ "ref": self.list_users
188
+ }
189
+
190
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alita_sdk
3
- Version: 0.3.182
3
+ Version: 0.3.183
4
4
  Summary: SDK for building langchain agents using resources from Alita
5
5
  Author-email: Artem Rozumenko <artyom.rozumenko@gmail.com>, Mikalai Biazruchka <mikalai_biazruchka@epam.com>, Roman Mitusov <roman_mitusov@epam.com>, Ivan Krakhmaliuk <lifedjik@gmail.com>, Artem Dubrovskiy <ad13box@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -124,6 +124,7 @@ Requires-Dist: shortuuid==1.0.13; extra == "tools"
124
124
  Requires-Dist: yarl==1.17.1; extra == "tools"
125
125
  Requires-Dist: langmem==0.0.27; extra == "tools"
126
126
  Requires-Dist: textract-py3==2.1.1; extra == "tools"
127
+ Requires-Dist: slack_sdk==3.35.0; extra == "tools"
127
128
  Provides-Extra: community
128
129
  Requires-Dist: retry-extended==0.2.3; extra == "community"
129
130
  Requires-Dist: pyobjtojson==0.3; extra == "community"