Skip to content

fix(tools): rewrite the semantic layer description around what it can do - #109

Closed
josep-reyero wants to merge 3 commits into
mainfrom
fix/semantic-layer-tool-description
Closed

fix(tools): rewrite the semantic layer description around what it can do#109
josep-reyero wants to merge 3 commits into
mainfrom
fix/semantic-layer-tool-description

Conversation

@josep-reyero

Copy link
Copy Markdown
Contributor

Supersedes #108, which was branched as fix/regional-dimension-names before the scope grew well past that. Includes both commits.

The problem

query_lfx_semantic_layer's description was 3,662 characters. Descriptions are cut at 2,048 before the model ever sees them, so 44% was invisible — the entity-prefix rule, the country/region routing tip and the tlf membership caveat all sat past the cut.

The visible half told the model that memberships were an exception without ever saying what to do instead. That is why regional questions kept landing on query_lfx_lens.

What the tool actually does

Rewriting it meant first establishing what is on offer. Probing the live Semantic Layer showed the old framing understated it in two ways that mattered:

  • Not "pre-aggregated metrics". MetricFlow compiles SQL per request. Nothing is stored.
  • Not only numbers. Grouping by a name dimension returns a ranked list of named organizations or people — which is exactly the shape of the stakeholder question this work exists to answer. An earlier draft framed the tool as "numbers, not records" and would have routed that question away from the tool built for it.

Also confirmed against the live API: cross-domain metrics combine in one query (joined on shared entities, no join syntax written by the caller), every metric carries metric_time at five grains, and 33 metrics ship built-in filters (current_* is active-only, total_contributors excludes bots) that a caller must not re-apply.

What changed

Description now names the six covered domains, states that country/region questions always belong here, and describes ranking, trending, multi-dimension breakdown and cross-domain combination. 2,021 bytes.

Syntax moved onto the parameter it governswhere carries the MetricFlow forms, group_by the entity__field rule and grain suffixes, metrics the outer-join semantics. Each jsonschema description is a separate field with its own budget, and the model reads it at the moment it fills that field. That redistribution is what made room; the description had been rationing characters against a limit the parameters do not share.

describehelp, since it is now a fallback for a failed query rather than a prerequisite. The old name still dispatches so a cached schema does not break.

Verification

Run end to end against the live stack, not by compiling SQL alone. Both motivating questions return real data:

Region Membership revenue Contributing orgs Contributors
North America $110,423,700 30,358 41,782
Europe $24,451,350 28,330 30,079

Ranked named lists work (top contributing orgs in Asia Pacific: FOSSASIA, National University of Singapore, SK Group, Canva).

Running it caught a bug the SQL check could not. The ranked-list example filtered lf_region on 'Asia' — which compiles fine and matches nothing. The value is 'Asia Pacific'. compileSql validates dimension names, never values. Fixed, and the region values are now enumerated in the example.

Tests

Budget is guarded in bytes, not characters — em-dashes cost three bytes each and the description ran ~30 bytes over its character count. Coverage extends to every parameter and to query_lfx_lens, which has far less headroom and is the likelier of the two to drift past the cut unnoticed.

TestSemanticLayerDescription also asserts the absence of pre-aggregated and returns numbers, not records, so neither framing comes back.

Depends on

Two lines lean on linuxfoundation/lfx-lens#27 (ranked suggestions on an unknown metric name). If that does not merge, those claims must be cut.

🤖 Generated with Claude Code

josep-reyero and others added 2 commits July 28, 2026 19:11
…s dimension

Verified against the dbt Semantic Layer API directly: searching
list_metrics for "country" or "region" returns zero metrics, because
those are dimensions. That is what stranded a real client session — it
searched metrics for a dimension concept, got an empty array with no
explanation, and abandoned the tool.

Also corrects a name this guidance had wrong: the organization HQ
dimension is entity-prefixed (activity_project_id__organization_lf_region),
not a bare organization_lf_region, so the previous wording would have
produced an invalid dimension.

Spell out what metrics and dimensions each are, and that list_metrics
searches metric names and descriptions only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
The description was 3,662 characters. Descriptions are cut at 2,048 before
the model sees them, so 44% of it was invisible — including the entity-prefix
rule, the country/region routing tip and the tlf membership caveat. The
visible half told the model memberships were an exception without ever saying
what to do instead, which is why regional questions kept landing on
query_lfx_lens.

Rewriting it meant first establishing what the tool actually offers. Probing
the live Semantic Layer showed the old framing understated it badly: it is not
"pre-aggregated metrics" (MetricFlow compiles SQL per request), and it does not
only return numbers — grouping by a name dimension returns a ranked list of
named organizations or people, which is exactly the shape of the stakeholder
question this work exists to answer.

So the description now names the six domains it covers, states that
country/region questions always belong here, and describes ranking, trending,
multi-dimension breakdown and cross-domain combination. Syntax moved onto the
parameter it governs — where carries the MetricFlow forms, group_by the
entity__field rule and grain suffixes — because each jsonschema description is
a separate field with its own budget, and the model reads it at the moment it
fills that field. That redistribution is what made room; the description had
been rationing characters against a limit the parameters do not share.

describe is renamed help, since it is now a fallback for a failed query rather
than a prerequisite. The old name still dispatches so a cached schema does not
break.

Verified end to end against the live stack rather than by compiling SQL alone:
both motivating questions return real data, and the ranked-list example was
wrong — it filtered lf_region on 'Asia', which compiles but matches nothing.
The value is 'Asia Pacific'. compileSql validates dimension names, never
values. The example is fixed and the region values are enumerated.

Tests now guard the budget in bytes rather than characters: em-dashes cost
three bytes each and the description ran ~30 bytes over its character count.
Coverage extends to every parameter and to query_lfx_lens, which has far less
headroom and is the likelier of the two to drift past the cut unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Copilot AI review requested due to automatic review settings July 29, 2026 09:05
@josep-reyero josep-reyero added the ai-assisted A task or activity that was supported by AI, such as CoPilot, ChatGPT, or other AI technology. label Jul 29, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Reworks semantic-layer guidance to fit schema limits and improve query routing.

Changes:

  • Rewrites tool and parameter descriptions.
  • Renames describe to help, retaining compatibility.
  • Adds schema-budget and guidance tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
internal/tools/lens.go Updates semantic-layer guidance and help handling.
internal/tools/lens_test.go Adds budget and schema regression coverage.

Comment thread internal/tools/lens.go Outdated
Metrics string `json:"metrics,omitempty" jsonschema:"Comma-separated metric names from list_metrics (required for get_dimensions and query). List several to combine them in one result: metrics from different domains are outer-joined on the dimensions they share, so a group present in only one domain still appears, with NULL for the other metric. You can only group such a query by dimensions the metrics have in common — get_dimensions with several metrics returns exactly that set. Many metrics are already filtered — current_* means active-only, total_contributors excludes bots — so do not repeat those conditions in where."`
Search string `json:"search,omitempty" jsonschema:"Filters results by name and description. For list_metrics use a topic word from COVERS ('contributor', 'membership', 'event', 'enrollment', 'maintainer', 'health'). For get_dimensions use the slice you are after, e.g. 'region', 'country', 'tier', 'name'."`
GroupBy string `json:"group_by,omitempty" jsonschema:"Comma-separated dimension qualified_names, copied verbatim from list_metrics or get_dimensions — they are entity__field and the entity prefix differs per metric, so never assemble one by hand. Group by a name dimension to turn a metric into a ranked list of organizations, people or projects. For a trend add metric_time__year, or __quarter, __month, __week, __day. The entities listed alongside a metric are join keys, not group-by values — grouping by one returns raw IDs, so use the matching name dimension instead."`
Where string `json:"where,omitempty" jsonschema:"MetricFlow filter expression; this clause does the actual data filtering. Categorical: {{ Dimension('country__lf_region') }} = 'Europe'. Time: {{ TimeDimension('asset_id__install_date', 'DAY') }} >= '2024-01-01'. Dates are yyyy-mm-dd. Use the qualified_name exactly as returned by list_metrics or get_dimensions. If you passed project_slug, include a project filter here too — find the matching project_slug or project_name dimension in the dimensions list."`
Comment thread internal/tools/lens.go Outdated
Where string `json:"where,omitempty" jsonschema:"Optional for query action. MetricFlow filter using {{ Dimension('qualified_name') }} = 'value' syntax. Include a project scope filter to scope results (find the correct project_slug or project_name dimension from list_metrics); may be omitted for global or cross-foundation queries. Example: {{ Dimension('registration_id__project_slug') }} = 'cncf'"`
OrderBy string `json:"order_by,omitempty" jsonschema:"Comma-separated sort fields, prefix with - for descending (for query)"`
Limit int `json:"limit,omitempty" jsonschema:"Max rows to return, max 500 (for query)"`
ProjectSlug string `json:"project_slug,omitempty" jsonschema:"Optional project slug from search_projects (e.g. 'cncf'). Omit it for global or cross-foundation questions — the normal case for country and region questions. When provided, the where clause must also carry a project filter and every project reference is validated against that foundation's subtree."`
Testing the description against a live client turned up a constraint we had
been designing around blind. Clients that defer tool schemas behind a search
index — Claude Desktop does — re-serialise the schema and replace OPTIONAL
parameter descriptions with a short generated summary. The 459-byte where
description arrived as "Filter conditions." and order_by as "Sort order.".
Temporarily marking limit required made its real text appear, which is what
pinned the cause down.

That invalidated the previous commit's main structural move. Syntax had been
pushed onto the parameter it governs precisely because each jsonschema
description is a separate field with its own budget — true on the wire, but
those fields are the ones that get summarised away. Roughly 3,300 bytes of
guidance was reaching this client as a paraphrase.

Cramming it all back into one description does not fit, and cramming it into
the action parameter would have been a workaround rather than a fix. Splitting
is the fix: discovery and querying become two tools, so each gets its own 2048
budget, the MetricFlow syntax lives in a tool description rather than on an
optional parameter, and metrics becomes genuinely required on the query tool
instead of optional — so its multi-metric join rules survive on their own
merits.

explore_lfx_semantic_layer carries discovery, the covered domains and the
routing boundary with query_lfx_lens. query_lfx_semantic_layer carries the
query and states its own where/order_by/limit syntax, so a caller never has to
call help first. The name and the gate are unchanged, so existing clients keep
working; a caller on a cached schema that still sends action=query is told
where querying moved rather than getting a bare unknown-action error.

TestCriticalGuidanceSurvivesSchemaCompaction is the guard: it walks both tool
descriptions plus their required parameters and fails if a token the model
cannot guess — the filter syntax, the date format, the limit ceiling, the
trend grain — is reachable only through an optional parameter. The optional
descriptions stay full and accurate for clients that pass them through; they
just are not the only copy.

Verified against the live stack: both motivating regional questions return the
same data through the new tool, discovery returns metrics with dimensions
inline, and the action=query redirect fires.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Copilot AI review requested due to automatic review settings July 29, 2026 10:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

internal/tools/lens.go:398

  • This alias does not preserve compatibility for the cached schema that existed before this PR. That schema only knows query_lfx_semantic_layer, so an old action=describe request is sent to handleQuerySemanticLayer, never to this new explore handler, and fails the new required metrics schema/validation. The same applies to old list_metrics and get_dimensions calls. Preserve legacy action dispatch on the old tool name (or explicitly treat the split as breaking) rather than claiming the cached schema remains usable.
	// "describe" is the pre-rename name for help, kept so a caller working
	// from a cached schema does not get an Unknown action error.
	case "help", "describe":
		return handleLensHelp(args.Target)

cmd/lfx-mcp-server/main.go:804

  • The new tool cannot actually be enabled by its advertised configuration name: with LFXMCP_TOOLS=explore_lfx_semantic_layer, this condition is false and neither semantic-layer tool is registered. Conversely, selecting only query_lfx_semantic_layer exposes both tools. This breaks the established per-tool -tools/LFXMCP_TOOLS behavior used by every registration above. Please either register the explore and query tools independently, or explicitly treat either configured name as enabling the pair.
	if enabledTools["query_lfx_semantic_layer"] && canRead && isStaff {
		tools.RegisterSemanticLayer(server)

@josep-reyero

Copy link
Copy Markdown
Contributor Author

Closing for now — the scope has outgrown this PR. Live testing surfaced two further problems (unknown filter literals silently returning zero rows, and no way to discover dimension values), so the work is being consolidated into a single change that covers description truncation, caller guidance, and the new discovery capability. A replacement PR will follow from a fresh branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted A task or activity that was supported by AI, such as CoPilot, ChatGPT, or other AI technology.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants