feat: Derive structural tags for profile catalogue filtering#402
feat: Derive structural tags for profile catalogue filtering#402hessius merged 5 commits intoversion/2.4.0from
Conversation
Add deriveStructuralTags() to profileAnalysis.ts that maps profile fingerprints to PRESET_TAG labels (technique, temperature range). Backend: Add derived_tags to /api/machine/profiles response using _extract_fingerprint() on profile objects. Handles happy path, partial fallback, and offline/history fallback with dict-to-namespace conversion. DirectModeInterceptor: Derive tags in _processProfileList() before stripping stage data. ProfileCatalogueView: Merge derived_tags with user_preferences tags for both filtering and display. All profiles now show technique and temperature tags even without AI-generated descriptions. Includes 14 new tests for deriveStructuralTags() and temperatureRange(), plus 2 backend tests for derived_tags in profile list responses. Closes #398 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds structurally-derived, user-facing tags (technique + temperature range) so the profile catalogue can filter and display useful tags even when AI/user overlay tags are absent.
Changes:
- Introduces
deriveStructuralTags()and centralizestemperatureRange()inprofileAnalysis.ts, with accompanying unit tests. - Extends server
/api/machine/profilesto includederived_tagsacross online, partial, and offline/history response paths. - Updates direct mode interception and the profile catalogue UI to merge
derived_tagswith preference-derived tags for filtering/display.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/web/src/services/interceptor/DirectModeInterceptor.ts | Adds derived_tags when adapting /api/v1/profile/list into /api/machine/profiles in direct mode. |
| apps/web/src/lib/profileRecommendation.ts | Imports shared temperatureRange() from profileAnalysis.ts (removes duplicate implementation). |
| apps/web/src/lib/profileAnalysis.ts | Adds exported temperatureRange() and new deriveStructuralTags() mapping fingerprint technique/temp to PRESET_TAG labels. |
| apps/web/src/lib/profileAnalysis.test.ts | Adds unit tests for temperatureRange() and deriveStructuralTags(). |
| apps/web/src/components/ProfileCatalogueView.tsx | Merges derived_tags with preference tags for available-tags computation, filtering, and badge display. |
| apps/server/api/routes/profiles.py | Computes and returns derived_tags for machine profile list responses (including offline/history fallback). |
| apps/server/test_main.py | Adds/extends tests asserting derived_tags presence and basic expected derived-tag content. |
| try: | ||
| fp = _extract_fingerprint(profile_obj) | ||
| except Exception: | ||
| return [] |
There was a problem hiding this comment.
_derive_structural_tags() swallows all exceptions and returns an empty list, which can mask real regressions in _extract_fingerprint (and makes it hard to diagnose why tags disappeared). Consider logging the exception (at least at debug/warning) and/or narrowing the except clause to expected error types.
| it('returns only flat + temperature for empty profile (no stages, no temperature)', () => { | ||
| const profile: AnalyzableProfile = { name: 'Empty', stages: [] } | ||
| const tags = deriveStructuralTags(profile) | ||
| // 0 stages: isFlat stays true (initial value), no temperature | ||
| expect(tags).toEqual(['Flat profile']) |
There was a problem hiding this comment.
The test title says "flat + temperature" but the fixture has no temperature and the assertion expects only ['Flat profile']. Please update the test name to match the scenario to avoid confusion when failures occur.
| in_history: true, | ||
| has_description: !!(p.display?.description || p.display?.shortDescription), | ||
| derived_tags: deriveStructuralTags(p as unknown as AnalyzableProfile), | ||
| })) |
There was a problem hiding this comment.
deriveStructuralTags(p as unknown as AnalyzableProfile) relies on a double-cast that defeats type-checking and can hide shape mismatches from /api/v1/profile/list. Consider changing _processProfileList to accept AnalyzableProfile[] (or making CachedProfile extend/align with AnalyzableProfile) so this call is type-safe, or derive tags only when the required fields (e.g. stages) are present.
| from services.cache_service import _get_cached_image, _set_cached_image | ||
| from services.gemini_service import get_vision_model, PROFILING_KNOWLEDGE | ||
| from services.profile_recommendation_service import recommendation_service | ||
| from services.profile_recommendation_service import recommendation_service, _extract_fingerprint |
There was a problem hiding this comment.
Importing _extract_fingerprint (a private underscored helper) from profile_recommendation_service creates a tight coupling between the API route and an internal implementation detail. Prefer exposing a public helper (e.g. extract_fingerprint) or moving structural-tag derivation behind a public method on recommendation_service so future refactors don’t silently break this endpoint.
- Make extract_fingerprint public (was _extract_fingerprint) to avoid importing a private symbol across modules - Narrow bare except clauses to (AttributeError, TypeError, KeyError) and add debug logging for structural tag derivation failures - Fix misleading test name: 'flat + temperature' → 'flat tag' for the no-temperature test case - Replace unsafe double-cast (as unknown as AnalyzableProfile) with runtime 'stages' field check in DirectModeInterceptor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review findings addressed in fb9003a1. Bare except in 2. Test name mismatch — Renamed from 'flat + temperature' to 'flat tag' to match the actual scenario (no temperature in fixture). 3. Double-cast in DirectModeInterceptor — Replaced 4. Private import — Renamed |
The Meticulous /api/v1/profile/list returns profiles without stages data. Previously, deriveStructuralTags() required stages to produce any tags, so in direct mode all profiles had empty derived_tags and the catalogue filter panel never appeared. Now deriveStructuralTags() gracefully handles missing stages: - stages: undefined → only temperature tags (from profile.temperature) - stages: [] → treated as flat profile (intentional empty) - stages: [...] → full fingerprint analysis (unchanged) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ve detection Add content-based tag derivation from stage dynamics (not names): - Weight range: Ristretto (≤35g), Normale (36-44g), Lungo (45-54g), Allongé (55g+) - Pressure range: Low (≤4), Medium (5-7), Standard (8-9), High (10+) bar - Structural bloom: zero-flow/low-power stages with time exit - Structural pre-infusion: power-type fill or low-pressure/flow first stages - Adaptive: profiles with $variable references in dynamics/limits/exits - Name-based fallback: bloom/pre-infusion from profile name for partial profiles All new tags work in both proxy (Python) and direct (Capacitor) mode. New PRESET_TAGS categories: weight (fuchsia) and pressure (red). 60 TypeScript tests (up from 39), 866 total web tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update existing test expectations for new weight/pressure tags. Add tests for Ristretto weight range, Adaptive detection from $variable references, and structural bloom from zero-flow stages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Expands profile catalogue structural tags with content-based analysis of stage dynamics, weights, and variable references. All tags work in both proxy (Python) and direct (Capacitor) mode.
New Tag Categories
Weight Range (from
final_weight)Pressure Range (from dynamics + limits peak pressure)
Structural Bloom (content-based, not name-based)
Structural Pre-infusion (content-based)
Adaptive/Parametric (from $variable references)
Name-based Fallback (for partial profiles without stages)
Files Changed
profileAnalysis.ts— Expanded fingerprint extraction, addedweightRange(),pressureRange(), structural detectionprofileAnalysis.test.ts— 60 tests (up from 39)tags.ts— Added weight/pressure entries to PRESET_TAGS + color mappingsindex.css—tag-weight(fuchsia) andtag-pressure(red) CSS classesprofileRecommendation.ts— Updated fallback fingerprint for new fieldsprofiles.py— Python backend parity for all new tag typestest_main.py— 3 new Python tests for weight, adaptive, bloom tagsTests
Closes #398