signalpilot-ai-internal 0.10.0__py3-none-any.whl → 0.11.24__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.
- signalpilot_ai_internal/__init__.py +1 -0
- signalpilot_ai_internal/_version.py +1 -1
- signalpilot_ai_internal/cache_service.py +22 -21
- signalpilot_ai_internal/composio_handlers.py +224 -0
- signalpilot_ai_internal/composio_service.py +511 -0
- signalpilot_ai_internal/database_config_handlers.py +182 -0
- signalpilot_ai_internal/database_config_service.py +166 -0
- signalpilot_ai_internal/databricks_schema_service.py +907 -0
- signalpilot_ai_internal/file_scanner_service.py +5 -146
- signalpilot_ai_internal/handlers.py +388 -9
- signalpilot_ai_internal/integrations_config.py +256 -0
- signalpilot_ai_internal/log_utils.py +31 -0
- signalpilot_ai_internal/mcp_handlers.py +532 -0
- signalpilot_ai_internal/mcp_server_manager.py +298 -0
- signalpilot_ai_internal/mcp_service.py +1255 -0
- signalpilot_ai_internal/oauth_token_store.py +141 -0
- signalpilot_ai_internal/schema_search_config.yml +17 -11
- signalpilot_ai_internal/schema_search_service.py +85 -4
- signalpilot_ai_internal/signalpilot_home.py +961 -0
- signalpilot_ai_internal/snowflake_schema_service.py +2 -0
- signalpilot_ai_internal/test_dbt_mcp_server.py +180 -0
- signalpilot_ai_internal/unified_database_schema_service.py +2 -0
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/package.json.orig → signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/package.json +15 -48
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/package.json → signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/package.json.orig +9 -52
- {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/plugin.json +7 -1
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.bab318d6caadb055e29c.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/129.868ca665e6fc225c20a0.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/179.fd45a2e75d471d0aa3b9.js +7 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.81105a94aa873fc51a94.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.a002dd4630d3b6404a90.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.cc6f6ecacd703bcdb468.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.817a883549d55a0e0576.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.a4daecd44f1e9364e44a.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.667225aab294fb5ed161.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/447.8138af2522716e5a926f.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.925c73e32f3c07448da0.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/477.aaa4cc9e87801fb45f5b.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.370056149a59022b700c.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/510.868ca665e6fc225c20a0.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.835f97f7ccfc70ff5c93.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.6c13335f73de089d6b1e.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/574.ad2709e91ebcac5bbe68.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.bddbab8e464fe31f0393.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.fda1bcdb10497b0a6ade.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.d046701f475fcbf6697d.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.c306dffd4cfe8a613d13.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/801.e39898b6f336539f228c.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.77cc0ca10a1860df1b52.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/936.4e2850b2af985ed0d378.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/956.eeffe67d7781fd63ef4b.js +2 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.055f50d20a31f3068c72.js +1 -0
- {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/third-party-licenses.json +47 -29
- {signalpilot_ai_internal-0.10.0.dist-info → signalpilot_ai_internal-0.11.24.dist-info}/METADATA +14 -31
- signalpilot_ai_internal-0.11.24.dist-info/RECORD +66 -0
- signalpilot_ai_internal-0.11.24.dist-info/licenses/LICENSE +7 -0
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.e2dadf63dc64d7b5f1ee.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.328403b5545f268b95c6.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.726e1da31a50868cb297.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/330.af2e9cb5def5ae2b84d5.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.972abe1d2d66f083f9cc.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.dbec4c2dc12e7b050dcc.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.fa432bdb7fb6b1c95ad6.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.37e271d7a80336daabe2.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.ad22ccddd74ee306fb56.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.73c7a9290b7d35a8b9c1.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.b58fc0093d080b8ee61c.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js +0 -2
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.e9acd2e1f9739037f1ab.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.9720593ee20b768da3ca.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.8e6edc9a965bdd578ca7.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.dc49867fafb03ea2ba4d.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/742.91e7b516c8699eea3373.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.2d75de1a8d2c3131a8db.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/786.770dc7bcab77e14cc135.js +0 -7
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/801.ca9e114a30896b669a3c.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.25ddd15aca09421d3765.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/888.34054db17bcf6e87ec95.js +0 -1
- signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.b05b2f0c9617ba28370d.js +0 -1
- signalpilot_ai_internal-0.10.0.dist-info/RECORD +0 -50
- signalpilot_ai_internal-0.10.0.dist-info/licenses/LICENSE +0 -29
- {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.11.24.data}/data/etc/jupyter/jupyter_server_config.d/signalpilot_ai.json +0 -0
- {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/install.json +0 -0
- /signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js.LICENSE.txt → /signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/956.eeffe67d7781fd63ef4b.js.LICENSE.txt +0 -0
- {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/style.js +0 -0
- {signalpilot_ai_internal-0.10.0.dist-info → signalpilot_ai_internal-0.11.24.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth Token Store - Secure storage for OAuth tokens in .env format
|
|
3
|
+
Stores tokens at <cache_dir>/connect/.env
|
|
4
|
+
(e.g., ~/Library/Caches/SignalPilotAI/connect/.env on macOS)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from .signalpilot_home import get_signalpilot_home
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OAuthTokenStore:
|
|
16
|
+
"""
|
|
17
|
+
Secure storage for OAuth tokens using .env format.
|
|
18
|
+
Tokens are stored with server-id prefixes for namespacing.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_instance = None
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self._home_manager = get_signalpilot_home()
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def get_instance(cls) -> 'OAuthTokenStore':
|
|
28
|
+
"""Get singleton instance."""
|
|
29
|
+
if cls._instance is None:
|
|
30
|
+
cls._instance = OAuthTokenStore()
|
|
31
|
+
return cls._instance
|
|
32
|
+
|
|
33
|
+
def store_tokens(self, integration_id: str, mcp_server_id: str, env_vars: Dict[str, str]):
|
|
34
|
+
"""
|
|
35
|
+
Store OAuth tokens for an integration.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
integration_id: The integration ID (e.g., 'notion', 'slack')
|
|
39
|
+
mcp_server_id: The MCP server ID associated with this integration
|
|
40
|
+
env_vars: Environment variables containing tokens
|
|
41
|
+
"""
|
|
42
|
+
# Store the registry entry (mapping server_id -> integration_id)
|
|
43
|
+
self._home_manager.set_oauth_registry_entry(mcp_server_id, integration_id)
|
|
44
|
+
|
|
45
|
+
# Store the actual tokens
|
|
46
|
+
self._home_manager.set_oauth_tokens(mcp_server_id, env_vars)
|
|
47
|
+
|
|
48
|
+
logger.info(f"[OAuthTokenStore] Stored tokens for {mcp_server_id}")
|
|
49
|
+
|
|
50
|
+
def get_tokens(self, mcp_server_id: str) -> Optional[Dict[str, str]]:
|
|
51
|
+
"""
|
|
52
|
+
Get OAuth tokens for an MCP server.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
mcp_server_id: The MCP server ID
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Environment variables dict or None if not found
|
|
59
|
+
"""
|
|
60
|
+
return self._home_manager.get_oauth_tokens(mcp_server_id)
|
|
61
|
+
|
|
62
|
+
def get_integration_id(self, mcp_server_id: str) -> Optional[str]:
|
|
63
|
+
"""
|
|
64
|
+
Get the integration ID for an MCP server.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
mcp_server_id: The MCP server ID
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Integration ID or None if not found
|
|
71
|
+
"""
|
|
72
|
+
registry = self._home_manager.get_oauth_registry()
|
|
73
|
+
return registry.get(mcp_server_id)
|
|
74
|
+
|
|
75
|
+
def is_oauth_server(self, mcp_server_id: str) -> bool:
|
|
76
|
+
"""
|
|
77
|
+
Check if an MCP server is an OAuth integration.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
mcp_server_id: The MCP server ID
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if this server has stored OAuth tokens
|
|
84
|
+
"""
|
|
85
|
+
registry = self._home_manager.get_oauth_registry()
|
|
86
|
+
return mcp_server_id in registry
|
|
87
|
+
|
|
88
|
+
def remove_tokens(self, mcp_server_id: str) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Remove OAuth tokens for an MCP server.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
mcp_server_id: The MCP server ID
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if tokens were removed, False if not found
|
|
97
|
+
"""
|
|
98
|
+
# Remove from registry
|
|
99
|
+
self._home_manager.remove_oauth_registry_entry(mcp_server_id)
|
|
100
|
+
|
|
101
|
+
# Remove the tokens
|
|
102
|
+
result = self._home_manager.remove_oauth_tokens(mcp_server_id)
|
|
103
|
+
|
|
104
|
+
if result:
|
|
105
|
+
logger.info(f"[OAuthTokenStore] Removed tokens for {mcp_server_id}")
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
def update_tokens(self, mcp_server_id: str, env_vars: Dict[str, str]) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Update OAuth tokens for an existing MCP server.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
mcp_server_id: The MCP server ID
|
|
115
|
+
env_vars: New environment variables containing tokens
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if tokens were updated, False if server not found
|
|
119
|
+
"""
|
|
120
|
+
if not self.is_oauth_server(mcp_server_id):
|
|
121
|
+
logger.warning(f"[OAuthTokenStore] Server {mcp_server_id} not found for update")
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
result = self._home_manager.set_oauth_tokens(mcp_server_id, env_vars)
|
|
125
|
+
if result:
|
|
126
|
+
logger.info(f"[OAuthTokenStore] Updated tokens for {mcp_server_id}")
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
def get_all_oauth_servers(self) -> Dict[str, str]:
|
|
130
|
+
"""
|
|
131
|
+
Get mapping of all OAuth MCP server IDs to their integration IDs.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dict mapping mcp_server_id -> integration_id
|
|
135
|
+
"""
|
|
136
|
+
return self._home_manager.get_oauth_registry()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_oauth_token_store() -> OAuthTokenStore:
|
|
140
|
+
"""Get the singleton instance of the OAuth token store."""
|
|
141
|
+
return OAuthTokenStore.get_instance()
|
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
logging:
|
|
2
|
-
level:
|
|
2
|
+
level: "WARNING"
|
|
3
3
|
|
|
4
4
|
embedding:
|
|
5
|
-
location:
|
|
6
|
-
model:
|
|
7
|
-
metric:
|
|
5
|
+
location: "memory" # Options: "memory", "vectordb" (coming soon)
|
|
6
|
+
model: "multi-qa-MiniLM-L6-cos-v1"
|
|
7
|
+
metric: "cosine" # Options: "cosine", "euclidean", "manhattan", "dot"
|
|
8
8
|
batch_size: 32
|
|
9
9
|
show_progress: false
|
|
10
|
-
cache_dir:
|
|
10
|
+
cache_dir: "/tmp/.schema_search_cache"
|
|
11
11
|
|
|
12
12
|
chunking:
|
|
13
|
-
strategy:
|
|
13
|
+
strategy: "raw" # Options: "raw", "llm"
|
|
14
14
|
max_tokens: 256
|
|
15
15
|
overlap_tokens: 50
|
|
16
|
-
model:
|
|
16
|
+
model: "gpt-4o-mini"
|
|
17
17
|
|
|
18
18
|
search:
|
|
19
|
-
strategy:
|
|
19
|
+
# Search strategy: "semantic" (embeddings), "bm25" (BM25 lexical), "fuzzy" (fuzzy string matching), "hybrid" (semantic + bm25)
|
|
20
|
+
strategy: "bm25"
|
|
20
21
|
initial_top_k: 20
|
|
21
22
|
rerank_top_k: 5
|
|
22
|
-
semantic_weight: 0.67
|
|
23
|
-
hops: 1
|
|
23
|
+
semantic_weight: 0.67 # For hybrid search (bm25_weight = 1 - semantic_weight)
|
|
24
|
+
hops: 1 # Number of foreign key hops for graph expansion (0-2 recommended)
|
|
24
25
|
|
|
25
26
|
reranker:
|
|
26
|
-
model
|
|
27
|
+
# CrossEncoder model for reranking. Set to null to disable reranking
|
|
28
|
+
model: null # "Alibaba-NLP/gte-reranker-modernbert-base"
|
|
27
29
|
|
|
28
30
|
schema:
|
|
29
31
|
include_columns: true
|
|
30
32
|
include_indices: true
|
|
31
33
|
include_foreign_keys: true
|
|
32
34
|
include_constraints: true
|
|
35
|
+
|
|
36
|
+
output:
|
|
37
|
+
format: "markdown" # Options: "json", "markdown"
|
|
38
|
+
limit: 5 # Default number of results to return
|
|
@@ -21,10 +21,64 @@ class SchemaSearchHandler(APIHandler):
|
|
|
21
21
|
for key, value in os.environ.items():
|
|
22
22
|
if key.endswith("_CONNECTION_JSON") and isinstance(value, str) and value.strip().startswith("{"):
|
|
23
23
|
config = json.loads(value)
|
|
24
|
+
|
|
25
|
+
# Special handling for Databricks
|
|
26
|
+
if config.get("type") == "databricks":
|
|
27
|
+
return self._build_databricks_url(config)
|
|
28
|
+
|
|
24
29
|
url = config.get("connectionUrl")
|
|
25
30
|
if url:
|
|
26
31
|
return url
|
|
27
32
|
return os.environ.get("DB_URL")
|
|
33
|
+
|
|
34
|
+
def _build_databricks_url(self, config: dict) -> Optional[str]:
|
|
35
|
+
"""Build Databricks URL in the format: databricks://token:{token}@{host}?http_path={http_path}&catalog={catalog}"""
|
|
36
|
+
import re
|
|
37
|
+
|
|
38
|
+
# Extract host from connectionUrl
|
|
39
|
+
connection_url = config.get('connectionUrl', '')
|
|
40
|
+
if not connection_url:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
url_match = re.match(r'https?://([^/]+)', connection_url)
|
|
44
|
+
if not url_match:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
host = url_match.group(1)
|
|
48
|
+
|
|
49
|
+
# Get access token based on auth type
|
|
50
|
+
auth_type = config.get('authType', 'pat')
|
|
51
|
+
if auth_type == 'pat':
|
|
52
|
+
token = config.get('accessToken', '')
|
|
53
|
+
else:
|
|
54
|
+
# For service principal, we would need to get OAuth token
|
|
55
|
+
# For now, return None to fallback to other methods
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
if not token:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# Get HTTP path
|
|
62
|
+
http_path = config.get('warehouseHttpPath') or config.get('httpPath', '')
|
|
63
|
+
if not http_path:
|
|
64
|
+
warehouse_id = config.get('warehouseId')
|
|
65
|
+
if warehouse_id:
|
|
66
|
+
http_path = f"/sql/1.0/warehouses/{warehouse_id}"
|
|
67
|
+
else:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# Get catalog (optional)
|
|
71
|
+
catalog = config.get('catalog', '')
|
|
72
|
+
schema = config.get('schema', '')
|
|
73
|
+
|
|
74
|
+
# Build the URL
|
|
75
|
+
query_parts = [f"http_path={http_path}"]
|
|
76
|
+
if catalog:
|
|
77
|
+
query_parts.append(f"catalog={catalog}")
|
|
78
|
+
if schema:
|
|
79
|
+
query_parts.append(f"schema={schema}")
|
|
80
|
+
|
|
81
|
+
return f"databricks://token:{token}@{host}?{'&'.join(query_parts)}"
|
|
28
82
|
|
|
29
83
|
@tornado.web.authenticated
|
|
30
84
|
async def post(self):
|
|
@@ -60,18 +114,25 @@ class SchemaSearchHandler(APIHandler):
|
|
|
60
114
|
|
|
61
115
|
if db_url_lower.startswith("snowflake://"):
|
|
62
116
|
self._ensure_snowflake_dependencies()
|
|
117
|
+
elif db_url_lower.startswith("databricks://"):
|
|
118
|
+
self._ensure_databricks_dependencies()
|
|
63
119
|
elif db_url_lower.startswith("postgresql") or db_url_lower.startswith("postgres") or db_url_lower.startswith("mysql+pymysql"):
|
|
64
120
|
pass
|
|
65
121
|
else:
|
|
66
122
|
self.set_status(400)
|
|
67
|
-
self.finish(json.dumps({"error": "Schema search currently supports PostgreSQL, MySQL, or
|
|
123
|
+
self.finish(json.dumps({"error": "Schema search currently supports PostgreSQL, MySQL, Snowflake, or Databricks connections"}))
|
|
68
124
|
return
|
|
69
125
|
|
|
70
126
|
engine = None
|
|
71
127
|
try:
|
|
72
128
|
engine = create_engine(db_url)
|
|
73
|
-
schema_search = SchemaSearch(
|
|
74
|
-
|
|
129
|
+
schema_search = SchemaSearch(
|
|
130
|
+
engine=engine,
|
|
131
|
+
config_path=str(self.CONFIG_PATH),
|
|
132
|
+
llm_api_key=os.environ.get("SCHEMA_SEARCH_LLM_API_KEY"),
|
|
133
|
+
llm_base_url=os.environ.get("SCHEMA_SEARCH_LLM_BASE_URL")
|
|
134
|
+
)
|
|
135
|
+
schema_search.index(force=False)
|
|
75
136
|
|
|
76
137
|
limit = body.get("limit")
|
|
77
138
|
if limit is not None:
|
|
@@ -84,7 +145,7 @@ class SchemaSearchHandler(APIHandler):
|
|
|
84
145
|
result = schema_search.search(query, limit=limit)
|
|
85
146
|
query_results.append({
|
|
86
147
|
"query": query,
|
|
87
|
-
"results": result
|
|
148
|
+
"results": self._coerce_schema_search_result(result)
|
|
88
149
|
})
|
|
89
150
|
|
|
90
151
|
self.finish(json.dumps({"results": query_results}))
|
|
@@ -101,9 +162,29 @@ class SchemaSearchHandler(APIHandler):
|
|
|
101
162
|
def _install_package(self, package: str) -> None:
|
|
102
163
|
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
|
|
103
164
|
|
|
165
|
+
def _coerce_schema_search_result(self, result):
|
|
166
|
+
if hasattr(result, "to_dict"):
|
|
167
|
+
try:
|
|
168
|
+
return result.to_dict()
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
if isinstance(result, str):
|
|
172
|
+
try:
|
|
173
|
+
return json.loads(result)
|
|
174
|
+
except json.JSONDecodeError:
|
|
175
|
+
return result
|
|
176
|
+
return result
|
|
177
|
+
|
|
104
178
|
def _ensure_snowflake_dependencies(self) -> None:
|
|
105
179
|
try:
|
|
106
180
|
import snowflake.sqlalchemy # type: ignore # noqa: F401
|
|
107
181
|
except ImportError:
|
|
108
182
|
self._install_package("snowflake-sqlalchemy")
|
|
109
183
|
import snowflake.sqlalchemy # type: ignore # noqa: F401
|
|
184
|
+
|
|
185
|
+
def _ensure_databricks_dependencies(self) -> None:
|
|
186
|
+
try:
|
|
187
|
+
import databricks.sqlalchemy # type: ignore # noqa: F401
|
|
188
|
+
except ImportError:
|
|
189
|
+
self._install_package("databricks-sqlalchemy")
|
|
190
|
+
import databricks.sqlalchemy # type: ignore # noqa: F401
|