-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add observability plugin system (#227)
Co-authored-by: Michael Neale <[email protected]> Co-authored-by: Lifei Zhou <[email protected]> Co-authored-by: Alice Hau <[email protected]>
- Loading branch information
1 parent
7066025
commit d30b524
Showing
20 changed files
with
300 additions
and
157 deletions.
There are no files selected for viewing
This file contains 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 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 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 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 was deleted.
Oops, something went wrong.
This file contains 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,20 @@ | ||
from functools import wraps | ||
from typing import Callable | ||
|
||
from exchange.observers.base import ObserverManager | ||
|
||
|
||
def observe_wrapper(*args, **kwargs) -> Callable: # noqa: ANN002, ANN003 | ||
"""Decorator to wrap a function with all registered observer plugins, dynamically fetched.""" | ||
|
||
def wrapper(func: Callable) -> Callable: | ||
@wraps(func) | ||
def dynamic_wrapped(*func_args, **func_kwargs) -> Callable: # noqa: ANN002, ANN003 | ||
wrapped = func | ||
for observer in ObserverManager.get_instance()._observers: | ||
wrapped = observer.observe_wrapper(*args, **kwargs)(wrapped) | ||
return wrapped(*func_args, **func_kwargs) | ||
|
||
return dynamic_wrapped | ||
|
||
return wrapper |
This file contains 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,43 @@ | ||
from abc import ABC, abstractmethod | ||
from typing import Callable, Type | ||
|
||
|
||
class Observer(ABC): | ||
@abstractmethod | ||
def initialize(self) -> None: | ||
pass | ||
|
||
@abstractmethod | ||
def observe_wrapper(*args, **kwargs) -> Callable: # noqa: ANN002, ANN003 | ||
pass | ||
|
||
@abstractmethod | ||
def finalize(self) -> None: | ||
pass | ||
|
||
|
||
class ObserverManager: | ||
_instance = None | ||
_observers: list[Observer] = [] | ||
|
||
@classmethod | ||
def get_instance(cls: Type["ObserverManager"]) -> "ObserverManager": | ||
if cls._instance is None: | ||
cls._instance = cls() | ||
return cls._instance | ||
|
||
def initialize(self, tracing: bool, observers: list[Observer]) -> None: | ||
from exchange.observers.langfuse import LangfuseObserver | ||
|
||
self._observers = observers | ||
for observer in self._observers: | ||
# LangfuseObserver has special behavior when tracing is _dis_abled. | ||
# Consider refactoring to make this less special-casey if that's common. | ||
if isinstance(observer, LangfuseObserver) and not tracing: | ||
observer.initialize_with_disabled_tracing() | ||
elif tracing: | ||
observer.initialize() | ||
|
||
def finalize(self) -> None: | ||
for observer in self._observers: | ||
observer.finalize() |
This file contains 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,100 @@ | ||
""" | ||
Langfuse Observer | ||
This observer provides integration with Langfuse, a tool for monitoring and tracing LLM applications. | ||
Usage: | ||
Include "langfuse" in your profile's list of observers to enable Langfuse integration. | ||
It automatically checks for Langfuse credentials in the .env.langfuse file and for a running Langfuse server. | ||
If these are found, it will set up the necessary client and context for tracing. | ||
Note: | ||
Run setup_langfuse.sh which automates the steps for running local Langfuse. | ||
""" | ||
|
||
import logging | ||
import os | ||
import sys | ||
from functools import cache, wraps | ||
from io import StringIO | ||
from typing import Callable | ||
|
||
from langfuse.decorators import langfuse_context | ||
|
||
from exchange.observers.base import Observer | ||
|
||
## These are the default configurations for local Langfuse server | ||
## Please refer to .env.langfuse.local file for local langfuse server setup configurations | ||
DEFAULT_LOCAL_LANGFUSE_HOST = "http://localhost:3000" | ||
DEFAULT_LOCAL_LANGFUSE_PUBLIC_KEY = "publickey-local" | ||
DEFAULT_LOCAL_LANGFUSE_SECRET_KEY = "secretkey-local" | ||
|
||
|
||
@cache | ||
def auth_check() -> bool: | ||
# Temporarily redirect stdout and stderr to suppress print statements from Langfuse | ||
temp_stderr = StringIO() | ||
sys.stderr = temp_stderr | ||
|
||
# Set environment variables if not specified | ||
os.environ.setdefault("LANGFUSE_PUBLIC_KEY", DEFAULT_LOCAL_LANGFUSE_PUBLIC_KEY) | ||
os.environ.setdefault("LANGFUSE_SECRET_KEY", DEFAULT_LOCAL_LANGFUSE_SECRET_KEY) | ||
os.environ.setdefault("LANGFUSE_HOST", DEFAULT_LOCAL_LANGFUSE_HOST) | ||
|
||
auth_val = langfuse_context.auth_check() | ||
|
||
# Restore stderr | ||
sys.stderr = sys.__stderr__ | ||
return auth_val | ||
|
||
|
||
class LangfuseObserver(Observer): | ||
def initialize(self) -> None: | ||
langfuse_auth = auth_check() | ||
if langfuse_auth: | ||
print("Local Langfuse initialized. View your traces at http://localhost:3000") | ||
else: | ||
raise RuntimeError( | ||
"You passed --tracing, but a Langfuse object was not found in the current context. " | ||
"Please initialize the local Langfuse server and restart Goose." | ||
) | ||
|
||
langfuse_context.configure(enabled=True) | ||
self.tracing = True | ||
|
||
def initialize_with_disabled_tracing(self) -> None: | ||
logging.getLogger("langfuse").setLevel(logging.ERROR) | ||
langfuse_context.configure(enabled=False) | ||
self.tracing = False | ||
|
||
def session_id_wrapper(self, func: Callable, session_id: str) -> Callable: | ||
@wraps(func) # This will preserve the metadata of 'func' | ||
def wrapper(*args, **kwargs) -> Callable: # noqa: ANN002, ANN003 | ||
langfuse_context.update_current_trace(session_id=session_id) | ||
return func(*args, **kwargs) | ||
|
||
return wrapper | ||
|
||
def observe_wrapper(self, *args, **kwargs) -> Callable: # noqa: ANN002, ANN003 | ||
def _wrapper(fn: Callable) -> Callable: | ||
if self.tracing and auth_check(): | ||
|
||
@wraps(fn) | ||
def wrapped_fn(*fargs, **fkwargs) -> Callable: # noqa: ANN002, ANN003 | ||
# group all traces under the same session | ||
if "session_id" in kwargs: | ||
session_id_function = kwargs.pop("session_id") | ||
session_id_value = session_id_function(fargs[0]) | ||
modified_fn = self.session_id_wrapper(fn, session_id_value) | ||
return langfuse_context.observe(*args, **kwargs)(modified_fn)(*fargs, **fkwargs) | ||
else: | ||
return langfuse_context.observe(*args, **kwargs)(fn)(*fargs, **fkwargs) | ||
|
||
return wrapped_fn | ||
else: | ||
return fn | ||
|
||
return _wrapper | ||
|
||
def finalize(self) -> None: | ||
langfuse_context.flush() |
This file contains 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 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 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 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 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 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 was deleted.
Oops, something went wrong.
Oops, something went wrong.