MindsDB 25.8.2.0__py3-none-any.whl → 25.9.1.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 +5 -45
- mindsdb/api/a2a/__init__.py +52 -0
- mindsdb/api/a2a/agent.py +17 -28
- mindsdb/api/a2a/common/server/server.py +17 -36
- mindsdb/api/a2a/common/server/task_manager.py +14 -28
- mindsdb/api/a2a/common/types.py +3 -4
- mindsdb/api/a2a/task_manager.py +43 -55
- mindsdb/api/a2a/utils.py +63 -0
- mindsdb/api/common/middleware.py +106 -0
- mindsdb/api/http/initialize.py +13 -15
- mindsdb/api/http/namespaces/agents.py +6 -7
- mindsdb/api/http/namespaces/auth.py +6 -14
- mindsdb/api/http/namespaces/config.py +0 -2
- mindsdb/api/http/namespaces/default.py +74 -106
- mindsdb/api/http/start.py +25 -44
- mindsdb/api/litellm/start.py +11 -10
- mindsdb/api/mcp/__init__.py +165 -0
- mindsdb/api/mysql/mysql_proxy/mysql_proxy.py +33 -64
- mindsdb/api/postgres/postgres_proxy/postgres_proxy.py +86 -85
- mindsdb/integrations/handlers/crate_handler/crate_handler.py +3 -7
- mindsdb/integrations/handlers/derby_handler/derby_handler.py +32 -34
- mindsdb/integrations/handlers/documentdb_handler/requirements.txt +1 -0
- mindsdb/integrations/handlers/dummy_data_handler/dummy_data_handler.py +12 -13
- mindsdb/integrations/handlers/google_books_handler/google_books_handler.py +45 -44
- mindsdb/integrations/handlers/google_calendar_handler/google_calendar_handler.py +101 -95
- mindsdb/integrations/handlers/google_content_shopping_handler/google_content_shopping_handler.py +129 -129
- mindsdb/integrations/handlers/google_fit_handler/google_fit_handler.py +59 -43
- mindsdb/integrations/handlers/google_search_handler/google_search_handler.py +38 -39
- mindsdb/integrations/handlers/informix_handler/informix_handler.py +5 -18
- mindsdb/integrations/handlers/maxdb_handler/maxdb_handler.py +22 -28
- mindsdb/integrations/handlers/monetdb_handler/monetdb_handler.py +3 -7
- mindsdb/integrations/handlers/mongodb_handler/mongodb_handler.py +53 -67
- mindsdb/integrations/handlers/mongodb_handler/requirements.txt +1 -0
- mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_ast.py +43 -68
- mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_parser.py +17 -25
- mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_query.py +10 -16
- mindsdb/integrations/handlers/mongodb_handler/utils/mongodb_render.py +43 -69
- mindsdb/integrations/libs/base.py +1 -1
- mindsdb/interfaces/agents/constants.py +17 -2
- mindsdb/interfaces/agents/langchain_agent.py +83 -18
- mindsdb/interfaces/knowledge_base/controller.py +3 -1
- mindsdb/interfaces/skills/custom/text2sql/mindsdb_sql_toolkit.py +7 -1
- mindsdb/interfaces/skills/skill_tool.py +7 -1
- mindsdb/interfaces/skills/sql_agent.py +6 -2
- mindsdb/utilities/config.py +3 -155
- mindsdb/utilities/fs.py +10 -4
- mindsdb/utilities/log.py +0 -25
- mindsdb/utilities/starters.py +0 -39
- {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/METADATA +265 -263
- {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/RECORD +54 -98
- mindsdb/api/a2a/__main__.py +0 -144
- mindsdb/api/a2a/run_a2a.py +0 -86
- mindsdb/api/common/check_auth.py +0 -42
- mindsdb/api/http/gunicorn_wrapper.py +0 -17
- mindsdb/api/mcp/start.py +0 -205
- mindsdb/api/mongo/__init__.py +0 -0
- mindsdb/api/mongo/classes/__init__.py +0 -5
- mindsdb/api/mongo/classes/query_sql.py +0 -19
- mindsdb/api/mongo/classes/responder.py +0 -45
- mindsdb/api/mongo/classes/responder_collection.py +0 -34
- mindsdb/api/mongo/classes/scram.py +0 -86
- mindsdb/api/mongo/classes/session.py +0 -23
- mindsdb/api/mongo/functions/__init__.py +0 -19
- mindsdb/api/mongo/responders/__init__.py +0 -73
- mindsdb/api/mongo/responders/add_shard.py +0 -13
- mindsdb/api/mongo/responders/aggregate.py +0 -90
- mindsdb/api/mongo/responders/buildinfo.py +0 -17
- mindsdb/api/mongo/responders/coll_stats.py +0 -63
- mindsdb/api/mongo/responders/company_id.py +0 -25
- mindsdb/api/mongo/responders/connection_status.py +0 -22
- mindsdb/api/mongo/responders/count.py +0 -21
- mindsdb/api/mongo/responders/db_stats.py +0 -32
- mindsdb/api/mongo/responders/delete.py +0 -105
- mindsdb/api/mongo/responders/describe.py +0 -23
- mindsdb/api/mongo/responders/end_sessions.py +0 -13
- mindsdb/api/mongo/responders/find.py +0 -175
- mindsdb/api/mongo/responders/get_cmd_line_opts.py +0 -18
- mindsdb/api/mongo/responders/get_free_monitoring_status.py +0 -14
- mindsdb/api/mongo/responders/get_parameter.py +0 -23
- mindsdb/api/mongo/responders/getlog.py +0 -14
- mindsdb/api/mongo/responders/host_info.py +0 -28
- mindsdb/api/mongo/responders/insert.py +0 -270
- mindsdb/api/mongo/responders/is_master.py +0 -20
- mindsdb/api/mongo/responders/is_master_lower.py +0 -13
- mindsdb/api/mongo/responders/list_collections.py +0 -55
- mindsdb/api/mongo/responders/list_databases.py +0 -37
- mindsdb/api/mongo/responders/list_indexes.py +0 -22
- mindsdb/api/mongo/responders/ping.py +0 -13
- mindsdb/api/mongo/responders/recv_chunk_start.py +0 -13
- mindsdb/api/mongo/responders/replsetgetstatus.py +0 -13
- mindsdb/api/mongo/responders/sasl_continue.py +0 -34
- mindsdb/api/mongo/responders/sasl_start.py +0 -33
- mindsdb/api/mongo/responders/update_range_deletions.py +0 -12
- mindsdb/api/mongo/responders/whatsmyuri.py +0 -18
- mindsdb/api/mongo/server.py +0 -388
- mindsdb/api/mongo/start.py +0 -15
- mindsdb/api/mongo/utilities/__init__.py +0 -0
- {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/WHEEL +0 -0
- {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/licenses/LICENSE +0 -0
- {mindsdb-25.8.2.0.dist-info → mindsdb-25.9.1.0.dist-info}/top_level.txt +0 -0
mindsdb/api/a2a/task_manager.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import AsyncIterable
|
|
1
|
+
from typing import AsyncIterable, Dict
|
|
2
2
|
from mindsdb.api.a2a.common.types import (
|
|
3
3
|
SendTaskRequest,
|
|
4
4
|
TaskSendParams,
|
|
@@ -18,12 +18,13 @@ from mindsdb.api.a2a.common.types import (
|
|
|
18
18
|
)
|
|
19
19
|
from mindsdb.api.a2a.common.server.task_manager import InMemoryTaskManager
|
|
20
20
|
from mindsdb.api.a2a.agent import MindsDBAgent
|
|
21
|
-
from mindsdb.api.a2a.utils import to_serializable
|
|
21
|
+
from mindsdb.api.a2a.utils import to_serializable, convert_a2a_message_to_qa_format
|
|
22
22
|
|
|
23
23
|
from typing import Union
|
|
24
24
|
import logging
|
|
25
25
|
import asyncio
|
|
26
26
|
import time
|
|
27
|
+
import traceback
|
|
27
28
|
|
|
28
29
|
logger = logging.getLogger(__name__)
|
|
29
30
|
|
|
@@ -46,19 +47,15 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
46
47
|
def __init__(
|
|
47
48
|
self,
|
|
48
49
|
project_name: str,
|
|
49
|
-
mindsdb_host: str,
|
|
50
|
-
mindsdb_port: int,
|
|
51
50
|
agent_name: str = None,
|
|
52
51
|
):
|
|
53
52
|
super().__init__()
|
|
54
53
|
self.project_name = project_name
|
|
55
|
-
self.mindsdb_host = mindsdb_host
|
|
56
|
-
self.mindsdb_port = mindsdb_port
|
|
57
54
|
self.agent_name = agent_name
|
|
58
55
|
self.tasks = {} # Task storage
|
|
59
56
|
self.lock = asyncio.Lock() # Lock for task operations
|
|
60
57
|
|
|
61
|
-
def _create_agent(self, agent_name: str = None) -> MindsDBAgent:
|
|
58
|
+
def _create_agent(self, user_info: Dict, agent_name: str = None) -> MindsDBAgent:
|
|
62
59
|
"""Create a new MindsDBAgent instance for the given agent name."""
|
|
63
60
|
if not agent_name:
|
|
64
61
|
raise ValueError("Agent name is required but was not provided in the request")
|
|
@@ -66,11 +63,12 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
66
63
|
return MindsDBAgent(
|
|
67
64
|
agent_name=agent_name,
|
|
68
65
|
project_name=self.project_name,
|
|
69
|
-
|
|
70
|
-
port=self.mindsdb_port,
|
|
66
|
+
user_info=user_info,
|
|
71
67
|
)
|
|
72
68
|
|
|
73
|
-
async def _stream_generator(
|
|
69
|
+
async def _stream_generator(
|
|
70
|
+
self, request: SendTaskStreamingRequest, user_info: Dict
|
|
71
|
+
) -> AsyncIterable[SendTaskStreamingResponse]:
|
|
74
72
|
task_send_params: TaskSendParams = request.params
|
|
75
73
|
query = self._get_user_query(task_send_params)
|
|
76
74
|
params = self._get_task_params(task_send_params)
|
|
@@ -92,24 +90,10 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
92
90
|
yield error_result
|
|
93
91
|
return # Early return from generator
|
|
94
92
|
|
|
95
|
-
agent = self._create_agent(agent_name)
|
|
93
|
+
agent = self._create_agent(user_info, agent_name)
|
|
96
94
|
|
|
97
|
-
# Get the history from the task
|
|
95
|
+
# Get the history from the task object (where it was properly extracted and stored)
|
|
98
96
|
history = task.history if task and task.history else []
|
|
99
|
-
logger.info(f"Using history with length {len(history)} for request")
|
|
100
|
-
|
|
101
|
-
# Log the history for debugging
|
|
102
|
-
logger.info(f"Conversation history for task {task_send_params.id}:")
|
|
103
|
-
for idx, msg in enumerate(history):
|
|
104
|
-
# Convert Message object to dict if needed
|
|
105
|
-
msg_dict = msg.dict() if hasattr(msg, "dict") else msg
|
|
106
|
-
role = msg_dict.get("role", "unknown")
|
|
107
|
-
text = ""
|
|
108
|
-
for part in msg_dict.get("parts", []):
|
|
109
|
-
if part.get("type") == "text":
|
|
110
|
-
text = part.get("text", "")
|
|
111
|
-
break
|
|
112
|
-
logger.info(f"Message {idx + 1} ({role}): {text[:100]}...")
|
|
113
97
|
|
|
114
98
|
if not streaming:
|
|
115
99
|
# If streaming is disabled, use invoke and return a single response
|
|
@@ -182,24 +166,24 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
182
166
|
|
|
183
167
|
# If streaming is enabled (default), use the streaming implementation
|
|
184
168
|
try:
|
|
185
|
-
logger.debug(f"
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)
|
|
196
|
-
async for item in agent.streaming_invoke(agent_messages, timeout=60):
|
|
169
|
+
logger.debug(f"Entering agent.stream() at {time.time()}")
|
|
170
|
+
# Create A2A message structure and convert using centralized utility
|
|
171
|
+
a2a_message = task_send_params.message.model_dump()
|
|
172
|
+
logger.debug(f"History: {history}")
|
|
173
|
+
if history:
|
|
174
|
+
a2a_message["history"] = [msg.model_dump() if hasattr(msg, "model_dump") else msg for msg in history]
|
|
175
|
+
|
|
176
|
+
# Convert to Q&A format using centralized utility function
|
|
177
|
+
all_messages = convert_a2a_message_to_qa_format(a2a_message)
|
|
178
|
+
|
|
179
|
+
async for item in agent.streaming_invoke(all_messages, timeout=60):
|
|
197
180
|
# Clean up: Remove verbose debug logs, keep only errors and essential info
|
|
198
181
|
if isinstance(item, dict) and "artifact" in item and "parts" in item["artifact"]:
|
|
199
182
|
item["artifact"]["parts"] = [to_serializable(p) for p in item["artifact"]["parts"]]
|
|
200
183
|
yield to_serializable(item)
|
|
201
184
|
except Exception as e:
|
|
202
185
|
logger.error(f"An error occurred while streaming the response: {e}")
|
|
186
|
+
logger.error(traceback.format_exc())
|
|
203
187
|
error_text = f"An error occurred while streaming the response: {str(e)}"
|
|
204
188
|
# Ensure all parts are plain dicts
|
|
205
189
|
parts = [{"type": "text", "text": error_text}]
|
|
@@ -235,19 +219,23 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
235
219
|
message = task_send_params.message
|
|
236
220
|
message_dict = message.dict() if hasattr(message, "dict") else message
|
|
237
221
|
|
|
238
|
-
# Get history from request if available
|
|
222
|
+
# Get history from request if available - check both locations
|
|
239
223
|
history = []
|
|
224
|
+
|
|
225
|
+
# First check if history is at top level (task_send_params.history)
|
|
240
226
|
if hasattr(task_send_params, "history") and task_send_params.history:
|
|
241
|
-
# Convert each history item to dict if needed
|
|
227
|
+
# Convert each history item to dict if needed
|
|
242
228
|
for item in task_send_params.history:
|
|
243
|
-
item_dict = item.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
229
|
+
item_dict = item.model_dump() if hasattr(item, "model_dump") else item
|
|
230
|
+
history.append(item_dict)
|
|
231
|
+
# Also check if history is nested under message (message.history)
|
|
232
|
+
elif hasattr(task_send_params.message, "history") and task_send_params.message.history:
|
|
233
|
+
for item in task_send_params.message.history:
|
|
234
|
+
item_dict = item.model_dump() if hasattr(item, "model_dump") else item
|
|
247
235
|
history.append(item_dict)
|
|
248
236
|
|
|
249
|
-
#
|
|
250
|
-
|
|
237
|
+
# DO NOT add current message to history - it should be processed separately
|
|
238
|
+
# The current message will be extracted during streaming from task_send_params.message
|
|
251
239
|
|
|
252
240
|
# Create a new task
|
|
253
241
|
task = Task(
|
|
@@ -321,27 +309,27 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
321
309
|
|
|
322
310
|
return None
|
|
323
311
|
|
|
324
|
-
async def on_send_task(self, request: SendTaskRequest) -> SendTaskResponse:
|
|
312
|
+
async def on_send_task(self, request: SendTaskRequest, user_info: Dict) -> SendTaskResponse:
|
|
325
313
|
error = self._validate_request(request)
|
|
326
314
|
if error:
|
|
327
315
|
return error
|
|
328
316
|
|
|
329
|
-
return await self._invoke(request)
|
|
317
|
+
return await self._invoke(request, user_info=user_info)
|
|
330
318
|
|
|
331
319
|
async def on_send_task_subscribe(
|
|
332
|
-
self, request: SendTaskStreamingRequest
|
|
320
|
+
self, request: SendTaskStreamingRequest, user_info: Dict
|
|
333
321
|
) -> AsyncIterable[SendTaskStreamingResponse]:
|
|
334
322
|
error = self._validate_request(request)
|
|
335
323
|
if error:
|
|
336
|
-
logger.info(f"
|
|
324
|
+
logger.info(f"Yielding error at {time.time()} for invalid request: {error}")
|
|
337
325
|
yield to_serializable(SendTaskStreamingResponse(id=request.id, error=to_serializable(error.error)))
|
|
338
326
|
return
|
|
339
327
|
|
|
340
328
|
# We can't await an async generator directly, so we need to use it as is
|
|
341
329
|
try:
|
|
342
|
-
logger.debug(f"
|
|
343
|
-
async for response in self._stream_generator(request):
|
|
344
|
-
logger.debug(f"
|
|
330
|
+
logger.debug(f"Entering streaming path at {time.time()}")
|
|
331
|
+
async for response in self._stream_generator(request, user_info):
|
|
332
|
+
logger.debug(f"Yielding streaming response at {time.time()} with: {str(response)[:120]}")
|
|
345
333
|
yield response
|
|
346
334
|
except Exception as e:
|
|
347
335
|
# If an error occurs, yield an error response
|
|
@@ -420,13 +408,13 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
420
408
|
"session_id": task_send_params.sessionId,
|
|
421
409
|
}
|
|
422
410
|
|
|
423
|
-
async def _invoke(self, request: SendTaskRequest) -> SendTaskResponse:
|
|
411
|
+
async def _invoke(self, request: SendTaskRequest, user_info: Dict) -> SendTaskResponse:
|
|
424
412
|
task_send_params: TaskSendParams = request.params
|
|
425
413
|
query = self._get_user_query(task_send_params)
|
|
426
414
|
params = self._get_task_params(task_send_params)
|
|
427
415
|
agent_name = params["agent_name"]
|
|
428
416
|
streaming = params["streaming"]
|
|
429
|
-
agent = self._create_agent(agent_name)
|
|
417
|
+
agent = self._create_agent(user_info, agent_name)
|
|
430
418
|
|
|
431
419
|
try:
|
|
432
420
|
# Get the history from the task
|
mindsdb/api/a2a/utils.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
from mindsdb.utilities.log import getLogger
|
|
3
|
+
|
|
4
|
+
logger = getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
1
7
|
def to_serializable(obj):
|
|
2
8
|
# Primitives
|
|
3
9
|
if isinstance(obj, (str, int, float, bool, type(None))):
|
|
@@ -19,3 +25,60 @@ def to_serializable(obj):
|
|
|
19
25
|
return [to_serializable(v) for v in obj]
|
|
20
26
|
# Fallback: string
|
|
21
27
|
return str(obj)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def convert_a2a_message_to_qa_format(a2a_message: Dict) -> List[Dict[str, str]]:
|
|
31
|
+
"""
|
|
32
|
+
Convert A2A message format to question/answer format.
|
|
33
|
+
|
|
34
|
+
This is the format that the langchain agent expects and ensure effective multi-turn conversation
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
a2a_message: A2A message containing history and current message parts
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of messages in question/answer format
|
|
41
|
+
"""
|
|
42
|
+
converted_messages = []
|
|
43
|
+
|
|
44
|
+
# Process conversation history first
|
|
45
|
+
if "history" in a2a_message and a2a_message["history"] is not None:
|
|
46
|
+
for hist_msg in a2a_message["history"]:
|
|
47
|
+
if hist_msg.get("role") == "user":
|
|
48
|
+
# Extract text from parts
|
|
49
|
+
text = ""
|
|
50
|
+
for part in hist_msg.get("parts", []):
|
|
51
|
+
if part.get("type") == "text":
|
|
52
|
+
text = part.get("text", "")
|
|
53
|
+
break
|
|
54
|
+
# Create question with empty answer initially
|
|
55
|
+
converted_messages.append({"question": text, "answer": ""})
|
|
56
|
+
elif hist_msg.get("role") in ["agent", "assistant"]:
|
|
57
|
+
# Extract text from parts
|
|
58
|
+
text = ""
|
|
59
|
+
for part in hist_msg.get("parts", []):
|
|
60
|
+
if part.get("type") == "text":
|
|
61
|
+
text = part.get("text", "")
|
|
62
|
+
break
|
|
63
|
+
# Pair with the most recent question that has empty answer
|
|
64
|
+
paired = False
|
|
65
|
+
for i in range(len(converted_messages) - 1, -1, -1):
|
|
66
|
+
if converted_messages[i].get("answer") == "":
|
|
67
|
+
converted_messages[i]["answer"] = text
|
|
68
|
+
paired = True
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
if not paired:
|
|
72
|
+
logger.warning("Could not pair agent response with question (no empty answer found)")
|
|
73
|
+
|
|
74
|
+
logger.debug(f"Converted {len(a2a_message['history'])} A2A history messages to Q&A format")
|
|
75
|
+
|
|
76
|
+
# Add current message as final question with empty answer
|
|
77
|
+
current_text = ""
|
|
78
|
+
for part in a2a_message.get("parts", []):
|
|
79
|
+
if part.get("type") == "text":
|
|
80
|
+
current_text = part.get("text", "")
|
|
81
|
+
break
|
|
82
|
+
converted_messages.append({"question": current_text, "answer": ""})
|
|
83
|
+
|
|
84
|
+
return converted_messages
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
2
|
+
from starlette.responses import JSONResponse
|
|
3
|
+
from starlette.requests import Request
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import Optional
|
|
6
|
+
import secrets
|
|
7
|
+
import hmac
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
import traceback
|
|
11
|
+
|
|
12
|
+
from mindsdb.utilities import log
|
|
13
|
+
from mindsdb.utilities.config import config
|
|
14
|
+
|
|
15
|
+
logger = log.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
SECRET_KEY = os.environ.get("AUTH_SECRET_KEY") or secrets.token_urlsafe(32)
|
|
18
|
+
# We store token (fingerprints) in memory, which means everyone is logged out if the process restarts
|
|
19
|
+
TOKENS = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_pat_fingerprint(token: str) -> str:
|
|
23
|
+
"""Hash the token with HMAC-SHA256 using secret_key as pepper."""
|
|
24
|
+
return hmac.new(SECRET_KEY.encode(), token.encode(), hashlib.sha256).hexdigest()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_pat() -> str:
|
|
28
|
+
logger.debug("Generating new auth token")
|
|
29
|
+
token = "pat_" + secrets.token_urlsafe(32)
|
|
30
|
+
TOKENS.append(get_pat_fingerprint(token))
|
|
31
|
+
return token
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def verify_pat(raw_token: str) -> bool:
|
|
35
|
+
"""Verify if the raw_token matches a stored fingerprint.
|
|
36
|
+
Returns token_id if valid, None if not.
|
|
37
|
+
"""
|
|
38
|
+
if not raw_token:
|
|
39
|
+
return False
|
|
40
|
+
fp = get_pat_fingerprint(raw_token)
|
|
41
|
+
for stored_fp in TOKENS:
|
|
42
|
+
if hmac.compare_digest(fp, stored_fp):
|
|
43
|
+
return True
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def revoke_pat(raw_token: str) -> bool:
|
|
48
|
+
"""Revoke raw_token from active tokens"""
|
|
49
|
+
if not raw_token:
|
|
50
|
+
return False
|
|
51
|
+
fp = get_pat_fingerprint(raw_token)
|
|
52
|
+
for stored_fp in TOKENS:
|
|
53
|
+
if hmac.compare_digest(fp, stored_fp):
|
|
54
|
+
TOKENS.remove(stored_fp)
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PATAuthMiddleware(BaseHTTPMiddleware):
|
|
60
|
+
def _extract_bearer(self, request: Request) -> Optional[str]:
|
|
61
|
+
h = request.headers.get("Authorization")
|
|
62
|
+
if not h or not h.startswith("Bearer "):
|
|
63
|
+
return None
|
|
64
|
+
return h.split(" ", 1)[1].strip() or None
|
|
65
|
+
|
|
66
|
+
async def dispatch(self, request: Request, call_next):
|
|
67
|
+
if config.get("auth", {}).get("http_auth_enabled", False) is False:
|
|
68
|
+
return await call_next(request)
|
|
69
|
+
|
|
70
|
+
token = self._extract_bearer(request)
|
|
71
|
+
if not token or not verify_pat(token):
|
|
72
|
+
return JSONResponse({"detail": "Unauthorized"}, status_code=HTTPStatus.UNAUTHORIZED)
|
|
73
|
+
|
|
74
|
+
request.state.user = config["auth"].get("username")
|
|
75
|
+
return await call_next(request)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Used by mysql and postgres protocols
|
|
79
|
+
def check_auth(username, password, scramble_func, salt, company_id, config):
|
|
80
|
+
try:
|
|
81
|
+
hardcoded_user = config["auth"].get("username")
|
|
82
|
+
hardcoded_password = config["auth"].get("password")
|
|
83
|
+
if hardcoded_password is None:
|
|
84
|
+
hardcoded_password = ""
|
|
85
|
+
hardcoded_password_hash = scramble_func(hardcoded_password, salt)
|
|
86
|
+
hardcoded_password = hardcoded_password.encode()
|
|
87
|
+
|
|
88
|
+
if password is None:
|
|
89
|
+
password = ""
|
|
90
|
+
if isinstance(password, str):
|
|
91
|
+
password = password.encode()
|
|
92
|
+
|
|
93
|
+
if username != hardcoded_user:
|
|
94
|
+
logger.warning(f"Check auth, user={username}: user mismatch")
|
|
95
|
+
return {"success": False}
|
|
96
|
+
|
|
97
|
+
if password != hardcoded_password and password != hardcoded_password_hash:
|
|
98
|
+
logger.warning(f"check auth, user={username}: password mismatch")
|
|
99
|
+
return {"success": False}
|
|
100
|
+
|
|
101
|
+
logger.info(f"Check auth, user={username}: Ok")
|
|
102
|
+
return {"success": True, "username": username}
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Check auth, user={username}: ERROR")
|
|
105
|
+
logger.error(e)
|
|
106
|
+
logger.error(traceback.format_exc())
|
mindsdb/api/http/initialize.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import secrets
|
|
3
2
|
import mimetypes
|
|
4
3
|
import threading
|
|
5
4
|
import traceback
|
|
@@ -27,7 +26,7 @@ from mindsdb.api.http.namespaces.chatbots import ns_conf as chatbots_ns
|
|
|
27
26
|
from mindsdb.api.http.namespaces.jobs import ns_conf as jobs_ns
|
|
28
27
|
from mindsdb.api.http.namespaces.config import ns_conf as conf_ns
|
|
29
28
|
from mindsdb.api.http.namespaces.databases import ns_conf as databases_ns
|
|
30
|
-
from mindsdb.api.http.namespaces.default import ns_conf as default_ns
|
|
29
|
+
from mindsdb.api.http.namespaces.default import ns_conf as default_ns
|
|
31
30
|
from mindsdb.api.http.namespaces.file import ns_conf as file_ns
|
|
32
31
|
from mindsdb.api.http.namespaces.handlers import ns_conf as handlers_ns
|
|
33
32
|
from mindsdb.api.http.namespaces.knowledge_bases import ns_conf as knowledge_bases_ns
|
|
@@ -53,6 +52,7 @@ from mindsdb.utilities.json_encoder import CustomJSONProvider
|
|
|
53
52
|
from mindsdb.utilities.ps import is_pid_listen_port, wait_func_is_true
|
|
54
53
|
from mindsdb.utilities.sentry import sentry_sdk # noqa: F401
|
|
55
54
|
from mindsdb.utilities.otel import trace # noqa: F401
|
|
55
|
+
from mindsdb.api.common.middleware import verify_pat
|
|
56
56
|
|
|
57
57
|
logger = log.getLogger(__name__)
|
|
58
58
|
|
|
@@ -314,12 +314,19 @@ def initialize_app(config, no_studio):
|
|
|
314
314
|
ctx.set_default()
|
|
315
315
|
config = Config()
|
|
316
316
|
|
|
317
|
+
h = request.headers.get("Authorization")
|
|
318
|
+
if not h or not h.startswith("Bearer "):
|
|
319
|
+
bearer = None
|
|
320
|
+
else:
|
|
321
|
+
bearer = h.split(" ", 1)[1].strip() or None
|
|
322
|
+
|
|
317
323
|
# region routes where auth is required
|
|
318
324
|
if (
|
|
319
325
|
config["auth"]["http_auth_enabled"] is True
|
|
320
326
|
and any(request.path.startswith(f"/api{ns.path}") for ns in protected_namespaces)
|
|
321
|
-
and
|
|
327
|
+
and verify_pat(bearer) is False
|
|
322
328
|
):
|
|
329
|
+
logger.debug(f"Auth failed for path {request.path}")
|
|
323
330
|
return http_error(
|
|
324
331
|
HTTPStatus.UNAUTHORIZED,
|
|
325
332
|
"Unauthorized",
|
|
@@ -340,29 +347,23 @@ def initialize_app(config, no_studio):
|
|
|
340
347
|
except Exception:
|
|
341
348
|
user_id = 0
|
|
342
349
|
|
|
343
|
-
try:
|
|
344
|
-
session_id = request.cookies.get("session")
|
|
345
|
-
except Exception:
|
|
346
|
-
session_id = "unknown"
|
|
347
|
-
|
|
348
350
|
if company_id is not None:
|
|
349
351
|
try:
|
|
350
352
|
company_id = int(company_id)
|
|
351
353
|
except Exception as e:
|
|
352
|
-
logger.error(f"
|
|
354
|
+
logger.error(f"Could not parse company id: {company_id} | exception: {e}")
|
|
353
355
|
company_id = None
|
|
354
356
|
|
|
355
357
|
if user_class is not None:
|
|
356
358
|
try:
|
|
357
359
|
user_class = int(user_class)
|
|
358
360
|
except Exception as e:
|
|
359
|
-
logger.error(f"
|
|
361
|
+
logger.error(f"Could not parse user_class: {user_class} | exception: {e}")
|
|
360
362
|
user_class = 0
|
|
361
363
|
else:
|
|
362
364
|
user_class = 0
|
|
363
365
|
|
|
364
366
|
ctx.user_id = user_id
|
|
365
|
-
ctx.session_id = session_id
|
|
366
367
|
ctx.company_id = company_id
|
|
367
368
|
ctx.user_class = user_class
|
|
368
369
|
ctx.email_confirmed = email_confirmed
|
|
@@ -394,14 +395,11 @@ def initialize_flask(config, init_static_thread, no_studio):
|
|
|
394
395
|
FlaskInstrumentor().instrument_app(app)
|
|
395
396
|
RequestsInstrumentor().instrument()
|
|
396
397
|
|
|
397
|
-
app.config["SECRET_KEY"] = os.environ.get("FLASK_SECRET_KEY", secrets.token_hex(32))
|
|
398
|
-
app.config["SESSION_COOKIE_NAME"] = "session"
|
|
399
|
-
app.config["PERMANENT_SESSION_LIFETIME"] = config["auth"]["http_permanent_session_lifetime"]
|
|
400
398
|
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 60
|
|
401
399
|
app.config["SWAGGER_HOST"] = "http://localhost:8000/mindsdb"
|
|
402
400
|
app.json = CustomJSONProvider()
|
|
403
401
|
|
|
404
|
-
authorizations = {"apikey": {"type": "
|
|
402
|
+
authorizations = {"apikey": {"type": "apiKey", "in": "header", "name": "Authorization"}}
|
|
405
403
|
|
|
406
404
|
logger.debug("Creating swagger API..")
|
|
407
405
|
api = Swagger_Api(
|
|
@@ -323,15 +323,16 @@ class AgentCompletionsStream(Resource):
|
|
|
323
323
|
@ns_conf.doc("agent_completions_stream")
|
|
324
324
|
@api_endpoint_metrics("POST", "/agents/agent/completions/stream")
|
|
325
325
|
def post(self, project_name, agent_name):
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
# Check for required parameters.
|
|
326
|
+
# Extract messages from request (HTTP format only)
|
|
329
327
|
if "messages" not in request.json:
|
|
330
|
-
logger.error("Missing 'messages' parameter in request body")
|
|
331
328
|
return http_error(
|
|
332
|
-
HTTPStatus.BAD_REQUEST,
|
|
329
|
+
HTTPStatus.BAD_REQUEST,
|
|
330
|
+
"Missing parameter",
|
|
331
|
+
'Must provide "messages" parameter in POST body',
|
|
333
332
|
)
|
|
334
333
|
|
|
334
|
+
messages = request.json["messages"]
|
|
335
|
+
|
|
335
336
|
session = SessionController()
|
|
336
337
|
try:
|
|
337
338
|
existing_agent = session.agents_controller.get_agent(agent_name, project_name=project_name)
|
|
@@ -346,8 +347,6 @@ class AgentCompletionsStream(Resource):
|
|
|
346
347
|
HTTPStatus.NOT_FOUND, "Project not found", f"Project with name {project_name} does not exist"
|
|
347
348
|
)
|
|
348
349
|
|
|
349
|
-
messages = request.json["messages"]
|
|
350
|
-
|
|
351
350
|
try:
|
|
352
351
|
gen = _completion_event_generator(agent_name, messages, project_name)
|
|
353
352
|
logger.info(f"Starting streaming response for agent {agent_name}")
|
|
@@ -4,7 +4,7 @@ import time
|
|
|
4
4
|
import urllib
|
|
5
5
|
|
|
6
6
|
import requests
|
|
7
|
-
from flask import redirect, request,
|
|
7
|
+
from flask import redirect, request, url_for
|
|
8
8
|
from flask_restx import Resource
|
|
9
9
|
|
|
10
10
|
from mindsdb.api.http.namespaces.configs.auth import ns_conf
|
|
@@ -21,9 +21,7 @@ def get_access_token() -> str:
|
|
|
21
21
|
Returns:
|
|
22
22
|
str: token
|
|
23
23
|
"""
|
|
24
|
-
return (
|
|
25
|
-
Config().get("auth", {}).get("oauth", {}).get("tokens", {}).get("access_token")
|
|
26
|
-
)
|
|
24
|
+
return Config().get("auth", {}).get("oauth", {}).get("tokens", {}).get("access_token")
|
|
27
25
|
|
|
28
26
|
|
|
29
27
|
def request_user_info(access_token: str = None) -> dict:
|
|
@@ -58,7 +56,7 @@ def request_user_info(access_token: str = None) -> dict:
|
|
|
58
56
|
@ns_conf.hide
|
|
59
57
|
class Auth(Resource):
|
|
60
58
|
@ns_conf.doc(params={"code": "authentification code"})
|
|
61
|
-
@api_endpoint_metrics(
|
|
59
|
+
@api_endpoint_metrics("GET", "/auth/code")
|
|
62
60
|
def get(self):
|
|
63
61
|
"""callback from auth server if authentification is successful"""
|
|
64
62
|
config = Config()
|
|
@@ -72,9 +70,7 @@ class Auth(Resource):
|
|
|
72
70
|
client_id = oauth_meta["client_id"]
|
|
73
71
|
client_secret = oauth_meta["client_secret"]
|
|
74
72
|
auth_server = oauth_meta["server_host"]
|
|
75
|
-
client_basic = base64.b64encode(
|
|
76
|
-
f"{client_id}:{client_secret}".encode()
|
|
77
|
-
).decode()
|
|
73
|
+
client_basic = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
|
|
78
74
|
|
|
79
75
|
redirect_uri = f"https://{public_hostname}{request.path}"
|
|
80
76
|
response = requests.post(
|
|
@@ -115,7 +111,7 @@ class Auth(Resource):
|
|
|
115
111
|
"public_hostname": public_hostname,
|
|
116
112
|
"ami_id": aws_meta_data.get("ami-id"),
|
|
117
113
|
},
|
|
118
|
-
headers={"Authorization": f
|
|
114
|
+
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
|
119
115
|
timeout=5,
|
|
120
116
|
)
|
|
121
117
|
if resp.status_code != 200:
|
|
@@ -123,10 +119,6 @@ class Auth(Resource):
|
|
|
123
119
|
except Exception as e:
|
|
124
120
|
logger.warning(f"Cant't send request to cloud server: {e}")
|
|
125
121
|
|
|
126
|
-
session["username"] = user_data["name"]
|
|
127
|
-
session["auth_provider"] = "cloud"
|
|
128
|
-
session.permanent = True
|
|
129
|
-
|
|
130
122
|
if request.path.endswith("/auth/callback/cloud_home"):
|
|
131
123
|
return redirect(f"https://{auth_server}")
|
|
132
124
|
else:
|
|
@@ -140,7 +132,7 @@ class CloudLoginRoute(Resource):
|
|
|
140
132
|
responses={302: "Redirect to auth server"},
|
|
141
133
|
params={"location": "final redirection should lead to that location"},
|
|
142
134
|
)
|
|
143
|
-
@api_endpoint_metrics(
|
|
135
|
+
@api_endpoint_metrics("GET", "/auth/cloud_login")
|
|
144
136
|
def get(self):
|
|
145
137
|
"""redirect to cloud login form"""
|
|
146
138
|
location = request.args.get("location")
|