feat(open-webui): full attachment menu with all source types
Replace the single attach popover with an Open WebUI-style multi-level
menu covering every attachment source:
- Upload Files: images stay inline (vision); other files are uploaded and
grounded via retrieval (`file-{id}` collection).
- Capture: take a photo from the webcam as an inline image.
- Attach Webpage: ingest a URL (existing).
- Attach Files: pick files already stored in Open WebUI.
- Attach Notes: inject the contents of a note as context.
- Attach Knowledge: nested picker with checkboxes, expandable to a base's
files (replaces the flat checkbox list).
- Reference Chats: bring a previous conversation in as context.
Knowledge bases, web pages and files are grounded by retrieval; notes and
referenced chats are injected verbatim as context blocks. Adds the
supporting integration methods (list files/notes, knowledge files, upload
file), request handlers and tRPC procedures.
This commit is contained in:
1 parent
800caf6e5e
commit
78bd66e803
8 files changed
+809
-124
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import { FormData } from "undici";
|
||||
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
@@ -10,8 +12,10 @@ import type {
|
||||
OpenWebUiChatListItem,
|
||||
OpenWebUiChatPayload,
|
||||
OpenWebUiCompletionMessage,
|
||||
OpenWebUiFileSummary,
|
||||
OpenWebUiKnowledge,
|
||||
OpenWebUiModel,
|
||||
OpenWebUiNote,
|
||||
OpenWebUiWebDocument,
|
||||
} from "./open-webui-types";
|
||||
import {
|
||||
@@ -19,9 +23,13 @@ import {
|
||||
openWebUiChatSchema,
|
||||
openWebUiCollectionQueryResponseSchema,
|
||||
openWebUiCompletionChunkSchema,
|
||||
openWebUiFileListSchema,
|
||||
openWebUiKnowledgeDetailSchema,
|
||||
openWebUiKnowledgeListSchema,
|
||||
openWebUiModelsResponseSchema,
|
||||
openWebUiNoteListSchema,
|
||||
openWebUiProcessWebResponseSchema,
|
||||
openWebUiUploadedFileSchema,
|
||||
} from "./open-webui-types";
|
||||
|
||||
const logger = createLogger({ module: "open-webui-integration" });
|
||||
@@ -70,6 +78,86 @@ export class OpenWebUiIntegration extends Integration {
|
||||
return Array.isArray(parsed) ? parsed : parsed.items;
|
||||
}
|
||||
|
||||
/**
|
||||
* List the user's uploaded files (for the "Attach Files" picker).
|
||||
*/
|
||||
public async listFilesAsync(): Promise<OpenWebUiFileSummary[]> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/files/"), {
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
|
||||
const parsed = openWebUiFileListSchema.parse(await response.json());
|
||||
const files = Array.isArray(parsed) ? parsed : parsed.items;
|
||||
return files.map((file) => ({ id: file.id, name: file.meta?.name ?? file.filename ?? file.id }));
|
||||
}
|
||||
|
||||
/**
|
||||
* List the files contained in a knowledge base (for the expandable picker).
|
||||
*/
|
||||
public async getKnowledgeFilesAsync(knowledgeId: string): Promise<OpenWebUiFileSummary[]> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/knowledge/${knowledgeId}`), {
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
|
||||
const parsed = openWebUiKnowledgeDetailSchema.parse(await response.json());
|
||||
return (parsed.files ?? []).map((file) => ({ id: file.id, name: file.meta?.name ?? file.filename ?? file.id }));
|
||||
}
|
||||
|
||||
/**
|
||||
* List the user's notes (for the "Attach Notes" picker).
|
||||
*/
|
||||
public async listNotesAsync(): Promise<OpenWebUiNote[]> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/notes/"), {
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
|
||||
const parsed = openWebUiNoteListSchema.parse(await response.json());
|
||||
const notes = Array.isArray(parsed) ? parsed : parsed.items;
|
||||
return notes.map((note) => ({
|
||||
id: note.id,
|
||||
title: note.title ?? "Untitled note",
|
||||
content: note.data?.content?.md ?? "",
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to Open WebUI so it can be retrieved from later. Returns the
|
||||
* new file id (its vector collection is `file-{id}`).
|
||||
*/
|
||||
public async uploadFileAsync(
|
||||
filename: string,
|
||||
base64Content: string,
|
||||
contentType: string,
|
||||
): Promise<OpenWebUiFileSummary> {
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([Buffer.from(base64Content, "base64")], { type: contentType }), filename);
|
||||
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/files/"), {
|
||||
method: "POST",
|
||||
headers: this.getAuthHeaders(),
|
||||
body: form,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
|
||||
const parsed = openWebUiUploadedFileSchema.parse(await response.json());
|
||||
return { id: parsed.id, name: parsed.meta?.name ?? parsed.filename ?? filename };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingest a web page into a temporary vector collection and return a handle to
|
||||
* retrieve from it. Open WebUI does not crawl URLs at completion time, so we
|
||||
|
||||
@@ -86,6 +86,61 @@ export const openWebUiKnowledgeListSchema = z.union([
|
||||
z.object({ items: z.array(openWebUiKnowledgeSchema) }),
|
||||
]);
|
||||
|
||||
// A file stored in Open WebUI (uploaded directly or part of a knowledge base).
|
||||
export const openWebUiFileSchema = z.object({
|
||||
id: z.string(),
|
||||
filename: z.string().nullish(),
|
||||
meta: z.object({ name: z.string().nullish(), content_type: z.string().nullish() }).nullish(),
|
||||
});
|
||||
|
||||
export type OpenWebUiFile = z.infer<typeof openWebUiFileSchema>;
|
||||
|
||||
export const openWebUiFileListSchema = z.union([
|
||||
z.array(openWebUiFileSchema),
|
||||
z.object({ items: z.array(openWebUiFileSchema) }),
|
||||
]);
|
||||
|
||||
// POST /api/v1/files/ — upload response (we only need the new file id + name).
|
||||
export const openWebUiUploadedFileSchema = z.object({
|
||||
id: z.string(),
|
||||
filename: z.string().nullish(),
|
||||
meta: z.object({ name: z.string().nullish() }).nullish(),
|
||||
});
|
||||
|
||||
// GET /api/v1/knowledge/{id} — knowledge base with its files.
|
||||
export const openWebUiKnowledgeDetailSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
files: z.array(openWebUiFileSchema).nullish(),
|
||||
});
|
||||
|
||||
export type OpenWebUiKnowledgeDetail = z.infer<typeof openWebUiKnowledgeDetailSchema>;
|
||||
|
||||
// GET /api/v1/notes/ — user notes (markdown content under data.content.md).
|
||||
export const openWebUiNoteSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string().nullish(),
|
||||
data: z.object({ content: z.object({ md: z.string().nullish() }).nullish() }).nullish(),
|
||||
});
|
||||
|
||||
export const openWebUiNoteListSchema = z.union([
|
||||
z.array(openWebUiNoteSchema),
|
||||
z.object({ items: z.array(openWebUiNoteSchema) }),
|
||||
]);
|
||||
|
||||
// A note flattened for the widget: id, title and its markdown body.
|
||||
export interface OpenWebUiNote {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
// A file flattened for the widget: id and a display name.
|
||||
export interface OpenWebUiFileSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// POST /api/v1/retrieval/process/web — ingests a URL into a temporary vector
|
||||
// collection. We retrieve from that collection at send time and inject the
|
||||
// results as context (Open WebUI does not fetch URLs at completion time).
|
||||
|
||||
Reference in new issue
Block a user