-
Notifications
You must be signed in to change notification settings - Fork 8.3k
(feat) Dev APIs: Add lifecycle events with AGUI protocol #10780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+688
−2
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
111 changes: 111 additions & 0 deletions
111
src/lfx/src/lfx/events/observability/lifecycle_events.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import functools | ||
| from collections.abc import Awaitable, Callable | ||
| from typing import Any | ||
|
|
||
| from ag_ui.encoder.encoder import EventEncoder | ||
|
|
||
| from lfx.log.logger import logger | ||
|
|
||
| AsyncMethod = Callable[..., Awaitable[Any]] | ||
|
|
||
| encoder: EventEncoder = EventEncoder() | ||
|
|
||
|
|
||
| def observable(observed_method: AsyncMethod) -> AsyncMethod: | ||
| """Decorator to make an async method observable by emitting lifecycle events. | ||
|
|
||
| Decorated classes are expected to implement specific methods to emit AGUI events: | ||
| - `before_callback_event(*args, **kwargs)`: Called before the decorated method executes. | ||
| It should return a dictionary representing the event payload. | ||
| - `after_callback_event(result, *args, **kwargs)`: Called after the decorated method | ||
| successfully completes. It should return a dictionary representing the event payload. | ||
| The `result` of the decorated method is passed as the first argument. | ||
| - `error_callback_event(exception, *args, **kwargs)`: (Optional) Called if the decorated | ||
| method raises an exception. It should return a dictionary representing the error event payload. | ||
| The `exception` is passed as the first argument. | ||
|
|
||
| If these methods are implemented, the decorator will call them to generate event payloads. | ||
| If an implementation is missing, the corresponding event publishing will be skipped without error. | ||
|
|
||
| Payloads returned by these methods can include custom metrics by placing them | ||
| under the 'langflow' key within the 'raw_events' dictionary. | ||
|
|
||
| Example: | ||
| class MyClass: | ||
| display_name = "My Observable Class" | ||
|
|
||
| def before_callback_event(self, *args, **kwargs): | ||
| return {"event_name": "my_method_started", "data": {"input_args": args}} | ||
|
|
||
| async def my_method(self, event_manager: EventManager, data: str): | ||
| # ... method logic ... | ||
| return "processed_data" | ||
|
|
||
| def after_callback_event(self, result, *args, **kwargs): | ||
| return {"event_name": "my_method_completed", "data": {"output": result}} | ||
|
|
||
| def error_callback_event(self, exception, *args, **kwargs): | ||
| return {"event_name": "my_method_failed", "error": str(exception)} | ||
|
|
||
| @observable | ||
| async def my_observable_method(self, event_manager: EventManager, data: str): | ||
| # ... method logic ... | ||
| pass | ||
| """ | ||
|
|
||
| async def check_event_manager(self, **kwargs): | ||
| if "event_manager" not in kwargs or kwargs["event_manager"] is None: | ||
| await logger.awarning( | ||
| f"EventManager not available/provided, skipping observable event publishing " | ||
| f"from {self.__class__.__name__}" | ||
| ) | ||
| return False | ||
| return True | ||
|
|
||
| async def before_callback(self, *args, **kwargs): | ||
| if not await check_event_manager(self, **kwargs): | ||
| return | ||
|
|
||
| if hasattr(self, "before_callback_event"): | ||
| event_payload = self.before_callback_event(*args, **kwargs) | ||
| event_payload = encoder.encode(event_payload) | ||
| # TODO: Publish event per request, would required context based queues | ||
| else: | ||
| await logger.awarning( | ||
| f"before_callback_event not implemented for {self.__class__.__name__}. Skipping event publishing." | ||
| ) | ||
|
|
||
| async def after_callback(self, res: Any | None = None, *args, **kwargs): | ||
| if not await check_event_manager(self, **kwargs): | ||
| return | ||
| if hasattr(self, "after_callback_event"): | ||
| event_payload = self.after_callback_event(res, *args, **kwargs) | ||
| event_payload = encoder.encode(event_payload) | ||
| # TODO: Publish event per request, would required context based queues | ||
| else: | ||
| await logger.awarning( | ||
| f"after_callback_event not implemented for {self.__class__.__name__}. Skipping event publishing." | ||
| ) | ||
|
|
||
| @functools.wraps(observed_method) | ||
| async def wrapper(self, *args, **kwargs): | ||
| await before_callback(self, *args, **kwargs) | ||
| result = None | ||
| try: | ||
| result = await observed_method(self, *args, **kwargs) | ||
| await after_callback(self, result, *args, **kwargs) | ||
| except Exception as e: | ||
| await logger.aerror(f"Exception in {self.__class__.__name__}: {e}") | ||
| if hasattr(self, "error_callback_event"): | ||
| try: | ||
| event_payload = self.error_callback_event(e, *args, **kwargs) | ||
dkaushik94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| event_payload = encoder.encode(event_payload) | ||
| # TODO: Publish event per request, would required context based queues | ||
| except Exception as callback_e: # noqa: BLE001 | ||
| await logger.aerror( | ||
| f"Exception during error_callback_event for {self.__class__.__name__}: {callback_e}" | ||
| ) | ||
| raise | ||
| return result | ||
|
|
||
| return wrapper | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,9 @@ | |
| from itertools import chain | ||
| from typing import TYPE_CHECKING, Any, cast | ||
|
|
||
| from ag_ui.core import RunFinishedEvent, RunStartedEvent | ||
|
|
||
| from lfx.events.observability.lifecycle_events import observable | ||
| from lfx.exceptions.component import ComponentBuildError | ||
| from lfx.graph.edge.base import CycleEdge, Edge | ||
| from lfx.graph.graph.constants import Finish, lazy_load_vertex_dict | ||
|
|
@@ -728,6 +731,7 @@ def _set_inputs(self, input_components: list[str], inputs: dict[str, str], input | |
| raise ValueError(msg) | ||
| vertex.update_raw_params(inputs, overwrite=True) | ||
|
|
||
| @observable | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Heads up: I will remove this once the PR is reviewed and everybody has understood what the intended use of the |
||
| async def _run( | ||
| self, | ||
| *, | ||
|
|
@@ -2309,3 +2313,22 @@ def __to_dict(self) -> dict[str, dict[str, list[str]]]: | |
| predecessors = [i.id for i in self.get_predecessors(vertex)] | ||
| result |= {vertex_id: {"successors": sucessors, "predecessors": predecessors}} | ||
| return result | ||
|
|
||
| def raw_event_metrics(self, optional_fields: dict | None = None) -> dict: | ||
| if optional_fields is None: | ||
| optional_fields = {} | ||
| import time | ||
|
|
||
| return {"timestamp": time.time(), **optional_fields} | ||
|
|
||
| def before_callback_event(self, *args, **kwargs) -> RunStartedEvent: # noqa: ARG002 | ||
| metrics = {} | ||
| if hasattr(self, "raw_event_metrics"): | ||
| metrics = self.raw_event_metrics({"total_components": len(self.vertices)}) | ||
| return RunStartedEvent(run_id=self._run_id, thread_id=self.flow_id, raw_event=metrics) | ||
|
|
||
| def after_callback_event(self, result: Any = None, *args, **kwargs) -> RunFinishedEvent: # noqa: ARG002 | ||
| metrics = {} | ||
| if hasattr(self, "raw_event_metrics"): | ||
| metrics = self.raw_event_metrics({"total_components": len(self.vertices)}) | ||
| return RunFinishedEvent(run_id=self._run_id, thread_id=self.flow_id, result=None, raw_event=metrics) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.