diff --git a/packages/exchange/src/exchange/langfuse/langfuse.py b/packages/exchange/src/exchange/langfuse/langfuse.py index 9b6dde91e..eb641db83 100644 --- a/packages/exchange/src/exchange/langfuse/langfuse.py +++ b/packages/exchange/src/exchange/langfuse/langfuse.py @@ -1,115 +1,60 @@ +""" +Langfuse Integration Module + +This module provides integration with Langfuse, a tool for monitoring and tracing LLM applications. + +Usage: + Import this module to enable Langfuse integration. + It will automatically check for Langfuse credentials in the .env.langfuse.local 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, a prerequisite for this Langfuse integration. +""" + import os import logging from typing import Callable from dotenv import load_dotenv -import subprocess -import time -import platform -import socket from langfuse.decorators import langfuse_context +import sys +from io import StringIO logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +if not logger.handlers: + handler = logging.StreamHandler() + handler.setLevel(logging.INFO) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + handler.setFormatter(formatter) + logger.addHandler(handler) -HAS_LANGFUSE_CREDENTIALS = False SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) -LANGFUSE_REPO_URL = "https://github.com/langfuse/langfuse.git" -LANGFUSE_CLONE_DIR = os.path.join(SCRIPT_DIR, "langfuse_clone") LANGFUSE_LOCAL_ENV_FILE_NAME = ".env.langfuse.local" LANGFUSE_LOCAL_INIT_ENV_FILE = os.path.join(SCRIPT_DIR, LANGFUSE_LOCAL_ENV_FILE_NAME) +HAS_LANGFUSE_CREDENTIALS = False + +# Temporarily redirect stdout and stderr to suppress print statements from Langfuse +temp_stderr = StringIO() +sys.stderr = temp_stderr -def _run_command(command): - """Run a shell command.""" - result = subprocess.run(command, shell=True, check=True, text=True) - return result - - -def _update_or_clone_repo(): - """Update or clone the Langfuse repository.""" - if os.path.isdir(os.path.join(LANGFUSE_CLONE_DIR, ".git")): - _run_command(f"git -C {LANGFUSE_CLONE_DIR} pull --rebase") - else: - _run_command(f"git clone {LANGFUSE_REPO_URL} {LANGFUSE_CLONE_DIR}") - - -def _copy_env_file(): - """Copy the environment file to the Langfuse clone directory.""" - if os.path.isfile(LANGFUSE_LOCAL_INIT_ENV_FILE): - subprocess.run(["cp", LANGFUSE_LOCAL_INIT_ENV_FILE, LANGFUSE_CLONE_DIR], check=True) - else: - logger.error("Environment file not found. Exiting.") - exit(1) - - -def _start_docker_compose(): - """Start Docker containers for Langfuse service and Postgres db.""" - _run_command(f"cd {LANGFUSE_CLONE_DIR} && docker compose --env-file ./{LANGFUSE_LOCAL_ENV_FILE_NAME} up --detach") - - -def _wait_for_service(): - """Wait for the Langfuse service to be available on localhost:3000.""" - while True: - try: - with socket.create_connection(("localhost", 3000), timeout=1): - break - except OSError: - time.sleep(1) - - -def _launch_browser(): - """Launch the default web browser to open the Langfuse service URL.""" - system = platform.system().lower() - url = "http://localhost:3000" - - if "linux" in system: - subprocess.run(["xdg-open", url]) - elif "darwin" in system: # macOS - subprocess.run(["open", url]) - else: - logger.info("Please open http://localhost:3000 to view Langfuse traces.") - - -def _print_login_variables(): - """Read and print the default email and password from the environment file.""" - if os.path.isfile(LANGFUSE_LOCAL_INIT_ENV_FILE): - print("Please log in with the initialization environment variables:") - env_vars = {} - with open(LANGFUSE_LOCAL_INIT_ENV_FILE) as f: - for line in f: - if "=" in line: - key, value = line.strip().split("=", 1) - env_vars[key] = value - print(f"Email: {env_vars.get('LANGFUSE_INIT_USER_EMAIL', 'Not found')}") - print(f"Password: {env_vars.get('LANGFUSE_INIT_USER_PASSWORD', 'Not found')}") - else: - logger.warning("Langfuse environment file with local credentials required for local login not found.") - - -def setup_langfuse(): - """Main function to set up and run the Langfuse service.""" - try: - _update_or_clone_repo() - _copy_env_file() - _start_docker_compose() - _wait_for_service() - _launch_browser() - _print_login_variables() - load_dotenv(LANGFUSE_LOCAL_INIT_ENV_FILE) - if langfuse_context.auth_check(): - logger.info(f"Langfuse credentials found. Find traces, if enabled, under {os.environ['LANGFUSE_HOST']}.") - global HAS_LANGFUSE_CREDENTIALS - HAS_LANGFUSE_CREDENTIALS = True - except Exception as e: - logger.warning(f"Trouble finding Langfuse or Langfuse credentials: {e}") +load_dotenv(LANGFUSE_LOCAL_INIT_ENV_FILE) +if langfuse_context.auth_check(): + HAS_LANGFUSE_CREDENTIALS = True + logger.info("Langfuse context and credentials found.") +else: + logger.warning("Langfuse context and credentials not found. Langfuse will not work.") + +# Restore stderr +sys.stderr = sys.__stderr__ def observe_wrapper(*args, **kwargs) -> Callable: """ A decorator that wraps a function with Langfuse context observation if credentials are available. - If Langfuse credentials are found, the function will be wrapped with Langfuse's observe method. + If Langfuse credentials were found, the function will be wrapped with Langfuse's observe method. Otherwise, the function will be returned as-is. Args: @@ -127,5 +72,3 @@ def _wrapper(fn: Callable) -> Callable: return fn return _wrapper - -setup_langfuse() \ No newline at end of file diff --git a/packages/exchange/src/exchange/langfuse/setup_langfuse.sh b/packages/exchange/src/exchange/langfuse/setup_langfuse.sh new file mode 100755 index 000000000..16a51edb3 --- /dev/null +++ b/packages/exchange/src/exchange/langfuse/setup_langfuse.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# setup_langfuse.sh +# +# This script sets up and runs Langfuse locally for development and testing purposes. +# +# Key functionalities: +# 1. Clones or updates the Langfuse repository +# 2. Copies the local environment file +# 3. Starts Langfuse using Docker Compose with default initialization variables. +# 4. Waits for the service to be available +# 5. Launches a browser to open the local Langfuse UI +# 6. Prints login credentials from the environment file +# +# Usage: +# ./setup_langfuse.sh +# +# Requirements: +# - Git +# - Docker and Docker Compose +# - A .env.langfuse.local file in the same directory as this script. You may modify .env.langfuse.local to update default initialization credentials. +# +# Note: This script is intended for local development use only. + + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LANGFUSE_REPO_URL="https://github.com/langfuse/langfuse.git" +LANGFUSE_CLONE_DIR="$SCRIPT_DIR/langfuse_clone" +LANGFUSE_LOCAL_ENV_FILE_NAME=".env.langfuse.local" +LANGFUSE_LOCAL_INIT_ENV_FILE="$SCRIPT_DIR/$LANGFUSE_LOCAL_ENV_FILE_NAME" + +update_or_clone_repo() { + if [ -d "$LANGFUSE_CLONE_DIR/.git" ]; then + git -C "$LANGFUSE_CLONE_DIR" pull --rebase + else + git clone "$LANGFUSE_REPO_URL" "$LANGFUSE_CLONE_DIR" + fi +} + +copy_env_file() { + if [ -f "$LANGFUSE_LOCAL_INIT_ENV_FILE" ]; then + cp "$LANGFUSE_LOCAL_INIT_ENV_FILE" "$LANGFUSE_CLONE_DIR" + else + echo "Environment file not found. Exiting." + exit 1 + fi +} + +start_docker_compose() { + cd "$LANGFUSE_CLONE_DIR" && docker compose --env-file "./$LANGFUSE_LOCAL_ENV_FILE_NAME" up --detach +} + +wait_for_service() { + while ! nc -z localhost 3000; do + sleep 1 + done +} + +launch_browser() { + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + xdg-open "http://localhost:3000" + elif [[ "$OSTYPE" == "darwin"* ]]; then + open "http://localhost:3000" + else + echo "Please open http://localhost:3000 to view Langfuse traces." + fi +} + +print_login_variables() { + if [ -f "$LANGFUSE_LOCAL_INIT_ENV_FILE" ]; then + echo "Please log in with the initialization environment variables:" + grep -E "LANGFUSE_INIT_USER_EMAIL|LANGFUSE_INIT_USER_PASSWORD" "$LANGFUSE_LOCAL_INIT_ENV_FILE" + else + echo "Langfuse environment file with local credentials required for local login not found." + fi +} + +update_or_clone_repo +copy_env_file +start_docker_compose +wait_for_service +launch_browser +print_login_variables \ No newline at end of file diff --git a/src/goose/cli/main.py b/src/goose/cli/main.py index 99e0fec9a..be5490d59 100644 --- a/src/goose/cli/main.py +++ b/src/goose/cli/main.py @@ -14,6 +14,7 @@ from goose.utils.autocomplete import SUPPORTED_SHELLS, setup_autocomplete from goose.utils.session_file import list_sorted_session_files + @click.group() def goose_cli() -> None: pass