mirror of
https://github.com/open-webui/open-webui
synced 2025-02-20 12:00:22 +00:00
feat: add/delete pipelines
This commit is contained in:
parent
c1cabf1415
commit
12e60d8ebf
@ -480,6 +480,92 @@ async def get_pipelines_list(user=Depends(get_admin_user)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AddPipelineForm(BaseModel):
|
||||||
|
url: str
|
||||||
|
urlIdx: int
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/pipelines/add")
|
||||||
|
async def add_pipeline(form_data: AddPipelineForm, user=Depends(get_admin_user)):
|
||||||
|
|
||||||
|
r = None
|
||||||
|
try:
|
||||||
|
urlIdx = form_data.urlIdx
|
||||||
|
|
||||||
|
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
||||||
|
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {key}"}
|
||||||
|
r = requests.post(
|
||||||
|
f"{url}/pipelines/add", headers=headers, json={"url": form_data.url}
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
return {**data}
|
||||||
|
except Exception as e:
|
||||||
|
# Handle connection error here
|
||||||
|
print(f"Connection error: {e}")
|
||||||
|
|
||||||
|
detail = "Pipeline not found"
|
||||||
|
if r is not None:
|
||||||
|
try:
|
||||||
|
res = r.json()
|
||||||
|
if "detail" in res:
|
||||||
|
detail = res["detail"]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
|
||||||
|
detail=detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeletePipelineForm(BaseModel):
|
||||||
|
id: str
|
||||||
|
urlIdx: int
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/pipelines/delete")
|
||||||
|
async def delete_pipeline(form_data: DeletePipelineForm, user=Depends(get_admin_user)):
|
||||||
|
|
||||||
|
r = None
|
||||||
|
try:
|
||||||
|
urlIdx = form_data.urlIdx
|
||||||
|
|
||||||
|
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
||||||
|
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {key}"}
|
||||||
|
r = requests.delete(
|
||||||
|
f"{url}/pipelines/delete", headers=headers, json={"id": form_data.id}
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
return {**data}
|
||||||
|
except Exception as e:
|
||||||
|
# Handle connection error here
|
||||||
|
print(f"Connection error: {e}")
|
||||||
|
|
||||||
|
detail = "Pipeline not found"
|
||||||
|
if r is not None:
|
||||||
|
try:
|
||||||
|
res = r.json()
|
||||||
|
if "detail" in res:
|
||||||
|
detail = res["detail"]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
|
||||||
|
detail=detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/pipelines")
|
@app.get("/api/pipelines")
|
||||||
async def get_pipelines(urlIdx: Optional[int] = None, user=Depends(get_admin_user)):
|
async def get_pipelines(urlIdx: Optional[int] = None, user=Depends(get_admin_user)):
|
||||||
models = await get_all_models()
|
models = await get_all_models()
|
||||||
|
@ -78,6 +78,78 @@ export const getPipelinesList = async (token: string = '') => {
|
|||||||
return pipelines;
|
return pipelines;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const downloadPipeline = async (token: string, url: string, urlIdx: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/pipelines/add`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: url,
|
||||||
|
urlIdx: urlIdx
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
if ('detail' in err) {
|
||||||
|
error = err.detail;
|
||||||
|
} else {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deletePipeline = async (token: string, id: string, urlIdx: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/pipelines/delete`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: id,
|
||||||
|
urlIdx: urlIdx
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
if ('detail' in err) {
|
||||||
|
error = err.detail;
|
||||||
|
} else {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getPipelines = async (token: string, urlIdx?: string) => {
|
export const getPipelines = async (token: string, urlIdx?: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
@ -12,7 +12,9 @@
|
|||||||
updatePipelineValves,
|
updatePipelineValves,
|
||||||
getPipelines,
|
getPipelines,
|
||||||
getModels,
|
getModels,
|
||||||
getPipelinesList
|
getPipelinesList,
|
||||||
|
downloadPipeline,
|
||||||
|
deletePipeline
|
||||||
} from '$lib/apis';
|
} from '$lib/apis';
|
||||||
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
@ -21,6 +23,8 @@
|
|||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
|
let downloading = false;
|
||||||
|
|
||||||
let PIPELINES_LIST = null;
|
let PIPELINES_LIST = null;
|
||||||
let selectedPipelinesUrlIdx = '';
|
let selectedPipelinesUrlIdx = '';
|
||||||
|
|
||||||
@ -30,6 +34,8 @@
|
|||||||
let valves_spec = null;
|
let valves_spec = null;
|
||||||
let selectedPipelineIdx = null;
|
let selectedPipelineIdx = null;
|
||||||
|
|
||||||
|
let pipelineDownloadUrl = '';
|
||||||
|
|
||||||
const updateHandler = async () => {
|
const updateHandler = async () => {
|
||||||
const pipeline = pipelines[selectedPipelineIdx];
|
const pipeline = pipelines[selectedPipelineIdx];
|
||||||
|
|
||||||
@ -80,6 +86,43 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addPipelineHandler = async () => {
|
||||||
|
downloading = true;
|
||||||
|
const res = await downloadPipeline(
|
||||||
|
localStorage.token,
|
||||||
|
pipelineDownloadUrl,
|
||||||
|
selectedPipelinesUrlIdx
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success('Pipeline downloaded successfully');
|
||||||
|
setPipelines();
|
||||||
|
models.set(await getModels(localStorage.token));
|
||||||
|
}
|
||||||
|
|
||||||
|
downloading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePipelineHandler = async () => {
|
||||||
|
const res = await deletePipeline(
|
||||||
|
localStorage.token,
|
||||||
|
pipelines[selectedPipelineIdx].id,
|
||||||
|
selectedPipelinesUrlIdx
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success('Pipeline deleted successfully');
|
||||||
|
setPipelines();
|
||||||
|
models.set(await getModels(localStorage.token));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
PIPELINES_LIST = await getPipelinesList(localStorage.token);
|
PIPELINES_LIST = await getPipelinesList(localStorage.token);
|
||||||
|
|
||||||
@ -97,7 +140,7 @@
|
|||||||
updateHandler();
|
updateHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-2 pr-1.5 overflow-y-scroll max-h-80 h-full">
|
<div class=" pr-1.5 overflow-y-scroll max-h-80 h-full">
|
||||||
{#if PIPELINES_LIST !== null}
|
{#if PIPELINES_LIST !== null}
|
||||||
<div class="flex w-full justify-between mb-2">
|
<div class="flex w-full justify-between mb-2">
|
||||||
<div class=" self-center text-sm font-semibold">
|
<div class=" self-center text-sm font-semibold">
|
||||||
@ -133,6 +176,75 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class=" my-2">
|
||||||
|
<div class=" mb-2 text-sm font-medium">
|
||||||
|
{$i18n.t('Install from Github URL')}
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1 mr-2">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('Enter Github Raw URL')}
|
||||||
|
bind:value={pipelineDownloadUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||||
|
on:click={() => {
|
||||||
|
addPipelineHandler();
|
||||||
|
}}
|
||||||
|
disabled={downloading}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{#if downloading}
|
||||||
|
<div class="self-center">
|
||||||
|
<svg
|
||||||
|
class=" w-4 h-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<style>
|
||||||
|
.spinner_ajPY {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: spinner_AtaB 0.75s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner_AtaB {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path
|
||||||
|
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||||
|
opacity=".25"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||||
|
class="spinner_ajPY"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-800 my-3 w-full" />
|
<hr class=" dark:border-gray-800 my-3 w-full" />
|
||||||
|
|
||||||
{#if pipelines !== null}
|
{#if pipelines !== null}
|
||||||
@ -145,7 +257,7 @@
|
|||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
{#if pipelines.length > 0}
|
{#if pipelines.length > 0}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="flex-1 pb-1">
|
<div class="flex-1">
|
||||||
<select
|
<select
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={selectedPipelineIdx}
|
bind:value={selectedPipelineIdx}
|
||||||
@ -162,6 +274,27 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||||
|
on:click={() => {
|
||||||
|
deletePipelineHandler();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user