Skip to content
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

Tracing: Added Langsmith Support #1069

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ cover.cov

# dev
.env
.envrc
vendor/*
service-account.json

embeddings/cybertron/models/*
examples/cybertron-embedding-example/models/*

1 change: 0 additions & 1 deletion callbacks/callbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
//nolint:all
type Handler interface {
HandleText(ctx context.Context, text string)
HandleLLMStart(ctx context.Context, prompts []string)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call was dead code, it was not called anywhere

HandleLLMGenerateContentStart(ctx context.Context, ms []llms.MessageContent)
HandleLLMGenerateContentEnd(ctx context.Context, res *llms.ContentResponse)
HandleLLMError(ctx context.Context, err error)
Expand Down
6 changes: 0 additions & 6 deletions callbacks/combining.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@ func (l CombiningHandler) HandleText(ctx context.Context, text string) {
}
}

func (l CombiningHandler) HandleLLMStart(ctx context.Context, prompts []string) {
for _, handle := range l.Callbacks {
handle.HandleLLMStart(ctx, prompts)
}
}

func (l CombiningHandler) HandleLLMGenerateContentStart(ctx context.Context, ms []llms.MessageContent) {
for _, handle := range l.Callbacks {
handle.HandleLLMGenerateContentStart(ctx, ms)
Expand Down
20 changes: 20 additions & 0 deletions callbacks/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package callbacks

import "context"

type contextKeyType int

// nolint: gochecknoglobals
var callbackHandlerKey = contextKeyType(0)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be const like _callbackHandlerKey to fix the lint.


func CallbackHandler(ctx context.Context) Handler {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function name is a somewhat redundant and vague. Maybe something like GetHandlerFromContext instead?

handler := ctx.Value(callbackHandlerKey)
if t, ok := handler.(Handler); ok {
return t
}
return nil
}

func WithCallback(ctx context.Context, handler Handler) context.Context {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think a name like this works outside of the context package: callbacks.WithCallback

return context.WithValue(ctx, callbackHandlerKey, handler)
}
28 changes: 28 additions & 0 deletions callbacks/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package callbacks

import (
"context"
"testing"

"github.com/stretchr/testify/require"
)

func TestCallbackHandler(t *testing.T) {
t.Parallel()
// Test case 1: Context with handler
handler := &SimpleHandler{}
ctx := WithCallback(context.Background(), handler)

got := CallbackHandler(ctx)
require.NotNil(t, got)
if got != handler {
t.Errorf("CallbackHandler() = %v, want %v", got, handler)
}

// Test case 2: Context without handler
emptyCtx := context.Background()
got = CallbackHandler(emptyCtx)
if got != nil {
t.Errorf("CallbackHandler() = %v, want nil", got)
}
}
4 changes: 0 additions & 4 deletions callbacks/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,6 @@ func (l LogHandler) HandleText(_ context.Context, text string) {
fmt.Println(text)
}

func (l LogHandler) HandleLLMStart(_ context.Context, prompts []string) {
fmt.Println("Entering LLM with prompts:", prompts)
}

func (l LogHandler) HandleLLMError(_ context.Context, err error) {
fmt.Println("Exiting LLM with error:", err)
}
Expand Down
1 change: 0 additions & 1 deletion callbacks/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ type SimpleHandler struct{}
var _ Handler = SimpleHandler{}

func (SimpleHandler) HandleText(context.Context, string) {}
func (SimpleHandler) HandleLLMStart(context.Context, []string) {}
func (SimpleHandler) HandleLLMGenerateContentStart(context.Context, []llms.MessageContent) {}
func (SimpleHandler) HandleLLMGenerateContentEnd(context.Context, *llms.ContentResponse) {}
func (SimpleHandler) HandleLLMError(context.Context, error) {}
Expand Down
31 changes: 24 additions & 7 deletions chains/chains.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type Chain interface {

// Call is the standard function used for executing chains.
func Call(ctx context.Context, c Chain, inputValues map[string]any, options ...ChainCallOption) (map[string]any, error) { // nolint: lll
ctx = setupChainCallbackHandler(ctx, c, options)

fullValues := make(map[string]any, 0)
for key, value := range inputValues {
fullValues[key] = value
Expand All @@ -42,21 +44,22 @@ func Call(ctx context.Context, c Chain, inputValues map[string]any, options ...C
fullValues[key] = value
}

callbacksHandler := getChainCallbackHandler(c)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also check if there is a callback handler in the chain

if callbacksHandler != nil {
callbacksHandler.HandleChainStart(ctx, inputValues)
chainCallbackHandlers := callbacks.CallbackHandler(ctx)

if chainCallbackHandlers != nil {
chainCallbackHandlers.HandleChainStart(ctx, inputValues)
}

outputValues, err := callChain(ctx, c, fullValues, options...)
if err != nil {
if callbacksHandler != nil {
callbacksHandler.HandleChainError(ctx, err)
if chainCallbackHandlers != nil {
chainCallbackHandlers.HandleChainError(ctx, err)
}
return outputValues, err
}

if callbacksHandler != nil {
callbacksHandler.HandleChainEnd(ctx, outputValues)
if chainCallbackHandlers != nil {
chainCallbackHandlers.HandleChainEnd(ctx, outputValues) // Call the chain end
}

if err = c.GetMemory().SaveContext(ctx, inputValues, outputValues); err != nil {
Expand All @@ -66,6 +69,20 @@ func Call(ctx context.Context, c Chain, inputValues map[string]any, options ...C
return outputValues, nil
}

// setupChainCallbackHandler sets up the chain callback handler in the context, which will be passed down to Chain calls.
// we will prioritize a callback handler set in the options, and fallback to a chain specific if set.
func setupChainCallbackHandler(ctx context.Context, c Chain, options []ChainCallOption) context.Context {
// if callback handler is set in options, prioritize that
if handler := getChainCallCallbackHandler(options); handler != nil {
return callbacks.WithCallback(ctx, handler)
}

if handler := getChainCallbackHandler(c); handler != nil {
return callbacks.WithCallback(ctx, handler)
}
return ctx
}

func callChain(
ctx context.Context,
c Chain,
Expand Down
8 changes: 8 additions & 0 deletions chains/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,11 @@ func getLLMCallOptions(options ...ChainCallOption) []llms.CallOption { //nolint:

return chainCallOption
}

func getChainCallCallbackHandler(options []ChainCallOption) callbacks.Handler {
opts := &chainCallOption{}
for _, option := range options {
option(opts)
}
return opts.CallbackHandler
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ func main() {
log.Fatal(err)
}
fmt.Println(resp.Choices[0].Content)

}

// executeToolCalls executes the tool calls in the response and returns the
Expand Down
45 changes: 45 additions & 0 deletions examples/langsmith-llm-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# LangSmith Tracing Example with OpenAI

Welcome to this example of using LangSmith tracing with OpenAI and Go! 🎉

This project demonstrates how to integrate LangSmith tracing into your LangChain Go applications, allowing you to monitor and debug your LLM interactions.

## What This Example Does

This example showcases several key features:

1. 🤖 Sets up a connection to OpenAI's GPT-4 model
2. 📊 Configures LangSmith tracing for monitoring LLM interactions
3. 🌐 Creates a translation chain that converts text between languages
4. 📝 Logs all langchain interactions using a custom logger

## How It Works

1. Creates an OpenAI client with the GPT-4 model
2. Sets up LangSmith client and tracer with:
- API key configuration
- Custom logging
- Project name tracking
3. Creates a translation chain with:
- System prompt defining the AI as a translation expert
- Human prompt template for translation requests
4. Executes the chain with tracing enabled

## Running the Example

To run this example, you'll need:

1. Go installed on your system
2. Environment variables set up:
- `OPENAI_API_KEY` - Your OpenAI API key
- `LANGCHAIN_API_KEY` - Your LangSmith API key
- `LANGCHAIN_PROJECT` - Your LangSmith project name (optional)

You can also provide the LangSmith configuration via flags:
```bash
go run . --langchain-api-key=your_key --langchain-project=your_project
```

## Example Output

The program will output the translation results in JSON format, and all interactions will be traced in your LangSmith dashboard.
41 changes: 41 additions & 0 deletions examples/langsmith-llm-example/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module github.com/tmc/langchaingo/examples/openai-gpt4o-example

go 1.22.0

toolchain go1.22.1

require github.com/tmc/langchaingo v0.1.13-pre.0

require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
golang.org/x/sys v0.20.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

// test for new pkg version
replace github.com/tmc/langchaingo => ../..
Loading