diff --git a/agents/models_openai_responses.go b/agents/models_openai_responses.go index aca3152..7bb3299 100644 --- a/agents/models_openai_responses.go +++ b/agents/models_openai_responses.go @@ -53,7 +53,12 @@ func (m OpenAIResponsesModel) GetResponse( var response *responses.Response err := tracing.ResponseSpan( - ctx, tracing.ResponseSpanParams{Disabled: params.Tracing.IsDisabled()}, + ctx, tracing.ResponseSpanParams{ + Disabled: params.Tracing.IsDisabled(), + Input: params.Input, + Model: string(m.Model), + Tools: params.Tools, + }, func(ctx context.Context, spanResponse tracing.Span) (err error) { defer func() { if err != nil { @@ -137,7 +142,12 @@ func (m OpenAIResponsesModel) StreamResponse( yield ModelStreamResponseCallback, ) error { return tracing.ResponseSpan( - ctx, tracing.ResponseSpanParams{Disabled: params.Tracing.IsDisabled()}, + ctx, tracing.ResponseSpanParams{ + Disabled: params.Tracing.IsDisabled(), + Input: params.Input, + Model: string(m.Model), + Tools: params.Tools, + }, func(ctx context.Context, spanResponse tracing.Span) (err error) { defer func() { if err != nil { diff --git a/examples/tracing/traceloop/main.go b/examples/tracing/traceloop/main.go new file mode 100644 index 0000000..ce18077 --- /dev/null +++ b/examples/tracing/traceloop/main.go @@ -0,0 +1,211 @@ +// Copyright 2025 The NLP Odyssey Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/nlpodyssey/openai-agents-go/agents" + "github.com/nlpodyssey/openai-agents-go/tracing" + "github.com/nlpodyssey/openai-agents-go/tracing/wrappers/traceloop" +) + +// Simple test tool for demonstration +type TestToolArgs struct { + Message string `json:"message"` +} + +func TestTool(_ context.Context, args TestToolArgs) (string, error) { + return fmt.Sprintf("Tool executed with message: %s", args.Message), nil +} + +var testTool = agents.NewFunctionTool("test_tool", "A simple test tool", TestTool) + +// Test tool for verifying tool call extraction +type EchoArgs struct { + Message string `json:"message"` +} + +func EchoTool(_ context.Context, args EchoArgs) (string, error) { + fmt.Printf("[TOOL EXECUTED] Echo: %s\n", args.Message) + return fmt.Sprintf("Echo: %s", args.Message), nil +} + +var echoTool = agents.NewFunctionTool("echo", "Echoes the input message", EchoTool) + +func testToolCalling() { + apiKey := "tl_4be59d06bb644ced90f8b21e2924a31e" + + ctx := context.Background() + + // Create the Traceloop processor + traceloopProcessor, err := traceloop.NewTracingProcessor(ctx, traceloop.ProcessorParams{ + APIKey: apiKey, + BaseURL: "api.traceloop.com", + Metadata: map[string]any{ + "environment": "test", + "version": "1.0.0", + }, + Tags: []string{"tool-test", "golang"}, + }) + if err != nil { + fmt.Printf("Failed to create Traceloop processor: %v\n", err) + return + } + + tracing.AddTraceProcessor(traceloopProcessor) + + // Create agent that is forced to use the tool + agent := agents.New("Tool Test Agent"). + WithInstructions("You MUST use the echo tool to repeat any message the user gives you. Always call the tool."). + WithTools(echoTool). + WithModel("gpt-4o-mini") + + // Run the test + err = tracing.RunTrace(ctx, tracing.TraceParams{ + WorkflowName: "Tool Calling Test", + TraceID: "trace_" + fmt.Sprintf("test_tool_%d", 123456), + }, func(ctx context.Context, trace tracing.Trace) error { + fmt.Println("Testing tool calling with traceloop integration...") + + result, err := agents.Run(ctx, agent, "Echo this message: 'Tool calling works!'") + if err != nil { + return fmt.Errorf("agent run failed: %w", err) + } + + fmt.Printf("Agent Response: %s\n", result.FinalOutput) + return nil + }) + + if err != nil { + fmt.Printf("Test failed: %v\n", err) + return + } + + fmt.Println("Tool calling test completed!") +} + +func main() { + // Set your Traceloop API key here or via environment variable + apiKey := "tl_4be59d06bb644ced90f8b21e2924a31e" + if apiKey == "" { + fmt.Println("Warning: TRACELOOP_API_KEY environment variable not set.") + fmt.Println("Please set your Traceloop API key to enable tracing.") + fmt.Println("You can get an API key from: https://app.traceloop.com") + return + } + + ctx := context.Background() + + // Create the Traceloop processor + traceloopProcessor, err := traceloop.NewTracingProcessor(ctx, traceloop.ProcessorParams{ + APIKey: apiKey, + BaseURL: "api.traceloop.com", // Use "api-staging.traceloop.com" for staging + Metadata: map[string]any{ + "environment": "development", + "version": "1.0.0", + }, + Tags: []string{"demo", "golang", "openai-agents"}, + }) + if err != nil { + fmt.Printf("Failed to create Traceloop processor: %v\n", err) + return + } + + // Add the Traceloop processor to the tracing system + // This will work alongside the default OpenAI backend processor + tracing.AddTraceProcessor(traceloopProcessor) + + // Create agents for demonstration + weatherAgent := agents.New("Weather Agent"). + WithInstructions("You are a helpful weather assistant. Always respond in a friendly manner."). + WithTools(agents.WebSearchTool{}). + WithModel("gpt-4o-mini") + + jokeAgent := agents.New("Joke Agent"). + WithInstructions("You tell funny jokes and make people laugh. Use the test tool if you need to send a message."). + WithTools(testTool). + WithModel("gpt-4o-mini") + + // Main agent with handoffs + mainAgent := agents.New("Assistant"). + WithInstructions(`You are a helpful assistant. + - For weather questions, handoff to the Weather Agent + - For jokes or funny requests, handoff to the Joke Agent + - For other questions, answer directly`). + WithAgentHandoffs(weatherAgent, jokeAgent). + WithModel("gpt-4o-mini") + + // Run a trace that demonstrates various agent interactions + err = tracing.RunTrace( + ctx, + tracing.TraceParams{ + WorkflowName: "Traceloop Integration Demo", + GroupID: "demo-conversation-001", + Metadata: map[string]any{ + "user_id": "demo-user", + "session_id": "demo-session-001", + "source": "traceloop-example", + }, + }, + func(ctx context.Context, trace tracing.Trace) error { + fmt.Printf("Starting demo workflow - Trace ID: %s\n", trace.TraceID()) + + // First interaction - simple question + result1, err := agents.Run(ctx, mainAgent, "Hello! Can you introduce yourself?") + if err != nil { + return err + } + fmt.Printf("Introduction: %s\n\n", result1.FinalOutput) + + // Second interaction - weather query + result2, err := agents.Run(ctx, mainAgent, "What's the weather like in New York today?") + if err != nil { + return err + } + fmt.Printf("Weather Response: %s\n\n", result2.FinalOutput) + + // Third interaction - joke request with explicit tool usage + result3, err := agents.Run(ctx, mainAgent, "Tell me a programming joke! When you handoff to the joke agent, make sure they use the test tool to send a greeting message.") + if err != nil { + return err + } + fmt.Printf("Joke Response: %s\n\n", result3.FinalOutput) + + return nil + }, + ) + + if err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + + fmt.Println("Demo completed! Check your Traceloop dashboard for the trace data.") + + // Shutdown the processor to ensure all data is flushed + traceloopProcessor.Shutdown(ctx) + + // Note about viewing traces + fmt.Println("\nYou can view your traces at: https://app.traceloop.com") + fmt.Println("The traces will appear as workflows with tasks for each agent interaction.") + fmt.Println("LLM calls will be captured with full prompt and response data.") + + // Also run tool calling test + fmt.Println("\n--- Running Tool Calling Test ---") + testToolCalling() +} diff --git a/examples/tracing/traceloop/tool_test.go b/examples/tracing/traceloop/tool_test.go new file mode 100644 index 0000000..c183463 --- /dev/null +++ b/examples/tracing/traceloop/tool_test.go @@ -0,0 +1,87 @@ +// Copyright 2025 The NLP Odyssey Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log" + + "github.com/nlpodyssey/openai-agents-go/agents" + "github.com/nlpodyssey/openai-agents-go/tracing" + "github.com/nlpodyssey/openai-agents-go/tracing/wrappers/traceloop" +) + +// Test tool for verifying tool call extraction +type EchoArgs struct { + Message string `json:"message"` +} + +func EchoTool(_ context.Context, args EchoArgs) (string, error) { + fmt.Printf("[TOOL EXECUTED] Echo: %s\n", args.Message) + return fmt.Sprintf("Echo: %s", args.Message), nil +} + +var echoTool = agents.NewFunctionTool("echo", "Echoes the input message", EchoTool) + +func testToolCalling() { + apiKey := "tl_4be59d06bb644ced90f8b21e2924a31e" + + ctx := context.Background() + + // Create the Traceloop processor + traceloopProcessor, err := traceloop.NewTracingProcessor(ctx, traceloop.ProcessorParams{ + APIKey: apiKey, + BaseURL: "api.traceloop.com", + Metadata: map[string]any{ + "environment": "test", + "version": "1.0.0", + }, + Tags: []string{"tool-test", "golang"}, + }) + if err != nil { + log.Fatalf("Failed to create Traceloop processor: %v", err) + } + + tracing.AddTraceProcessor(traceloopProcessor) + + // Create agent that is forced to use the tool + agent := agents.New("Tool Test Agent"). + WithInstructions("You MUST use the echo tool to repeat any message the user gives you. Always call the tool."). + WithTools(echoTool). + WithModel("gpt-4o-mini") + + // Run the test + err = tracing.RunTrace(ctx, tracing.TraceParams{ + WorkflowName: "Tool Calling Test", + TraceID: "test-tool-trace-" + fmt.Sprintf("%d", 123456), + }, func(ctx context.Context, trace tracing.Trace) error { + fmt.Println("Testing tool calling with traceloop integration...") + + result, err := agents.Run(ctx, agent, "Echo this message: 'Tool calling works!'") + if err != nil { + return fmt.Errorf("agent run failed: %w", err) + } + + fmt.Printf("Agent Response: %s\n", result.FinalOutput) + return nil + }) + + if err != nil { + log.Fatalf("Test failed: %v", err) + } + + fmt.Println("Tool calling test completed!") +} \ No newline at end of file diff --git a/go.mod b/go.mod index 0ebdc2a..7c8cbe0 100644 --- a/go.mod +++ b/go.mod @@ -30,32 +30,69 @@ require ( github.com/openai/openai-go/v2 v2.0.2 github.com/playwright-community/playwright-go v0.5200.0 github.com/stretchr/testify v1.10.0 + github.com/traceloop/go-openllmetry/traceloop-sdk v0.1.0 github.com/xeipuuv/gojsonschema v1.2.0 ) require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/cenkalti/backoff v2.2.1+incompatible // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/go-audio/riff v1.0.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.11.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jinzhu/copier v0.4.0 // indirect + github.com/kluctl/go-embed-python v0.0.0-3.11.6-20231002-1 // indirect + github.com/kluctl/go-jinja2 v0.0.0-20240108142937-8839259d2537 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/sashabaranov/go-openai v1.18.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/traceloop/go-openllmetry/semconv-ai v0.0.0-20250405130248-6b2b4b41102b // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect golang.org/x/crypto v0.37.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.24.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect + google.golang.org/grpc v1.60.1 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 280ccc1..21d16d6 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,12 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -14,10 +19,29 @@ github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g= github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -27,6 +51,13 @@ github.com/gordonklaus/portaudio v0.0.0-20250206071425-98a94950218b h1:WEuQWBxel github.com/gordonklaus/portaudio v0.0.0-20250206071425-98a94950218b/go.mod h1:esZFQEUwqC+l76f2R8bIWSwXMaPbp79PppwZ1eJhFco= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -37,6 +68,14 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/kluctl/go-embed-python v0.0.0-3.11.6-20231002-1 h1:L+ZH/eN5gE7eh3BTye/Z8td8YjbhEs6hzybVByz2twQ= +github.com/kluctl/go-embed-python v0.0.0-3.11.6-20231002-1/go.mod h1:2/V+QZL7VyhTXtKHorARyA7UYOizVV37M8kkXMEk+Kg= +github.com/kluctl/go-jinja2 v0.0.0-20240108142937-8839259d2537 h1:oG9FYqprfbAI9kQtec4D0gPwJqLJlS+euknEVz25gp0= +github.com/kluctl/go-jinja2 v0.0.0-20240108142937-8839259d2537/go.mod h1:7FmUmt2zgHJfJE82ZNY/AHNGsGdyHBaF3OA12r4Zj+8= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -53,12 +92,18 @@ github.com/modelcontextprotocol/go-sdk v0.2.0 h1:PESNYOmyM1c369tRkzXLY5hHrazj8x9 github.com/modelcontextprotocol/go-sdk v0.2.0/go.mod h1:0sL9zUKKs2FTTkeCCVnKqbLJTw5TScefPAzojjU459E= github.com/openai/openai-go/v2 v2.0.2 h1:DlB9pnhhSRm2NuQNijB3j2U8fhDSk3sFX9ULK5hUs0o= github.com/openai/openai-go/v2 v2.0.2/go.mod h1:sIUkR+Cu/PMUVkSKhkk742PRURkQOCFhiwJ7eRSBqmk= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/playwright-community/playwright-go v0.5200.0 h1:z/5LGuX2tBrg3ug1HupMXLjIG93f1d2MWdDsNhkMQ9c= github.com/playwright-community/playwright-go v0.5200.0/go.mod h1:UnnyQZaqUOO5ywAZu60+N4EiWReUqX1MQBBA3Oofvf8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sashabaranov/go-openai v1.18.1 h1:AnLoJrFaFtcUYWCtz+8V0zrlXxkiwqpWlAmCAZUnDNQ= +github.com/sashabaranov/go-openai v1.18.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= @@ -76,6 +121,10 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/traceloop/go-openllmetry/semconv-ai v0.0.0-20250405130248-6b2b4b41102b h1:+U2PMGQGDoxvikp1nxkLaPlIDI37qcm9GEDjlibSR60= +github.com/traceloop/go-openllmetry/semconv-ai v0.0.0-20250405130248-6b2b4b41102b/go.mod h1:+e6rTO5swnV2JCuTc/fGSv8NMaKdgFatugW3DkVjv58= +github.com/traceloop/go-openllmetry/traceloop-sdk v0.1.0 h1:KmNwe4DuQCZHFLO5vOvVfKNvGdQ5Ardrw/iC2cnfIC4= +github.com/traceloop/go-openllmetry/traceloop-sdk v0.1.0/go.mod h1:+O+gQjXMq2zczhQIekWCb/Rjzq8mr0MUiv9dKP0Tn5Y= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -88,6 +137,26 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 h1:H2JFgRcGiyHg7H7bwcwaQJYrNFqCqrbTQ8K4p1OvDu8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0/go.mod h1:WfCWp1bGoYK8MeULtI15MmQVczfR+bFkk0DF3h06QmQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= @@ -100,6 +169,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -109,10 +180,13 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -133,9 +207,24 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 h1:SeZZZx0cP0fqUyA+oRzP9k7cSwJlvDFiROO72uwD6i0= +google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97/go.mod h1:t1VqOqqvce95G3hIDCT5FeO3YUc6Q4Oe24L/+rNMxRk= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tracing/create.go b/tracing/create.go index 058cc45..5c7bebd 100644 --- a/tracing/create.go +++ b/tracing/create.go @@ -17,6 +17,7 @@ package tracing import ( "cmp" "context" + "reflect" "github.com/openai/openai-go/v2/responses" ) @@ -219,6 +220,12 @@ func GenerationSpan(ctx context.Context, params GenerationSpanParams, fn func(co type ResponseSpanParams struct { // The OpenAI Response object. Response *responses.Response + // Optional input messages for tracing processors that need prompt context + Input any + // The request model name for tracing processors + Model string + // Optional list of tool definitions available during this request + Tools any // The ID of the span. Optional. If not provided, we will generate an ID. // We recommend using GenSpanID to generate a span ID, to guarantee that // IDs are correctly formatted. @@ -237,6 +244,9 @@ type ResponseSpanParams struct { func NewResponseSpan(ctx context.Context, params ResponseSpanParams) Span { spanData := &ResponseSpanData{ Response: params.Response, + Input: params.Input, + Model: params.Model, + Tools: processToolsForSerialization(params.Tools), } return GetTraceProvider().CreateSpan( ctx, @@ -247,6 +257,149 @@ func NewResponseSpan(ctx context.Context, params ResponseSpanParams) Span { ) } +// processToolsForSerialization converts tools to a serializable format +func processToolsForSerialization(tools any) []map[string]interface{} { + if tools == nil { + return nil + } + + v := reflect.ValueOf(tools) + if v.Kind() != reflect.Slice { + return nil + } + + var toolDefinitions []map[string]interface{} + + for i := 0; i < v.Len(); i++ { + tool := v.Index(i).Interface() + if toolDef := createSerializableToolDefinition(tool); toolDef != nil { + toolDefinitions = append(toolDefinitions, toolDef) + } + } + + return toolDefinitions +} + +// createSerializableToolDefinition creates a serializable tool definition +func createSerializableToolDefinition(tool any) map[string]interface{} { + if tool == nil { + return nil + } + + // Get the tool name using the ToolName() method + toolNameMethod := reflect.ValueOf(tool).MethodByName("ToolName") + if !toolNameMethod.IsValid() { + return nil + } + + toolNameResults := toolNameMethod.Call(nil) + if len(toolNameResults) == 0 || toolNameResults[0].Kind() != reflect.String { + return nil + } + toolName := toolNameResults[0].String() + + result := map[string]interface{}{ + "type": "function", + "function": map[string]interface{}{ + "name": toolName, + }, + } + + // Use reflection to access the tool's fields + v := reflect.ValueOf(tool) + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return result + } + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + return result + } + + // Extract description for FunctionTool + if v.Type().Name() == "FunctionTool" { + if descField := v.FieldByName("Description"); descField.IsValid() && descField.Kind() == reflect.String { + if desc := descField.String(); desc != "" { + result["function"].(map[string]interface{})["description"] = desc + } + } + + // Extract ParamsJSONSchema and sanitize it + if paramsField := v.FieldByName("ParamsJSONSchema"); paramsField.IsValid() && !paramsField.IsNil() { + if paramsInterface := paramsField.Interface(); paramsInterface != nil { + if paramsMap, ok := paramsInterface.(map[string]any); ok && paramsMap != nil { + parameters := sanitizeParametersForSerialization(paramsMap) + result["function"].(map[string]interface{})["parameters"] = parameters + } + } + } + } + + return result +} + +// sanitizeParametersForSerialization removes non-serializable fields from tool parameters +func sanitizeParametersForSerialization(params map[string]any) map[string]interface{} { + result := make(map[string]interface{}) + + for key, value := range params { + if isSerializable(value) { + if nestedMap, ok := value.(map[string]any); ok { + result[key] = sanitizeParametersForSerialization(nestedMap) + } else { + result[key] = value + } + } + } + + return result +} + +// isSerializable checks if a value can be JSON marshaled +func isSerializable(value any) bool { + if value == nil { + return true + } + + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Func, reflect.Chan, reflect.UnsafePointer: + return false + case reflect.Ptr: + if v.IsNil() { + return true + } + return isSerializable(v.Elem().Interface()) + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + if !isSerializable(v.Index(i).Interface()) { + return false + } + } + return true + case reflect.Map: + for _, key := range v.MapKeys() { + if !isSerializable(key.Interface()) || !isSerializable(v.MapIndex(key).Interface()) { + return false + } + } + return true + case reflect.Struct: + // For structs, we need to check each field + t := v.Type() + for i := 0; i < v.NumField(); i++ { + if t.Field(i).IsExported() && !isSerializable(v.Field(i).Interface()) { + return false + } + } + return true + default: + return true + } +} + func ResponseSpan(ctx context.Context, params ResponseSpanParams, fn func(context.Context, Span) error) error { return NewResponseSpan(ctx, params).Run(ctx, fn) } diff --git a/tracing/span_data.go b/tracing/span_data.go index 040a03a..71fa04b 100644 --- a/tracing/span_data.go +++ b/tracing/span_data.go @@ -124,7 +124,7 @@ func (sd GenerationSpanData) Export() map[string]any { } // ResponseSpanData represents a Response Span in the trace. -// Includes response and input. +// Includes response, input, request model information, and tool definitions. type ResponseSpanData struct { // Optional response. Response *responses.Response @@ -133,6 +133,15 @@ type ResponseSpanData struct { // This is not used by the OpenAI trace processors, but is useful for // other tracing processor implementations. Input any + + // Optional request model name. + // This is useful for tracing processors to track what model was requested, + // especially when the response model might be different or missing. + Model string + + // Optional tool definitions available during this request. + // This should be a serializable representation of tool schemas. + Tools []map[string]interface{} } func (ResponseSpanData) Type() string { return "response" } @@ -142,9 +151,15 @@ func (sd ResponseSpanData) Export() map[string]any { if sd.Response != nil { responseID = sd.Response.ID } + var model any + if sd.Model != "" { + model = sd.Model + } return map[string]any{ "type": sd.Type(), "response_id": responseID, + "model": model, + "tools": sd.Tools, } } diff --git a/tracing/wrappers/traceloop/README.md b/tracing/wrappers/traceloop/README.md new file mode 100644 index 0000000..ed0a735 --- /dev/null +++ b/tracing/wrappers/traceloop/README.md @@ -0,0 +1,225 @@ +# Traceloop Integration for OpenAI Agents Go + +This integration allows you to send traces from the OpenAI Agents Go SDK to [Traceloop](https://www.traceloop.com/openllmetry), an open-source observability platform for LLM applications based on OpenTelemetry. + +## Features + +- **Complete trace integration**: Captures all agent interactions, function calls, and LLM generations +- **Workflow and task organization**: Maps traces to Traceloop workflows and spans to tasks +- **LLM call tracking**: Detailed prompt and completion logging with usage statistics +- **OpenTelemetry based**: Built on OpenTelemetry standards for maximum compatibility +- **Multiple destinations**: Send to Traceloop, Datadog, Honeycomb, and [20+ other platforms](https://github.com/traceloop/go-openllmetry#-supported-and-tested-destinations) + +## Setup + +### 1. Install Dependencies + +First, install the Traceloop SDK: + +```bash +go get github.com/traceloop/go-openllmetry/traceloop-sdk +``` + +### 2. Get API Key + +Sign up at [Traceloop](https://app.traceloop.com) to get your API key. + +### 3. Environment Variables + +Set your Traceloop API key: + +```bash +export TRACELOOP_API_KEY="your-traceloop-api-key" +``` + +### 4. Run the Example + +```bash +cd examples/tracing/traceloop +go run main.go +``` + +## Usage + +### Basic Integration + +```go +package main + +import ( + "context" + "os" + + "github.com/nlpodyssey/openai-agents-go/agents" + "github.com/nlpodyssey/openai-agents-go/tracing" + "github.com/nlpodyssey/openai-agents-go/tracing/wrappers/traceloop" +) + +func main() { + ctx := context.Background() + + // Create the Traceloop processor + traceloopProcessor, err := traceloop.NewTracingProcessor(ctx, traceloop.ProcessorParams{ + APIKey: os.Getenv("TRACELOOP_API_KEY"), + BaseURL: "api.traceloop.com", + Metadata: map[string]any{ + "environment": "production", + "version": "1.0.0", + }, + Tags: []string{"production", "golang"}, + }) + if err != nil { + panic(err) + } + + // Add to tracing system (works alongside default OpenAI processor) + tracing.AddTraceProcessor(traceloopProcessor) + + // Or replace default processors entirely + // tracing.SetTraceProcessors([]tracing.Processor{traceloopProcessor}) + + // Use agents normally - tracing happens automatically + agent := agents.New("Assistant").WithModel("gpt-4o-mini") + result, err := agents.Run(ctx, agent, "Hello!") + // ... rest of your code +} +``` + +### Configuration Options + +```go +params := traceloop.ProcessorParams{ + APIKey: "your-api-key", // Required + BaseURL: "api.traceloop.com", // Default, use "api-staging.traceloop.com" for staging + Metadata: map[string]any{ // Optional metadata for all workflows + "user_id": "123", + "environment": "prod", + }, + Tags: []string{"tag1", "tag2"}, // Optional tags for all workflows +} +``` + +## What Gets Traced + +The Traceloop processor automatically captures: + +- **Workflows**: Complete agent execution flows with metadata +- **Tasks**: Individual agent spans, function calls, and LLM generations +- **LLM Calls**: Full prompt and completion data with usage statistics +- **Agent Handoffs**: Transitions between different agents +- **Tool Usage**: Function calls with inputs and outputs +- **Error Tracking**: Span errors and failures + +## Data Mapping + +| OpenAI Agents Concept | Traceloop Concept | Notes | +|----------------------|-------------------|-------| +| Trace | Workflow | Root-level workflow execution | +| Agent Span | Task | Agent execution task | +| Function Span | Task | Tool/function call task | +| Generation/Response Span | Task + LLM Span | LLM call with prompt/completion | +| Span hierarchy | Task nesting | Maintained within workflows | +| Metadata | Workflow attributes | User ID, session ID, etc. | + +## Viewing Traces + +After running your application: + +1. Go to [Traceloop Dashboard](https://app.traceloop.com) +2. Navigate to your project +3. View workflows in the "Traces" section +4. Explore task hierarchy and LLM interactions +5. Analyze performance, costs, and debug issues + +## Advanced Usage + +### Multiple Processors + +You can run multiple tracing processors simultaneously: + +```go +// Send to both Traceloop and OpenAI backend +tracing.AddTraceProcessor(traceloopProcessor) +tracing.AddTraceProcessor(customProcessor) +``` + +### Custom Metadata Per Trace + +```go +err := tracing.RunTrace( + ctx, + tracing.TraceParams{ + WorkflowName: "Custom Workflow", + GroupID: "conversation-123", // Links related traces + Metadata: map[string]any{ + "user_id": "abc", + "session_id": "xyz", + "custom_field": "value", + }, + }, + func(ctx context.Context, trace tracing.Trace) error { + // Your workflow logic + return nil + }, +) +``` + +### Alternative Destinations + +Since Traceloop is built on OpenTelemetry, you can easily send data to other platforms. See the [Traceloop documentation](https://github.com/traceloop/go-openllmetry#-supported-and-tested-destinations) for supported destinations including: + +- Datadog +- Honeycomb +- New Relic +- Grafana +- Langfuse +- And many more... + +## Troubleshooting + +### Common Issues + +1. **Missing API Key**: Set `TRACELOOP_API_KEY` environment variable +2. **Import Errors**: Run `go get github.com/traceloop/go-openllmetry/traceloop-sdk` +3. **Network Issues**: Check firewall settings for `api.traceloop.com` +4. **Empty Traces**: Ensure agents are making LLM calls (not just returning cached responses) + +### Debug Mode + +Enable debug logging to see what's being sent to Traceloop: + +```go +// Add debug logging in your processor initialization +fmt.Printf("Traceloop processor created successfully\n") +``` + +## Comparison with LangSmith + +| Feature | Traceloop | LangSmith | +|---------|-----------|-----------| +| **Base Technology** | OpenTelemetry | Custom API | +| **Destinations** | 20+ platforms | LangSmith only | +| **Open Source** | ✅ | ❌ | +| **Self-hosting** | ✅ | Limited | +| **Enterprise Features** | Available | Available | +| **Learning Curve** | Moderate | Easy | +| **Integration Effort** | Low | Low | + +## Performance Considerations + +- OpenTelemetry-based architecture is highly optimized +- Background processing with automatic batching +- Non-blocking operation (won't slow down your main workflow) +- Configurable sampling rates available +- Memory-efficient span tracking + +## License + +Same as the OpenAI Agents Go SDK - Apache License 2.0. + +## Links + +- [Traceloop Website](https://www.traceloop.com) +- [Traceloop Go SDK](https://github.com/traceloop/go-openllmetry) +- [OpenTelemetry Go](https://opentelemetry.io/docs/languages/go/) +- [Supported Destinations](https://github.com/traceloop/go-openllmetry#-supported-and-tested-destinations) \ No newline at end of file diff --git a/tracing/wrappers/traceloop/processor.go b/tracing/wrappers/traceloop/processor.go new file mode 100644 index 0000000..f8b0472 --- /dev/null +++ b/tracing/wrappers/traceloop/processor.go @@ -0,0 +1,848 @@ +// Copyright 2025 The NLP Odyssey Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package traceloop + +import ( + "context" + "fmt" + "os" + "reflect" + "sync" + + "github.com/nlpodyssey/openai-agents-go/tracing" + "github.com/openai/openai-go/v2/responses" + sdk "github.com/traceloop/go-openllmetry/traceloop-sdk" +) + +// TracingProcessor implements tracing.Processor to send traces to Traceloop +type TracingProcessor struct { + client *sdk.Traceloop + + // Track workflows and tasks for parent-child relationships + workflows map[string]*sdk.Workflow + tasks map[string]*sdk.Task + llmSpans map[string]*sdk.LLMSpan + mu sync.RWMutex +} + +// ProcessorParams configuration for the Traceloop processor +type ProcessorParams struct { + // Traceloop API key. Required - pass from main + APIKey string + // Traceloop Base URL. Defaults to api.traceloop.com + BaseURL string + // Optional metadata to attach to all workflows + Metadata map[string]any + // Optional tags to attach to all workflows + Tags []string +} + +// NewTracingProcessor creates a new Traceloop tracing processor +func NewTracingProcessor(ctx context.Context, params ProcessorParams) (*TracingProcessor, error) { + baseURL := params.BaseURL + if baseURL == "" { + baseURL = "api.traceloop.com" + } + + client, err := sdk.NewClient(ctx, sdk.Config{ + BaseURL: baseURL, + APIKey: params.APIKey, + }) + if err != nil { + return nil, fmt.Errorf("failed to create Traceloop client: %w", err) + } + + return &TracingProcessor{ + client: client, + workflows: make(map[string]*sdk.Workflow), + tasks: make(map[string]*sdk.Task), + llmSpans: make(map[string]*sdk.LLMSpan), + }, nil +} + +// OnTraceStart implements tracing.Processor +func (p *TracingProcessor) OnTraceStart(ctx context.Context, trace tracing.Trace) error { + if p.client == nil { + fmt.Fprintf(os.Stderr, "Traceloop client not initialized, skipping trace export\n") + return nil + } + + workflowName := trace.Name() + if workflowName == "" { + workflowName = "Agent workflow" + } + + // Create workflow attributes + attrs := sdk.WorkflowAttributes{ + Name: workflowName, + } + + // Add metadata from trace + if traceDict := trace.Export(); traceDict != nil { + if metadata, ok := traceDict["metadata"].(map[string]string); ok { + // Convert metadata to workflow attributes if needed + for k, v := range metadata { + attrs.AssociationProperties[k] = v + } + } + } + + workflow := p.client.NewWorkflow(ctx, attrs) + + p.mu.Lock() + p.workflows[trace.TraceID()] = workflow + p.mu.Unlock() + + return nil +} + +// OnTraceEnd implements tracing.Processor +func (p *TracingProcessor) OnTraceEnd(ctx context.Context, trace tracing.Trace) error { + if p.client == nil { + return nil + } + + p.mu.Lock() + workflow, exists := p.workflows[trace.TraceID()] + if exists { + delete(p.workflows, trace.TraceID()) + } + p.mu.Unlock() + + if exists && workflow != nil { + workflow.End() + } + + return nil +} + +// OnSpanStart implements tracing.Processor +func (p *TracingProcessor) OnSpanStart(ctx context.Context, span tracing.Span) error { + if p.client == nil { + return nil + } + + // Find parent workflow + p.mu.RLock() + workflow := p.workflows[span.TraceID()] + p.mu.RUnlock() + + if workflow == nil { + fmt.Fprintf(os.Stderr, "No workflow found for span, skipping: %s\n", span.SpanID()) + return nil + } + + taskName := p.getTaskName(span) + task := workflow.NewTask(taskName) + + p.mu.Lock() + p.tasks[span.SpanID()] = task + p.mu.Unlock() + + // For LLM spans (generation/response), start logging the prompt + if p.isLLMSpan(span) { + prompt := p.extractPrompt(span) + if prompt != nil { + llmSpan, err := task.LogPrompt(*prompt) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to log prompt: %v\n", err) + return err + } + + p.mu.Lock() + p.llmSpans[span.SpanID()] = &llmSpan + p.mu.Unlock() + } + } + + return nil +} + +// OnSpanEnd implements tracing.Processor +func (p *TracingProcessor) OnSpanEnd(ctx context.Context, span tracing.Span) error { + if p.client == nil { + return nil + } + + + p.mu.Lock() + task, taskExists := p.tasks[span.SpanID()] + llmSpan, llmExists := p.llmSpans[span.SpanID()] + if taskExists { + delete(p.tasks, span.SpanID()) + } + if llmExists { + delete(p.llmSpans, span.SpanID()) + } + p.mu.Unlock() + + // Log completion for LLM spans + if llmExists && llmSpan != nil && p.isLLMSpan(span) { + completion := p.extractCompletion(span) + usage := p.extractUsage(span) + + if completion != nil { + llmSpan.LogCompletion(ctx, *completion, usage) + } + } + + // End the task + if taskExists && task != nil { + task.End() + } + + return nil +} + +// Shutdown implements tracing.Processor +func (p *TracingProcessor) Shutdown(ctx context.Context) error { + if p.client != nil { + p.client.Shutdown(ctx) + } + return nil +} + +// ForceFlush implements tracing.Processor +func (p *TracingProcessor) ForceFlush(ctx context.Context) error { + // Traceloop SDK handles flushing internally + return nil +} + +// Helper methods + +func (p *TracingProcessor) getTaskName(span tracing.Span) string { + spanData := span.SpanData() + if spanData == nil { + return "unknown_task" + } + + switch data := spanData.(type) { + case *tracing.AgentSpanData: + return fmt.Sprintf("agent_%s", data.Name) + case *tracing.FunctionSpanData: + return fmt.Sprintf("function_%s", data.Name) + case *tracing.GenerationSpanData: + if data.Model != "" { + return fmt.Sprintf("llm_%s", data.Model) + } + return "llm_generation" + case *tracing.ResponseSpanData: + return "llm_response" + default: + return spanData.Type() + } +} + +func (p *TracingProcessor) isLLMSpan(span tracing.Span) bool { + spanData := span.SpanData() + if spanData == nil { + return false + } + + switch spanData.(type) { + case *tracing.GenerationSpanData, *tracing.ResponseSpanData: + return true + default: + return false + } +} + +func (p *TracingProcessor) extractPrompt(span tracing.Span) *sdk.Prompt { + spanData := span.SpanData() + if spanData == nil { + return nil + } + + switch data := spanData.(type) { + case *tracing.GenerationSpanData: + prompt := &sdk.Prompt{ + Vendor: "openai", + Mode: "chat", + } + + if data.Model != "" { + prompt.Model = data.Model + } + + if data.Input != nil { + messages := p.convertMessagesToTraceloop(data.Input) + prompt.Messages = messages + } + + return prompt + + case *tracing.ResponseSpanData: + prompt := &sdk.Prompt{ + Vendor: "openai", + Mode: "chat", + Model: "gpt-4", // Default, will be updated if available + } + + // Prefer request model over response model for more accurate reporting + if data.Model != "" { + prompt.Model = data.Model + } else if data.Response != nil && data.Response.Model != "" { + prompt.Model = data.Response.Model + } + + if data.Input != nil { + // Handle InputItems from agents package using reflection + messages := p.extractMessagesFromInputUsingReflection(data.Input) + prompt.Messages = messages + + // Fallback to legacy format if no messages extracted + if len(messages) == 0 { + if inputSlice, ok := data.Input.([]map[string]any); ok { + // Handle legacy []map[string]any format + messages := make([]sdk.Message, len(inputSlice)) + for i, msg := range inputSlice { + if content, ok := msg["content"].(string); ok { + role := "user" // default + if r, ok := msg["role"].(string); ok { + role = r + } + messages[i] = sdk.Message{ + Index: i, + Content: content, + Role: role, + } + } + } + prompt.Messages = messages + } + } + } + + // Extract tool definitions if available + prompt.Tools = p.extractToolsFromSpan(span) + + return prompt + } + + return nil +} + +func (p *TracingProcessor) extractCompletion(span tracing.Span) *sdk.Completion { + spanData := span.SpanData() + if spanData == nil { + return nil + } + + switch data := spanData.(type) { + case *tracing.GenerationSpanData: + completion := &sdk.Completion{} + + if data.Model != "" { + completion.Model = data.Model + } + + if data.Output != nil { + messages := p.convertMessagesToTraceloop(data.Output) + completion.Messages = messages + } + + return completion + + case *tracing.ResponseSpanData: + completion := &sdk.Completion{ + Model: "gpt-4", // Default + } + + // Prefer request model over response model for more accurate reporting + if data.Model != "" { + completion.Model = data.Model + } else if data.Response != nil && data.Response.Model != "" { + completion.Model = data.Response.Model + } + + if data.Response != nil { + // Extract response content and tool calls from the actual response output + if len(data.Response.Output) > 0 { + messages := p.extractMessagesWithToolCallsFromResponseOutput(data.Response.Output) + completion.Messages = messages + } else { + // Fallback to simple response ID + completion.Messages = []sdk.Message{ + { + Index: 0, + Content: fmt.Sprintf("Response ID: %s", data.Response.ID), + Role: "assistant", + }, + } + } + } + + return completion + } + + return nil +} + +func (p *TracingProcessor) extractUsage(span tracing.Span) sdk.Usage { + spanData := span.SpanData() + if spanData == nil { + return sdk.Usage{} + } + + if data, ok := spanData.(*tracing.GenerationSpanData); ok && data.Usage != nil { + // Convert usage data if available + usage := sdk.Usage{} + + // Try to extract usage information from the usage data + // Check for standard OpenAI field names first + if totalTokens, ok := data.Usage["total_tokens"].(int); ok { + usage.TotalTokens = totalTokens + } + + if promptTokens, ok := data.Usage["prompt_tokens"].(int); ok { + usage.PromptTokens = promptTokens + } else if inputTokens, ok := data.Usage["input_tokens"].(int); ok { + usage.PromptTokens = inputTokens + } + + if completionTokens, ok := data.Usage["completion_tokens"].(int); ok { + usage.CompletionTokens = completionTokens + } else if outputTokens, ok := data.Usage["output_tokens"].(int); ok { + usage.CompletionTokens = outputTokens + } + + // Calculate total if not provided + if usage.TotalTokens == 0 && (usage.PromptTokens > 0 || usage.CompletionTokens > 0) { + usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens + } + + return usage + } + + if data, ok := spanData.(*tracing.ResponseSpanData); ok && data.Response != nil { + usage := sdk.Usage{} + + // Extract usage from OpenAI response usage fields + if data.Response.Usage.InputTokens > 0 { + usage.PromptTokens = int(data.Response.Usage.InputTokens) + } + if data.Response.Usage.OutputTokens > 0 { + usage.CompletionTokens = int(data.Response.Usage.OutputTokens) + } + + // Calculate total tokens + usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens + + return usage + } + + return sdk.Usage{} +} + +func (p *TracingProcessor) convertMessagesToTraceloop(input any) []sdk.Message { + if input == nil { + return nil + } + + // Try to convert input to message format + if inputSlice, ok := input.([]map[string]any); ok { + messages := make([]sdk.Message, len(inputSlice)) + for i, msg := range inputSlice { + content := "" + role := "user" + + if c, ok := msg["content"].(string); ok { + content = c + } + if r, ok := msg["role"].(string); ok { + role = r + } + + messages[i] = sdk.Message{ + Index: i, + Content: content, + Role: role, + } + } + return messages + } + + return nil +} + +// extractMessagesFromInputUsingReflection extracts messages from any input format using reflection +func (p *TracingProcessor) extractMessagesFromInputUsingReflection(input interface{}) []sdk.Message { + if input == nil { + return nil + } + + // Use reflection to handle the InputItems type + v := reflect.ValueOf(input) + if v.Kind() == reflect.Slice { + var messages []sdk.Message + + for i := 0; i < v.Len(); i++ { + item := v.Index(i).Interface() + if msg := p.extractMessageFromInputItem(item, i); msg != nil { + messages = append(messages, *msg) + } + } + return messages + } + + return nil +} + +// extractMessageFromInputItem extracts a message from a TResponseInputItem using reflection +func (p *TracingProcessor) extractMessageFromInputItem(item interface{}, index int) *sdk.Message { + if item == nil { + return nil + } + + // Use reflection to access the OfMessage field + v := reflect.ValueOf(item) + if v.Kind() == reflect.Struct { + // Look for the OfMessage field + ofMessageField := v.FieldByName("OfMessage") + if ofMessageField.IsValid() && !ofMessageField.IsNil() { + // Get the actual message object + messageValue := ofMessageField.Elem() // Dereference the pointer + if messageValue.IsValid() && messageValue.Kind() == reflect.Struct { + // Look for Content and Role fields in the message + contentField := messageValue.FieldByName("Content") + roleField := messageValue.FieldByName("Role") + + content := "" + role := "user" // default + + if contentField.IsValid() { + // Content is a union type, try to extract string value + if contentField.Kind() == reflect.String { + content = contentField.String() + } else { + // Try to extract from union type by examining its structure + contentStr := fmt.Sprintf("%v", contentField.Interface()) + // Look for the actual text content in the string representation + // From debug: Content = {Hello! Can you introduce yourself? [] {{}}} + // Need to extract the full text before the array brackets + if len(contentStr) > 2 && contentStr[0] == '{' { + // Find the end of the text content (before " []" or similar) + end := len(contentStr) + bracketStart := -1 + + // Look for " [" which indicates the start of the array part + for i := 1; i < len(contentStr)-1; i++ { + if contentStr[i] == ' ' && contentStr[i+1] == '[' { + bracketStart = i + break + } + } + + if bracketStart > 1 { + end = bracketStart + } else { + // Fallback: find the last } before the end + for i := len(contentStr) - 1; i > 1; i-- { + if contentStr[i] == '}' { + end = i + break + } + } + } + + if end > 1 { + content = contentStr[1:end] + } + } + } + } + + if roleField.IsValid() && roleField.Kind() == reflect.String { + role = roleField.String() + } + + if content != "" { + return &sdk.Message{ + Index: index, + Content: content, + Role: role, + } + } + } + } + } + + return nil +} + +// extractToolsFromSpan extracts tool definitions from the span context for the prompt +func (p *TracingProcessor) extractToolsFromSpan(span tracing.Span) []sdk.Tool { + spanData := span.SpanData() + if spanData == nil { + return []sdk.Tool{} + } + + // Extract tools from ResponseSpanData + if data, ok := spanData.(*tracing.ResponseSpanData); ok && data.Tools != nil { + return p.convertSerializedToolsToSDK(data.Tools) + } + + return []sdk.Tool{} +} + +// convertSerializedToolsToSDK converts serialized tool definitions to SDK tools +func (p *TracingProcessor) convertSerializedToolsToSDK(tools []map[string]interface{}) []sdk.Tool { + var sdkTools []sdk.Tool + + for _, toolDef := range tools { + if tool := p.convertSerializedToolToSDK(toolDef); tool != nil { + sdkTools = append(sdkTools, *tool) + } + } + + return sdkTools +} + +// convertSerializedToolToSDK converts a single serialized tool to SDK tool +func (p *TracingProcessor) convertSerializedToolToSDK(toolDef map[string]interface{}) *sdk.Tool { + if toolDef == nil { + return nil + } + + // Extract function information + functionData, ok := toolDef["function"].(map[string]interface{}) + if !ok { + return nil + } + + name, ok := functionData["name"].(string) + if !ok { + return nil + } + + description := "" + if desc, ok := functionData["description"].(string); ok { + description = desc + } + + parameters := map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + } + if params, ok := functionData["parameters"].(map[string]interface{}); ok { + parameters = params + } + + return &sdk.Tool{ + Type: "function", + Function: sdk.ToolFunction{ + Name: name, + Description: description, + Parameters: parameters, + }, + } +} + + +// extractMessagesWithToolCallsFromResponseOutput extracts messages including tool calls from response output +func (p *TracingProcessor) extractMessagesWithToolCallsFromResponseOutput(outputs []responses.ResponseOutputItemUnion) []sdk.Message { + messages := make([]sdk.Message, 0) + messageIndex := 0 + + for _, output := range outputs { + switch output.Type { + case "message": + // Handle regular message content + if len(output.Content) > 0 { + for _, content := range output.Content { + if content.Text != "" { + messages = append(messages, sdk.Message{ + Index: messageIndex, + Content: content.Text, + Role: "assistant", + }) + messageIndex++ + } + } + } + + case "function_call": + // Handle function tool calls using proper ToolCall structure + toolCall := sdk.ToolCall{ + ID: output.CallID, + Type: "function", + Function: sdk.ToolCallFunction{ + Name: output.Name, + Arguments: output.Arguments, + }, + } + + message := sdk.Message{ + Index: messageIndex, + Content: "", // Tool calls typically don't have text content + Role: "assistant", + ToolCalls: []sdk.ToolCall{toolCall}, + } + messages = append(messages, message) + messageIndex++ + + case "computer_call": + // Handle computer tool calls using proper ToolCall structure + toolCall := sdk.ToolCall{ + ID: output.CallID, + Type: "function", + Function: sdk.ToolCallFunction{ + Name: "computer_use", + Arguments: fmt.Sprintf(`{"action": "%s", "id": "%s"}`, output.Action.Type, output.ID), + }, + } + + message := sdk.Message{ + Index: messageIndex, + Content: "", + Role: "assistant", + ToolCalls: []sdk.ToolCall{toolCall}, + } + messages = append(messages, message) + messageIndex++ + + case "file_search_call": + // Handle file search tool calls using proper ToolCall structure + toolCall := sdk.ToolCall{ + ID: output.CallID, + Type: "function", + Function: sdk.ToolCallFunction{ + Name: "file_search", + Arguments: fmt.Sprintf(`{"id": "%s", "status": "%s"}`, output.ID, output.Status), + }, + } + + message := sdk.Message{ + Index: messageIndex, + Content: "", + Role: "assistant", + ToolCalls: []sdk.ToolCall{toolCall}, + } + messages = append(messages, message) + messageIndex++ + + case "web_search_call": + // Handle web search tool calls using proper ToolCall structure + toolCall := sdk.ToolCall{ + ID: output.CallID, + Type: "function", + Function: sdk.ToolCallFunction{ + Name: "web_search", + Arguments: fmt.Sprintf(`{"id": "%s", "status": "%s"}`, output.ID, output.Status), + }, + } + + message := sdk.Message{ + Index: messageIndex, + Content: "", + Role: "assistant", + ToolCalls: []sdk.ToolCall{toolCall}, + } + messages = append(messages, message) + messageIndex++ + + case "local_shell_call": + // Handle local shell tool calls using proper ToolCall structure + toolCall := sdk.ToolCall{ + ID: output.CallID, + Type: "function", + Function: sdk.ToolCallFunction{ + Name: "local_shell", + Arguments: fmt.Sprintf(`{"id": "%s"}`, output.ID), + }, + } + + message := sdk.Message{ + Index: messageIndex, + Content: "", + Role: "assistant", + ToolCalls: []sdk.ToolCall{toolCall}, + } + messages = append(messages, message) + messageIndex++ + + case "code_interpreter_call": + // Handle code interpreter tool calls using proper ToolCall structure + toolCall := sdk.ToolCall{ + ID: output.CallID, + Type: "function", + Function: sdk.ToolCallFunction{ + Name: "code_interpreter", + Arguments: fmt.Sprintf(`{"id": "%s"}`, output.ID), + }, + } + + message := sdk.Message{ + Index: messageIndex, + Content: "", + Role: "assistant", + ToolCalls: []sdk.ToolCall{toolCall}, + } + messages = append(messages, message) + messageIndex++ + + case "image_generation_call": + // Handle image generation tool calls using proper ToolCall structure + toolCall := sdk.ToolCall{ + ID: output.CallID, + Type: "function", + Function: sdk.ToolCallFunction{ + Name: "image_generation", + Arguments: fmt.Sprintf(`{"id": "%s"}`, output.ID), + }, + } + + message := sdk.Message{ + Index: messageIndex, + Content: "", + Role: "assistant", + ToolCalls: []sdk.ToolCall{toolCall}, + } + messages = append(messages, message) + messageIndex++ + + case "mcp_call": + // Handle MCP tool calls using proper ToolCall structure + toolCall := sdk.ToolCall{ + ID: output.CallID, + Type: "function", + Function: sdk.ToolCallFunction{ + Name: output.Name, + Arguments: output.Arguments, + }, + } + + message := sdk.Message{ + Index: messageIndex, + Content: "", + Role: "assistant", + ToolCalls: []sdk.ToolCall{toolCall}, + } + messages = append(messages, message) + messageIndex++ + } + } + + // If no messages were extracted, create a fallback message + if len(messages) == 0 { + messages = append(messages, sdk.Message{ + Index: 0, + Content: "No extractable content found", + Role: "assistant", + }) + } + + return messages +}