Repository navigation
Conversation
…ure scan, cost estimate vs Cost Management actuals)
- setup skill asks whether to profile ADF resources; bootstrap.sh gets a --with-profile flag that installs flowx from the checkout with the `profile` extra (pip install -e '<root>[profile]'). - MCP app deploy: `with_profile` widget (deploy_app.py) / WITH_PROFILE=1 (deploy.sh) append the pyproject `profile` extra to the app requirements; off by default. - Register `profile` as an MCP command (thin wrapper over `python -m flowx.adapter profile`); returns report_md on success and the adapter's exit 2 + install hint when the extra is missing. - Docs: MCP usage and a note that ADF Datasets are intentionally not scanned. Co-authored-by: Isaac <no-reply@databricks.com> Co-authored-by: omnigent <noreply@omnigent.ai>
lorenzorubi-db
left a comment
There was a problem hiding this comment.
Looks great: setup opt-in + MCP profile is the right shape, and the tests are great. One thing is needed before merge (rebase + profile → azure, below); the inline comments are small and optional.
- Rebase onto the new feat/adf-profiler. I rebased #143 onto main, which now includes #154 (integration tests) and #157 (extract phase). The old base commit
eb3a6f9is gone; the branch is noweb654cc(the profiler) plus three commits: the exact factory-name match for actuals, a golden-reference integration test, and --days defaults to 45. To replay just your commit:
git fetch origin
git rebase --onto origin/feat/adf-profiler eb3a6f9 feat/adf-profiler-setup-mcp
There's one conflict, in app/deploy_app.py. main reworded app_description to "flowx MCP server — Lakeflow jobs migration tools"; keep that line and your with_profile lines. Everything else applies cleanly.
- Rename the profile extra to azure. #157 added an azure extra with exactly the same packages as profile (azure-identity>=1.17, requests>=2.31), so #143 dropped profile and the profiler now uses [azure]. As-is, after the rebase:
- app/deploy_helpers.py (app_requirements) and app/deploy.sh read optional-dependencies["profile"], so both raise a KeyError.
- scripts/bootstrap.sh runs pip install -e "${PLUGIN_ROOT}[profile]". That fails silently: pip only warns that the extra doesn't exist and installs flowx without the Azure packages, and profile then exits 2 with the install hint.
Places to update (all [profile] / ["profile"] / "the profile extra"):
- app/deploy_helpers.py, app/deploy.sh, app/README.md
- scripts/bootstrap.sh
- skills/flowx-setup/SKILL.md, skills/flowx-profile/SKILL.md
- src/flowx/mcp/server.py (server instructions + tool docstring)
- tests/unit/test_setup_profile_opt_in.py
Design question:
extract needs the same azure extra, and its skill already tells users to run pip install -e '<plugin_dir>[azure]' by hand. Should setup ask a single question covering both, e.g. "Do you want the live Azure features (pull ADF definitions with extract, estimate current spend with profile)?", with --with-azure / with_azure / WITH_AZURE=1 instead of the profile-only names? Then the extract skill could point to setup instead of its manual install line. I'd lean yes, but I'm happy either way, and fine as a follow-up if you'd rather not widen this PR.
| if [ "${WITH_PROFILE:-0}" = "1" ]; then | ||
| echo "==> Adding the profile command's Azure deps (WITH_PROFILE=1)" | ||
| python3 -c 'import sys, tomllib; print("\n".join(tomllib.load(open(sys.argv[1], "rb"))["project"]["optional-dependencies"]["profile"]))' \ | ||
| "$REPO_ROOT/pyproject.toml" >> "$STAGE_DIR/requirements.txt" | ||
| fi |
There was a problem hiding this comment.
tomllib needs Python ≥ 3.11, a guard upfront gives a clear message
| if [ "${WITH_PROFILE:-0}" = "1" ]; then | |
| echo "==> Adding the profile command's Azure deps (WITH_PROFILE=1)" | |
| python3 -c 'import sys, tomllib; print("\n".join(tomllib.load(open(sys.argv[1], "rb"))["project"]["optional-dependencies"]["profile"]))' \ | |
| "$REPO_ROOT/pyproject.toml" >> "$STAGE_DIR/requirements.txt" | |
| fi | |
| if [ "${WITH_PROFILE:-0}" = "1" ]; then | |
| if ! python3 -c 'import tomllib' 2>/dev/null; then | |
| echo "WITH_PROFILE=1 needs python3 >= 3.11 (it reads pyproject.toml with tomllib); found $(python3 --version 2>&1)" >&2 | |
| exit 1 | |
| fi | |
| echo "==> Adding the profile command's Azure deps (WITH_PROFILE=1)" | |
| python3 -c 'import sys, tomllib; print("\n".join(tomllib.load(open(sys.argv[1], "rb"))["project"]["optional-dependencies"]["profile"]))' \ | |
| "$REPO_ROOT/pyproject.toml" >> "$STAGE_DIR/requirements.txt" | |
| fi |
| for key in ("days", "subscription_id", "resource_group", "factory_name"): | ||
| if p.get(key): | ||
| args += [f"--{key.replace('_', '-')}", p[key]] | ||
| result = runner.run_adapter(args) |
There was a problem hiding this comment.
Long scans may not fit in a single MCP call. The scan makes one REST call per pipeline run (~0.3 s each), so a subscription-wide scan of a busy factory can run for hours. The MCP client's request limit is probably shorter than FLOWX_MCP_TIMEOUT (30 min), so raising that doesn't help, and catching TimeoutExpired here would only improve the error message for a call the client has already abandoned.
Not for this PR, but I think MCP profile should eventually be async: start the scan, return a run id, and poll for the report. Until then, could the docstring and the skill say that MCP profile is for scoped scans (resource_group / factory_name, fewer days), and that subscription-wide scans should run from a notebook?
| install hint. The app also needs Azure service principal credentials (`AZURE_CLIENT_ID`, | ||
| `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID`) as app env vars, e.g. from a secret. |
There was a problem hiding this comment.
app/app.yaml has no env: section, so "as app env vars, e.g. from a secret" may leave the user stuck:
| install hint. The app also needs Azure service principal credentials (`AZURE_CLIENT_ID`, | |
| `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID`) as app env vars, e.g. from a secret. | |
| install hint. The app also needs an Azure service principal. Add the client secret to the app as a | |
| **Secret** resource (Apps → your app → Edit → App resources; resource key e.g. `azure-sp-secret`, | |
| permission *Can read*), then add to `app/app.yaml` before deploying: | |
| env: | |
| - name: AZURE_TENANT_ID | |
| value: "<tenant id>" | |
| - name: AZURE_CLIENT_ID | |
| value: "<client id>" | |
| - name: AZURE_CLIENT_SECRET | |
| valueFrom: azure-sp-secret | |
| The service principal needs Reader and Data Factory Contributor on the subscription or resource group | |
| it scans, and Cost Management Reader on the subscription (see `skills/flowx-profile/sources/adf.md`). |
| report_md = runner.read_text(out / "metadata" / "tco" / "cost_comparison.md") if result.ok else None | ||
| return _phase_result(result, out, report_md=report_md) | ||
|
|
There was a problem hiding this comment.
In the app, output_dir is inside the container, so only report_md reaches the user; cost_comparison.csv is left out. Since it should be small, returning it too is the simplest:
| report_md = runner.read_text(out / "metadata" / "tco" / "cost_comparison.md") if result.ok else None | |
| return _phase_result(result, out, report_md=report_md) | |
| # Only read the reports on success, so a failed run never returns stale ones from an earlier run. | |
| report_dir = out / "metadata" / "tco" | |
| report_md = runner.read_text(report_dir / "cost_comparison.md") if result.ok else None | |
| report_csv = runner.read_text(report_dir / "cost_comparison.csv") if result.ok else None | |
| return _phase_result(result, out, report_md=report_md, report_csv=report_csv) |
| configuration is available; re-call once with the complete answers to apply (see below). | ||
| - "profile": source(req: "adf"), output_dir, days(default 90), subscription_id, resource_group, | ||
| factory_name (only with resource_group) — live Azure scan estimating current ADF spend; returns | ||
| ``report_md`` (metadata/tco/cost_comparison.md). Needs the server's ``profile`` extra (else exit |
There was a problem hiding this comment.
and add report_csv next to report_md here
| answers(list of "ID=VALUE"), interactive(bool, default true), lookup_csv — runs | ||
| discover→convert→package, returning the full option schema once (status "needs_input") when | ||
| configuration is available; re-call once with the complete answers to apply (see below). | ||
| - "profile": source(req: "adf"), output_dir, days(default 90), subscription_id, resource_group, |
There was a problem hiding this comment.
ADF keeps only 45 days of pipeline run history, so days above 45 adds nothing, and 90 as the documented default is misleading.
| - "profile": source(req: "adf"), output_dir, days(default 90), subscription_id, resource_group, | |
| - "profile": source(req: "adf"), output_dir, days(default 45, ADF's run-history limit), subscription_id, | |
| resource_group, |
Stacks on #143 (
feat/adf-profiler). Merge that first; this PR's base isfeat/adf-profiler.What changed
Setup asks about profiling, so neither path installs the Azure deps unless the user says yes
skills/flowx-setup/SKILL.md: new Step 2 asks "Do you want to profile your ADF resources?" and both paths act on the answer.scripts/bootstrap.sh: new--with-profileflag. With it, bootstrap also runspip install -e '<plugin_root>[profile]'. That installs from the local checkout, so the installed version always matches the source tree. Without the flag nothing changes. Unknown flags exit 2 with a usage line before any work starts.with_profilewidget inapp/deploy_app.py(defaultno) andWITH_PROFILE=1forapp/deploy.shappend theprofileextra to the app'srequirements.txt. Both read the extra straight frompyproject.toml, so there is no second requirements list to keep in sync (this matches the intent oftest_profiler_agents_doc).MCP
profilecommand (src/flowx/mcp/server.py)_cmd_profileis a thin wrapper overpython -m flowx.adapter profilethroughrunner.run_adapter. It takessource(required),output_dir,days,subscription_id,resource_group, andfactory_name.report_md(metadata/tco/cost_comparison.md). It skips that read on failure, so a stale report from an earlier run is never returned.ok: false,returncode: 2, and the install hint inprocess.stderr, with no traceback.AZURE_*env vars, so a secret can't show up in the echoedprocess.command.app/README.mdcommand table.flowx.mcp/flowx.mcp.serverstill doesn't importazure.Docs
skills/flowx-profile/SKILL.md: explains MCP usage, replaces the "not exposed as an MCP command yet" note, and adds: ADF Datasets are intentionally not scanned (definitions only, no runtime meter).How I verified
PYTHONPATH=src uv run pytest tests/unit -v: 1383 test cases collected, 1381 passed, 3 skipped, 2 failed.test_airflow_provider_sync.py) also fail on the base commit. They are local-environment only: my globalcommit.gpgsign=truemakes the fixture'sgit commitfail with "No secret key". They are not related to this change.test_mcp_profile.py,test_mcp_migrate.py,test_mcp_source_routing.py), which skip when themcpextra isn't installed. That is also true in CI.mcpavailable (pinned touv.lockversions): 26 passed, including the 6 new tests intest_mcp_profile.py.flowx(command="profile", …)call through FastMCP without azure installed. It returnedreturncode: 2and the install hint.uv run ruff format --check src/ tests/,uv run ruff check src/ tests/, anduv run mypy src/flowx/all pass.test_mcp_profile.py: argv dispatch, report readback, missing extra (exit 2 + hint, no stale report),sourcerequired, no azure on server import.test_setup_profile_opt_in.py: bootstrap rejects unknown flags, bootstrap installs-e "${PLUGIN_ROOT}[profile]", app requirements leave out azure by default and add the pyproject extra when asked.test_profiler_lazy_imports.pyto importflowx.mcpandflowx.mcp.runner.Notes
inputsdiscovery doesn't coverprofile: the adapter'sinputssubcommand only accepts discover/convert/package, and widening it was outside this change.flowx profileADF TCO profiler phase + flowx-profile skill #143 didn't add one either.This pull request and its description were written by Isaac.