@@ -28,7 +28,7 @@ jobs:
|
|||||||
uv run pytest tests -m 'not integration'
|
uv run pytest tests -m 'not integration'
|
||||||
|
|
||||||
goose:
|
goose:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -48,27 +48,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
uv run pytest tests -m 'not integration'
|
uv run pytest tests -m 'not integration'
|
||||||
|
|
||||||
langfuse-wrapper:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install UV
|
|
||||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
||||||
|
|
||||||
- name: Source Cargo Environment
|
|
||||||
run: source $HOME/.cargo/env
|
|
||||||
|
|
||||||
- name: Ruff
|
|
||||||
run: |
|
|
||||||
uvx ruff check packages/langfuse-wrapper
|
|
||||||
uvx ruff format packages/langfuse-wrapper --check
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
working-directory: ./packages/langfuse-wrapper
|
|
||||||
run: |
|
|
||||||
uv run pytest tests -m 'not integration'
|
|
||||||
|
|
||||||
# This runs integration tests of the OpenAI API, using Ollama to host models.
|
# This runs integration tests of the OpenAI API, using Ollama to host models.
|
||||||
# This lets us test PRs from forks which can't access secrets like API keys.
|
# This lets us test PRs from forks which can't access secrets like API keys.
|
||||||
|
|||||||
@@ -125,6 +125,3 @@ docs/docs/reference
|
|||||||
|
|
||||||
# uv lock file
|
# uv lock file
|
||||||
uv.lock
|
uv.lock
|
||||||
|
|
||||||
# langfuse docker file
|
|
||||||
**/packages/langfuse-wrapper/scripts/docker-compose.yaml
|
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ dependencies = [
|
|||||||
"tiktoken>=0.7.0",
|
"tiktoken>=0.7.0",
|
||||||
"httpx>=0.27.0",
|
"httpx>=0.27.0",
|
||||||
"tenacity>=9.0.0",
|
"tenacity>=9.0.0",
|
||||||
"python-dotenv>=1.0.1",
|
|
||||||
"langfuse-wrapper"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
@@ -49,10 +47,3 @@ ai-exchange = "exchange:module_name"
|
|||||||
markers = [
|
markers = [
|
||||||
"integration: marks tests that need to authenticate (deselect with '-m \"not integration\"')",
|
"integration: marks tests that need to authenticate (deselect with '-m \"not integration\"')",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
langfuse-wrapper = { workspace = true}
|
|
||||||
|
|
||||||
[tool.uv.workspace]
|
|
||||||
members = ["../langfuse-wrapper"]
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from exchange.moderators.truncate import ContextTruncate
|
|||||||
from exchange.providers import Provider, Usage
|
from exchange.providers import Provider, Usage
|
||||||
from exchange.tool import Tool
|
from exchange.tool import Tool
|
||||||
from exchange.token_usage_collector import _token_usage_collector
|
from exchange.token_usage_collector import _token_usage_collector
|
||||||
from langfuse_wrapper.langfuse_wrapper import observe_wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def validate_tool_output(output: str) -> None:
|
def validate_tool_output(output: str) -> None:
|
||||||
@@ -128,7 +127,6 @@ class Exchange:
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@observe_wrapper()
|
|
||||||
def call_function(self, tool_use: ToolUse) -> ToolResult:
|
def call_function(self, tool_use: ToolUse) -> ToolResult:
|
||||||
"""Call the function indicated by the tool use"""
|
"""Call the function indicated by the tool use"""
|
||||||
tool = self._toolmap.get(tool_use.name)
|
tool = self._toolmap.get(tool_use.name)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from exchange.content import Text, ToolResult, ToolUse
|
|||||||
from exchange.providers.base import Provider, Usage
|
from exchange.providers.base import Provider, Usage
|
||||||
from tenacity import retry, wait_fixed, stop_after_attempt
|
from tenacity import retry, wait_fixed, stop_after_attempt
|
||||||
from exchange.providers.utils import retry_if_status, raise_for_status
|
from exchange.providers.utils import retry_if_status, raise_for_status
|
||||||
from langfuse_wrapper.langfuse_wrapper import observe_wrapper
|
|
||||||
|
|
||||||
ANTHROPIC_HOST = "https://api.anthropic.com/v1/messages"
|
ANTHROPIC_HOST = "https://api.anthropic.com/v1/messages"
|
||||||
|
|
||||||
@@ -124,7 +123,6 @@ class AnthropicProvider(Provider):
|
|||||||
messages_spec.append(converted)
|
messages_spec.append(converted)
|
||||||
return messages_spec
|
return messages_spec
|
||||||
|
|
||||||
@observe_wrapper(as_type="generation")
|
|
||||||
def complete(
|
def complete(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from exchange.providers import Provider, Usage
|
|||||||
from tenacity import retry, wait_fixed, stop_after_attempt
|
from tenacity import retry, wait_fixed, stop_after_attempt
|
||||||
from exchange.providers.utils import raise_for_status, retry_if_status
|
from exchange.providers.utils import raise_for_status, retry_if_status
|
||||||
from exchange.tool import Tool
|
from exchange.tool import Tool
|
||||||
from langfuse_wrapper.langfuse_wrapper import observe_wrapper
|
|
||||||
|
|
||||||
SERVICE = "bedrock-runtime"
|
SERVICE = "bedrock-runtime"
|
||||||
UTC = timezone.utc
|
UTC = timezone.utc
|
||||||
@@ -176,7 +175,6 @@ class BedrockProvider(Provider):
|
|||||||
)
|
)
|
||||||
return cls(client=client)
|
return cls(client=client)
|
||||||
|
|
||||||
@observe_wrapper(as_type="generation")
|
|
||||||
def complete(
|
def complete(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from exchange.providers.utils import (
|
|||||||
tools_to_openai_spec,
|
tools_to_openai_spec,
|
||||||
)
|
)
|
||||||
from exchange.tool import Tool
|
from exchange.tool import Tool
|
||||||
from langfuse_wrapper.langfuse_wrapper import observe_wrapper
|
|
||||||
|
|
||||||
retry_procedure = retry(
|
retry_procedure = retry(
|
||||||
wait=wait_fixed(2),
|
wait=wait_fixed(2),
|
||||||
@@ -69,7 +69,6 @@ class DatabricksProvider(Provider):
|
|||||||
total_tokens=total_tokens,
|
total_tokens=total_tokens,
|
||||||
)
|
)
|
||||||
|
|
||||||
@observe_wrapper(as_type="generation")
|
|
||||||
def complete(
|
def complete(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from exchange.content import Text, ToolResult, ToolUse
|
|||||||
from exchange.providers.base import Provider, Usage
|
from exchange.providers.base import Provider, Usage
|
||||||
from tenacity import retry, wait_fixed, stop_after_attempt
|
from tenacity import retry, wait_fixed, stop_after_attempt
|
||||||
from exchange.providers.utils import raise_for_status, retry_if_status
|
from exchange.providers.utils import raise_for_status, retry_if_status
|
||||||
from langfuse_wrapper.langfuse_wrapper import observe_wrapper
|
|
||||||
|
|
||||||
GOOGLE_HOST = "https://generativelanguage.googleapis.com/v1beta"
|
GOOGLE_HOST = "https://generativelanguage.googleapis.com/v1beta"
|
||||||
|
|
||||||
@@ -122,7 +121,6 @@ class GoogleProvider(Provider):
|
|||||||
|
|
||||||
return messages_spec
|
return messages_spec
|
||||||
|
|
||||||
@observe_wrapper(as_type="generation")
|
|
||||||
def complete(
|
def complete(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from exchange.providers.utils import (
|
|||||||
from exchange.tool import Tool
|
from exchange.tool import Tool
|
||||||
from tenacity import retry, wait_fixed, stop_after_attempt
|
from tenacity import retry, wait_fixed, stop_after_attempt
|
||||||
from exchange.providers.utils import retry_if_status
|
from exchange.providers.utils import retry_if_status
|
||||||
from langfuse_wrapper.langfuse_wrapper import observe_wrapper
|
|
||||||
|
|
||||||
OPENAI_HOST = "https://api.openai.com/"
|
OPENAI_HOST = "https://api.openai.com/"
|
||||||
|
|
||||||
@@ -66,7 +65,6 @@ class OpenAiProvider(Provider):
|
|||||||
total_tokens=total_tokens,
|
total_tokens=total_tokens,
|
||||||
)
|
)
|
||||||
|
|
||||||
@observe_wrapper(as_type="generation")
|
|
||||||
def complete(
|
def complete(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
lint.select = ["E", "W", "F", "N"]
|
|
||||||
line-length = 120
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Langfuse Wrapper
|
|
||||||
|
|
||||||
This package provides a wrapper for [Langfuse](https://langfuse.com/). The wrapper serves to initialize Langfuse appropriately if the Langfuse server is running locally and otherwise to skip applying the Langfuse observe descorators.
|
|
||||||
|
|
||||||
**Note: This Langfuse integration is experimental and we don't currently have integration tests for it.**
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Start your local Langfuse server
|
|
||||||
|
|
||||||
Run `setup_langfuse.sh` to start your local Langfuse server. It requires Docker.
|
|
||||||
|
|
||||||
Read more about local Langfuse deployments [here](https://langfuse.com/docs/deployment/local).
|
|
||||||
|
|
||||||
### Exchange and Goose integration
|
|
||||||
|
|
||||||
Import `from langfuse_wrapper.langfuse_wrapper import observe_wrapper` and use the `observe_wrapper()` decorator on functions you wish to enable tracing for. `observe_wrapper` functions the same way as Langfuse's observe decorator.
|
|
||||||
|
|
||||||
Read more about Langfuse's decorator-based tracing [here](https://langfuse.com/docs/sdk/python/decorators).
|
|
||||||
|
|
||||||
In Goose, initialization requires certain environment variables to be present:
|
|
||||||
|
|
||||||
- `LANGFUSE_PUBLIC_KEY`: Your Langfuse public key
|
|
||||||
- `LANGFUSE_SECRET_KEY`: Your Langfuse secret key
|
|
||||||
- `LANGFUSE_BASE_URL`: The base URL of your Langfuse instance
|
|
||||||
|
|
||||||
By default your local deployment and Goose will use the values in `env/.env.langfuse.local`.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# These variables are default initialization variables for the Langfuse server
|
|
||||||
LANGFUSE_INIT_PROJECT_NAME=goose-local
|
|
||||||
LANGFUSE_INIT_PROJECT_PUBLIC_KEY=publickey-local
|
|
||||||
LANGFUSE_INIT_PROJECT_SECRET_KEY=secretkey-local
|
|
||||||
LANGFUSE_INIT_USER_EMAIL=local@block.xyz
|
|
||||||
LANGFUSE_INIT_USER_NAME=localdev
|
|
||||||
LANGFUSE_INIT_USER_PASSWORD=localpwd
|
|
||||||
|
|
||||||
LANGFUSE_INIT_ORG_ID=local-id
|
|
||||||
LANGFUSE_INIT_ORG_NAME=local-org
|
|
||||||
LANGFUSE_INIT_PROJECT_ID=goose
|
|
||||||
|
|
||||||
# These variables are used by Goose
|
|
||||||
LANGFUSE_PUBLIC_KEY=publickey-local
|
|
||||||
LANGFUSE_SECRET_KEY=secretkey-local
|
|
||||||
LANGFUSE_HOST=http://localhost:3000
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "langfuse-wrapper"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "A wrapper for Langfuse integration"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
author = [{ name = "Block", email = "ai-oss-tools@block.xyz" }]
|
|
||||||
packages = [{ include = "langfuse_wrapper", from = "src" }]
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
"langfuse>=2.38.2",
|
|
||||||
"python-dotenv>=1.0.1"
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
|
||||||
packages = ["src/langfuse_wrapper"]
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["hatchling"]
|
|
||||||
build-backend = "hatchling.build"
|
|
||||||
|
|
||||||
[tool.uv]
|
|
||||||
dev-dependencies = ["pytest>=8.3.2"]
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
|
||||||
markers = [
|
|
||||||
"integration: marks tests that need to authenticate (deselect with '-m \"not integration\"')",
|
|
||||||
]
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# setup_langfuse.sh
|
|
||||||
#
|
|
||||||
# This script sets up and runs Langfuse locally for development and testing purposes.
|
|
||||||
#
|
|
||||||
# Key functionalities:
|
|
||||||
# 1. Downloads the latest docker-compose.yaml from the Langfuse repository
|
|
||||||
# 2. Starts Langfuse using Docker Compose with default initialization variables
|
|
||||||
# 3. Waits for the service to be available
|
|
||||||
# 4. Launches a browser to open the local Langfuse UI
|
|
||||||
# 5. Prints login credentials from the environment file
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./setup_langfuse.sh
|
|
||||||
#
|
|
||||||
# Requirements:
|
|
||||||
# - Docker
|
|
||||||
# - curl
|
|
||||||
# - A .env.langfuse.local file in the env directory
|
|
||||||
#
|
|
||||||
# Note: This script is intended for local development use only.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(realpath "$(dirname "${BASH_SOURCE[0]}")")
|
|
||||||
LANGFUSE_DOCKER_COMPOSE_URL="https://raw.githubusercontent.com/langfuse/langfuse/main/docker-compose.yml"
|
|
||||||
LANGFUSE_DOCKER_COMPOSE_FILE="docker-compose.yaml"
|
|
||||||
LANGFUSE_ENV_FILE="$SCRIPT_DIR/../env/.env.langfuse.local"
|
|
||||||
|
|
||||||
check_dependencies() {
|
|
||||||
local dependencies=("curl" "docker")
|
|
||||||
local missing_dependencies=()
|
|
||||||
|
|
||||||
for cmd in "${dependencies[@]}"; do
|
|
||||||
if ! command -v "$cmd" &> /dev/null; then
|
|
||||||
missing_dependencies+=("$cmd")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ ${#missing_dependencies[@]} -ne 0 ]; then
|
|
||||||
echo "Missing dependencies: ${missing_dependencies[*]}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
download_docker_compose() {
|
|
||||||
if ! curl --fail --location --output "$SCRIPT_DIR/docker-compose.yaml" "$LANGFUSE_DOCKER_COMPOSE_URL"; then
|
|
||||||
echo "Failed to download docker-compose file from $LANGFUSE_DOCKER_COMPOSE_URL"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
start_docker_compose() {
|
|
||||||
docker compose --env-file "$LANGFUSE_ENV_FILE" -f "$LANGFUSE_DOCKER_COMPOSE_FILE" up --detach
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_service() {
|
|
||||||
echo "Waiting for Langfuse to start..."
|
|
||||||
local retries=10
|
|
||||||
local count=0
|
|
||||||
until curl --silent http://localhost:3000 > /dev/null; do
|
|
||||||
((count++))
|
|
||||||
if [ "$count" -ge "$retries" ]; then
|
|
||||||
echo "Max retries reached. Langfuse did not start in time."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo "Langfuse is now available!"
|
|
||||||
}
|
|
||||||
|
|
||||||
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_ENV_FILE" ]; then
|
|
||||||
echo "If not already logged in use the following credentials to log in:"
|
|
||||||
grep -E "LANGFUSE_INIT_USER_EMAIL|LANGFUSE_INIT_USER_PASSWORD" "$LANGFUSE_ENV_FILE"
|
|
||||||
else
|
|
||||||
echo "Langfuse environment file with local credentials not found."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_dependencies
|
|
||||||
pushd "$SCRIPT_DIR" > /dev/null
|
|
||||||
download_docker_compose
|
|
||||||
start_docker_compose
|
|
||||||
wait_for_service
|
|
||||||
print_login_variables
|
|
||||||
launch_browser
|
|
||||||
popd > /dev/null
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from langfuse_wrapper.langfuse_wrapper import observe_wrapper # noqa
|
|
||||||
|
|
||||||
module_name = "langfuse-wrapper"
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
"""
|
|
||||||
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 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 os
|
|
||||||
from typing import Callable
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from langfuse.decorators import langfuse_context
|
|
||||||
import sys
|
|
||||||
from io import StringIO
|
|
||||||
from pathlib import Path
|
|
||||||
from functools import wraps # Add this import
|
|
||||||
|
|
||||||
|
|
||||||
def find_package_root(start_path: Path, marker_file="pyproject.toml") -> Path:
|
|
||||||
while start_path != start_path.parent:
|
|
||||||
if (start_path / marker_file).exists():
|
|
||||||
return start_path
|
|
||||||
start_path = start_path.parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def auth_check() -> bool:
|
|
||||||
# Temporarily redirect stdout and stderr to suppress print statements from Langfuse
|
|
||||||
temp_stderr = StringIO()
|
|
||||||
sys.stderr = temp_stderr
|
|
||||||
|
|
||||||
# Load environment variables
|
|
||||||
load_dotenv(LANGFUSE_ENV_FILE, override=True)
|
|
||||||
|
|
||||||
auth_val = langfuse_context.auth_check()
|
|
||||||
|
|
||||||
# Restore stderr
|
|
||||||
sys.stderr = sys.__stderr__
|
|
||||||
return auth_val
|
|
||||||
|
|
||||||
|
|
||||||
CURRENT_DIR = Path(__file__).parent
|
|
||||||
PACKAGE_ROOT = find_package_root(CURRENT_DIR)
|
|
||||||
|
|
||||||
LANGFUSE_ENV_FILE = os.path.join(PACKAGE_ROOT, "env", ".env.langfuse.local")
|
|
||||||
HAS_LANGFUSE_CREDENTIALS = False
|
|
||||||
load_dotenv(LANGFUSE_ENV_FILE, override=True)
|
|
||||||
|
|
||||||
HAS_LANGFUSE_CREDENTIALS = auth_check()
|
|
||||||
|
|
||||||
|
|
||||||
def observe_wrapper(*args, **kwargs) -> Callable: # noqa
|
|
||||||
"""
|
|
||||||
A decorator that wraps a function with Langfuse context observation if credentials are available.
|
|
||||||
|
|
||||||
If Langfuse credentials were found, the function will be wrapped with Langfuse's observe method.
|
|
||||||
Otherwise, the function will be returned as-is.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
*args: Positional arguments to pass to langfuse_context.observe.
|
|
||||||
**kwargs: Keyword arguments to pass to langfuse_context.observe.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Callable: The wrapped function if credentials are available, otherwise the original function.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _wrapper(fn: Callable) -> Callable:
|
|
||||||
if HAS_LANGFUSE_CREDENTIALS:
|
|
||||||
|
|
||||||
@wraps(fn)
|
|
||||||
def wrapped_fn(*fargs, **fkwargs):
|
|
||||||
return langfuse_context.observe(*args, **kwargs)(fn)(*fargs, **fkwargs)
|
|
||||||
|
|
||||||
return wrapped_fn
|
|
||||||
else:
|
|
||||||
return fn
|
|
||||||
|
|
||||||
return _wrapper
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
from langfuse_wrapper.langfuse_wrapper import observe_wrapper
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_langfuse_context():
|
|
||||||
with patch("langfuse_wrapper.langfuse_wrapper.langfuse_context") as mock:
|
|
||||||
yield mock
|
|
||||||
|
|
||||||
|
|
||||||
@patch("langfuse_wrapper.langfuse_wrapper.HAS_LANGFUSE_CREDENTIALS", True)
|
|
||||||
def test_function_is_wrapped(mock_langfuse_context):
|
|
||||||
mock_observe = MagicMock(side_effect=lambda *args, **kwargs: lambda fn: fn)
|
|
||||||
mock_langfuse_context.observe = mock_observe
|
|
||||||
|
|
||||||
def original_function(x: int, y: int) -> int:
|
|
||||||
return x + y
|
|
||||||
|
|
||||||
# test function before we decorate it with
|
|
||||||
# @observe_wrapper("arg1", kwarg1="kwarg1")
|
|
||||||
assert not hasattr(original_function, "__wrapped__")
|
|
||||||
|
|
||||||
# ensure we args get passed along (e.g. @observe(capture_input=False, capture_output=False))
|
|
||||||
decorated_function = observe_wrapper("arg1", kwarg1="kwarg1")(original_function)
|
|
||||||
assert hasattr(decorated_function, "__wrapped__")
|
|
||||||
assert decorated_function.__wrapped__ is original_function, "Function is not properly wrapped"
|
|
||||||
|
|
||||||
assert decorated_function(2, 3) == 5
|
|
||||||
mock_observe.assert_called_once()
|
|
||||||
mock_observe.assert_called_with("arg1", kwarg1="kwarg1")
|
|
||||||
|
|
||||||
|
|
||||||
@patch("langfuse_wrapper.langfuse_wrapper.HAS_LANGFUSE_CREDENTIALS", False)
|
|
||||||
def test_function_is_not_wrapped(mock_langfuse_context):
|
|
||||||
mock_observe = MagicMock(return_value=lambda f: f)
|
|
||||||
mock_langfuse_context.observe = mock_observe
|
|
||||||
|
|
||||||
@observe_wrapper("arg1", kwarg1="kwarg1")
|
|
||||||
def hello() -> str:
|
|
||||||
return "Hello"
|
|
||||||
|
|
||||||
assert not hasattr(hello, "__wrapped__")
|
|
||||||
assert hello() == "Hello"
|
|
||||||
|
|
||||||
mock_observe.assert_not_called()
|
|
||||||
+1
-4
@@ -12,9 +12,7 @@ dependencies = [
|
|||||||
"click>=8.1.7",
|
"click>=8.1.7",
|
||||||
"prompt-toolkit>=3.0.47",
|
"prompt-toolkit>=3.0.47",
|
||||||
"keyring>=25.4.1",
|
"keyring>=25.4.1",
|
||||||
"langfuse-wrapper",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
author = [{ name = "Block", email = "ai-oss-tools@block.xyz" }]
|
author = [{ name = "Block", email = "ai-oss-tools@block.xyz" }]
|
||||||
packages = [{ include = "goose", from = "src" }]
|
packages = [{ include = "goose", from = "src" }]
|
||||||
|
|
||||||
@@ -67,12 +65,11 @@ dev-dependencies = [
|
|||||||
"mkdocstrings-python>=1.11.1",
|
"mkdocstrings-python>=1.11.1",
|
||||||
"mkdocstrings>=0.26.1",
|
"mkdocstrings>=0.26.1",
|
||||||
"pytest-mock>=3.14.0",
|
"pytest-mock>=3.14.0",
|
||||||
"pytest>=8.3.2",
|
"pytest>=8.3.2"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
ai-exchange = { workspace = true }
|
ai-exchange = { workspace = true }
|
||||||
langfuse-wrapper = { workspace = true }
|
|
||||||
|
|
||||||
[tool.uv.workspace]
|
[tool.uv.workspace]
|
||||||
members = ["packages/*"]
|
members = ["packages/*"]
|
||||||
|
|||||||
+3
-10
@@ -136,10 +136,7 @@ def get_session_files() -> dict[str, Path]:
|
|||||||
@click.option("--profile")
|
@click.option("--profile")
|
||||||
@click.option("--plan", type=click.Path(exists=True))
|
@click.option("--plan", type=click.Path(exists=True))
|
||||||
@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="INFO")
|
@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="INFO")
|
||||||
@click.option("--tracing", is_flag=True, required=False)
|
def session_start(name: Optional[str], profile: str, log_level: str, plan: Optional[str] = None) -> None:
|
||||||
def session_start(
|
|
||||||
name: Optional[str], profile: str, log_level: str, plan: Optional[str] = None, tracing: bool = False
|
|
||||||
) -> None:
|
|
||||||
"""Start a new goose session"""
|
"""Start a new goose session"""
|
||||||
if plan:
|
if plan:
|
||||||
yaml = YAML()
|
yaml = YAML()
|
||||||
@@ -147,12 +144,8 @@ def session_start(
|
|||||||
_plan = yaml.load(f)
|
_plan = yaml.load(f)
|
||||||
else:
|
else:
|
||||||
_plan = None
|
_plan = None
|
||||||
|
session = Session(name=name, profile=profile, plan=_plan, log_level=log_level)
|
||||||
try:
|
session.run()
|
||||||
session = Session(name=name, profile=profile, plan=_plan, log_level=log_level, tracing=tracing)
|
|
||||||
session.run()
|
|
||||||
except RuntimeError as e:
|
|
||||||
print(f"[red]Error: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args(ctx: click.Context, param: click.Parameter, value: str) -> dict[str, str]:
|
def parse_args(ctx: click.Context, param: click.Parameter, value: str) -> dict[str, str]:
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import traceback
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
import logging
|
|
||||||
from langfuse.decorators import langfuse_context
|
|
||||||
|
|
||||||
from exchange import Message, ToolResult, ToolUse, Text
|
from exchange import Message, Text, ToolResult, ToolUse
|
||||||
from langfuse_wrapper.langfuse_wrapper import observe_wrapper, auth_check
|
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
@@ -65,7 +62,6 @@ class Session:
|
|||||||
profile: Optional[str] = None,
|
profile: Optional[str] = None,
|
||||||
plan: Optional[dict] = None,
|
plan: Optional[dict] = None,
|
||||||
log_level: Optional[str] = "INFO",
|
log_level: Optional[str] = "INFO",
|
||||||
tracing: bool = False,
|
|
||||||
**kwargs: dict[str, Any],
|
**kwargs: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
if name is None:
|
if name is None:
|
||||||
@@ -76,20 +72,10 @@ class Session:
|
|||||||
self.prompt_session = GoosePromptSession()
|
self.prompt_session = GoosePromptSession()
|
||||||
self.status_indicator = Status("", spinner="dots")
|
self.status_indicator = Status("", spinner="dots")
|
||||||
self.notifier = SessionNotifier(self.status_indicator)
|
self.notifier = SessionNotifier(self.status_indicator)
|
||||||
if not tracing:
|
|
||||||
logging.getLogger("langfuse").setLevel(logging.ERROR)
|
|
||||||
else:
|
|
||||||
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=tracing)
|
|
||||||
self.exchange = create_exchange(profile=load_profile(profile), notifier=self.notifier)
|
self.exchange = create_exchange(profile=load_profile(profile), notifier=self.notifier)
|
||||||
setup_logging(log_file_directory=LOG_PATH, log_level=log_level)
|
setup_logging(log_file_directory=LOG_PATH, log_level=log_level)
|
||||||
|
|
||||||
self.exchange.messages.extend(self._get_initial_messages())
|
self.exchange.messages.extend(self._get_initial_messages())
|
||||||
|
|
||||||
if len(self.exchange.messages) == 0 and plan:
|
if len(self.exchange.messages) == 0 and plan:
|
||||||
@@ -200,7 +186,6 @@ class Session:
|
|||||||
self._remove_empty_session()
|
self._remove_empty_session()
|
||||||
self._log_cost()
|
self._log_cost()
|
||||||
|
|
||||||
@observe_wrapper()
|
|
||||||
def reply(self) -> None:
|
def reply(self) -> None:
|
||||||
"""Reply to the last user message, calling tools as needed"""
|
"""Reply to the last user message, calling tools as needed"""
|
||||||
self.status_indicator.update("responding")
|
self.status_indicator.update("responding")
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ def test_session_start_command_with_session_name(mock_session):
|
|||||||
mock_session_class, mock_session_instance = mock_session
|
mock_session_class, mock_session_instance = mock_session
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
runner.invoke(goose_cli, ["session", "start", "session1", "--profile", "default"])
|
runner.invoke(goose_cli, ["session", "start", "session1", "--profile", "default"])
|
||||||
mock_session_class.assert_called_once_with(
|
mock_session_class.assert_called_once_with(name="session1", profile="default", plan=None, log_level="INFO")
|
||||||
name="session1", profile="default", plan=None, log_level="INFO", tracing=False
|
|
||||||
)
|
|
||||||
mock_session_instance.run.assert_called_once()
|
mock_session_instance.run.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user