diff --git a/backend/open_webui/events.py b/backend/open_webui/events.py index a8a74865aa..33a16fe1d1 100644 --- a/backend/open_webui/events.py +++ b/backend/open_webui/events.py @@ -1,10 +1,13 @@ from __future__ import annotations +import asyncio +import inspect import logging import time import uuid from dataclasses import asdict, dataclass, field from enum import StrEnum +from types import SimpleNamespace from typing import Any from open_webui.env import VERSION @@ -436,7 +439,7 @@ def build_event( class WebhookEventSink: - async def handle_event(self, app: Any, event: Event) -> None: + async def handle_event(self, app: Any, event: Event, request: Any | None = None) -> None: name = getattr(getattr(app, 'state', None), 'WEBUI_NAME', 'Open WebUI') subject = event.subject or {} subject_id = subject.get('id') @@ -452,7 +455,66 @@ class WebhookEventSink: log.exception('Event webhook failed for %s', webhook.get('id')) -EVENT_SINKS = [WebhookEventSink()] +async def dispatch_event_functions(app: Any, event: Event, request: Any | None = None) -> None: + from open_webui.models.functions import Functions + from open_webui.utils.plugin import get_function_module_from_cache + + context = request or SimpleNamespace(app=app) + event_payload = event.model_dump() + + try: + event_functions = await Functions.get_functions_by_type('event', active_only=True) + except Exception: + log.exception('Event functions could not be loaded for %s', event.event) + return + + for function in event_functions: + try: + function_module, _, _ = await get_function_module_from_cache(context, function.id, function=function) + handler = getattr(function_module, 'event', None) + if not handler: + continue + + if hasattr(function_module, 'valves') and hasattr(function_module, 'Valves'): + valves = await Functions.get_function_valves_by_id(function.id) + function_module.valves = function_module.Valves(**(valves if valves else {})) + + sig = inspect.signature(handler) + accepts_kwargs = any(param.kind == inspect.Parameter.VAR_KEYWORD for param in sig.parameters.values()) + extra_params = { + 'event': event_payload, + '__id__': function.id, + '__event__': event, + '__event_id__': event.id, + '__event_name__': event.event, + '__app__': app, + '__request__': request, + } + params = { + key: value for key, value in extra_params.items() if accepts_kwargs or key in sig.parameters + } + + if inspect.iscoroutinefunction(handler): + await handler(**params) + else: + handler(**params) + except Exception: + log.exception('Event function failed for %s', function.id) + + +def schedule_event_function_dispatch(app: Any, event: Event, request: Any | None = None) -> None: + try: + asyncio.create_task(dispatch_event_functions(app, event, request)) + except RuntimeError: + log.exception('Event functions could not be scheduled for %s', event.event) + + +class EventFunctionSink: + async def handle_event(self, app: Any, event: Event, request: Any | None = None) -> None: + schedule_event_function_dispatch(app, event, request) + + +EVENT_SINKS = [EventFunctionSink(), WebhookEventSink()] async def publish_event( @@ -467,6 +529,7 @@ async def publish_event( message: str | None = None, ) -> None: app = getattr(request_or_app, 'app', request_or_app) + request = request_or_app if hasattr(request_or_app, 'app') else None event_payload = build_event( request_or_app, event, @@ -480,6 +543,6 @@ async def publish_event( for sink in EVENT_SINKS: try: - await sink.handle_event(app, event_payload) + await sink.handle_event(app, event_payload, request=request) except Exception: log.exception('Event sink failed for %s', event.value) diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index 6087824085..ef6084c896 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -285,6 +285,8 @@ async def load_function_module_by_id(function_id: str, content: str | None = Non return module.Filter(), 'filter', frontmatter elif hasattr(module, 'Action'): return module.Action(), 'action', frontmatter + elif hasattr(module, 'Event'): + return module.Event(), 'event', frontmatter else: raise Exception('No Function class found in the module') except Exception as e: diff --git a/src/lib/components/admin/Functions.svelte b/src/lib/components/admin/Functions.svelte index 3e3e69f537..6f5de19559 100644 --- a/src/lib/components/admin/Functions.svelte +++ b/src/lib/components/admin/Functions.svelte @@ -410,7 +410,8 @@ items={[ { value: 'pipe', label: $i18n.t('Pipe') }, { value: 'filter', label: $i18n.t('Filter') }, - { value: 'action', label: $i18n.t('Action') } + { value: 'action', label: $i18n.t('Action') }, + { value: 'event', label: $i18n.t('Event') } ]} /> diff --git a/src/lib/components/admin/Functions/FunctionEditor.svelte b/src/lib/components/admin/Functions/FunctionEditor.svelte index eca65e929f..f97b713bca 100644 --- a/src/lib/components/admin/Functions/FunctionEditor.svelte +++ b/src/lib/components/admin/Functions/FunctionEditor.svelte @@ -41,7 +41,8 @@ } let codeEditor; - let boilerplate = `""" + let starterType = 'filter'; + const filterBoilerplate = `""" title: Example Filter author: open-webui author_url: https://github.com/open-webui @@ -109,6 +110,52 @@ class Filter: return body `; + const eventBoilerplate = `""" +title: Example Event +author: open-webui +author_url: https://github.com/open-webui +funding_url: https://github.com/open-webui +version: 0.1 +""" + +from pydantic import BaseModel + + +class Event: + class Valves(BaseModel): + pass + + def __init__(self): + self.valves = self.Valves() + + async def event( + self, + event: dict, + __event_id__: str = None, + __event_name__: str = None, + __id__: str = None, + __app__=None, + __request__=None, + ): + print(f"event:{__name__}") + print(f"event:id:{__event_id__}") + print(f"event:name:{__event_name__}") + print(f"event:payload:{event}") +`; + let boilerplate = filterBoilerplate; + + /** @param {'filter' | 'event'} type */ + const setStarterType = (type) => { + starterType = type; + boilerplate = type === 'event' ? eventBoilerplate : filterBoilerplate; + content = boilerplate; + _content = boilerplate; + }; + + /** @param {string} value */ + const selectStarterType = (value) => { + setStarterType(value === 'event' ? 'event' : 'filter'); + }; const _boilerplate = `from pydantic import BaseModel from typing import Optional, Union, Generator, Iterator @@ -328,7 +375,18 @@ class Pipe: -
+
+ {#if !edit} + + {/if}