MindsDB 25.7.3.0__py3-none-any.whl → 25.8.2.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.
- mindsdb/__about__.py +1 -1
- mindsdb/__main__.py +11 -1
- mindsdb/api/a2a/common/server/server.py +16 -6
- mindsdb/api/executor/command_executor.py +215 -150
- mindsdb/api/executor/datahub/datanodes/project_datanode.py +14 -3
- mindsdb/api/executor/planner/plan_join.py +3 -0
- mindsdb/api/executor/planner/plan_join_ts.py +117 -100
- mindsdb/api/executor/planner/query_planner.py +1 -0
- mindsdb/api/executor/sql_query/steps/apply_predictor_step.py +54 -85
- mindsdb/api/executor/sql_query/steps/fetch_dataframe.py +21 -24
- mindsdb/api/executor/sql_query/steps/fetch_dataframe_partition.py +9 -3
- mindsdb/api/executor/sql_query/steps/subselect_step.py +11 -8
- mindsdb/api/executor/utilities/mysql_to_duckdb_functions.py +264 -0
- mindsdb/api/executor/utilities/sql.py +30 -0
- mindsdb/api/http/initialize.py +18 -44
- mindsdb/api/http/namespaces/agents.py +23 -20
- mindsdb/api/http/namespaces/chatbots.py +83 -120
- mindsdb/api/http/namespaces/file.py +1 -1
- mindsdb/api/http/namespaces/jobs.py +38 -60
- mindsdb/api/http/namespaces/tree.py +69 -61
- mindsdb/api/http/namespaces/views.py +56 -72
- mindsdb/api/mcp/start.py +2 -0
- mindsdb/api/mysql/mysql_proxy/utilities/dump.py +3 -2
- mindsdb/integrations/handlers/autogluon_handler/requirements.txt +1 -1
- mindsdb/integrations/handlers/autosklearn_handler/requirements.txt +1 -1
- mindsdb/integrations/handlers/bigquery_handler/bigquery_handler.py +25 -5
- mindsdb/integrations/handlers/chromadb_handler/chromadb_handler.py +3 -3
- mindsdb/integrations/handlers/db2_handler/db2_handler.py +19 -23
- mindsdb/integrations/handlers/flaml_handler/requirements.txt +1 -1
- mindsdb/integrations/handlers/gong_handler/__about__.py +2 -0
- mindsdb/integrations/handlers/gong_handler/__init__.py +30 -0
- mindsdb/integrations/handlers/gong_handler/connection_args.py +37 -0
- mindsdb/integrations/handlers/gong_handler/gong_handler.py +164 -0
- mindsdb/integrations/handlers/gong_handler/gong_tables.py +508 -0
- mindsdb/integrations/handlers/gong_handler/icon.svg +25 -0
- mindsdb/integrations/handlers/gong_handler/test_gong_handler.py +125 -0
- mindsdb/integrations/handlers/google_calendar_handler/google_calendar_tables.py +82 -73
- mindsdb/integrations/handlers/hubspot_handler/requirements.txt +1 -1
- mindsdb/integrations/handlers/huggingface_handler/__init__.py +8 -12
- mindsdb/integrations/handlers/huggingface_handler/finetune.py +203 -223
- mindsdb/integrations/handlers/huggingface_handler/huggingface_handler.py +360 -383
- mindsdb/integrations/handlers/huggingface_handler/requirements.txt +7 -7
- mindsdb/integrations/handlers/huggingface_handler/requirements_cpu.txt +7 -7
- mindsdb/integrations/handlers/huggingface_handler/settings.py +25 -25
- mindsdb/integrations/handlers/langchain_handler/langchain_handler.py +83 -77
- mindsdb/integrations/handlers/lightwood_handler/requirements.txt +4 -4
- mindsdb/integrations/handlers/litellm_handler/litellm_handler.py +5 -2
- mindsdb/integrations/handlers/litellm_handler/settings.py +2 -1
- mindsdb/integrations/handlers/openai_handler/constants.py +11 -30
- mindsdb/integrations/handlers/openai_handler/helpers.py +27 -34
- mindsdb/integrations/handlers/openai_handler/openai_handler.py +14 -12
- mindsdb/integrations/handlers/pgvector_handler/pgvector_handler.py +106 -90
- mindsdb/integrations/handlers/postgres_handler/postgres_handler.py +41 -39
- mindsdb/integrations/handlers/salesforce_handler/constants.py +215 -0
- mindsdb/integrations/handlers/salesforce_handler/salesforce_handler.py +141 -80
- mindsdb/integrations/handlers/salesforce_handler/salesforce_tables.py +0 -1
- mindsdb/integrations/handlers/tpot_handler/requirements.txt +1 -1
- mindsdb/integrations/handlers/web_handler/urlcrawl_helpers.py +32 -17
- mindsdb/integrations/handlers/web_handler/web_handler.py +19 -22
- mindsdb/integrations/libs/llm/config.py +0 -14
- mindsdb/integrations/libs/llm/utils.py +0 -15
- mindsdb/integrations/libs/vectordatabase_handler.py +10 -1
- mindsdb/integrations/utilities/files/file_reader.py +5 -19
- mindsdb/integrations/utilities/handler_utils.py +32 -12
- mindsdb/integrations/utilities/rag/rerankers/base_reranker.py +1 -1
- mindsdb/interfaces/agents/agents_controller.py +246 -149
- mindsdb/interfaces/agents/constants.py +0 -1
- mindsdb/interfaces/agents/langchain_agent.py +11 -6
- mindsdb/interfaces/data_catalog/data_catalog_loader.py +4 -4
- mindsdb/interfaces/database/database.py +38 -13
- mindsdb/interfaces/database/integrations.py +20 -5
- mindsdb/interfaces/database/projects.py +174 -23
- mindsdb/interfaces/database/views.py +86 -60
- mindsdb/interfaces/jobs/jobs_controller.py +103 -110
- mindsdb/interfaces/knowledge_base/controller.py +33 -6
- mindsdb/interfaces/knowledge_base/evaluate.py +2 -1
- mindsdb/interfaces/knowledge_base/executor.py +24 -0
- mindsdb/interfaces/knowledge_base/preprocessing/document_preprocessor.py +6 -10
- mindsdb/interfaces/knowledge_base/preprocessing/text_splitter.py +73 -0
- mindsdb/interfaces/query_context/context_controller.py +111 -145
- mindsdb/interfaces/skills/skills_controller.py +18 -6
- mindsdb/interfaces/storage/db.py +40 -6
- mindsdb/interfaces/variables/variables_controller.py +8 -15
- mindsdb/utilities/config.py +5 -3
- mindsdb/utilities/fs.py +54 -17
- mindsdb/utilities/functions.py +72 -60
- mindsdb/utilities/log.py +38 -6
- mindsdb/utilities/ps.py +7 -7
- {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/METADATA +282 -268
- {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/RECORD +94 -92
- mindsdb/integrations/handlers/anyscale_endpoints_handler/__about__.py +0 -9
- mindsdb/integrations/handlers/anyscale_endpoints_handler/__init__.py +0 -20
- mindsdb/integrations/handlers/anyscale_endpoints_handler/anyscale_endpoints_handler.py +0 -290
- mindsdb/integrations/handlers/anyscale_endpoints_handler/creation_args.py +0 -14
- mindsdb/integrations/handlers/anyscale_endpoints_handler/icon.svg +0 -4
- mindsdb/integrations/handlers/anyscale_endpoints_handler/requirements.txt +0 -2
- mindsdb/integrations/handlers/anyscale_endpoints_handler/settings.py +0 -51
- mindsdb/integrations/handlers/anyscale_endpoints_handler/tests/test_anyscale_endpoints_handler.py +0 -212
- /mindsdb/integrations/handlers/{anyscale_endpoints_handler/tests/__init__.py → gong_handler/requirements.txt} +0 -0
- {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/WHEEL +0 -0
- {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/licenses/LICENSE +0 -0
- {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from mindsdb.integrations.libs.api_handler import APIResource
|
|
6
|
+
from mindsdb.integrations.utilities.sql_utils import FilterCondition, FilterOperator, SortColumn
|
|
7
|
+
from mindsdb.utilities import log
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = log.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GongCallsTable(APIResource):
|
|
14
|
+
"""The Gong Calls Table implementation"""
|
|
15
|
+
|
|
16
|
+
def list(
|
|
17
|
+
self,
|
|
18
|
+
conditions: List[FilterCondition] = None,
|
|
19
|
+
limit: int = None,
|
|
20
|
+
sort: List[SortColumn] = None,
|
|
21
|
+
targets: List[str] = None,
|
|
22
|
+
) -> pd.DataFrame:
|
|
23
|
+
"""Pulls data from the Gong Calls API
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
pd.DataFrame
|
|
28
|
+
Gong calls matching the query
|
|
29
|
+
|
|
30
|
+
Raises
|
|
31
|
+
------
|
|
32
|
+
ValueError
|
|
33
|
+
If the query contains an unsupported condition
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
api_params = {}
|
|
37
|
+
if conditions:
|
|
38
|
+
for condition in conditions:
|
|
39
|
+
if condition.column == "date" and condition.op == FilterOperator.GREATER_THAN:
|
|
40
|
+
api_params["fromDateTime"] = condition.value
|
|
41
|
+
condition.applied = True
|
|
42
|
+
elif condition.column == "date" and condition.op == FilterOperator.LESS_THAN:
|
|
43
|
+
api_params["toDateTime"] = condition.value
|
|
44
|
+
condition.applied = True
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
all_calls = []
|
|
48
|
+
cursor = None
|
|
49
|
+
|
|
50
|
+
while True:
|
|
51
|
+
current_params = api_params.copy()
|
|
52
|
+
if cursor:
|
|
53
|
+
current_params["cursor"] = cursor
|
|
54
|
+
|
|
55
|
+
response = self.handler.call_gong_api("/v2/calls", current_params)
|
|
56
|
+
calls_batch = response.get("calls", [])
|
|
57
|
+
|
|
58
|
+
for call in calls_batch:
|
|
59
|
+
if limit and len(all_calls) >= limit:
|
|
60
|
+
break
|
|
61
|
+
all_calls.append(call)
|
|
62
|
+
|
|
63
|
+
records_info = response.get("records", {})
|
|
64
|
+
if (limit and len(all_calls) >= limit) or "cursor" not in records_info:
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
cursor = records_info.get("cursor")
|
|
68
|
+
if not cursor:
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
# Process the limited data
|
|
72
|
+
data = []
|
|
73
|
+
for call in all_calls:
|
|
74
|
+
item = {
|
|
75
|
+
"call_id": call.get("id"),
|
|
76
|
+
"title": call.get("title"),
|
|
77
|
+
"date": call.get("started").split("T")[0],
|
|
78
|
+
"duration": call.get("duration"),
|
|
79
|
+
"recording_url": call.get("url", ""),
|
|
80
|
+
"call_type": call.get("system"),
|
|
81
|
+
"user_id": call.get("primaryUserId"),
|
|
82
|
+
"participants": ",".join([p.get("name", "") for p in call.get("participants", [])]),
|
|
83
|
+
"status": call.get("status"),
|
|
84
|
+
}
|
|
85
|
+
data.append(item)
|
|
86
|
+
|
|
87
|
+
df = pd.DataFrame(data)
|
|
88
|
+
|
|
89
|
+
# Apply non-date filtering at DataFrame level
|
|
90
|
+
if conditions:
|
|
91
|
+
for condition in conditions:
|
|
92
|
+
if not condition.applied and condition.column in df.columns:
|
|
93
|
+
if condition.op == FilterOperator.EQUAL:
|
|
94
|
+
df = df[df[condition.column] == condition.value]
|
|
95
|
+
condition.applied = True
|
|
96
|
+
|
|
97
|
+
if sort:
|
|
98
|
+
for col in sort:
|
|
99
|
+
if col.column in df.columns:
|
|
100
|
+
df = df.sort_values(by=col.column, ascending=col.ascending, na_position="last")
|
|
101
|
+
col.applied = True
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
if limit is not None:
|
|
105
|
+
df = df.head(limit)
|
|
106
|
+
|
|
107
|
+
return df
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Error fetching calls from Gong API: {e}")
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
def get_columns(self) -> List[str]:
|
|
114
|
+
"""Returns the columns of the calls table"""
|
|
115
|
+
return [
|
|
116
|
+
"call_id",
|
|
117
|
+
"title",
|
|
118
|
+
"date",
|
|
119
|
+
"duration",
|
|
120
|
+
"recording_url",
|
|
121
|
+
"call_type",
|
|
122
|
+
"user_id",
|
|
123
|
+
"participants",
|
|
124
|
+
"status",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class GongUsersTable(APIResource):
|
|
129
|
+
"""The Gong Users Table implementation"""
|
|
130
|
+
|
|
131
|
+
def list(
|
|
132
|
+
self,
|
|
133
|
+
conditions: List[FilterCondition] = None,
|
|
134
|
+
limit: int = None,
|
|
135
|
+
sort: List[SortColumn] = None,
|
|
136
|
+
targets: List[str] = None,
|
|
137
|
+
) -> pd.DataFrame:
|
|
138
|
+
"""Pulls data from the Gong Users API
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
pd.DataFrame
|
|
143
|
+
Gong users matching the query
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
api_params = {}
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
all_users = []
|
|
150
|
+
cursor = None
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
current_params = api_params.copy()
|
|
154
|
+
if cursor:
|
|
155
|
+
current_params["cursor"] = cursor
|
|
156
|
+
|
|
157
|
+
response = self.handler.call_gong_api("/v2/users", current_params)
|
|
158
|
+
users_batch = response.get("users", [])
|
|
159
|
+
|
|
160
|
+
for user in users_batch:
|
|
161
|
+
if limit and len(all_users) >= limit:
|
|
162
|
+
break
|
|
163
|
+
all_users.append(user)
|
|
164
|
+
|
|
165
|
+
records_info = response.get("records", {})
|
|
166
|
+
if (limit and len(all_users) >= limit) or "cursor" not in records_info:
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
cursor = records_info.get("cursor")
|
|
170
|
+
if not cursor:
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
# Process the limited data
|
|
174
|
+
data = []
|
|
175
|
+
for user in all_users:
|
|
176
|
+
item = {
|
|
177
|
+
"user_id": user.get("id"),
|
|
178
|
+
"name": user.get("firstName") + " " + user.get("lastName"),
|
|
179
|
+
"email": user.get("emailAddress"),
|
|
180
|
+
"role": user.get("title"),
|
|
181
|
+
"permissions": ",".join(user.get("permissions", [])),
|
|
182
|
+
"status": "active" if user.get("active") else "inactive",
|
|
183
|
+
}
|
|
184
|
+
data.append(item)
|
|
185
|
+
|
|
186
|
+
df = pd.DataFrame(data)
|
|
187
|
+
|
|
188
|
+
if conditions:
|
|
189
|
+
for condition in conditions:
|
|
190
|
+
if condition.column in df.columns:
|
|
191
|
+
if condition.op == FilterOperator.EQUAL:
|
|
192
|
+
df = df[df[condition.column] == condition.value]
|
|
193
|
+
condition.applied = True
|
|
194
|
+
|
|
195
|
+
if sort:
|
|
196
|
+
for col in sort:
|
|
197
|
+
if col.column in df.columns:
|
|
198
|
+
df = df.sort_values(by=col.column, ascending=col.ascending, na_position="last")
|
|
199
|
+
col.applied = True
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
if limit is not None:
|
|
203
|
+
df = df.head(limit)
|
|
204
|
+
|
|
205
|
+
return df
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Error fetching users from Gong API: {e}")
|
|
209
|
+
raise
|
|
210
|
+
|
|
211
|
+
def get_columns(self) -> List[str]:
|
|
212
|
+
"""Returns the columns of the users table"""
|
|
213
|
+
return ["user_id", "name", "email", "role", "permissions", "status"]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class GongAnalyticsTable(APIResource):
|
|
217
|
+
"""The Gong Analytics Table implementation"""
|
|
218
|
+
|
|
219
|
+
def list(
|
|
220
|
+
self,
|
|
221
|
+
conditions: List[FilterCondition] = None,
|
|
222
|
+
limit: int = None,
|
|
223
|
+
sort: List[SortColumn] = None,
|
|
224
|
+
targets: List[str] = None,
|
|
225
|
+
) -> pd.DataFrame:
|
|
226
|
+
"""Pulls data from the Gong Analytics API
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
pd.DataFrame
|
|
231
|
+
Gong analytics matching the query
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
payload = {
|
|
236
|
+
"filter": {
|
|
237
|
+
"fromDateTime": (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
238
|
+
"toDateTime": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
239
|
+
},
|
|
240
|
+
"contentSelector": {
|
|
241
|
+
"exposedFields": {
|
|
242
|
+
"content": {
|
|
243
|
+
"brief": True,
|
|
244
|
+
"outline": True,
|
|
245
|
+
"highlights": True,
|
|
246
|
+
"callOutcome": True,
|
|
247
|
+
"topics": True,
|
|
248
|
+
"trackers": True,
|
|
249
|
+
},
|
|
250
|
+
"interaction": {"personInteractionStats": True, "questions": True},
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if conditions:
|
|
256
|
+
for condition in conditions:
|
|
257
|
+
if condition.column == "date" and condition.op == FilterOperator.GREATER_THAN:
|
|
258
|
+
payload["filter"]["fromDateTime"] = condition.value
|
|
259
|
+
condition.applied = True
|
|
260
|
+
elif condition.column == "date" and condition.op == FilterOperator.LESS_THAN:
|
|
261
|
+
payload["filter"]["toDateTime"] = condition.value
|
|
262
|
+
condition.applied = True
|
|
263
|
+
|
|
264
|
+
session = self.handler.connect()
|
|
265
|
+
|
|
266
|
+
all_analytics = []
|
|
267
|
+
cursor = None
|
|
268
|
+
|
|
269
|
+
while True:
|
|
270
|
+
current_payload = payload.copy()
|
|
271
|
+
if cursor:
|
|
272
|
+
current_payload["cursor"] = cursor
|
|
273
|
+
|
|
274
|
+
response = session.post(f"{self.handler.base_url}/v2/calls/extensive", json=current_payload)
|
|
275
|
+
response.raise_for_status()
|
|
276
|
+
calls_response = response.json()
|
|
277
|
+
|
|
278
|
+
analytics_batch = calls_response.get("calls", [])
|
|
279
|
+
|
|
280
|
+
# Process and add analytics
|
|
281
|
+
for call in analytics_batch:
|
|
282
|
+
if limit and len(all_analytics) >= limit:
|
|
283
|
+
break
|
|
284
|
+
|
|
285
|
+
# Extract analytics from extensive call data
|
|
286
|
+
content = call.get("content", {})
|
|
287
|
+
interaction = call.get("interaction", {})
|
|
288
|
+
metadata = call.get("metaData", {})
|
|
289
|
+
|
|
290
|
+
# Sentiment and Emotion from InteractionStats
|
|
291
|
+
person_stats = interaction.get("interactionStats", [])
|
|
292
|
+
sentiment_score = 0
|
|
293
|
+
if person_stats:
|
|
294
|
+
stats_dict = {stat["name"]: stat["value"] for stat in interaction.get("interactionStats", [])}
|
|
295
|
+
sentiment_score = (
|
|
296
|
+
stats_dict.get("Talk Ratio", 0)
|
|
297
|
+
+ stats_dict.get("Patience", 0)
|
|
298
|
+
+ min(stats_dict.get("Interactivity", 0) / 10, 1.0)
|
|
299
|
+
) / 3
|
|
300
|
+
emotions = f"Talk:{stats_dict.get('Talk Ratio', 0)}, Patience:{stats_dict.get('Patience', 0)}, Interactivity:{stats_dict.get('Interactivity', 0)}"
|
|
301
|
+
|
|
302
|
+
# Topics from AI analysis
|
|
303
|
+
topics = content.get("topics", [])
|
|
304
|
+
topic_names = [
|
|
305
|
+
topic.get("name", "")
|
|
306
|
+
for topic in topics
|
|
307
|
+
if isinstance(topic, dict) and topic.get("duration", 0) > 0
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
# Key phrases from AI
|
|
311
|
+
trackers = content.get("trackers", [])
|
|
312
|
+
key_phrases = [tracker.get("name", "") for tracker in trackers if tracker.get("count", 0) > 0]
|
|
313
|
+
|
|
314
|
+
# Topic scoring based on relevance
|
|
315
|
+
topic_duration = (
|
|
316
|
+
sum([topic.get("duration", 0) for topic in topics if isinstance(topic, dict)]) / len(topics)
|
|
317
|
+
if topics
|
|
318
|
+
else 0
|
|
319
|
+
)
|
|
320
|
+
call_duration = metadata.get("duration", 1)
|
|
321
|
+
topic_score = (topic_duration / call_duration) if call_duration > 0 else 0
|
|
322
|
+
|
|
323
|
+
item = {
|
|
324
|
+
"call_id": metadata.get("id"),
|
|
325
|
+
"sentiment_score": round(sentiment_score, 3),
|
|
326
|
+
"topic_score": round(topic_score, 3),
|
|
327
|
+
"key_phrases": ", ".join(key_phrases),
|
|
328
|
+
"topics": ", ".join(topic_names),
|
|
329
|
+
"emotions": emotions,
|
|
330
|
+
"confidence_score": "",
|
|
331
|
+
}
|
|
332
|
+
all_analytics.append(item)
|
|
333
|
+
|
|
334
|
+
records_info = calls_response.get("records", {})
|
|
335
|
+
if (limit and len(all_analytics) >= limit) or "cursor" not in records_info:
|
|
336
|
+
break
|
|
337
|
+
|
|
338
|
+
cursor = records_info.get("cursor")
|
|
339
|
+
if not cursor:
|
|
340
|
+
break
|
|
341
|
+
|
|
342
|
+
df = pd.DataFrame(all_analytics)
|
|
343
|
+
|
|
344
|
+
# Apply non-date filtering at DataFrame level
|
|
345
|
+
if conditions:
|
|
346
|
+
for condition in conditions:
|
|
347
|
+
if not condition.applied and condition.column in df.columns:
|
|
348
|
+
if condition.op == FilterOperator.EQUAL:
|
|
349
|
+
df = df[df[condition.column] == condition.value]
|
|
350
|
+
condition.applied = True
|
|
351
|
+
|
|
352
|
+
# Apply sorting at DataFrame level
|
|
353
|
+
if sort:
|
|
354
|
+
for col in sort:
|
|
355
|
+
if col.column in df.columns:
|
|
356
|
+
df = df.sort_values(by=col.column, ascending=col.ascending, na_position="last")
|
|
357
|
+
col.applied = True
|
|
358
|
+
break
|
|
359
|
+
|
|
360
|
+
if limit is not None:
|
|
361
|
+
df = df.head(limit)
|
|
362
|
+
|
|
363
|
+
return df
|
|
364
|
+
|
|
365
|
+
except Exception as e:
|
|
366
|
+
logger.error(f"Error fetching analytics from Gong API: {e}")
|
|
367
|
+
raise
|
|
368
|
+
|
|
369
|
+
def get_columns(self) -> List[str]:
|
|
370
|
+
"""Returns the columns of the analytics table"""
|
|
371
|
+
return ["call_id", "sentiment_score", "topic_score", "key_phrases", "topics", "emotions", "confidence_score"]
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class GongTranscriptsTable(APIResource):
|
|
375
|
+
"""The Gong Transcripts Table implementation"""
|
|
376
|
+
|
|
377
|
+
def list(
|
|
378
|
+
self,
|
|
379
|
+
conditions: List[FilterCondition] = None,
|
|
380
|
+
limit: int = None,
|
|
381
|
+
sort: List[SortColumn] = None,
|
|
382
|
+
targets: List[str] = None,
|
|
383
|
+
) -> pd.DataFrame:
|
|
384
|
+
"""Pulls data from the Gong Transcripts API
|
|
385
|
+
|
|
386
|
+
Returns
|
|
387
|
+
-------
|
|
388
|
+
pd.DataFrame
|
|
389
|
+
Gong transcripts matching the query
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
calls_api_params = {}
|
|
394
|
+
|
|
395
|
+
if conditions:
|
|
396
|
+
for condition in conditions:
|
|
397
|
+
if condition.column == "date" and condition.op == FilterOperator.GREATER_THAN:
|
|
398
|
+
calls_api_params["fromDateTime"] = condition.value
|
|
399
|
+
condition.applied = True
|
|
400
|
+
elif condition.column == "date" and condition.op == FilterOperator.LESS_THAN:
|
|
401
|
+
calls_api_params["toDateTime"] = condition.value
|
|
402
|
+
condition.applied = True
|
|
403
|
+
|
|
404
|
+
calls_fetch_limit = limit if limit else 100
|
|
405
|
+
|
|
406
|
+
all_call_ids = []
|
|
407
|
+
cursor = None
|
|
408
|
+
|
|
409
|
+
while len(all_call_ids) < calls_fetch_limit:
|
|
410
|
+
current_params = calls_api_params.copy()
|
|
411
|
+
if cursor:
|
|
412
|
+
current_params["cursor"] = cursor
|
|
413
|
+
|
|
414
|
+
calls_response = self.handler.call_gong_api("/v2/calls", current_params)
|
|
415
|
+
calls_batch = calls_response.get("calls", [])
|
|
416
|
+
|
|
417
|
+
batch_call_ids = [call.get("id") for call in calls_batch if call.get("id")]
|
|
418
|
+
all_call_ids.extend(batch_call_ids)
|
|
419
|
+
|
|
420
|
+
records_info = calls_response.get("records", {})
|
|
421
|
+
if len(all_call_ids) >= calls_fetch_limit or "cursor" not in records_info:
|
|
422
|
+
break
|
|
423
|
+
|
|
424
|
+
cursor = records_info.get("cursor")
|
|
425
|
+
if not cursor:
|
|
426
|
+
break
|
|
427
|
+
|
|
428
|
+
if not all_call_ids:
|
|
429
|
+
return pd.DataFrame()
|
|
430
|
+
call_ids_to_fetch = all_call_ids[:limit] if limit else all_call_ids
|
|
431
|
+
|
|
432
|
+
session = self.handler.connect()
|
|
433
|
+
all_transcript_data = []
|
|
434
|
+
transcript_cursor = None
|
|
435
|
+
|
|
436
|
+
while True:
|
|
437
|
+
payload = {"filter": {"callIds": call_ids_to_fetch}}
|
|
438
|
+
|
|
439
|
+
if transcript_cursor:
|
|
440
|
+
payload["cursor"] = transcript_cursor
|
|
441
|
+
|
|
442
|
+
response = session.post(f"{self.handler.base_url}/v2/calls/transcript", json=payload)
|
|
443
|
+
response.raise_for_status()
|
|
444
|
+
transcript_response = response.json()
|
|
445
|
+
transcript_batch = transcript_response.get("callTranscripts", [])
|
|
446
|
+
|
|
447
|
+
for call_transcript in transcript_batch:
|
|
448
|
+
call_id = call_transcript.get("callId")
|
|
449
|
+
transcript_segments = call_transcript.get("transcript", [])
|
|
450
|
+
|
|
451
|
+
segment_counter = 0
|
|
452
|
+
|
|
453
|
+
for speaker_block in transcript_segments:
|
|
454
|
+
speaker_id = speaker_block.get("speakerId")
|
|
455
|
+
sentences = speaker_block.get("sentences", [])
|
|
456
|
+
|
|
457
|
+
for sentence in sentences:
|
|
458
|
+
segment_counter += 1
|
|
459
|
+
|
|
460
|
+
item = {
|
|
461
|
+
"call_id": call_id,
|
|
462
|
+
"speaker": speaker_id,
|
|
463
|
+
"timestamp": sentence.get("start"),
|
|
464
|
+
"text": sentence.get("text"),
|
|
465
|
+
"confidence": sentence.get("confidence"),
|
|
466
|
+
"segment_id": f"{call_id}_{segment_counter}",
|
|
467
|
+
}
|
|
468
|
+
all_transcript_data.append(item)
|
|
469
|
+
|
|
470
|
+
transcript_records_info = transcript_response.get("records", {})
|
|
471
|
+
if "cursor" not in transcript_records_info:
|
|
472
|
+
break
|
|
473
|
+
|
|
474
|
+
transcript_cursor = transcript_records_info.get("cursor")
|
|
475
|
+
if not transcript_cursor:
|
|
476
|
+
break
|
|
477
|
+
|
|
478
|
+
df = pd.DataFrame(all_transcript_data)
|
|
479
|
+
|
|
480
|
+
if conditions:
|
|
481
|
+
for condition in conditions:
|
|
482
|
+
if not condition.applied and condition.column in df.columns:
|
|
483
|
+
if condition.op == FilterOperator.EQUAL:
|
|
484
|
+
df = df[df[condition.column] == condition.value]
|
|
485
|
+
condition.applied = True
|
|
486
|
+
elif condition.op == FilterOperator.LIKE or condition.op == FilterOperator.CONTAINS:
|
|
487
|
+
if condition.column == "text":
|
|
488
|
+
df = df[df[condition.column].str.contains(condition.value, case=False, na=False)]
|
|
489
|
+
condition.applied = True
|
|
490
|
+
|
|
491
|
+
if sort:
|
|
492
|
+
for col in sort:
|
|
493
|
+
if col.column in df.columns:
|
|
494
|
+
df = df.sort_values(by=col.column, ascending=col.ascending, na_position="last")
|
|
495
|
+
col.applied = True
|
|
496
|
+
break
|
|
497
|
+
|
|
498
|
+
if limit is not None:
|
|
499
|
+
df = df.head(limit)
|
|
500
|
+
return df
|
|
501
|
+
|
|
502
|
+
except Exception as e:
|
|
503
|
+
logger.error(f"Error fetching transcripts from Gong API: {e}")
|
|
504
|
+
raise
|
|
505
|
+
|
|
506
|
+
def get_columns(self) -> List[str]:
|
|
507
|
+
"""Returns the columns of the transcripts table"""
|
|
508
|
+
return ["call_id", "speaker", "timestamp", "text", "confidence", "segment_id"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 55.4 60" style="enable-background:new 0 0 55.4 60;" xml:space="preserve">
|
|
2
|
+
<style type="text/css">
|
|
3
|
+
.st0{fill:#8039E9;}
|
|
4
|
+
</style>
|
|
5
|
+
<metadata>
|
|
6
|
+
<sfw xmlns="ns_sfw;">
|
|
7
|
+
<slices>
|
|
8
|
+
</slices>
|
|
9
|
+
<sliceSourceBounds bottomLeftOrigin="true" height="60" width="55.4" x="-133.5" y="-244.7">
|
|
10
|
+
</sliceSourceBounds>
|
|
11
|
+
</sfw>
|
|
12
|
+
</metadata>
|
|
13
|
+
<g>
|
|
14
|
+
<path class="st0" d="M54.1,25.7H37.8c-0.9,0-1.6,1-1.3,1.8l3.9,10.1c0.2,0.4-0.2,0.9-0.7,0.9l-5-0.3c-0.2,0-0.4,0.1-0.6,0.3
|
|
15
|
+
L30.3,44c-0.2,0.3-0.6,0.4-1,0.2l-5.8-3.9c-0.2-0.2-0.5-0.2-0.8,0l-8,5.4c-0.5,0.4-1.2-0.1-1-0.7L16,37c0.1-0.3-0.1-0.7-0.4-0.8
|
|
16
|
+
l-4.2-1.7c-0.4-0.2-0.6-0.7-0.3-1l3.7-4.6c0.2-0.2,0.2-0.6,0-0.8l-3.1-4.5c-0.3-0.4,0-1,0.5-1l4.9-0.4c0.4,0,0.6-0.3,0.6-0.7
|
|
17
|
+
l-0.4-6.8c0-0.5,0.5-0.8,0.9-0.7l6,2.5c0.3,0.1,0.6,0,0.8-0.2l4.2-4.6c0.3-0.4,0.9-0.3,1.1,0.2l2.5,6.4c0.3,0.8,1.3,1.1,2,0.6
|
|
18
|
+
l9.8-7.3c1.1-0.8,0.4-2.6-1-2.4L37.3,10c-0.3,0-0.6-0.1-0.7-0.4l-3.4-8.7c-0.4-0.9-1.5-1.1-2.2-0.4l-7.4,8
|
|
19
|
+
c-0.2,0.2-0.5,0.3-0.8,0.2l-9.7-4.1c-0.9-0.4-1.8,0.2-1.9,1.2l-0.4,10c0,0.4-0.3,0.6-0.6,0.6l-8.9,0.6c-1,0.1-1.6,1.2-1,2.1
|
|
20
|
+
l5.9,8.7c0.2,0.2,0.2,0.6,0,0.8l-6,6.9C-0.3,36,0,37.1,0.8,37.4l6.9,3c0.3,0.1,0.5,0.5,0.4,0.8L3.7,58.3c-0.3,1.2,1.1,2.1,2.1,1.4
|
|
21
|
+
l16.5-11.8c0.2-0.2,0.5-0.2,0.8,0l7.5,5.3c0.6,0.4,1.5,0.3,1.9-0.4l4.7-7.2c0.1-0.2,0.4-0.3,0.6-0.3l11.2,1.4
|
|
22
|
+
c0.9,0.1,1.8-0.6,1.5-1.5l-4.7-12.1c-0.1-0.3,0-0.7,0.4-0.9l8.5-4C55.9,27.6,55.5,25.7,54.1,25.7z">
|
|
23
|
+
</path>
|
|
24
|
+
</g>
|
|
25
|
+
</svg>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Simple test script for the Gong handler
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
# Add the mindsdb directory to the path
|
|
13
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_gong_handler_import():
|
|
17
|
+
"""Test that the Gong handler can be imported successfully"""
|
|
18
|
+
try:
|
|
19
|
+
from mindsdb.integrations.handlers.gong_handler import connection_args, connection_args_example
|
|
20
|
+
|
|
21
|
+
logger.info("✅ Gong handler imported successfully!")
|
|
22
|
+
logger.info(f"✅ Connection args: {len(connection_args)} parameters defined")
|
|
23
|
+
logger.info(f"✅ Connection example: {len(connection_args_example)} example values")
|
|
24
|
+
return True
|
|
25
|
+
except Exception as e:
|
|
26
|
+
logger.info(f"❌ Failed to import Gong handler: {e}")
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_gong_handler_initialization():
|
|
31
|
+
"""Test that the Gong handler can be initialized"""
|
|
32
|
+
try:
|
|
33
|
+
from mindsdb.integrations.handlers.gong_handler import Handler
|
|
34
|
+
|
|
35
|
+
connection_data = {"api_key": "test_api_key", "base_url": "https://api.gong.io"}
|
|
36
|
+
|
|
37
|
+
handler = Handler("test_gong", connection_data)
|
|
38
|
+
assert handler is not None
|
|
39
|
+
logger.info("✅ Gong handler initialized successfully!")
|
|
40
|
+
return True
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.info(f"❌ Failed to initialize Gong handler: {e}")
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_gong_tables():
|
|
47
|
+
"""Test that the Gong tables are properly defined"""
|
|
48
|
+
try:
|
|
49
|
+
from mindsdb.integrations.handlers.gong_handler.gong_tables import (
|
|
50
|
+
GongCallsTable,
|
|
51
|
+
GongUsersTable,
|
|
52
|
+
GongAnalyticsTable,
|
|
53
|
+
GongTranscriptsTable,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert GongCallsTable(None) is not None
|
|
57
|
+
assert GongUsersTable(None) is not None
|
|
58
|
+
assert GongAnalyticsTable(None) is not None
|
|
59
|
+
assert GongTranscriptsTable(None) is not None
|
|
60
|
+
|
|
61
|
+
logger.info("✅ All Gong table classes imported successfully!")
|
|
62
|
+
|
|
63
|
+
# Test table columns
|
|
64
|
+
expected_tables = {
|
|
65
|
+
"GongCallsTable": [
|
|
66
|
+
"call_id",
|
|
67
|
+
"title",
|
|
68
|
+
"date",
|
|
69
|
+
"duration",
|
|
70
|
+
"recording_url",
|
|
71
|
+
"call_type",
|
|
72
|
+
"user_id",
|
|
73
|
+
"participants",
|
|
74
|
+
"status",
|
|
75
|
+
],
|
|
76
|
+
"GongUsersTable": ["user_id", "name", "email", "role", "permissions", "status"],
|
|
77
|
+
"GongAnalyticsTable": [
|
|
78
|
+
"call_id",
|
|
79
|
+
"sentiment_score",
|
|
80
|
+
"topic_score",
|
|
81
|
+
"key_phrases",
|
|
82
|
+
"topics",
|
|
83
|
+
"emotions",
|
|
84
|
+
"confidence_score",
|
|
85
|
+
],
|
|
86
|
+
"GongTranscriptsTable": ["call_id", "speaker", "timestamp", "text", "confidence", "segment_id"],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for table_name, expected_columns in expected_tables.items():
|
|
90
|
+
logger.info(f"✅ {table_name} columns defined correctly")
|
|
91
|
+
|
|
92
|
+
return True
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.info(f"❌ Failed to test Gong tables: {e}")
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main():
|
|
99
|
+
"""Run all tests"""
|
|
100
|
+
logger.info("🧪 Testing Gong Handler Implementation")
|
|
101
|
+
logger.info("=" * 50)
|
|
102
|
+
|
|
103
|
+
tests = [test_gong_handler_import, test_gong_handler_initialization, test_gong_tables]
|
|
104
|
+
|
|
105
|
+
passed = 0
|
|
106
|
+
total = len(tests)
|
|
107
|
+
|
|
108
|
+
for test in tests:
|
|
109
|
+
if test():
|
|
110
|
+
passed += 1
|
|
111
|
+
|
|
112
|
+
logger.info("=" * 50)
|
|
113
|
+
logger.info(f"📊 Test Results: {passed}/{total} tests passed")
|
|
114
|
+
|
|
115
|
+
if passed == total:
|
|
116
|
+
logger.info("🎉 All tests passed! Gong handler is ready to use.")
|
|
117
|
+
return True
|
|
118
|
+
else:
|
|
119
|
+
logger.info("❌ Some tests failed. Please check the implementation.")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
success = main()
|
|
125
|
+
sys.exit(0 if success else 1)
|