Skip to content

Commit 6b9a18f

Browse files
committed
enable image generation in assistant settings
1 parent a60bb02 commit 6b9a18f

File tree

11 files changed

+220
-44
lines changed

11 files changed

+220
-44
lines changed

backend/alembic/versions/20260407_add_spaces_image_generation_models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ def upgrade() -> None:
2525
server_default=sa.text("now()"),
2626
nullable=False,
2727
),
28+
sa.Column(
29+
"updated_at",
30+
sa.DateTime(timezone=True),
31+
server_default=sa.text("now()"),
32+
nullable=False,
33+
),
2834
sa.ForeignKeyConstraint(
2935
["space_id"], ["spaces.id"], ondelete="CASCADE"
3036
),

backend/src/intric/assistants/assistant.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ async def ask(
380380
extended_logging=self.logging_enabled,
381381
model_kwargs=self.completion_model_kwargs,
382382
version=version,
383-
use_image_generation=self.image_generation_enabled,
383+
use_image_generation=self.image_generation_enabled and image_generation_model is not None,
384384
image_generation_model=image_generation_model,
385385
web_search_results=web_search_results,
386386
mcp_servers=[] if self.has_knowledge() else self.mcp_servers,

backend/src/intric/files/file_service.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,31 @@ async def save_file(self, upload_file: UploadFile):
3232
# Token counting will happen when the file is used in an assistant context
3333
return saved_file
3434

35+
@staticmethod
36+
def _detect_image_type(image_data: bytes) -> tuple[str, str]:
37+
"""Detect image format from magic bytes. Returns (mimetype, extension)."""
38+
if image_data[:8] == b"\x89PNG\r\n\x1a\n":
39+
return "image/png", "png"
40+
if image_data[:2] == b"\xff\xd8":
41+
return "image/jpeg", "jpeg"
42+
if image_data[:4] == b"RIFF" and image_data[8:12] == b"WEBP":
43+
return "image/webp", "webp"
44+
if image_data[:6] in (b"GIF87a", b"GIF89a"):
45+
return "image/gif", "gif"
46+
return "image/png", "png"
47+
3548
async def save_image_from_bytes(
3649
self,
3750
image_data: bytes,
38-
name: str = "generated_image.jpeg",
39-
mimetype: str = "image/jpeg",
4051
):
4152
"""Create a file from raw image bytes returned by an AI model.
4253
4354
Uses a separate database session that commits immediately so the file
4455
is visible to other requests (e.g. download) before the calling
4556
streaming transaction completes.
4657
"""
58+
mimetype, ext = self._detect_image_type(image_data)
59+
name = f"generated_image.{ext}"
4760
checksum = hashlib.md5(image_data).hexdigest()
4861
size = len(image_data)
4962

frontend/apps/web/messages/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,11 @@
13531353
"embedding_models": "Embedding models",
13541354
"transcription_models": "Transcription models",
13551355
"image_generation_models": "Image generation",
1356+
"image_generation_models_description": "Choose which image generation models will be available in this space.",
1357+
"image_generation_toggle": "Image generation",
1358+
"image_generation_toggle_description": "Allow this assistant to generate images.",
1359+
"enable_image_generation": "Enable",
1360+
"disable_image_generation": "Disable",
13561361
"edit_security_classification": "Edit security classification",
13571362
"error_changing_model_status": "Error changing status of",
13581363
"error_changing_default_status": "Error changing default status",

frontend/apps/web/messages/sv.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,11 @@
13511351
"embedding_models": "Embedding",
13521352
"transcription_models": "Transkription",
13531353
"image_generation_models": "Bildgenerering",
1354+
"image_generation_models_description": "Välj vilka bildgenereringsmodeller som ska vara tillgängliga i denna yta.",
1355+
"image_generation_toggle": "Bildgenerering",
1356+
"image_generation_toggle_description": "Tillåt denna assistent att generera bilder.",
1357+
"enable_image_generation": "Aktivera",
1358+
"disable_image_generation": "Inaktivera",
13541359
"edit_security_classification": "Redigera säkerhetsklassificering",
13551360
"error_changing_model_status": "Fel vid ändring av status för",
13561361
"error_changing_default_status": "Fel vid ändring av standardstatus",

frontend/apps/web/src/lib/components/AsyncImage.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
{#if url}
2525
<img
2626
src={url}
27+
crossorigin="anonymous"
2728
class="relative m-0 p-0 transition-opacity duration-200"
2829
style="opacity: 0; "
2930
onload={(ev) => {

frontend/apps/web/src/lib/features/assistants/AssistantEditor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function initAssistantEditor(data: {
2020
defaults: {
2121
prompt: { description: "", text: "" },
2222
insight_enabled: false,
23+
image_generation_enabled: false,
2324
mcp_tools: []
2425
},
2526
updateResource: async (resource, changes) => {
@@ -31,6 +32,7 @@ function initAssistantEditor(data: {
3132
name: true,
3233
description: true,
3334
insight_enabled: true,
35+
image_generation_enabled: true,
3436
completion_model: { id: true },
3537
completion_model_kwargs: true,
3638
prompt: { description: true, text: true },

frontend/apps/web/src/routes/(app)/spaces/[spaceId]/assistants/[assistantId]/edit/+page.svelte

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,10 @@
316316
<!-- Knowledge and MCP are mutually exclusive. Only disable knowledge when MCP is active
317317
AND no knowledge exists. If both somehow exist (legacy data), allow editing both
318318
so the user can remove one to resolve the conflict. -->
319-
{@const hasAnyKnowledge = ($update.groups?.length ?? 0) > 0 || ($update.websites?.length ?? 0) > 0 || ($update.integration_knowledge_list?.length ?? 0) > 0}
319+
{@const hasAnyKnowledge =
320+
($update.groups?.length ?? 0) > 0 ||
321+
($update.websites?.length ?? 0) > 0 ||
322+
($update.integration_knowledge_list?.length ?? 0) > 0}
320323
{@const hasAnyMCP = ($update.mcp_servers?.length ?? 0) > 0}
321324
{@const knowledgeDisabledByMCP = hasAnyMCP && !hasAnyKnowledge}
322325
<Settings.Row
@@ -332,11 +335,14 @@
332335
}}
333336
>
334337
{#if knowledgeDisabledByMCP}
335-
<p class="label-warning border-label-default bg-label-dimmer text-label-stronger mb-2 rounded-md border px-2 py-1 text-sm">
336-
<span class="font-bold">{m.warning()}:&nbsp;</span>{m.knowledge_disabled_when_mcp_active()}
338+
<p
339+
class="label-warning border-label-default bg-label-dimmer text-label-stronger mb-2 rounded-md border px-2 py-1 text-sm"
340+
>
341+
<span class="font-bold">{m.warning()}:&nbsp;</span
342+
>{m.knowledge_disabled_when_mcp_active()}
337343
</p>
338344
{/if}
339-
<div class={knowledgeDisabledByMCP ? 'opacity-50 pointer-events-none' : ''}>
345+
<div class={knowledgeDisabledByMCP ? "pointer-events-none opacity-50" : ""}>
340346
<SelectKnowledgeV2
341347
originMode="personal"
342348
bind:selectedWebsites={$update.websites}
@@ -359,11 +365,14 @@
359365
}}
360366
>
361367
{#if knowledgeDisabledByMCP}
362-
<p class="label-warning border-label-default bg-label-dimmer text-label-stronger mb-2 rounded-md border px-2 py-1 text-sm">
363-
<span class="font-bold">{m.warning()}:&nbsp;</span>{m.knowledge_disabled_when_mcp_active()}
368+
<p
369+
class="label-warning border-label-default bg-label-dimmer text-label-stronger mb-2 rounded-md border px-2 py-1 text-sm"
370+
>
371+
<span class="font-bold">{m.warning()}:&nbsp;</span
372+
>{m.knowledge_disabled_when_mcp_active()}
364373
</p>
365374
{/if}
366-
<div class={knowledgeDisabledByMCP ? 'opacity-50 pointer-events-none' : ''}>
375+
<div class={knowledgeDisabledByMCP ? "pointer-events-none opacity-50" : ""}>
367376
<SelectKnowledgeV2
368377
originMode="organization"
369378
bind:selectedWebsites={$update.websites}
@@ -427,28 +436,61 @@
427436

428437
<!-- Same mutual exclusivity logic as above: only disable MCP when knowledge
429438
is active AND no MCP exists. If both exist (legacy data), keep both editable. -->
430-
{@const mcpDisabledByKnowledge = (($update.groups?.length ?? 0) > 0 || ($update.websites?.length ?? 0) > 0 || ($update.integration_knowledge_list?.length ?? 0) > 0) && ($update.mcp_servers?.length ?? 0) === 0}
439+
{@const mcpDisabledByKnowledge =
440+
(($update.groups?.length ?? 0) > 0 ||
441+
($update.websites?.length ?? 0) > 0 ||
442+
($update.integration_knowledge_list?.length ?? 0) > 0) &&
443+
($update.mcp_servers?.length ?? 0) === 0}
431444
<Settings.Group title={m.mcp_servers()}>
432445
<Settings.Row
433446
title={m.mcp_servers()}
434447
description={m.select_mcp_servers_description()}
435-
hasChanges={$currentChanges.diff.mcp_servers !== undefined || $currentChanges.diff.mcp_tools !== undefined}
448+
hasChanges={$currentChanges.diff.mcp_servers !== undefined ||
449+
$currentChanges.diff.mcp_tools !== undefined}
436450
revertFn={() => {
437451
discardChanges("mcp_servers");
438452
discardChanges("mcp_tools");
439453
}}
440454
>
441455
{#if mcpDisabledByKnowledge}
442-
<p class="label-warning border-label-default bg-label-dimmer text-label-stronger mb-2 rounded-md border px-2 py-1 text-sm">
443-
<span class="font-bold">{m.warning()}:&nbsp;</span>{m.mcp_disabled_when_knowledge_active()}
456+
<p
457+
class="label-warning border-label-default bg-label-dimmer text-label-stronger mb-2 rounded-md border px-2 py-1 text-sm"
458+
>
459+
<span class="font-bold">{m.warning()}:&nbsp;</span
460+
>{m.mcp_disabled_when_knowledge_active()}
444461
</p>
445462
{/if}
446-
<div class={mcpDisabledByKnowledge ? 'opacity-50 pointer-events-none' : ''}>
447-
<SelectMCPServers bind:selectedMCPServers={$update.mcp_servers} bind:selectedMCPTools={$update.mcp_tools} selectedModel={$update.completion_model} />
463+
<div class={mcpDisabledByKnowledge ? "pointer-events-none opacity-50" : ""}>
464+
<SelectMCPServers
465+
bind:selectedMCPServers={$update.mcp_servers}
466+
bind:selectedMCPTools={$update.mcp_tools}
467+
selectedModel={$update.completion_model}
468+
/>
448469
</div>
449-
</Settings.Row>
470+
</Settings.Row>
450471
</Settings.Group>
451472

473+
{#if $currentSpace.image_generation_models?.length > 0}
474+
<Settings.Group title={m.image_generation_models()}>
475+
<Settings.Row
476+
hasChanges={$currentChanges.diff.image_generation_enabled !== undefined}
477+
revertFn={() => {
478+
discardChanges("image_generation_enabled");
479+
}}
480+
title={m.image_generation_toggle()}
481+
description={m.image_generation_toggle_description()}
482+
>
483+
<div class="border-default flex h-14 border-b py-2">
484+
<Input.RadioSwitch
485+
bind:value={$update.image_generation_enabled}
486+
labelTrue={m.enable_image_generation()}
487+
labelFalse={m.disable_image_generation()}
488+
></Input.RadioSwitch>
489+
</div>
490+
</Settings.Row>
491+
</Settings.Group>
492+
{/if}
493+
452494
<Settings.Group title={m.security_and_privacy()}>
453495
<Settings.Row
454496
hasChanges={$currentChanges.diff.data_retention_days !== undefined}

frontend/apps/web/src/routes/(app)/spaces/[spaceId]/settings/+page.svelte

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import { Page, Settings } from "$lib/components/layout";
1717
import SpaceStorageOverview from "./SpaceStorageOverview.svelte";
1818
import SelectTranscriptionModels from "./SelectTranscriptionModels.svelte";
19+
import SelectImageGenerationModels from "./SelectImageGenerationModels.svelte";
1920
import { writable } from "svelte/store";
2021
import { getIntric } from "$lib/core/Intric.js";
2122
import ChangeSecurityClassification from "./ChangeSecurityClassification.svelte";
@@ -34,6 +35,9 @@
3435
let transcriptionModels = $derived(
3536
models.transcriptionModels.filter((model) => model.is_org_enabled)
3637
);
38+
let imageGenerationModels = $derived(
39+
models.imageGenerationModels.filter((model) => model.is_org_enabled)
40+
);
3741
3842
const spaces = getSpacesManager();
3943
const currentSpace = spaces.state.currentSpace;
@@ -61,7 +65,8 @@
6165
// Navigation guard for unsaved changes
6266
beforeNavigate((navigate) => {
6367
if ($currentChanges.hasUnsavedChanges) {
64-
const confirmMessage = m.unsaved_changes_warning?.() ?? "Du har osparade ändringar. Vill du lämna sidan?";
68+
const confirmMessage =
69+
m.unsaved_changes_warning?.() ?? "Du har osparade ändringar. Vill du lämna sidan?";
6570
if (!confirm(confirmMessage)) {
6671
navigate.cancel();
6772
return;
@@ -160,47 +165,47 @@
160165
<Page.Title title={m.settings()}></Page.Title>
161166
<Page.Flex>
162167
{#if $currentChanges.hasUnsavedChanges}
163-
<Button
164-
variant="destructive"
165-
disabled={$isSaving}
166-
on:click={() => discardChanges()}
167-
>{m.discard_all_changes()}</Button>
168+
<Button variant="destructive" disabled={$isSaving} on:click={() => discardChanges()}
169+
>{m.discard_all_changes()}</Button
170+
>
168171
<Button
169172
variant="positive"
170173
class="h-8 w-32 whitespace-nowrap"
171174
disabled={$isSaving}
172-
on:click={handleSave}
173-
>{$isSaving ? m.loading() : m.save_changes()}</Button>
175+
on:click={handleSave}>{$isSaving ? m.loading() : m.save_changes()}</Button
176+
>
174177
{:else}
175178
{#if showSaveSuccess}
176179
<p class="text-positive-stronger px-4" transition:fade>{m.all_changes_saved()}</p>
177180
{/if}
178-
<Button variant="primary" class="w-32" href={`/spaces/${$currentSpace.routeId}`}>{m.done()}</Button>
181+
<Button variant="primary" class="w-32" href={`/spaces/${$currentSpace.routeId}`}
182+
>{m.done()}</Button
183+
>
179184
{/if}
180185
</Page.Flex>
181186
</Page.Header>
182187

183188
<Page.Main>
184189
<Settings.Page>
185190
{#if !isOrgSpace}
186-
<Settings.Group title={m.general()}>
187-
<EditNameAndDescription></EditNameAndDescription>
188-
<Settings.Row
189-
title={m.avatar()}
190-
description={m.avatar_description()}
191-
hasChanges={$currentChanges.diff.icon_id !== undefined}
192-
revertFn={() => discardChanges("icon_id")}
193-
>
194-
<IconUpload
195-
{iconUrl}
196-
uploading={iconUploading}
197-
error={iconError}
198-
on:upload={handleIconUpload}
199-
on:delete={handleIconDelete}
200-
/>
201-
</Settings.Row>
202-
<SpaceStorageOverview></SpaceStorageOverview>
203-
</Settings.Group>
191+
<Settings.Group title={m.general()}>
192+
<EditNameAndDescription></EditNameAndDescription>
193+
<Settings.Row
194+
title={m.avatar()}
195+
description={m.avatar_description()}
196+
hasChanges={$currentChanges.diff.icon_id !== undefined}
197+
revertFn={() => discardChanges("icon_id")}
198+
>
199+
<IconUpload
200+
{iconUrl}
201+
uploading={iconUploading}
202+
error={iconError}
203+
on:upload={handleIconUpload}
204+
on:delete={handleIconDelete}
205+
/>
206+
</Settings.Row>
207+
<SpaceStorageOverview></SpaceStorageOverview>
208+
</Settings.Group>
204209
{/if}
205210
{#if !isOrgSpace}
206211
<Settings.Group title={m.security_and_privacy()}>
@@ -226,6 +231,9 @@
226231
<SelectTranscriptionModels selectableModels={transcriptionModels}
227232
></SelectTranscriptionModels>
228233

234+
<SelectImageGenerationModels selectableModels={imageGenerationModels}
235+
></SelectImageGenerationModels>
236+
229237
<SelectMCPServers selectableServers={data.mcpServers}></SelectMCPServers>
230238
</Settings.Group>
231239

0 commit comments

Comments
 (0)