Skip to content

perf(powerbi): M-Query performance improvements - #70

Open
MrBlack1995 wants to merge 38 commits into
databrickslabs:pbi-optimizationfrom
MrBlack1995:feat/pbi-mquery-performance
Open

perf(powerbi): M-Query performance improvements#70
MrBlack1995 wants to merge 38 commits into
databrickslabs:pbi-optimizationfrom
MrBlack1995:feat/pbi-mquery-performance

Conversation

@MrBlack1995

Copy link
Copy Markdown
Contributor

Stacked on #67 (prompt-optimization). Scoped to PowerBI M-Query pipeline performance.

Status

Draft — opening early to track. First commit is the UCMV coverage evaluation +
best-effort roadmap doc (SC/PAAT/DCC analysis); the M-Query performance changes
land in follow-up commits.

Included so far

  • docs(powerbi): UCMV coverage evaluation + best-effort roadmap — consolidates
    the SC (100% measure match, 74% coverage), PAAT (~10-19% ceiling) and DCC
    (~0-5% ceiling) dataset analyses and the repo changes they imply.

Planned (M-Query performance)

  • To be filled in as the performance work lands.

Base

Targets prompt-optimization (PR #67), not main, so the diff shows only the
M-Query work stacked on top of the prompt-optimization changes.

This pull request and its description were written by Isaac.

nehmetohme and others added 17 commits July 24, 2026 13:29
Adds a prompt-optimization stack built on MLflow GenAI + GEPA
(reflective prompt evolution) that works end-to-end inside Kasal:

Template optimization (Configuration -> Prompts):
- Six generation templates (detect_intent, generate_agent, generate_task,
  generate_crew, generate_crew_plan, generate_job_name) get a per-template
  Optimize action that runs GEPA against real usage data mined from the
  LLM interaction logs, with junk filtering and format+correctness scoring.
- Single consolidated "Prompts" surface; optimization opens in a scoped
  dialog next to each template.

Crew optimization (crew catalog -> Optimize Prompts):
- Full crew-in-the-loop GEPA: every metric call executes the crew for real
  and judges score the actual deliverable. Agent role/goal/backstory and
  task description/expected_output are evolved together via a labeled
  crew-document serialization with a strict parser (malformed candidates
  score 0 without spending an execution).
- Hard execution cap: the budget the user picks is a promise; the
  predict_fn refuses executions beyond the cap even when GEPA evaluates
  candidates in parallel. Stop button cancels between iterations.

Human judgment loop (all inside Kasal, persisted as MLflow assessments):
- Grade past evaluation answers 0-10 with per-judge attribution, comments,
  and ground-truth Expectations (log_feedback + log_expectation).
- Create custom LLM judges (make_judge) in-app; judges live in a library
  and are assigned per crew via a crew-scoped registered copy; built-in
  graded 0-10 quality judge always on.
- Harvested crew thumbs and human assessments feed both the judge rubric
  and the GEPA reflection objective, so non-expert feedback autonomously
  steers prompt evolution.

Infrastructure:
- MLflow registry/tracking handling that respects the launch
  MLFLOW_TRACKING_URI (preserved as KASAL_LAUNCH_MLFLOW_TRACKING_URI
  before main.py forces "databricks"), local OSS registry gated behind
  MCP_SERVER_ENABLED, databricks-uc otherwise.
- Cross-loop safety: worker threads reach the app DB only via
  run_coroutine_threadsafe onto the main loop with UserContext
  re-established; crew execution polling uses fresh sessions.
- Autolog import-hook deadlock avoidance during optimization spans;
  reflection-model preflight so a dead provider fails fast instead of
  burning budget.
- New dependency: gepa (required by MLflow's GepaPromptOptimizer).
  uv.lock churn beyond the gepa addition is uv re-canonicalization only;
  no other package versions changed.

25 unit tests cover doc serialization round-trips, judge value grading,
budget accounting, registry gating, and apply/cancel flows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…make human feedback reach the mutator

Live diagnosis of flat runs (score X -> X, "baseline won") on a real crew
with 12 human grade-0 notes exposed four compounding causes:

1. Budget burned re-measuring the baseline. GEPA re-evaluates the SAME
   candidate doc repeatedly (upfront smoke test, baseline valset pass, and
   a fresh reflective-minibatch pass every iteration). Each re-run cost a
   real crew execution, so a 4-execution run bought exactly ONE distinct
   candidate (observed: total_metric_calls=7, candidates=1). Fix: cache
   deliverables by candidate-doc hash — each DISTINCT candidate costs one
   execution; re-evaluations are free and post-cap re-evals of the
   baseline stay truthful.

2. Judge noise made acceptance a coin flip. The same prompts drew grades
   of 0.0 and then 4/10 two minutes apart; the one candidate that DID
   incorporate the human requirements lost to a lucky baseline draw.
   Fix: cache the judge verdict by deliverable hash — within a run,
   identical outputs always score identically, so candidate-vs-baseline
   comparisons are stable.

3. Reflection was blind to WHY candidates failed. Scorers returned bare
   floats, and MLflow only forwards textual rationales to GEPA's
   reflective dataset from Feedback objects — so the mutator saw "0.0"
   but never "wrong region, rentals instead of sales". The judge was even
   instructed to reply with only the number. Fix: the judge now critiques
   first (quoting the violated requirement) and grades on the last line;
   output_correct returns Feedback(value, rationale) including registered
   judges' rationales; aggregation unwraps Feedback values. Harvested
   human Expectations additionally ride the train row's expectations
   channel, which the reflective dataset surfaces explicitly.

4. Invisible feedback usage + misleading timestamps. Users could not see
   that their grades were harvested ("we are not using the grading"), and
   naive-UTC created_at rendered a 01:20 local run as "11:20 PM". Fix:
   runs now report human_feedback_count ("guided by N human notes" chip)
   and candidates_tried ("N variants tried" chip); timestamps are
   timezone-aware so browsers render local time. Harvest also iterates
   traces oldest-first so the keep-last-12 slice keeps the NEWEST notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…single-example datasets

A live run WITH the previous fixes still returned "baseline won" while
producing a candidate that fully incorporated the human requirements.
proposals.json showed why: minibatch score 0.9 vs 0.9 — a TIE, rejected
by GEPA's strict-improvement acceptance. Three compounding causes, each
verified against gepa 0.1.4 source and live A/B judge experiments:

1. No gradient: the judge graded EVERY real deliverable 0/10. Feeding
   the raw harvest ("human_grade: 0.0 ..." x13) anchored it — the same
   compliant answer scored 0/10 under the litany rubric and 6/10 under a
   requirements checklist. And raw complaint sentences as checklist
   items made a 30B judge fail every mark by quoting the requirement
   itself as evidence. Now: harvested notes are LLM-distilled once per
   run into <=5 testable imperatives; the judge marks each R<n>
   PASS/FAIL and must quote the violating passage VERBATIM from the
   answer (cannot quote -> PASS), plus a Q quality mark. The grade is
   COMPUTED from the marks (0.8 x pass-fraction + 0.2 x Q), never taken
   from the model's arithmetic — a judge writing "40" previously
   clamped to a perfect 10. Validated live: compliant answer 0.96,
   Geneva-containing answer 0.60 with correct verbatim quotes.

2. gepa defaults hostile to 1-example datasets: reflection_minibatch_size
   defaults to 3, so each proposal step evaluated the SAME single example
   three times — three concurrent predict calls raced past the result
   cache (two crew executions finished the same second) and burned 3
   executions per candidate. Now reflection_minibatch_size=1,
   acceptance_criterion="improvement_or_equal" (lateral moves survive
   flat regions), cache_evaluation=True (gepa-side result cache skips
   repeat metric calls entirely), and a lock serializes
   check-then-execute so concurrent identical calls cannot double-run
   the crew.

3. Metric budget conflated with execution budget: cached re-evaluations
   still consumed GEPA's metric-call budget, so a 10-execution run
   stopped after 4 executions with budget left. The optimizer now gets
   metric headroom (2x executions + 3) while the user's number remains
   a hard cap on real crew executions.

Also: judge context no longer includes the crew objective line (the
task text said "cities like Zurich, Geneva" while the human demanded
German-side only — the contradiction primed hallucinated FAILs), and
eval-trace logging failures are logged at warning (a baseline eval
vanished silently, leaving nothing to grade).

11 new unit tests: requirement distillation/parsing and checklist-mark
grading (including the model-arithmetic and duplicate-mark hazards).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ic reflection dead-loops the search

With everything else fixed, a live run still returned "baseline won"
after spending only 2 of 10 executions: proposals.json showed ELEVEN
byte-identical proposals (same 2268 chars, same 0.571 -> 0.356 scores),
each a free cache-hit rejection until the metric budget drained.

The reflection prompt is identical every iteration in this regime —
same parent (the baseline never gets displaced), same single training
example, same cached judge rationale — so a reflection endpoint with a
deterministic default (temperature ~0) regenerates the exact same
candidate forever. The result caches introduced earlier correctly made
those repeats free, which turned determinism into a zero-exploration
loop instead of a budget fire.

gepa's LM wrapper forwards reflection_lm_kwargs to litellm.completion;
an explicit temperature=1.0 makes each iteration propose a genuinely
different variant, so the execution budget is spent on NEW candidates
and a good draw can beat the baseline. (Judge and distillation calls
already run at temperature 0 — grading stays deterministic.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion and pin the mutation output format

Two more live-run killers found after the temperature change shipped:

1. Reflection was served from Kasal's own cache. llm_manager enables a
   process-global litellm disk cache at import time, and gepa's LM
   wrapper rides the same litellm in the same process — so the identical
   per-iteration reflection prompt got the identical cached response
   forever (observed live: duration=0.00s, byte-identical proposals at
   temperature 1.0). Reflection calls now send cache={"no-cache": True}.

2. gepa's default proposal template says "write a new instruction ...
   within ``` blocks", which invited the reflection model to restructure:
   it returned {"instruction": "..."} JSON blobs that lost the
   [AGENT]/[TASK] document structure, so every proposal free-rejected
   without ever executing (observed live: 11/11 malformed, run ended
   after 1 execution). A custom reflection_prompt_template now pins the
   output contract to the crew-doc format. Validated offline against the
   live reflection endpoint: with the fenced-output instruction 2 of 3
   samples degenerated to a bare "```"; with the no-fence contract at
   temperature 0.8, 4 of 4 samples parsed, were distinct, and carried
   the human requirements into the mutated fields.

Also: _parse_crew_doc strips surviving markdown fences before parsing
(a recoverable wrapper should not cost a candidate), with tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing panel

Crew deliverables are mostly GFM tables; the read-only TextField showed
them as raw pipe soup. The grading panel now renders them with
react-markdown + remark-gfm (the Documentation viewer's stack), with
compact table styling, scroll containment, and external links opening
in a new tab.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…elete from library, fix cross-crew visibility

Judges were create-only from Kasal. Now:

- Edit: assigned judge chips and library rows open an edit dialog for
  instructions and/or model. Updating registers a new version under the
  same registry name (MLflow scorers are versioned; latest wins —
  verified live against the local registry). Editing an assigned copy
  changes what that crew's runs use; editing a library judge leaves
  already-assigned snapshots untouched, and the dialog says so.
  list_judges now returns full instructions (4000 chars, was a 500-char
  preview) so the edit round-trip cannot truncate-corrupt a judge.

- Delete: library judges get a delete action with a confirm dialog
  (assigned copies survive; the chip's unassign already covered those).

- Cross-crew visibility fix: creating a judge from a crew's dialog
  registered ONLY the crew-scoped copy — no library original existed,
  so the judge never appeared in any other crew's Assign menu (observed
  live). Creation now registers the shared library original AND the
  crew-scoped copy.

- Experiment pinning: scorers are per-experiment, and none of the judge
  CRUD paths pinned one — a fresh worker's active experiment is
  Default/0, so judges could register into or list from the wrong
  experiment and silently vanish. All five judge bodies now pin the
  launch experiment ('kasal' fallback), matching the optimization runs.

Verified end-to-end against the live API: create-from-crew registers
both names, update re-versions the target only, delete cleans up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce, backend and frontend

Backend — 93 tests:
- service (67): crew-doc serialize/parse round-trip incl. multiline
  continuation and malformed-candidate rejection (the GEPA mutation
  contract), judge verdict normalization (numeric/bool/categorical),
  requirement distillation + checklist-mark grading (incl. the
  model-arithmetic and duplicate-mark hazards), judge lifecycle against
  a faked mlflow registry (dual library+scoped registration, {{ outputs }}
  autofix, update keeps omitted fields, assign snapshots, delete all
  versions, per-operation experiment pinning, local-mode gating), run
  registry behaviors (public-field allowlist never leaks task handles,
  timezone-aware sort, cancel transitions/guards, prune spares active
  runs), plus the existing mining/scorer/registry/apply coverage
- router (26): every endpoint's handler — ValueError → 400 for client
  input, missing runs → 404, response-schema wrapping incl. the
  progress-chip fields, judge CRUD passthrough, eval-feedback numeric
  coercion, user-context publication, and the request-schema bounds
  (budget 4..40 default 10; template name is a closed set)

Frontend — 46 tests:
- PromptOptimizationService (19): every method's endpoint, payload
  undefined-stripping, judge-name URL encoding, zero-grade preservation,
  list unwrapping and boolean coercion
- CrewOptimizeDialog (10): judge scoping by crew registry prefix
  (assigned chips vs library menu; other crews' judges invisible;
  same-name library duplicate excluded), edit-by-full-name, delete
  confirm, create auto-assign, honest-progress chips, budget-as-cap
  start request, ungraded-first eval ordering with counts, markdown
  answer rendering, grade submission
- PromptConfiguration (5): Optimize action gated to optimizable
  templates and to an onOptimize handler; edit dialog
- PromptOptimization (4): fixedTemplate hides the picker and filters
  runs; start payload; backend rejection surfaced
- Prompts (3): dialog opens scoped to the chosen template and closes
- optimizableTemplates (2): mirrors the backend TEMPLATE_TASKS set
- CrewFlowDialog optimize entry (3): source-level guards in the
  established CrewFeedback.test.tsx style (heavy dialog): click
  isolation and dialog wiring

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…any explicit temperature

A crew run on kimi-k2.7-code-highspeed "completed" after a single
execution: gepa made 10 reflection attempts and produced ZERO proposals
(no proposals.json artifact, no subsample scores). Direct reproduction
against Moonshot with gepa's exact request showed why — every call
400'd with "invalid temperature: only 1 is allowed for this model";
the same call without a temperature returns a clean, parseable crew
doc (thinking rides the separate reasoning_content field).

_resolve_reflection_model now returns (uri, env, provider) and the
crew body builds reflection_lm_kwargs per provider: Kimi gets only the
litellm cache bypass (its forced default sampling is already diverse);
everyone else keeps temperature 0.8. The template-mode optimizer gains
the same cache bypass (it never sent a temperature, so it was
Kimi-safe but cache-exposed).

New test pins the kimi resolver path (key lookup, URI, provider tag);
resolver tests updated for the 3-tuple.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…provider plumbing outside the manager

The optimization service had grown its own provider layer for the calls
MLflow/gepa make directly (reflection, registered judges, preflight):
a URI resolver, a provider→env-key table, per-tenant API keys written
into the shared process env, and request quirks re-implemented per
provider. Each of those independently produced live failures (retired
DeepSeek names, Kimi temperature 400s, key-lookup drift) that LLMManager
already handles centrally for the rest of Kasal.

Now every LLM call goes through LLMManager.completion:

- GEPA reflection: gepa accepts a LanguageModel callable, but MLflow's
  GepaPromptOptimizer pins reflection_lm to a litellm string. A one-time
  idempotent patch on gepa.optimize swaps in a per-worker-thread
  callable (each run owns one thread) backed by _sync_llm_completion.
  The callable adds a unique system-line cache-buster (the process-
  global litellm cache replayed one proposal forever for the identical
  per-iteration prompt) and passes temperature=0.8 — the MANAGER drops
  it for providers that reject it (Kimi), so the provider-aware kwargs
  from the previous commit are deleted again.

- Registered judges: rendered and invoked HERE via LLMManager instead
  of mlflow's own model client. Judges store a plain Kasal model key
  (wrapped as 'openai:/<key>' only to satisfy make_judge's URI shape);
  legacy URI-storing judges are stripped back to their best key.

- Preflight: a manager-routed ping instead of a raw litellm call with
  env juggling.

Deleted: _resolve_reflection_model, _REFLECTION_KEY_ENV, reflection
env application/restore in both sync bodies, reflection_lm_kwargs, and
the reflection_provider threading. Security side effect: per-tenant
API keys are no longer written into os.environ.

Tests: resolver suite replaced with coverage of the new pure helpers
(key stripping, grade parsing, bridge install/override/idempotency,
preflight error wrapping, reflection message shaping incl. distinct
cache-busters); judge lifecycle updated for key storage. 95 backend
tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm install re-resolves and rewrites package-lock.json on every run
(optional/peer dependency metadata shifts with npm/registry state),
causing spurious lockfile churn on every local build and deploy.
npm ci installs strictly from the lockfile and never modifies it.

Also resyncs package-lock.json, which had drifted out of sync with
package.json (npm ci was failing on missing optional platform deps)
independent of this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Maximilian Kohnen <57352546+mckohnen@users.noreply.github.com>
…T/DCC)

Consolidated digest of three CCHBC dataset runs and the repo changes they imply:

- SC: success baseline — 30 views, 476/476 measures exact-match to ground truth
  (100%), +41 vs the prior run; 74% coverage of all source measures.
- PAAT: report skin, no M-Query source tables; 46 measures resolvable → ~10-19%
  best-effort ceiling once physical source names are supplied.
- DCC: KPI/data-quality report; 40% measure-on-measure DAX, 0 resolvable → ~0-5%
  ceiling, needs composition + table-expr translators.

Actionable repo changes: fact_source_map + allow_best_effort config keys, a
best-effort view generator reusing measure_resolutions, not-emitted docs reusing
the SC machinery, and evaluate_ucmv.py as the CI gate (correctness + coverage
never-regress). Report-layer limits documented (selectors/disconnected tables
never translate).
Most 'no UC Metric Views' outcomes (PAAT/DCC) are thin reports on a separate/
upstream semantic model, not conversion failures — the model with the M-Queries and
physical tables is a different dataset than the one extracted. Document this as the
primary fix:

- NEW customer-facing guide thin-report-and-source-resolution.md (three model kinds,
  how to check via lineage / REST GET /reports/{id} -> datasetId, and what to do).
  Registered in the in-app Documentation viewer (Power BI migration section).
- Roadmap doc: add 'resolve the upstream model' as the preferred fix (Kasal already
  extracts by dataset_id; automate thin-report detection + follow the upstream link);
  fact_source_map is the fallback-of-the-fallback for unreachable models.

Clearly flags the best-effort limits: tables not mapped are skipped, selector/
measure-on-measure DAX may not convert, every best-effort measure is TODO: verify —
tables/measures may be missing.
When the normal path produces 0 views (thin report — no M-Query source tables),
allow drafting thin metric views IF the user opts in (allow_best_effort) AND supplies
physical sources (fact_source_map: pbi_table -> catalog.schema.table). This is the
fallback-of-the-fallback; the preferred fix remains converting the upstream semantic
model (see thin-report-and-source-resolution.md).

- New config keys allow_best_effort + fact_source_map (forwarded generically via
  tool_configs; no tool_factory allowlist change needed).
- _build_best_effort_views: emits one view per supplied source from measures that
  ALREADY resolved to real aggregatable SQL in config.measure_resolutions (reusing
  base_expr + base_filters). NEVER emits TODO/selector/empty resolutions, so the
  'no silently-wrong SQL' contract holds. Every measure flagged 'TODO: verify'.
- Returns a best_effort_report making the GAP explicit (skipped tables/measures,
  'may be MISSING' warning) for the UI to surface.
- Gated: fires only when yaml_output is empty AND both keys present. Default path
  unchanged.
- 9 unit tests (gate, never-emit-unresolved, filters, missing-table reporting);
  existing 28 generator tests still green.

Co-authored-by: Isaac
…ention)

public/docs/*.md are tracked in this repo (not gitignored build artifacts), so the
in-app Documentation viewer serves them without a build step. Add the committed copy
of thin-report-and-source-resolution.md alongside the other powerbi docs so the
sidebar entry loads.

Co-authored-by: Isaac
A thin-report run used to yield a silent fallback JSON with no explanation. Add
_diagnose_zero_views: on any empty run, classify WHY and tell the user what to do,
from two deterministic signals (source tables present? measures resolvable?):
- thin_report_no_source_tables + resolvable>0 → 'point at the upstream model, or
  enable allow_best_effort + fact_source_map (tables/measures may be missing)'
- thin_report + resolvable==0 (the DCC signature) → 'these measures cannot be
  converted; re-run against the upstream model if one exists'
- sources_present_no_views → 'translation issue, no source mapping needed'

Surfaced as output.zero_view_diagnosis (+ a warning log). Turns the silent empty
result into an actionable next step pointing at the real fix (upstream model). 4
new tests; 13 total in the file, all green.

Co-authored-by: Isaac
Concrete code-change plan (companion to the HTTP-header serving-cost path in the
kasal_consumption_tracking repo): stamp custom_tags={'app':'kasal'} at resource
CREATE time so system.billing.usage can attribute cost. Covers the resources Kasal
provisions — Lakebase instance (lakebase_service.create_instance ~L359), VS endpoint
(databricks_vector_endpoint_repository.create_endpoint payload), the App (budget
policy / app.yaml). Exact sites + SDK-field caveats + verification query + rollout
order. Honestly scoped: shared serving endpoints and connect-to-existing resources
are NOT tag-attributable (the header token->$ path handles serving; volume is the
ceiling for the rest).

Co-authored-by: Isaac
@CLAassistant

CLAassistant commented Aug 3, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
3 out of 5 committers have signed the CLA.

✅ MrBlack1995
✅ mckohnen
✅ david-schwarz-db
❌ nehmetohme
❌ mckohnen-cchbc
You have signed the CLA already but the status is still pending? Let us recheck it.

Flow runs logged "InvalidRequestError: This session is provisioning a new
connection; concurrent operations are not permitted" from
execution_name_service, which then fell back to a timestamp run name
("Execution-20260805_141402") instead of the LLM-generated one.

Root cause: request_scoped_session() intentionally REUSES the request-scoped
session when the `_request_session` ContextVar is set. Name generation runs on
a task that inherited the HTTP request's context, so every nested "open my own
session" call — notably the model-config read in
LLMManager.configure_crewai_llm — got that shared session and ran concurrently
with the in-flight request. AsyncSession is not concurrency-safe.

Fixes:
- generate_execution_name: in standalone mode (session=None) call
  detach_request_session() first, so nested reads open private sessions. This
  is the documented remedy (src/db/session.py) and was previously only used by
  crew_generation_service.
- _get_name_template / _log_llm_interaction: use async_session_factory (a
  private session) rather than request_scoped_session, so the naming step can
  never join — and so never contend with — the request transaction.
- crewai_flow_service: build ExecutionNameService with session=None instead of
  self.session (the request session).

Two existing tests caught real bugs in the first cut (unsafe _session access;
patch targets that no longer matched) — code fixed, patches updated to the
symbols the code now calls. Added regression tests asserting standalone mode
detaches and injected-session mode does NOT (detaching there would break the
caller's transaction).

Verified: 11/11 name-service, 390 service, 2396 engine/flow/UCMV tests pass;
a real flow run now gets its LLM-generated name with no traceback.

Co-authored-by: Isaac
@MrBlack1995
MrBlack1995 marked this pull request as ready for review August 5, 2026 13:08
MrBlack1995 and others added 9 commits August 6, 2026 07:46
… it declined

Two related defects, both surfaced by running the re-evaluation sweep against
real CCHBC data (104 recorded non-transpiled measures).

1. Stale skip_reason (dax_llm_fallback.py) — a CORRECTNESS bug in what we show
   customers. The regex fast path sets
   "routed to LLM (fast-path dropped a DAX component)" to mean "hand this to the
   LLM". When the LLM subsequently ALSO declined, the code recorded dax_class but
   left that routing text in place, so an INTERMEDIATE state was reported as the
   final one: the "Not transpiled" panel and the re-evaluation report both implied
   the LLM never got a turn, for measures it had already rejected. Now the decline
   path writes a terminal verdict naming the decider and the reason, e.g.
   "LLM declined [unsupported] — no UC metric-view equivalent". Fixed on both the
   live and the cached-decline path (the cache had the same gap).

2. dax_class gate (reevaluate.py) — the sweep retried measures the LLM had
   already classified as untranslatable, so it looked busy while proposing
   nothing. dax_class in {unsupported, display_layer, out_of_scope,
   architecture_change} now means "already declined at this capability level" and
   is skipped by default (opt-in via include_impossible). Checked BEFORE the
   free-text markers because the LLM's own verdict is the more reliable signal.

Effect on the real Aug-5 run: 73 -> 5 retried per dataset (321 -> 15 overall).
The 5 survivors are the genuinely actionable ones — blocked by the silent-wrong
guard, not by LLM refusal (unresolved measure ref; additive term dropped; ratio
denominator dropped), two of them classed composed / translatable_direct, i.e.
recoverable once composition/guard improves.

Tests: +7 re-evaluation (LLM-declined skipped, opt-in override, guard-blocked
still retried), +3 for the terminal-reason helper. 27 re-evaluation, 28
LLM-fallback, 995 metric_view_utils all pass. The 3 failures in the wider tools
suite (llm_model_default) pre-exist on HEAD and are unrelated.

Co-authored-by: Isaac
Direct-embedded-SQL resolution missed reference-following (staging
query passthrough) and parameter-driven Value.NativeQuery (string-
concatenation) cases, so DCC/PAAT-scale reports with parameterized
M queries silently lost source SQL. Adds mquery_let_evaluator.py to
evaluate M `let` blocks narrowly enough to resolve both patterns.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Maximilian Kohnen <57352546+mckohnen@users.noreply.github.com>
Crew 1 (Pipeline Config Generator) and Crew 2 (UC Metric View
Generator) run in separate OS subprocesses, so a /tmp handoff never
worked (confirmed empirically). Crew 2's own independent extraction
was structurally weaker (no reference-following / parameter-
substitution resolution). Persists Crew 1's admin_tables + expressions
to the powerbi_extraction table, keyed by job_id, and has Crew 2 look
it up and rebuild mquery_json/measures_json via the same static
methods Crew 1 used — so quality matches instead of degrading.

Also:
- Tier-gates Crew 1's fallback extraction (Fabric TMDL, SP-retry Admin
  Scanner) on EITHER admin_tables OR expressions being empty, each
  tier only filling what's still missing.
- Adds admin_client_id/admin_client_secret to Crew 2's schema/UI as an
  optional SP-retry fallback tier.
- Adds per-table wall-clock logging through the fact-table processing
  loop (pipeline.py) and the DAX-vs-SQL validation loop
  (uc_metric_view_generator_tool.py) — needed to diagnose a large-
  report timeout (see the flow-timeout/logging-visibility commit).
- Adds _diagnostics to the tool's output (db_fallback_fired_for,
  preinject_*_json_chars, etc.) so the handoff is inspectable from
  execution_trace without re-deriving it from logs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Maximilian Kohnen <57352546+mckohnen@users.noreply.github.com>
run_schema_self_heal shared one connection/transaction across all
healer steps. On Postgres, one failed statement aborts the entire
transaction, so every step after the first failure silently no-opped
(observed in production against Lakebase). A per-step SAVEPOINT
(conn.begin_nested()) didn't fully fix it either: healers already
swallow their own errors internally, so the savepoint wrapper never
saw an exception to react to — it only discovered the poisoned
transaction when it tried to auto-commit, and by then a plain
ROLLBACK TO SAVEPOINT didn't reliably leave the connection usable for
the next step's own SAVEPOINT statement (confirmed in production:
every later step failed too, each at its own SAVEPOINT line).

Switches run_schema_self_heal to take an AsyncEngine and open a fresh
connection + transaction per step, so there's no shared transaction
state left for one step's failure to poison. Updated all 4 call
sites (init_db, main.py's Lakebase hot-swap, and both
lakebase_service.py connect/expand-schema paths) accordingly, and
fixed the powerbi_extraction.expressions column healer's is_sqlite
detection (was checking the app's global DATABASE_URI instead of the
actual connection's dialect) along with 6 other _ensure_* functions
that had the same bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Maximilian Kohnen <57352546+mckohnen@users.noreply.github.com>
completion_with_usage calls litellm.completion() directly and only
copied api_key/api_base off the configured LLM object, dropping the
297s/300s timeout that configure_crewai_llm already computes and that
completion()'s CrewAI LLM.call() path already respects. Callers on
this path (e.g. dax_llm_fallback.py's per-measure DAX translation)
could hang indefinitely on one stuck/throttled call — confirmed in
production as a 50+ minute silent stall with zero further LLM spans,
on a report whose DAX-LLM fallback was hitting sustained
REQUEST_LIMIT_EXCEEDED throttling from the Databricks serving
endpoint. Forwarding "timeout" alongside api_key/api_base closes the
gap; a throttled call now fails within the existing bound instead of
hanging, and dax_llm_fallback.py's existing fail-open fallback path
takes over immediately.

Also bumps the flow's per-crew kickoff timeout from 20 to 55 minutes
(flow_methods.py) — MetricViewPipeline processes fact tables
sequentially, and a report with many of them can legitimately exceed
20 minutes even with calls now bounded. Leaves a 5 min margin under
process_flow_executor's outer 1-hour subprocess watchdog.

Adds the UCMV custom-tool packages (uc_metric_view_generator_tool,
pipeline_config_generator_tool, metric_view_utils,
metric_view_validation_utils) to the subprocess logging allowlist
(logging_config.py). Their loggers were never routed to flow.log/
execution_logs, so none of their output — including the per-table
timing added in the DB-handoff commit — was ever visible; this is
what let the throttling and the subsequent hang actually be diagnosed
from execution_logs instead of guesswork.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Maximilian Kohnen <57352546+mckohnen@users.noreply.github.com>
Merges the admin_client_id/admin_client_secret keys from this branch
with allow_best_effort/fact_source_map from the colleague's thin-
report best-effort work — both are additive, opt-in config fields on
the same tuple.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Maximilian Kohnen <57352546+mckohnen@users.noreply.github.com>
@MrBlack1995
MrBlack1995 force-pushed the feat/pbi-mquery-performance branch from 6870de1 to 220785c Compare August 17, 2026 07:24
@MrBlack1995
MrBlack1995 changed the base branch from prompt-optimization to pbi-optimization August 18, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants