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.
Files changed (85) hide show
  1. signalpilot_ai_internal/__init__.py +1 -0
  2. signalpilot_ai_internal/_version.py +1 -1
  3. signalpilot_ai_internal/cache_service.py +22 -21
  4. signalpilot_ai_internal/composio_handlers.py +224 -0
  5. signalpilot_ai_internal/composio_service.py +511 -0
  6. signalpilot_ai_internal/database_config_handlers.py +182 -0
  7. signalpilot_ai_internal/database_config_service.py +166 -0
  8. signalpilot_ai_internal/databricks_schema_service.py +907 -0
  9. signalpilot_ai_internal/file_scanner_service.py +5 -146
  10. signalpilot_ai_internal/handlers.py +388 -9
  11. signalpilot_ai_internal/integrations_config.py +256 -0
  12. signalpilot_ai_internal/log_utils.py +31 -0
  13. signalpilot_ai_internal/mcp_handlers.py +532 -0
  14. signalpilot_ai_internal/mcp_server_manager.py +298 -0
  15. signalpilot_ai_internal/mcp_service.py +1255 -0
  16. signalpilot_ai_internal/oauth_token_store.py +141 -0
  17. signalpilot_ai_internal/schema_search_config.yml +17 -11
  18. signalpilot_ai_internal/schema_search_service.py +85 -4
  19. signalpilot_ai_internal/signalpilot_home.py +961 -0
  20. signalpilot_ai_internal/snowflake_schema_service.py +2 -0
  21. signalpilot_ai_internal/test_dbt_mcp_server.py +180 -0
  22. signalpilot_ai_internal/unified_database_schema_service.py +2 -0
  23. 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
  24. 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
  25. {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
  26. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.bab318d6caadb055e29c.js +1 -0
  27. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/129.868ca665e6fc225c20a0.js +1 -0
  28. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/179.fd45a2e75d471d0aa3b9.js +7 -0
  29. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.81105a94aa873fc51a94.js +1 -0
  30. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.a002dd4630d3b6404a90.js +1 -0
  31. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.cc6f6ecacd703bcdb468.js +1 -0
  32. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.817a883549d55a0e0576.js +1 -0
  33. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.a4daecd44f1e9364e44a.js +1 -0
  34. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.667225aab294fb5ed161.js +1 -0
  35. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/447.8138af2522716e5a926f.js +1 -0
  36. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.925c73e32f3c07448da0.js +1 -0
  37. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/477.aaa4cc9e87801fb45f5b.js +1 -0
  38. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.370056149a59022b700c.js +1 -0
  39. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/510.868ca665e6fc225c20a0.js +1 -0
  40. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.835f97f7ccfc70ff5c93.js +1 -0
  41. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.6c13335f73de089d6b1e.js +1 -0
  42. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/574.ad2709e91ebcac5bbe68.js +1 -0
  43. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.bddbab8e464fe31f0393.js +1 -0
  44. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.fda1bcdb10497b0a6ade.js +1 -0
  45. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.d046701f475fcbf6697d.js +1 -0
  46. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.c306dffd4cfe8a613d13.js +1 -0
  47. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/801.e39898b6f336539f228c.js +1 -0
  48. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.77cc0ca10a1860df1b52.js +1 -0
  49. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/936.4e2850b2af985ed0d378.js +1 -0
  50. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/956.eeffe67d7781fd63ef4b.js +2 -0
  51. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.055f50d20a31f3068c72.js +1 -0
  52. {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
  53. {signalpilot_ai_internal-0.10.0.dist-info → signalpilot_ai_internal-0.11.24.dist-info}/METADATA +14 -31
  54. signalpilot_ai_internal-0.11.24.dist-info/RECORD +66 -0
  55. signalpilot_ai_internal-0.11.24.dist-info/licenses/LICENSE +7 -0
  56. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.e2dadf63dc64d7b5f1ee.js +0 -1
  57. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.328403b5545f268b95c6.js +0 -1
  58. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.726e1da31a50868cb297.js +0 -1
  59. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/330.af2e9cb5def5ae2b84d5.js +0 -1
  60. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.972abe1d2d66f083f9cc.js +0 -1
  61. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.dbec4c2dc12e7b050dcc.js +0 -1
  62. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.fa432bdb7fb6b1c95ad6.js +0 -1
  63. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.37e271d7a80336daabe2.js +0 -1
  64. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.ad22ccddd74ee306fb56.js +0 -1
  65. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.73c7a9290b7d35a8b9c1.js +0 -1
  66. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.b58fc0093d080b8ee61c.js +0 -1
  67. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js +0 -2
  68. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.e9acd2e1f9739037f1ab.js +0 -1
  69. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.9720593ee20b768da3ca.js +0 -1
  70. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.8e6edc9a965bdd578ca7.js +0 -1
  71. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.dc49867fafb03ea2ba4d.js +0 -1
  72. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/742.91e7b516c8699eea3373.js +0 -1
  73. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.2d75de1a8d2c3131a8db.js +0 -1
  74. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/786.770dc7bcab77e14cc135.js +0 -7
  75. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/801.ca9e114a30896b669a3c.js +0 -1
  76. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.25ddd15aca09421d3765.js +0 -1
  77. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/888.34054db17bcf6e87ec95.js +0 -1
  78. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.b05b2f0c9617ba28370d.js +0 -1
  79. signalpilot_ai_internal-0.10.0.dist-info/RECORD +0 -50
  80. signalpilot_ai_internal-0.10.0.dist-info/licenses/LICENSE +0 -29
  81. {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
  82. {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
  83. /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
  84. {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
  85. {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: 'WARNING'
2
+ level: "WARNING"
3
3
 
4
4
  embedding:
5
- location: 'memory'
6
- model: 'multi-qa-MiniLM-L6-cos-v1'
7
- metric: 'cosine'
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: '/tmp/.schema_search_cache'
10
+ cache_dir: "/tmp/.schema_search_cache"
11
11
 
12
12
  chunking:
13
- strategy: 'raw'
13
+ strategy: "raw" # Options: "raw", "llm"
14
14
  max_tokens: 256
15
15
  overlap_tokens: 50
16
- model: 'gpt-4o-mini'
16
+ model: "gpt-4o-mini"
17
17
 
18
18
  search:
19
- strategy: 'bm25'
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: null
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 Snowflake connections"}))
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(engine=engine, config_path=str(self.CONFIG_PATH))
74
- schema_search.index()
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