Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 53 additions & 12 deletions backend/src/analytics_agent/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,36 @@ class UpdateDisplayRequest(BaseModel):
{"name": "preview_table", "label": "Preview data"},
{"name": "execute_sql", "label": "Execute SQL"},
],
"hive": [
{"name": "list_tables", "label": "List tables"},
{"name": "get_schema", "label": "Table schema"},
{"name": "preview_table", "label": "Preview data"},
{"name": "execute_sql", "label": "Execute SQL"},
],
"mysql": [
{"name": "list_tables", "label": "List tables"},
{"name": "get_schema", "label": "Table schema"},
{"name": "preview_table", "label": "Preview data"},
{"name": "execute_sql", "label": "Execute SQL"},
],
"postgresql": [
{"name": "list_tables", "label": "List tables"},
{"name": "get_schema", "label": "Table schema"},
{"name": "preview_table", "label": "Preview data"},
{"name": "execute_sql", "label": "Execute SQL"},
],
"sqlite": [
{"name": "list_tables", "label": "List tables"},
{"name": "get_schema", "label": "Table schema"},
{"name": "preview_table", "label": "Preview data"},
{"name": "execute_sql", "label": "Execute SQL"},
],
"duckdb": [
{"name": "list_tables", "label": "List tables"},
{"name": "get_schema", "label": "Table schema"},
{"name": "preview_table", "label": "Preview data"},
{"name": "execute_sql", "label": "Execute SQL"},
],
"sqlalchemy": [
{"name": "list_tables", "label": "List tables"},
{"name": "get_schema", "label": "Table schema"},
Expand Down Expand Up @@ -249,6 +273,30 @@ def _build_tool_toggles(
return result


def _compute_engine_status(engine_type: str, conn_cfg: dict, sso_connected: bool = False) -> str:
"""Return 'connected' or 'unconfigured' for an engine connection."""
from analytics_agent.engines.factory import _CONNECTOR_MAP

spec = _CONNECTOR_MAP.get(engine_type)
if spec is not None:
return (
"connected"
if spec.is_configured(conn_cfg, sso_connected=sso_connected)
else "unconfigured"
)

if engine_type in ("mysql", "sqlalchemy", "postgresql", "sqlite", "duckdb"):
host = conn_cfg.get("host", "")
database = conn_cfg.get("database", conn_cfg.get("db", ""))
has_url = bool(conn_cfg.get("url"))
# File-based engines need only `database`; server engines need host too.
file_based = engine_type in ("sqlite", "duckdb")
if has_url or (file_based and bool(database)) or (host and database):
return "connected"

return "unconfigured"


# --- Connection helpers ---


Expand Down Expand Up @@ -596,15 +644,9 @@ async def list_connections(session: AsyncSession = Depends(get_session)):
is_sso_connected = cred is not None and cred.auth_type == "sso_externalbrowser"

if intg.type == "snowflake":
from analytics_agent.engines.factory import _CONNECTOR_MAP as _CM

account = conn_cfg.get("account", "")
user = conn_cfg.get("user", "")
status_str = (
"connected"
if _CM["snowflake"].is_configured(conn_cfg, sso_connected=is_sso_connected)
else "unconfigured"
)
status_str = _compute_engine_status(intg.type, conn_cfg, sso_connected=is_sso_connected)
# Detect active auth method so the frontend can pre-select the right tab.
if is_sso_connected:
active_auth_method = "sso"
Expand Down Expand Up @@ -667,7 +709,7 @@ async def list_connections(session: AsyncSession = Depends(get_session)):
conn_cfg.get(k) or os.environ.get(_CM["bigquery"].env_map.get(k, ""), "")
for k in _CM["bigquery"].credential_keys
)
status_str = "connected" if _CM["bigquery"].is_configured(conn_cfg) else "unconfigured"
status_str = _compute_engine_status(intg.type, conn_cfg)
fields = [
ConnectionField(
key="project",
Expand All @@ -690,13 +732,13 @@ async def list_connections(session: AsyncSession = Depends(get_session)):
placeholder='{"type":"service_account",...}',
),
]
elif intg.type in ("mysql", "sqlalchemy", "postgresql", "sqlite"):
elif intg.type in ("mysql", "sqlalchemy", "postgresql", "sqlite", "duckdb"):
host = conn_cfg.get("host", "")
database = conn_cfg.get("database", conn_cfg.get("db", ""))
port = str(conn_cfg.get("port", ""))
user = conn_cfg.get("user", conn_cfg.get("username", ""))
has_url = bool(conn_cfg.get("url"))
status_str = "connected" if (has_url or (host and database)) else "unconfigured"
status_str = _compute_engine_status(intg.type, conn_cfg)
if has_url:
fields = [
ConnectionField(
Expand Down Expand Up @@ -736,8 +778,8 @@ async def list_connections(session: AsyncSession = Depends(get_session)):
from analytics_agent.engines.factory import _CONNECTOR_MAP as _CM

spec = _CM.get(intg.type)
status_str = _compute_engine_status(intg.type, conn_cfg)
if spec is not None and spec.display_fields:
status_str = "connected" if spec.is_configured(conn_cfg) else "unconfigured"
fields = []
for df in spec.display_fields:
raw = conn_cfg.get(df.key, "") or os.environ.get(
Expand All @@ -755,7 +797,6 @@ async def list_connections(session: AsyncSession = Depends(get_session)):
)
)
else:
status_str = "unconfigured"
fields = []

oauth_status = (
Expand Down
2 changes: 2 additions & 0 deletions backend/src/analytics_agent/engines/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ def get_secret_env_vars(engine_type: str) -> dict[str, str]:
"mysql": SQLAlchemyQueryEngine,
"sqlite": SQLAlchemyQueryEngine,
"postgresql": SQLAlchemyQueryEngine,
"duckdb": SQLAlchemyQueryEngine,
"sqlalchemy": SQLAlchemyQueryEngine,
}.get(engine_type)
return getattr(cls, "secret_env_vars", {}) if cls else {}
Expand All @@ -208,6 +209,7 @@ def _make_connector(connection_cfg: dict) -> MCPQueryEngine:
"mysql": SQLAlchemyQueryEngine,
"sqlite": SQLAlchemyQueryEngine,
"postgresql": SQLAlchemyQueryEngine,
"duckdb": SQLAlchemyQueryEngine,
"sqlalchemy": SQLAlchemyQueryEngine,
"mcp": MCPQueryEngine,
"mcp-stdio": MCPQueryEngine,
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/Settings/connections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { hivePlugin } from "./plugins/hive";
import { mysqlPlugin } from "./plugins/mysql";
import { postgresqlPlugin } from "./plugins/postgresql";
import { sqlitePlugin } from "./plugins/sqlite";
import { duckdbPlugin } from "./plugins/duckdb";
import { datahubPlugin } from "./plugins/datahub";
import { datahubMcpPlugin } from "./plugins/datahub-mcp";
import { customMcpEnginePlugin, customMcpContextPlugin } from "./plugins/custom-mcp";
Expand All @@ -24,6 +25,7 @@ export const CONNECTION_PLUGINS: ConnectionPlugin[] = [
mysqlPlugin,
postgresqlPlugin,
sqlitePlugin,
duckdbPlugin,
customMcpEnginePlugin,

// Context platforms
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/Settings/connections/plugins/duckdb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SimpleFormShell } from "../SimpleFormShell";
import type { ConnectionPlugin, NewConnectionPayload } from "../types";

const FIELDS = [
{ key: "database", label: "Database file path", type: "mono" as const,
placeholder: "/absolute/path/to/database.duckdb", required: true },
];

export const duckdbPlugin: ConnectionPlugin = {
id: "duckdb",
serviceId: "duckdb",
label: "DuckDB",
category: "engine",
transport: "native",
description: "Connect to a local DuckDB database file",
Form: ({ onDone, onCancel }) => (
<SimpleFormShell
fields={FIELDS}
onCancel={onCancel}
onDone={(payload: NewConnectionPayload) =>
onDone({ ...payload, config: { dialect: "duckdb", ...payload.config } })
}
/>
),
};
Loading
Loading