MindsDB 25.9.3rc1__py3-none-any.whl → 25.10.0__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.

Potentially problematic release.


This version of MindsDB might be problematic. Click here for more details.

Files changed (90) hide show
  1. mindsdb/__about__.py +1 -1
  2. mindsdb/__main__.py +1 -9
  3. mindsdb/api/a2a/__init__.py +1 -1
  4. mindsdb/api/a2a/agent.py +9 -1
  5. mindsdb/api/a2a/common/server/server.py +4 -0
  6. mindsdb/api/a2a/common/server/task_manager.py +8 -1
  7. mindsdb/api/a2a/common/types.py +66 -0
  8. mindsdb/api/a2a/task_manager.py +50 -0
  9. mindsdb/api/common/middleware.py +1 -1
  10. mindsdb/api/executor/command_executor.py +49 -36
  11. mindsdb/api/executor/datahub/datanodes/information_schema_datanode.py +7 -13
  12. mindsdb/api/executor/datahub/datanodes/integration_datanode.py +2 -2
  13. mindsdb/api/executor/datahub/datanodes/system_tables.py +2 -1
  14. mindsdb/api/executor/planner/query_prepare.py +2 -20
  15. mindsdb/api/executor/utilities/sql.py +5 -4
  16. mindsdb/api/http/initialize.py +76 -60
  17. mindsdb/api/http/namespaces/agents.py +0 -3
  18. mindsdb/api/http/namespaces/chatbots.py +0 -5
  19. mindsdb/api/http/namespaces/file.py +2 -0
  20. mindsdb/api/http/namespaces/handlers.py +10 -5
  21. mindsdb/api/http/namespaces/knowledge_bases.py +20 -0
  22. mindsdb/api/http/namespaces/sql.py +2 -2
  23. mindsdb/api/http/start.py +2 -2
  24. mindsdb/api/mysql/mysql_proxy/utilities/dump.py +8 -2
  25. mindsdb/integrations/handlers/byom_handler/byom_handler.py +2 -10
  26. mindsdb/integrations/handlers/databricks_handler/databricks_handler.py +98 -46
  27. mindsdb/integrations/handlers/druid_handler/druid_handler.py +32 -40
  28. mindsdb/integrations/handlers/gitlab_handler/gitlab_handler.py +5 -2
  29. mindsdb/integrations/handlers/mssql_handler/mssql_handler.py +438 -100
  30. mindsdb/integrations/handlers/mssql_handler/requirements_odbc.txt +3 -0
  31. mindsdb/integrations/handlers/mysql_handler/mysql_handler.py +235 -3
  32. mindsdb/integrations/handlers/oracle_handler/__init__.py +2 -0
  33. mindsdb/integrations/handlers/oracle_handler/connection_args.py +7 -1
  34. mindsdb/integrations/handlers/oracle_handler/oracle_handler.py +321 -16
  35. mindsdb/integrations/handlers/oracle_handler/requirements.txt +1 -1
  36. mindsdb/integrations/handlers/postgres_handler/postgres_handler.py +2 -2
  37. mindsdb/integrations/handlers/shopify_handler/requirements.txt +1 -0
  38. mindsdb/integrations/handlers/shopify_handler/shopify_handler.py +57 -3
  39. mindsdb/integrations/handlers/zendesk_handler/zendesk_tables.py +144 -111
  40. mindsdb/integrations/libs/response.py +2 -2
  41. mindsdb/integrations/utilities/handlers/auth_utilities/snowflake/__init__.py +1 -0
  42. mindsdb/integrations/utilities/handlers/auth_utilities/snowflake/snowflake_jwt_gen.py +151 -0
  43. mindsdb/integrations/utilities/rag/rerankers/base_reranker.py +24 -21
  44. mindsdb/interfaces/agents/agents_controller.py +0 -2
  45. mindsdb/interfaces/data_catalog/data_catalog_loader.py +6 -7
  46. mindsdb/interfaces/data_catalog/data_catalog_reader.py +15 -4
  47. mindsdb/interfaces/database/data_handlers_cache.py +190 -0
  48. mindsdb/interfaces/database/database.py +3 -3
  49. mindsdb/interfaces/database/integrations.py +1 -121
  50. mindsdb/interfaces/database/projects.py +2 -6
  51. mindsdb/interfaces/database/views.py +1 -4
  52. mindsdb/interfaces/jobs/jobs_controller.py +0 -4
  53. mindsdb/interfaces/jobs/scheduler.py +0 -1
  54. mindsdb/interfaces/knowledge_base/controller.py +197 -108
  55. mindsdb/interfaces/knowledge_base/evaluate.py +36 -41
  56. mindsdb/interfaces/knowledge_base/executor.py +11 -0
  57. mindsdb/interfaces/knowledge_base/llm_client.py +51 -17
  58. mindsdb/interfaces/model/model_controller.py +4 -4
  59. mindsdb/interfaces/skills/custom/text2sql/mindsdb_sql_toolkit.py +4 -10
  60. mindsdb/interfaces/skills/skills_controller.py +1 -4
  61. mindsdb/interfaces/storage/db.py +16 -6
  62. mindsdb/interfaces/triggers/triggers_controller.py +1 -3
  63. mindsdb/utilities/config.py +19 -2
  64. mindsdb/utilities/exception.py +2 -2
  65. mindsdb/utilities/json_encoder.py +24 -10
  66. mindsdb/utilities/render/sqlalchemy_render.py +15 -14
  67. mindsdb/utilities/starters.py +0 -10
  68. {mindsdb-25.9.3rc1.dist-info → mindsdb-25.10.0.dist-info}/METADATA +278 -264
  69. {mindsdb-25.9.3rc1.dist-info → mindsdb-25.10.0.dist-info}/RECORD +72 -86
  70. mindsdb/api/postgres/__init__.py +0 -0
  71. mindsdb/api/postgres/postgres_proxy/__init__.py +0 -0
  72. mindsdb/api/postgres/postgres_proxy/executor/__init__.py +0 -1
  73. mindsdb/api/postgres/postgres_proxy/executor/executor.py +0 -182
  74. mindsdb/api/postgres/postgres_proxy/postgres_packets/__init__.py +0 -0
  75. mindsdb/api/postgres/postgres_proxy/postgres_packets/errors.py +0 -322
  76. mindsdb/api/postgres/postgres_proxy/postgres_packets/postgres_fields.py +0 -34
  77. mindsdb/api/postgres/postgres_proxy/postgres_packets/postgres_message.py +0 -31
  78. mindsdb/api/postgres/postgres_proxy/postgres_packets/postgres_message_formats.py +0 -1265
  79. mindsdb/api/postgres/postgres_proxy/postgres_packets/postgres_message_identifiers.py +0 -31
  80. mindsdb/api/postgres/postgres_proxy/postgres_packets/postgres_packets.py +0 -265
  81. mindsdb/api/postgres/postgres_proxy/postgres_proxy.py +0 -477
  82. mindsdb/api/postgres/postgres_proxy/utilities/__init__.py +0 -10
  83. mindsdb/api/postgres/start.py +0 -11
  84. mindsdb/integrations/handlers/mssql_handler/tests/__init__.py +0 -0
  85. mindsdb/integrations/handlers/mssql_handler/tests/test_mssql_handler.py +0 -169
  86. mindsdb/integrations/handlers/oracle_handler/tests/__init__.py +0 -0
  87. mindsdb/integrations/handlers/oracle_handler/tests/test_oracle_handler.py +0 -32
  88. {mindsdb-25.9.3rc1.dist-info → mindsdb-25.10.0.dist-info}/WHEEL +0 -0
  89. {mindsdb-25.9.3rc1.dist-info → mindsdb-25.10.0.dist-info}/licenses/LICENSE +0 -0
  90. {mindsdb-25.9.3rc1.dist-info → mindsdb-25.10.0.dist-info}/top_level.txt +0 -0
@@ -34,41 +34,31 @@ class ZendeskUsersTable(APITable):
34
34
  If the query contains an unsupported condition
35
35
  """
36
36
 
37
- select_statement_parser = SELECTQueryParser(
38
- query,
39
- 'users',
40
- self.get_columns()
41
- )
37
+ select_statement_parser = SELECTQueryParser(query, "users", self.get_columns())
42
38
 
43
- selected_columns, where_conditions, order_by_conditions, result_limit = (
44
- select_statement_parser.parse_query()
45
- )
39
+ selected_columns, where_conditions, order_by_conditions, result_limit = select_statement_parser.parse_query()
46
40
 
47
- subset_where_conditions = {}
41
+ subset_where_conditions = []
42
+ api_filters = {}
48
43
  for op, arg1, arg2 in where_conditions:
49
44
  if arg1 in self.get_columns():
50
- if op != '=':
45
+ if op != "=":
51
46
  raise NotImplementedError(f"Unknown op: {op}. Only '=' is supported.")
52
- subset_where_conditions[arg1] = arg2
47
+ api_filters[arg1] = arg2
48
+ subset_where_conditions.append([op, arg1, arg2])
53
49
 
54
- count = 0
55
- result = self.handler.zen_client.users(**subset_where_conditions)
50
+ result = self.handler.zen_client.users(**api_filters)
56
51
  response = []
57
52
  if isinstance(result, zenpy.lib.generator.BaseResultGenerator):
58
- while count <= result_limit:
59
- response.append(result.next().to_dict())
60
- count += 1
53
+ for user in result:
54
+ response.append(user.to_dict())
61
55
  else:
62
56
  response.append(result.to_dict())
63
57
 
64
58
  df = pd.DataFrame(response, columns=self.get_columns())
65
59
 
66
60
  select_statement_executor = SELECTQueryExecutor(
67
- df,
68
- selected_columns,
69
- subset_where_conditions,
70
- order_by_conditions,
71
- result_limit
61
+ df, selected_columns, subset_where_conditions, order_by_conditions, result_limit
72
62
  )
73
63
 
74
64
  df = select_statement_executor.execute_query()
@@ -85,15 +75,43 @@ class ZendeskUsersTable(APITable):
85
75
  """
86
76
 
87
77
  return [
88
- "active", "alias", "chat_only", "created_at", "custom_role_id",
89
- "details", "email", "external_id", "id", "last_login_at",
90
- "locale", "locale_id", "moderator", "name", "notes",
91
- "only_private_comments", "organization_id", "phone", "photo",
92
- "restricted_agent", "role", "shared", "shared_agent",
93
- "signature", "suspended", "tags", "ticket_restriction",
94
- "time_zone", "two_factor_auth_enabled", "updated_at", "url",
95
- "verified", "iana_time_zone", "shared_phone_number", "role_type",
96
- "default_group_id", "report_csv"
78
+ "active",
79
+ "alias",
80
+ "chat_only",
81
+ "created_at",
82
+ "custom_role_id",
83
+ "details",
84
+ "email",
85
+ "external_id",
86
+ "id",
87
+ "last_login_at",
88
+ "locale",
89
+ "locale_id",
90
+ "moderator",
91
+ "name",
92
+ "notes",
93
+ "only_private_comments",
94
+ "organization_id",
95
+ "phone",
96
+ "photo",
97
+ "restricted_agent",
98
+ "role",
99
+ "shared",
100
+ "shared_agent",
101
+ "signature",
102
+ "suspended",
103
+ "tags",
104
+ "ticket_restriction",
105
+ "time_zone",
106
+ "two_factor_auth_enabled",
107
+ "updated_at",
108
+ "url",
109
+ "verified",
110
+ "iana_time_zone",
111
+ "shared_phone_number",
112
+ "role_type",
113
+ "default_group_id",
114
+ "report_csv",
97
115
  ]
98
116
 
99
117
 
@@ -119,41 +137,31 @@ class ZendeskTicketsTable(APITable):
119
137
  If the query contains an unsupported condition
120
138
  """
121
139
 
122
- select_statement_parser = SELECTQueryParser(
123
- query,
124
- 'tickets',
125
- self.get_columns()
126
- )
140
+ select_statement_parser = SELECTQueryParser(query, "tickets", self.get_columns())
127
141
 
128
- selected_columns, where_conditions, order_by_conditions, result_limit = (
129
- select_statement_parser.parse_query()
130
- )
142
+ selected_columns, where_conditions, order_by_conditions, result_limit = select_statement_parser.parse_query()
131
143
 
132
- subset_where_conditions = {}
144
+ subset_where_conditions = []
145
+ api_filters = {}
133
146
  for op, arg1, arg2 in where_conditions:
134
147
  if arg1 in self.get_columns():
135
- if op != '=':
148
+ if op != "=":
136
149
  raise NotImplementedError(f"Unknown op: {op}. Only '=' is supported.")
137
- subset_where_conditions[arg1] = arg2
150
+ api_filters[arg1] = arg2
151
+ subset_where_conditions.append([op, arg1, arg2])
138
152
 
139
- count = 0
140
- result = self.handler.zen_client.tickets(**subset_where_conditions)
153
+ result = self.handler.zen_client.tickets(**api_filters)
141
154
  response = []
142
155
  if isinstance(result, zenpy.lib.generator.BaseResultGenerator):
143
- while count <= result_limit:
144
- response.append(result.next().to_dict())
145
- count += 1
156
+ for ticket in result:
157
+ response.append(ticket.to_dict())
146
158
  else:
147
159
  response.append(result.to_dict())
148
160
 
149
161
  df = pd.DataFrame(response, columns=self.get_columns())
150
162
 
151
163
  select_statement_executor = SELECTQueryExecutor(
152
- df,
153
- selected_columns,
154
- subset_where_conditions,
155
- order_by_conditions,
156
- result_limit
164
+ df, selected_columns, subset_where_conditions, order_by_conditions, result_limit
157
165
  )
158
166
 
159
167
  df = select_statement_executor.execute_query()
@@ -170,20 +178,54 @@ class ZendeskTicketsTable(APITable):
170
178
  """
171
179
 
172
180
  return [
173
- "assignee_id", "brand_id", "collaborator_ids", "created_at",
174
- "custom_fields", "description", "due_at", "external_id",
175
- "fields", "forum_topic_id", "group_id", "has_incidents", "id",
176
- "organization_id", "priority", "problem_id", "raw_subject",
177
- "recipient", "requester_id", "sharing_agreement_ids", "status",
178
- "subject", "submitter_id", "tags", "type", "updated_at", "url",
179
- "generated_timestamp", "follower_ids", "email_cc_ids", "is_public",
180
- "custom_status_id", "followup_ids", "ticket_form_id",
181
- "allow_channelback", "allow_attachments", "from_messaging_channel",
182
- "satisfaction_rating.assignee_id", "satisfaction_rating.created_at",
183
- "satisfaction_rating.group_id", "satisfaction_rating.id",
184
- "satisfaction_rating.requester_id", "satisfaction_rating.score",
185
- "satisfaction_rating.ticket_id", "satisfaction_rating.updated_at",
186
- "satisfaction_rating.url", "via.channel", "via.source.rel"
181
+ "assignee_id",
182
+ "brand_id",
183
+ "collaborator_ids",
184
+ "created_at",
185
+ "custom_fields",
186
+ "description",
187
+ "due_at",
188
+ "external_id",
189
+ "fields",
190
+ "forum_topic_id",
191
+ "group_id",
192
+ "has_incidents",
193
+ "id",
194
+ "organization_id",
195
+ "priority",
196
+ "problem_id",
197
+ "raw_subject",
198
+ "recipient",
199
+ "requester_id",
200
+ "sharing_agreement_ids",
201
+ "status",
202
+ "subject",
203
+ "submitter_id",
204
+ "tags",
205
+ "type",
206
+ "updated_at",
207
+ "url",
208
+ "generated_timestamp",
209
+ "follower_ids",
210
+ "email_cc_ids",
211
+ "is_public",
212
+ "custom_status_id",
213
+ "followup_ids",
214
+ "ticket_form_id",
215
+ "allow_channelback",
216
+ "allow_attachments",
217
+ "from_messaging_channel",
218
+ "satisfaction_rating.assignee_id",
219
+ "satisfaction_rating.created_at",
220
+ "satisfaction_rating.group_id",
221
+ "satisfaction_rating.id",
222
+ "satisfaction_rating.requester_id",
223
+ "satisfaction_rating.score",
224
+ "satisfaction_rating.ticket_id",
225
+ "satisfaction_rating.updated_at",
226
+ "satisfaction_rating.url",
227
+ "via.channel",
228
+ "via.source.rel",
187
229
  ]
188
230
 
189
231
 
@@ -209,41 +251,31 @@ class ZendeskTriggersTable(APITable):
209
251
  If the query contains an unsupported condition
210
252
  """
211
253
 
212
- select_statement_parser = SELECTQueryParser(
213
- query,
214
- 'triggers',
215
- self.get_columns()
216
- )
254
+ select_statement_parser = SELECTQueryParser(query, "triggers", self.get_columns())
217
255
 
218
- selected_columns, where_conditions, order_by_conditions, result_limit = (
219
- select_statement_parser.parse_query()
220
- )
256
+ selected_columns, where_conditions, order_by_conditions, result_limit = select_statement_parser.parse_query()
221
257
 
222
- subset_where_conditions = {}
258
+ subset_where_conditions = []
259
+ api_filters = {}
223
260
  for op, arg1, arg2 in where_conditions:
224
261
  if arg1 in self.get_columns():
225
- if op != '=':
262
+ if op != "=":
226
263
  raise NotImplementedError(f"Unknown op: {op}. Only '=' is supported.")
227
- subset_where_conditions[arg1] = arg2
264
+ api_filters[arg1] = arg2
265
+ subset_where_conditions.append([op, arg1, arg2])
228
266
 
229
- count = 0
230
- result = self.handler.zen_client.triggers(**subset_where_conditions)
267
+ result = self.handler.zen_client.triggers(**api_filters)
231
268
  response = []
232
269
  if isinstance(result, zenpy.lib.generator.BaseResultGenerator):
233
- while count <= result_limit:
234
- response.append(result.next().to_dict())
235
- count += 1
270
+ for trigger in result:
271
+ response.append(trigger.to_dict())
236
272
  else:
237
273
  response.append(result.to_dict())
238
274
 
239
275
  df = pd.DataFrame(response, columns=self.get_columns())
240
276
 
241
277
  select_statement_executor = SELECTQueryExecutor(
242
- df,
243
- selected_columns,
244
- subset_where_conditions,
245
- order_by_conditions,
246
- result_limit
278
+ df, selected_columns, subset_where_conditions, order_by_conditions, result_limit
247
279
  )
248
280
 
249
281
  df = select_statement_executor.execute_query()
@@ -260,9 +292,20 @@ class ZendeskTriggersTable(APITable):
260
292
  """
261
293
 
262
294
  return [
263
- "actions", "active", "description", "id", "position", "title",
264
- "url", "updated_at", "created_at", "default", "raw_title",
265
- "category_id", "conditions.all", "conditions.any"
295
+ "actions",
296
+ "active",
297
+ "description",
298
+ "id",
299
+ "position",
300
+ "title",
301
+ "url",
302
+ "updated_at",
303
+ "created_at",
304
+ "default",
305
+ "raw_title",
306
+ "category_id",
307
+ "conditions.all",
308
+ "conditions.any",
266
309
  ]
267
310
 
268
311
 
@@ -288,41 +331,31 @@ class ZendeskActivitiesTable(APITable):
288
331
  If the query contains an unsupported condition
289
332
  """
290
333
 
291
- select_statement_parser = SELECTQueryParser(
292
- query,
293
- 'activities',
294
- self.get_columns()
295
- )
334
+ select_statement_parser = SELECTQueryParser(query, "activities", self.get_columns())
296
335
 
297
- selected_columns, where_conditions, order_by_conditions, result_limit = (
298
- select_statement_parser.parse_query()
299
- )
336
+ selected_columns, where_conditions, order_by_conditions, result_limit = select_statement_parser.parse_query()
300
337
 
301
- subset_where_conditions = {}
338
+ subset_where_conditions = []
339
+ api_filters = {}
302
340
  for op, arg1, arg2 in where_conditions:
303
341
  if arg1 in self.get_columns():
304
- if op != '=':
342
+ if op != "=":
305
343
  raise NotImplementedError(f"Unknown op: {op}. Only '=' is supported.")
306
- subset_where_conditions[arg1] = arg2
344
+ api_filters[arg1] = arg2
345
+ subset_where_conditions.append([op, arg1, arg2])
307
346
 
308
- count = 0
309
- result = self.handler.zen_client.activities(**subset_where_conditions)
347
+ result = self.handler.zen_client.activities(**api_filters)
310
348
  response = []
311
349
  if isinstance(result, zenpy.lib.generator.BaseResultGenerator):
312
- while count <= result_limit:
313
- response.append(result.next().to_dict())
314
- count += 1
350
+ for activity in result:
351
+ response.append(activity.to_dict())
315
352
  else:
316
353
  response.append(result.to_dict())
317
354
 
318
355
  df = pd.DataFrame(response, columns=self.get_columns())
319
356
 
320
357
  select_statement_executor = SELECTQueryExecutor(
321
- df,
322
- selected_columns,
323
- subset_where_conditions,
324
- order_by_conditions,
325
- result_limit
358
+ df, selected_columns, subset_where_conditions, order_by_conditions, result_limit
326
359
  )
327
360
 
328
361
  df = select_statement_executor.execute_query()
@@ -459,5 +492,5 @@ class ZendeskActivitiesTable(APITable):
459
492
  "target.ticket.type",
460
493
  "target.ticket.updated_at",
461
494
  "target.ticket.url",
462
- "target.ticket.via"
495
+ "target.ticket.via",
463
496
  ]
@@ -50,7 +50,7 @@ class HandlerResponse:
50
50
  error_message: str | None = None,
51
51
  affected_rows: int | None = None,
52
52
  mysql_types: list[MYSQL_DATA_TYPE] | None = None,
53
- is_acceptable_error: bool = False,
53
+ is_expected_error: bool = False,
54
54
  ) -> None:
55
55
  self.resp_type = resp_type
56
56
  self.query = query
@@ -61,7 +61,7 @@ class HandlerResponse:
61
61
  if isinstance(self.affected_rows, int) is False or self.affected_rows < 0:
62
62
  self.affected_rows = 0
63
63
  self.mysql_types = mysql_types
64
- self.is_acceptable_error = is_acceptable_error
64
+ self.is_expected_error = is_expected_error
65
65
  self.exception = None
66
66
  current_exception = sys.exc_info()
67
67
  if current_exception[0] is not None:
@@ -0,0 +1 @@
1
+ from .snowflake_jwt_gen import get_validated_jwt as get_validated_jwt
@@ -0,0 +1,151 @@
1
+ # Based on https://docs.snowflake.com/en/developer-guide/sql-api/authenticating
2
+
3
+ import time
4
+ import base64
5
+ import hashlib
6
+ import logging
7
+ from datetime import timedelta, timezone, datetime
8
+
9
+ from cryptography.hazmat.primitives.serialization import load_pem_private_key
10
+ from cryptography.hazmat.primitives.serialization import Encoding
11
+ from cryptography.hazmat.primitives.serialization import PublicFormat
12
+ from cryptography.hazmat.backends import default_backend
13
+ import jwt
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ ISSUER = "iss"
18
+ EXPIRE_TIME = "exp"
19
+ ISSUE_TIME = "iat"
20
+ SUBJECT = "sub"
21
+
22
+
23
+ class JWTGenerator(object):
24
+ """
25
+ Creates and signs a JWT with the specified private key file, username, and account identifier. The JWTGenerator keeps the
26
+ generated token and only regenerates the token if a specified period of time has passed.
27
+ """
28
+
29
+ LIFETIME = timedelta(minutes=60) # The tokens will have a 59 minute lifetime
30
+ ALGORITHM = "RS256" # Tokens will be generated using RSA with SHA256
31
+
32
+ def __init__(self, account: str, user: str, private_key: str, lifetime: timedelta = LIFETIME):
33
+ """
34
+ __init__ creates an object that generates JWTs for the specified user, account identifier, and private key.
35
+ :param account: Your Snowflake account identifier. See https://docs.snowflake.com/en/user-guide/admin-account-identifier.html. Note that if you are using the account locator, exclude any region information from the account locator.
36
+ :param user: The Snowflake username.
37
+ :param private_key: The private key file used for signing the JWTs.
38
+ :param lifetime: The number of minutes (as a timedelta) during which the key will be valid.
39
+ """
40
+
41
+ logger.info(
42
+ """Creating JWTGenerator with arguments
43
+ account : %s, user : %s, lifetime : %s""",
44
+ account,
45
+ user,
46
+ lifetime,
47
+ )
48
+
49
+ # Construct the fully qualified name of the user in uppercase.
50
+ self.account = self.prepare_account_name_for_jwt(account)
51
+ self.user = user.upper()
52
+ self.qualified_username = self.account + "." + self.user
53
+
54
+ self.lifetime = lifetime
55
+ self.renew_time = datetime.now(timezone.utc)
56
+ self.token = None
57
+
58
+ self.private_key = load_pem_private_key(private_key.encode(), None, default_backend())
59
+
60
+ def prepare_account_name_for_jwt(self, raw_account: str) -> str:
61
+ """
62
+ Prepare the account identifier for use in the JWT.
63
+ For the JWT, the account identifier must not include the subdomain or any region or cloud provider information.
64
+ :param raw_account: The specified account identifier.
65
+ :return: The account identifier in a form that can be used to generate JWT.
66
+ """
67
+ account = raw_account
68
+ if ".global" not in account:
69
+ # Handle the general case.
70
+ idx = account.find(".")
71
+ if idx > 0:
72
+ account = account[0:idx]
73
+ else:
74
+ # Handle the replication case.
75
+ idx = account.find("-")
76
+ if idx > 0:
77
+ account = account[0:idx]
78
+ # Use uppercase for the account identifier.
79
+ return account.upper()
80
+
81
+ def get_token(self) -> str:
82
+ """
83
+ Generates a new JWT.
84
+ :return: the new token
85
+ """
86
+ now = datetime.now(timezone.utc) # Fetch the current time
87
+
88
+ # Prepare the fields for the payload.
89
+ # Generate the public key fingerprint for the issuer in the payload.
90
+ public_key_fp = self.calculate_public_key_fingerprint(self.private_key)
91
+
92
+ # Create our payload
93
+ payload = {
94
+ # Set the issuer to the fully qualified username concatenated with the public key fingerprint.
95
+ ISSUER: self.qualified_username + "." + public_key_fp,
96
+ # Set the subject to the fully qualified username.
97
+ SUBJECT: self.qualified_username,
98
+ # Set the issue time to now.
99
+ ISSUE_TIME: now,
100
+ # Set the expiration time, based on the lifetime specified for this object.
101
+ EXPIRE_TIME: now + self.lifetime,
102
+ }
103
+
104
+ # Regenerate the actual token
105
+ token = jwt.encode(payload, key=self.private_key, algorithm=JWTGenerator.ALGORITHM)
106
+ # If you are using a version of PyJWT prior to 2.0, jwt.encode returns a byte string, rather than a string.
107
+ # If the token is a byte string, convert it to a string.
108
+ if isinstance(token, bytes):
109
+ token = token.decode("utf-8")
110
+ self.token = token
111
+
112
+ return self.token
113
+
114
+ def calculate_public_key_fingerprint(self, private_key: str) -> str:
115
+ """
116
+ Given a private key in PEM format, return the public key fingerprint.
117
+ :param private_key: private key string
118
+ :return: public key fingerprint
119
+ """
120
+ # Get the raw bytes of public key.
121
+ public_key_raw = private_key.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
122
+
123
+ # Get the sha256 hash of the raw bytes.
124
+ sha256hash = hashlib.sha256()
125
+ sha256hash.update(public_key_raw)
126
+
127
+ # Base64-encode the value and prepend the prefix 'SHA256:'.
128
+ public_key_fp = "SHA256:" + base64.b64encode(sha256hash.digest()).decode("utf-8")
129
+ logger.info("Public key fingerprint is %s", public_key_fp)
130
+
131
+ return public_key_fp
132
+
133
+
134
+ def get_validated_jwt(token: str, account: str, user: str, private_key: str) -> str:
135
+ try:
136
+ content = jwt.decode(token, algorithms=[JWTGenerator.ALGORITHM], options={"verify_signature": False})
137
+
138
+ expired = content.get("exp", 0)
139
+ # add 5 seconds before limit
140
+ if expired - 5 > time.time():
141
+ # keep the same
142
+ return token
143
+
144
+ except jwt.DecodeError:
145
+ # wrong key
146
+ ...
147
+
148
+ # generate new token
149
+ if private_key is None:
150
+ raise ValueError("Private key is missing")
151
+ return JWTGenerator(account, user, private_key).get_token()
@@ -7,7 +7,6 @@ import math
7
7
  import os
8
8
  import random
9
9
  from abc import ABC
10
- from textwrap import dedent
11
10
  from typing import Any, List, Optional, Tuple
12
11
 
13
12
  from openai import AsyncOpenAI, AsyncAzureOpenAI
@@ -27,6 +26,16 @@ from mindsdb.integrations.libs.base import BaseMLEngine
27
26
  log = logging.getLogger(__name__)
28
27
 
29
28
 
29
+ def get_event_loop():
30
+ try:
31
+ loop = asyncio.get_running_loop()
32
+ except RuntimeError:
33
+ # If no running loop exists, create a new one
34
+ loop = asyncio.new_event_loop()
35
+ asyncio.set_event_loop(loop)
36
+ return loop
37
+
38
+
30
39
  class BaseLLMReranker(BaseModel, ABC):
31
40
  filtering_threshold: float = 0.0 # Default threshold for filtering
32
41
  provider: str = "openai"
@@ -74,7 +83,12 @@ class BaseLLMReranker(BaseModel, ABC):
74
83
  timeout=self.request_timeout,
75
84
  max_retries=2,
76
85
  )
77
- elif self.provider == "openai":
86
+ elif self.provider in ("openai", "ollama"):
87
+ if self.provider == "ollama":
88
+ self.method = "no-logprobs"
89
+ if self.api_key is None:
90
+ self.api_key = "n/a"
91
+
78
92
  api_key_var: str = "OPENAI_API_KEY"
79
93
  openai_api_key = self.api_key or os.getenv(api_key_var)
80
94
  if not openai_api_key:
@@ -84,7 +98,6 @@ class BaseLLMReranker(BaseModel, ABC):
84
98
  self.client = AsyncOpenAI(
85
99
  api_key=openai_api_key, base_url=base_url, timeout=self.request_timeout, max_retries=2
86
100
  )
87
-
88
101
  else:
89
102
  # try to use litellm
90
103
  from mindsdb.api.executor.controllers.session_controller import SessionController
@@ -99,7 +112,7 @@ class BaseLLMReranker(BaseModel, ABC):
99
112
  self.method = "no-logprobs"
100
113
 
101
114
  async def _call_llm(self, messages):
102
- if self.provider in ("azure_openai", "openai"):
115
+ if self.provider in ("azure_openai", "openai", "ollama"):
103
116
  return await self.client.chat.completions.create(
104
117
  model=self.model,
105
118
  messages=messages,
@@ -134,7 +147,7 @@ class BaseLLMReranker(BaseModel, ABC):
134
147
  for idx, result in enumerate(results):
135
148
  if isinstance(result, Exception):
136
149
  log.error(f"Error processing document {i + idx}: {str(result)}")
137
- raise RuntimeError(f"Error during reranking: {result}")
150
+ raise RuntimeError(f"Error during reranking: {result}") from result
138
151
 
139
152
  score = result["relevance_score"]
140
153
 
@@ -217,13 +230,11 @@ class BaseLLMReranker(BaseModel, ABC):
217
230
  return rerank_data
218
231
 
219
232
  async def search_relevancy_no_logprob(self, query: str, document: str) -> Any:
220
- prompt = dedent(
221
- f"""
222
- Score the relevance between search query and user message on scale between 0 and 100 per cents.
223
- Consider semantic meaning, key concepts, and contextual relevance.
224
- Return ONLY a numerical score between 0 and 100 per cents. No other text. Stop after sending a number
225
- Search query: {query}
226
- """
233
+ prompt = (
234
+ f"Score the relevance between search query and user message on scale between 0 and 100 per cents. "
235
+ f"Consider semantic meaning, key concepts, and contextual relevance. "
236
+ f"Return ONLY a numerical score between 0 and 100 per cents. No other text. Stop after sending a number. "
237
+ f"Search query: {query}"
227
238
  )
228
239
 
229
240
  response = await self._call_llm(
@@ -392,16 +403,8 @@ class BaseLLMReranker(BaseModel, ABC):
392
403
  def get_scores(self, query: str, documents: list[str]):
393
404
  query_document_pairs = [(query, doc) for doc in documents]
394
405
  # Create event loop and run async code
395
- import asyncio
396
-
397
- try:
398
- loop = asyncio.get_running_loop()
399
- except RuntimeError:
400
- # If no running loop exists, create a new one
401
- loop = asyncio.new_event_loop()
402
- asyncio.set_event_loop(loop)
403
406
 
404
- documents_and_scores = loop.run_until_complete(self._rank(query_document_pairs))
407
+ documents_and_scores = get_event_loop().run_until_complete(self._rank(query_document_pairs))
405
408
 
406
409
  scores = [score for _, score in documents_and_scores]
407
410
  return scores
@@ -423,8 +423,6 @@ class AgentsController:
423
423
  raise ValueError("It is forbidden to change properties of the demo object")
424
424
 
425
425
  if name is not None and name != agent_name:
426
- if not name.islower():
427
- raise ValueError(f"The name must be in lower case: {name}")
428
426
  # Check to see if updated name already exists
429
427
  agent_with_new_name = self.get_agent(name, project_name=project_name)
430
428
  if agent_with_new_name is not None: