Prima Get an API key

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
  • alchemy_labs/backend/api/routers/prima_design_router.py
  • alchemy_labs/backend/services/prima/design_service.py
  • alchemy_labs/backend/services/design/template_service.py
  • alchemy_labs/backend/schemas/design_schemas.py
  • alchemy_labs/backend/services/prima/router.py
  • alchemy_labs/backend/models/design_models.py
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

RouteAuthTenancyCodesNotes
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 workspace200 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)samesame, same placeholderAlways 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)samesame placeholder200 List[PrimaDesignTemplateInfo]Query param intent; delegates straight to template_service.get_templates_for_prima
GET /api/prima/design/quick-actions (196-263)samenone (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:

FunctionRaisesReturns
PrimaDesignService.chat(message, current_canvas, brand_assets, templates, conversation_history, intent) (470-607)ValueError only, on a missing/misbehaving Mistral credential or providerTuple[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:

  1. Read settings.MISTRAL_API_KEY (unwrapping SecretStr if present); if falsy, raise ValueError naming MISTRAL_API_KEY explicitly in the German error string (494-501); this check does not reflect whether Infomaniak (the actual default transport) is configured, see Stack assumptions.
  2. Build three context strings from what the caller passed in, never from a fresh DB read inside this function: _build_brand_context (319-342, turns brand_assets["colors"]["primary"|"secondary"|"accent"] and typography["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)" if current_canvas has no objects, else one line per object, text objects truncated to 50 chars).
  3. Format DESIGN_SYSTEM_PROMPT (86-196) with those three strings plus the literal DESIGN_OPERATIONS_DSL block (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).
  4. If intent was given, prefix it onto the user's message as "[Intent: {intent}] {message}" (517-518); this is the only place intent affects the AI call; it is never validated against a known set here (contrast GET /quick-actions's hardcoded list, which the request's intent need not match).
  5. Build a PrimaRequest(message=..., conversation_history=..., stream=False, temperature=0.4, max_tokens=2048) (521-527) and call self.router.route_request(request, system_prompt) with no tools argument, so the router's plain-completion path runs, never its Mistral function-calling path (530-535).
  6. On success, call self._parse_ai_response(response.content) (548). This tries, in order: a fenced json ... 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 own try/except json.JSONDecodeError; if all three fail, return None.
  7. If parsing failed entirely, return a fallback DesignResponse with operations=[], a fixed apology response string, no thinking/template_used, two hardcoded suggestions, 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_operations finds.
  8. Otherwise extract operations, response, thinking, template_used, suggestions from the parsed dict (562-566); if template_used was not given directly, backfill it from the first select_template operation that carries a template_id (569-573).
  9. Convert each raw operation dict into a DesignOperation object with no type coercion or required-field enforcement beyond .get(..., default) (576-594); an operation missing op becomes DesignOperation(op=""), which reaches _validate_operations and is flagged as "Unbekannte Operation ''" (445-446), but still round-trips to the response.
  10. 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_response never raises out of chat(): every extraction stage is caught individually, and a total failure degrades to a fixed fallback response plus one warning, never an unhandled json.JSONDecodeError reaching the router (388-412, 550-559; no test in this reference set exercises this path, see Tests).
  • _validate_operations is warnings-only: it can never shrink, reorder, or alter operations, the exact list built in step 9 above is what the router serializes back (design_service.py 414-468 returns only a List[str]; prima_design_router.py 71-89 constructs DesignOperations straight from response.operations, not from anything warnings-filtered).
  • An element_names check with no elements on the canvas is vacuously permissive: 459 gates the "element not found" warning on element_names being non-empty, so set_text against 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 /chat is the transport shape (SSE framing) and that its own exceptions become a 200-status error event instead of a 4xx/5xx (prima_design_router.py 143-149 vs 91-101); the underlying AI call, parsing, and validation are byte-for-byte the same call to PrimaDesignService.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.py 270-276): _get_user_workspace is a hardcoded uuid4() placeholder with an explicit # TODO above 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, and get_templates_for_prima always returns an empty list in real use. select_template operations the AI proposes will always fail the template_ids check 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 reason maturity: broken above, 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.py 494-501 vs core/config.py 675/689 and services/mistral/client.py 246-269): chat()'s own precondition check reads settings.MISTRAL_API_KEY, described elsewhere in the same config file as the legacy direct-Mistral credential, while the actual transport defaults to Infomaniak via PRIMA_LLM_PROVIDER and resolves a different key (infomaniak_api_key()). A deployment with Infomaniak correctly configured and MISTRAL_API_KEY unset fails this check with a 400 naming the wrong variable; a deployment with only MISTRAL_API_KEY set but PRIMA_LLM_PROVIDER left at its infomaniak default 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.ts 104-120 vs design_service.py 35-36): the apply_brand operation is documented to the AI as applying brand colors, fonts, and logo, but the one frontend implementation of it only sets fontFamily on 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_operations port 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_request 417-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_templates table 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.

  1. grep -n "def _get_user_workspace" -A 6 api/routers/prima_design_router.py shows the function body containing uuid4() and no query against a workspace-membership table, confirming the placeholder.
  2. grep -c "_get_user_workspace(current_user)" api/routers/prima_design_router.py prints 3 (chat, stream, templates each call it independently).
  3. grep -n "MISTRAL_API_KEY" services/prima/design_service.py prints one line inside chat() (494), and grep -n "PRIMA_LLM_PROVIDER" services/prima/design_service.py prints nothing; confirms chat() never reads the variable that actually selects the transport.
  4. grep -c "^def test_\|^ def test_" -r ../backend/tests 2>/dev/null | grep -i design combined with grep -rln "PrimaDesignService\|prima_design_router" backend/tests/ prints no files, confirming zero dedicated test coverage.
  5. 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 sync router property's nested lazy-load has no leading underscore), matching the six private helpers cited under Application logic.
  6. grep -n "yield f\"data: {json.dumps(chunk)}" -A 1 api/routers/prima_design_router.py and grep -n "yield {" -A 4 services/prima/design_service.py together show the stream produces exactly one "complete"-typed chunk before [DONE], confirming /stream is 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.