Module kit / Media, audio and design
Prima AI Design Chat and Operations DSL
media-audio-design/prima-ai-design-chat-and-operations-dsl
| Kind | backend |
|---|---|
| Domain | media-audio-design |
| Maturity | broken |
| Reusable | extract |
| Stack | fastapi-0.118, starlette-0.48, sqlalchemy-2.0-sync, postgres, pydantic-2.12, mistral-api-http |
| Depends on | _shared/fastapi-project-layout, _shared/auth-dependency-pattern, _shared/workspace-scoping |
| Frontends | media-audio-design/ui/picasso-design-gallery |
| Reference implementation |
|
| Measured on | alchemy_labs main 62ba4fc41009b454e1c033e5036c7e8577db2061 2026-09-12 |
| Version | 1 |
Purpose
A user types a natural-language design request ("make an Instagram post for our summer sale") into the Picasso editor's chat panel; this recipe turns that message, plus the current Fabric.js canvas JSON, the user's resolved brand colors/fonts/logo, and the workspace's available templates, into a validated list of structured canvas operations (select_template, set_text, set_color, move_element, add_element, etc.) that the frontend replays against the canvas (design_service.py 1-7, DESIGN_OPERATIONS_DSL 26-82). The property meant to make this trustworthy is that a malformed or hallucinated operation never crashes the request: _parse_ai_response degrades through three extraction strategies before giving up (design_service.py 388-412), and _validate_operations only ever appends warnings, it never drops or mutates an operation (414-468), but that same "never block" design means an operation naming a template or element that does not exist is still returned to the frontend and silently executed as a no-op there (useDesignOperations.ts 92-299 checks if (obj)/if (template) before acting, never throws).
When to use
Trigger phrases: "Picasso chat", "Prima design chat", "design operations DSL", "select_template", "apply_brand", "canvas operations", "Fabric.js operations from AI", "why didn't Prima pick a template", "design chat warnings". Read it before touching PrimaDesignService.chat/stream_chat, _parse_ai_response, _validate_operations, DESIGN_OPERATIONS_DSL/DESIGN_SYSTEM_PROMPT, or _get_user_workspace/_get_brand_assets in prima_design_router.py.
Not this recipe: useDesignOperations.ts's actual Fabric.js execution of each op (add_element, duplicate, image swap via FabricImage.fromURL); that is the frontend module's own logic, only described here at the level of "what each op is contracted to do"; DesignTemplate CRUD (create/update/duplicate a template): no router in this reference set exposes that, template_service.py's other methods are out of scope, only get_templates_for_prima (264-311) is used here; Design/DesignMedia persistence (saving a finished design, exporting it): design_service.py never imports the Design model it references in a comment; the brand-essence data model itself (BrandEssence.to_dict()), only _get_brand_assets's reshaping of it (prima_design_router.py 279-348) is in scope; the Mistral/Infomaniak transport itself (services/mistral/client.py, services/prima/router.py's tool-calling path), only that PrimaDesignService.chat calls router.route_request with tools=None (design_service.py 521-535, so the Mistral-tools function-calling path in router.py 470-536 is never reached from this recipe, only its plain-completion path).
Stack assumptions
At 62ba4fc: fastapi==0.118.2, starlette==0.48.0 (SSE via StreamingResponse), synchronous handlers on sqlalchemy.orm.Session (SQLAlchemy==2.0.43), pydantic==2.12.0 (schemas/design_schemas.py still imports the v1-style validator decorator, not field_validator, only used elsewhere in that file for DesignTemplateBase, not for DesignOperation/PrimaDesignChatRequest). No dedicated Prima design client library; the AI call goes through services/prima/router.get_prima_router() → PrimaRouter.route_request → the single registered provider, MistralProvider (router.py 75-95, 136-168), which itself defers its transport to services/mistral/client.get_mistral_client(). Foundations assumed: S4 (fastapi-project-layout) for mount_router and the app; S5 (auth-dependency-pattern) for get_current_user (JWT bearer/cookie/API-key priority, dependencies.py 187+); S6 (workspace-scoping) is assumed by the endpoint's shape (every route calls _get_user_workspace) but is not actually honored, see Security & tenancy and Gotchas.
Environment: MISTRAL_API_KEY (core/config.py 675, SecretStr, default None): PrimaDesignService.chat gates on this exact setting before doing anything else (design_service.py 494-501, raising ValueError → 400 if unset), but per core/config.py 675's own description this is "the direct api.mistral.ai, legacy/explicit opt-in only" credential; the transport the request actually uses is chosen by PRIMA_LLM_PROVIDER (core/config.py 689, default "infomaniak"), and MistralClient.__init__ resolves its real API key from infomaniak_api_key() whenever that default holds (services/mistral/client.py 246-269). So the precondition check and the actual transport read two different credentials by default (Gotchas). Fail behaviour: chat() raises ValueError (mapped to HTTP 400 by the router, 91-95) on a missing Mistral credential, on ProviderNotConfiguredError from the router, and on any other exception from router.route_request (design_service.py 537-545); a JSON-parse failure of the AI's own reply does not raise, it returns a fallback DesignResponse with empty operations and one warning string (548-559).
Data model
This recipe reads one table and writes none. design_templates (models/design_models.py 33-92; migration 20260205_add_design_tables.py, revision 20260205_design_tables, down-revision 20260124_li_fat_id):
design_templates (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
name varchar(255) NOT NULL,
description text,
category varchar(100), -- e.g. "instagram", "linkedin", "flyer"
width integer NOT NULL DEFAULT 1080,
height integer NOT NULL DEFAULT 1080,
canvas_data jsonb NOT NULL DEFAULT '{}', -- Fabric.js canvas JSON
thumbnail_url text,
element_mappings jsonb DEFAULT '{}', -- element name -> Fabric object id, e.g. {"headline": "obj_123"}
brand_mappings jsonb DEFAULT '{}', -- e.g. {"background": "primary"}
editable_elements jsonb DEFAULT '[]',
locked_elements jsonb DEFAULT '[]',
is_public boolean DEFAULT true,
is_archived boolean DEFAULT false,
tags text[] DEFAULT '{}',
created_at, updated_at timestamp
);
INDEX idx_design_template_workspace (workspace_id)
INDEX idx_design_template_category (category)
INDEX idx_design_template_public (is_public, is_archived)
What this recipe reads from a row (template_service.get_templates_for_prima, 264-311): id, name, description, category, width, height, tags, and elements, which is not a column, it is list(t.element_mappings.keys()), the names the AI may reference in an operation (300-311). The canvas JSON this recipe reasons over (_build_canvas_context, design_service.py 360-386) is never a design_templates row directly; it is whatever current_canvas the frontend sent in the request body (PrimaDesignChatRequest.current_canvas, design_schemas.py 366), a live Fabric.js {width, height, objects: [...]} shape, each object read for type, name, and (for text) text/fontSize. Nothing is written by this recipe: chat() and stream_chat() are both read-then-respond, no db.add/db.commit appears in design_service.py or in the three router handlers.
API surface
| Route | Auth | Tenancy | Codes | Notes |
|---|---|---|---|---|
POST /api/prima/design/chat (prima_design_router.py 30-101, response_model=PrimaDesignChatResponse) | get_current_user (JWT/cookie/API-key) | _get_user_workspace(current_user), a uuid4() placeholder (270-276), NOT the user's real workspace | 200 PrimaDesignChatResponse; 400 on ValueError (Mistral not configured, provider not configured, AI request failed); 500 with the fixed German string "Ein Fehler ist aufgetreten. Bitte versuche es erneut." on any other exception (96-101) | Body: PrimaDesignChatRequest (message, current_canvas, conversation_history, design_id, template_id, intent, all but message optional, design_schemas.py 363-370) |
POST /api/prima/design/stream (104-159) | same | same, same placeholder | Always 200 with text/event-stream; a ValueError or any other exception inside generate() is caught and turned into an SSE {"type":"error","message":...} event, never a non-200 status (143-149) | Headers Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no (154-158); despite the name, this calls the exact same non-streaming PrimaDesignService.stream_chat, which itself just awaits chat() and yields one {"type":"complete","data":..., "warnings":...} chunk before [DONE] (design_service.py 609-639); there is no token-level streaming anywhere in this path |
GET /api/prima/design/templates (162-193) | same | same placeholder | 200 List[PrimaDesignTemplateInfo] | Query param intent; delegates straight to template_service.get_templates_for_prima |
GET /api/prima/design/quick-actions (196-263) | same | none (no DB access) | 200 List[dict] | Returns a hardcoded list of 8 intents (instagram, instagram-story, linkedin, linkedin-banner, facebook, youtube-thumbnail, flyer, poster) with fixed pixel dimensions, e.g. instagram-story 1080x1920, poster 3508x4960; nothing here is workspace- or DB-derived |
The two entrypoints /chat and /stream actually call, not routes but the real reimplementation surface:
| Function | Raises | Returns |
|---|---|---|
PrimaDesignService.chat(message, current_canvas, brand_assets, templates, conversation_history, intent) (470-607) | ValueError only, on a missing/misbehaving Mistral credential or provider | Tuple[DesignResponse, List[str]], the response object and the validator's warning strings |
PrimaDesignService.stream_chat(...) (609-639) | same, propagated from chat() | an async generator yielding exactly one dict, {"type": "complete", "data": DesignResponse.to_dict(), "warnings": [...]} |
Application logic
PrimaDesignService.chat (470-607), in commit order:
- Read
settings.MISTRAL_API_KEY(unwrappingSecretStrif present); if falsy, raiseValueErrornamingMISTRAL_API_KEYexplicitly in the German error string (494-501); this check does not reflect whether Infomaniak (the actual default transport) is configured, see Stack assumptions. - Build three context strings from what the caller passed in, never from a fresh DB read inside this function:
_build_brand_context(319-342, turnsbrand_assets["colors"]["primary"|"secondary"|"accent"]andtypography["headings"|"body"]into bullet lines,"Keine Marken-Assets verfügbar"if empty);_build_templates_context(344-358, one line per template with its editable element names);_build_canvas_context(360-386,"Leerer Canvas (kein Design geladen)"ifcurrent_canvashas noobjects, else one line per object, text objects truncated to 50 chars). - Format
DESIGN_SYSTEM_PROMPT(86-196) with those three strings plus the literalDESIGN_OPERATIONS_DSLblock (26-82, documents 11 ops:select_template,apply_brand,set_text,set_style,set_color,set_image,move_element,resize_element,add_element,remove_element,duplicate); the prompt also embeds three full worked examples in German and a fixed output-JSON shape (thinking,operations,response,suggestions). - If
intentwas given, prefix it onto the user's message as"[Intent: {intent}] {message}"(517-518); this is the only placeintentaffects the AI call; it is never validated against a known set here (contrastGET /quick-actions's hardcoded list, which the request'sintentneed not match). - Build a
PrimaRequest(message=..., conversation_history=..., stream=False, temperature=0.4, max_tokens=2048)(521-527) and callself.router.route_request(request, system_prompt)with notoolsargument, so the router's plain-completion path runs, never its Mistral function-calling path (530-535). - On success, call
self._parse_ai_response(response.content)(548). This tries, in order: a fencedjson ...code block via regex (_parse_ai_response, 391-396); the entire trimmed content as JSON (398-402); the first{...}span found by a greedy regex (404-410). Each stage is wrapped in its owntry/except json.JSONDecodeError; if all three fail, returnNone. - If parsing failed entirely, return a fallback
DesignResponsewithoperations=[], a fixed apologyresponsestring, nothinking/template_used, two hardcodedsuggestions, and the warning list["AI-Antwort konnte nicht interpretiert werden"](550-559); this is the only path that returns a non-empty warning list unconditionally regardless of what_validate_operationsfinds. - Otherwise extract
operations,response,thinking,template_used,suggestionsfrom the parsed dict (562-566); iftemplate_usedwas not given directly, backfill it from the firstselect_templateoperation that carries atemplate_id(569-573). - Convert each raw operation dict into a
DesignOperationobject with no type coercion or required-field enforcement beyond.get(..., default)(576-594); an operation missingopbecomesDesignOperation(op=""), which reaches_validate_operationsand is flagged as"Unbekannte Operation ''"(445-446), but still round-trips to the response. - Call
_validate_operations(raw_operations, current_canvas, templates or [])(597) and return(DesignResponse(...), warnings).
_validate_operations (414-468): builds element_names as the lowercased name of every object in current_canvas["objects"] (424-428) and template_ids as the set of ids in the templates the caller passed in (not re-queried, 431); for each operation, in order: unknown op (not one of the 11 named in valid_operations, 433-438) → warning, continue (skips the rest of that op's checks); select_template with a non-empty template_id not in template_ids → warning; for set_text/set_color/set_style/move_element/resize_element/remove_element/duplicate/set_image, an element that is non-empty, not literally "background", and (only when element_names is itself non-empty) not found in element_names → warning; add_element with an element_type not in {"text","rect","circle","image","polygon","line"} → warning. Nothing here removes an operation, coerces a value, or aborts the request; this function only ever appends to warnings and returns it (Purpose).
Invariants
_parse_ai_responsenever raises out ofchat(): every extraction stage is caught individually, and a total failure degrades to a fixed fallback response plus one warning, never an unhandledjson.JSONDecodeErrorreaching the router (388-412, 550-559; no test in this reference set exercises this path, see Tests)._validate_operationsis warnings-only: it can never shrink, reorder, or alteroperations, the exact list built in step 9 above is what the router serializes back (design_service.py 414-468 returns only aList[str];prima_design_router.py71-89 constructsDesignOperations straight fromresponse.operations, not from anything warnings-filtered).- An
element_namescheck with no elements on the canvas is vacuously permissive:459gates the "element not found" warning onelement_namesbeing non-empty, soset_textagainst an empty canvas never warns about a missing element, it only warns when the canvas has named objects and this one is not among them. /stream's only real difference from/chatis the transport shape (SSE framing) and that its own exceptions become a 200-status error event instead of a 4xx/5xx (prima_design_router.py143-149 vs 91-101); the underlying AI call, parsing, and validation are byte-for-byte the same call toPrimaDesignService.chat(design_service.py 626-633).- Every route's tenancy input is the same broken call:
_get_user_workspace(current_user)is invoked independently in/chat,/stream, and/templates(lines 45, 119, 173) and returns a new random UUID each time, never the same value twice for the same user, let alone the user's real workspace (Gotchas).
Frontend
The module that renders this:
media-audio-design/ui/picasso-design-gallery the Picasso AI-first design editor: PicassoChat.tsx (chat
panel, calls designChat/streamDesignChat), QuickActions.tsx
(calls getQuickActions), PicassoEditor.tsx (hosts the Fabric.js
canvas), and the useDesignOperations hook
(frontend/src/components/picasso/hooks/useDesignOperations.ts,
320 lines) that is the actual executor for every op this
recipe's DSL documents
frontend/src/services/primaDesignService.ts (233 lines) is the typed client this recipe's contract is measured against: designChat (102-105, plain api.post), streamDesignChat (111-158, a hand-rolled SSE line-reader over fetch with a manual Authorization: Bearer header, distinct from the api axios instance designChat uses), getTemplatesForPrima (167-174), getQuickActions (177-179). useDesignOperations.ts's executeOperation (89-299) switches on all 11 op values and is the ground truth for what each operation actually does to the canvas: set_color/set_style's fill/stroke resolve brand-color keywords ("primary"|"secondary"|"accent"|"neutral") to the first hex in the matching brand array, defaulting to a fixed fallback hex per key if the brand array is empty (28-52); apply_brand (104-120) only sets fontFamily on text objects by name-substring match (.includes('headline')), it never touches color despite DESIGN_OPERATIONS_DSL's doc comment claiming it "wendet automatisch die Markenidentität ... an" (design_service.py 35-36), a documented-vs-implemented mismatch, not something this backend recipe can fix. Minimum admin/debug surface: none exists at this commit; the only visibility into what Prima returned is the logger.warning line per validation warning (prima_design_router.py 66-67) and logger.error on a parse failure (design_service.py 551) or a request failure (543-545), both plain log lines, no dedicated route or table.
Security & tenancy
Auth dependency: get_current_user (api/dependencies.py 187+), JWT bearer/cookie first, then X-API-Key; standard Labs session, nothing design-specific. Tenancy is where this recipe fails: _get_user_workspace (prima_design_router.py 270-276) is a literal placeholder, from uuid import uuid4; return uuid4(), with a # TODO: Get actual workspace from user's workspace memberships comment immediately above it. It ignores the authenticated user argument entirely. Every call to template_service.get_templates_for_prima(workspace_id=<random>, ...) filters DesignTemplate.workspace_id == <random> (template_service.py 282-286), which cannot match any real row, so in practice /chat, /stream, and /templates all reason over an empty template list regardless of what the calling workspace actually has (Gotchas). _get_brand_assets, by contrast, is correctly scoped: it queries BrandEssence.user_id == user.id directly off the authenticated user (prima_design_router.py 286-288), so brand colors/fonts/logo are real per-user data even while templates are not.
Fail direction: the Mistral-credential check fails closed (ValueError → 400) before any AI call is attempted; a parse failure of the AI's own reply fails open into a fixed apology response rather than an error, by design (Purpose); _validate_operations never fails closed at all, it is advisory only, so an operation naming an element or template that doesn't exist still reaches the frontend and is executed as a silent no-op there. Secrets at rest: none held by this recipe, MISTRAL_API_KEY is a SecretStr read once per request and never logged; response.content (the raw AI reply) is logged only as a length (design_service.py 536, len(response.content)), and on a parse failure the full content is logged at error level (551); this is the one place in this recipe a full LLM completion (which may echo back canvas text/brand copy the user supplied) lands in application logs. Rate limits: none specific to this router in this reference set. Attacker-controlled inputs: message, current_canvas (arbitrary JSON, only shallow-read by _build_canvas_context, never executed or evaluated), conversation_history (passed straight into PrimaRequest, forwarded to the Mistral/Infomaniak call), intent (concatenated into the message, unvalidated against the quick-action list), template_id/design_id on the request body (accepted by the schema but not read anywhere in chat/stream_chat/the three router handlers in this reference set).
Tests
No test file in backend/tests/ exercises PrimaDesignService, prima_design_router, _parse_ai_response, _validate_operations, or DESIGN_OPERATIONS_DSL at 62ba4fc; grep -rln "PrimaDesignService\|prima_design_router\|design_chat\|DESIGN_OPERATIONS_DSL" backend/tests/ returns nothing. The one hit for a nearby name, backend/tests/v2/coverage/test_forms_g6_templateservice_crud_design_normalization_service_layer_db_backed_.py, tests services/forms/template_service.TemplateService (form builder templates), a different class in a different module from services/design/template_service.DesignTemplateService used here; it is not a test of this recipe despite the filename's "design_normalization" fragment (that phrase refers to normalizing a form's design/style config, not the Picasso design editor). This recipe is untested: a rebuild has no known-answer vector to match against for _parse_ai_response's three-stage extraction, _validate_operations's warning strings, or the SSE framing of /stream.
Deploy & ops
Config keys: MISTRAL_API_KEY (core/config.py 675, default None, SecretStr); PRIMA_LLM_PROVIDER (689, default "infomaniak", the value that actually governs the transport get_mistral_client() resolves, see Stack assumptions). Migration: 20260205_add_design_tables.py (revision 20260205_design_tables, down-revision 20260124_li_fat_id) creates design_templates along with designs and design_media (not otherwise used by this recipe) in one migration, no later revision touches design_templates' columns in this reference set. No scheduler job, no background worker; every code path here runs inline inside the request. Mount: app_fastapi.py 222 imports prima_design_router, line 975 mounts it with mount_router(app, prima_design_router.router, prefix="/api", tags=["Prima Design"], surfaces=LABS_SURFACES), LABS_SURFACES is frozenset({LABS}) (core/surfaces.py 177), an ordinary authenticated Labs-app surface, not public and not agent/AI-gateway scoped.
Argus-style checks: none specific to this router exist in this reference set (no dedicated health probe for /prima/design/*). Rollback: nothing stateful to unwind, this recipe writes no rows; rolling back the code changes nothing about design_templates itself.
Gotchas
- 2026-09-12 (measured at 62ba4fc,
prima_design_router.py270-276):_get_user_workspaceis a hardcodeduuid4()placeholder with an explicit# TODOabove it, called independently in/chat,/stream, and/templates(lines 45, 119, 173), so each call gets its own fresh random UUID. Every template lookup this endpoint makes is therefore scoped to a workspace that cannot exist, andget_templates_for_primaalways returns an empty list in real use.select_templateoperations the AI proposes will always fail thetemplate_idscheck in_validate_operations(a guaranteed warning), and the AI itself is told"Keine Templates verfügbar"in its own prompt context every time (_build_templates_context, 346-347). This is the central reasonmaturity: brokenabove, not a judgment on the chat/parse/validate logic, which is otherwise self-contained and does not depend on a real workspace. - 2026-09-12 (measured at 62ba4fc,
design_service.py494-501 vscore/config.py675/689 andservices/mistral/client.py246-269):chat()'s own precondition check readssettings.MISTRAL_API_KEY, described elsewhere in the same config file as the legacy direct-Mistral credential, while the actual transport defaults to Infomaniak viaPRIMA_LLM_PROVIDERand resolves a different key (infomaniak_api_key()). A deployment with Infomaniak correctly configured andMISTRAL_API_KEYunset fails this check with a 400 naming the wrong variable; a deployment with onlyMISTRAL_API_KEYset butPRIMA_LLM_PROVIDERleft at itsinfomaniakdefault passes this check while the real call may still fail downstream for an unrelated reason. No code in this reference set reconciles the two. - 2026-09-12 (measured at 62ba4fc,
useDesignOperations.ts104-120 vsdesign_service.py35-36): theapply_brandoperation is documented to the AI as applying brand colors, fonts, and logo, but the one frontend implementation of it only setsfontFamilyon text objects; it never touches fill color or the logo. This backend recipe cannot fix a frontend gap, but the AI is actively told a capability exists that the executor does not honor. - 2026-09-12 (measured at 62ba4fc): no automated test covers any function in this recipe (Tests). A rebuild has no known-answer fixtures to check its
_parse_ai_response/_validate_operationsport against.
Reference implementation
Paths relative to the repo root; all at main 62ba4fc41009b454e1c033e5036c7e8577db2061 unless noted.
alchemy_labs/backend/api/routers/prima_design_router.py(349 lines):design_chat(30-101);design_chat_stream(104-159);get_templates_for_prima(162-193);get_quick_actions(196-263);_get_user_workspace(270-276);_get_brand_assets(279-348).alchemy_labs/backend/services/prima/design_service.py(640 lines):DESIGN_OPERATIONS_DSL(26-82);DESIGN_SYSTEM_PROMPT(86-196);DesignOperation(199-262);DesignResponse(265-289);PrimaDesignService.__init__/router(295-304);_color_list_to_str(306-317);_build_brand_context(319-342);_build_templates_context(344-358);_build_canvas_context(360-386);_parse_ai_response(388-412);_validate_operations(414-468);chat(470-607);stream_chat(609-639).alchemy_labs/backend/services/design/template_service.py:get_templates_for_prima(264-311).alchemy_labs/backend/schemas/design_schemas.py:DesignOperationType(29-41);DesignOperation(48-59);PrimaDesignChatRequest(363-370);PrimaDesignChatResponse(373-380);PrimaDesignTemplateInfo(383-392).alchemy_labs/backend/services/prima/router.py:MistralProvider(75-134,is_configured/chat_completion);PrimaRouter(136-,get_provider/route_request417-536+);get_prima_router(737-742).alchemy_labs/backend/services/mistral/client.py:MistralClient.__init__'s provider resolution (246-269);get_mistral_client(716-727).alchemy_labs/backend/models/design_models.py:DesignTemplate(33-92).alchemy_labs/backend/core/config.py:MISTRAL_API_KEY(675);PRIMA_LLM_PROVIDER(689).alchemy_labs/backend/app_fastapi.py: import (222), mount (975-976).alchemy_labs/backend/core/surfaces.py:LABS_SURFACES(177).alchemy_labs/backend/migrations/versions/20260205_add_design_tables.py:design_templatestable creation.- Frontend (context, not owned by this backend recipe):
alchemy_labs/frontend/src/services/primaDesignService.ts(233 lines);alchemy_labs/frontend/src/components/picasso/hooks/useDesignOperations.ts(320 lines);alchemy_labs/frontend/src/components/picasso/PicassoChat.tsx,PicassoEditor.tsx,QuickActions.tsx.
Deviations of the reference from an ideal implementation, all dated 2026-09-12: _get_user_workspace never resolves a real workspace (Gotchas); the Mistral-configured precondition checks the wrong credential by default; /stream is SSE-shaped but non-streaming under the hood; _validate_operations is advisory only with no enforcement path; error responses use ad hoc HTTPException(detail=<German string>) rather than the shared ErrorResponse/ErrorCode envelope (schemas/error_schemas.py); no test coverage exists for any function in this recipe.
Acceptance checks
Run from alchemy_labs/backend at 62ba4fc. These name what a verifier can check without a live Mistral/Infomaniak credential or a real workspace; none of them exercise a live AI call.
grep -n "def _get_user_workspace" -A 6 api/routers/prima_design_router.pyshows the function body containinguuid4()and no query against a workspace-membership table, confirming the placeholder.grep -c "_get_user_workspace(current_user)" api/routers/prima_design_router.pyprints3(chat, stream, templates each call it independently).grep -n "MISTRAL_API_KEY" services/prima/design_service.pyprints one line insidechat()(494), andgrep -n "PRIMA_LLM_PROVIDER" services/prima/design_service.pyprints nothing; confirmschat()never reads the variable that actually selects the transport.grep -c "^def test_\|^ def test_" -r ../backend/tests 2>/dev/null | grep -i designcombined withgrep -rln "PrimaDesignService\|prima_design_router" backend/tests/prints no files, confirming zero dedicated test coverage.python -c "import ast; m = ast.parse(open('services/prima/design_service.py').read()); print([n.name for n in ast.walk(m) if isinstance(n, ast.FunctionDef) and n.name.startswith('_')])"lists_color_list_to_str, _build_brand_context, _build_templates_context, _build_canvas_context, _parse_ai_response, _validate_operations(plus the syncrouterproperty's nested lazy-load has no leading underscore), matching the six private helpers cited under Application logic.grep -n "yield f\"data: {json.dumps(chunk)}" -A 1 api/routers/prima_design_router.pyandgrep -n "yield {" -A 4 services/prima/design_service.pytogether show the stream produces exactly one"complete"-typed chunk before[DONE], confirming/streamis non-token-streaming.
Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.