Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 4 additions & 2 deletions apps/ai-observability/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ Each app exists to test one thing the others don't:
- `openai-agents/python-travel-triage` — tracing processor; the SDK emits the tree
- `vercel-ai/nextjs-support-chat` — per-request identity; framework bootstrap
- `manual-capture/node-http-chat` — hand-built tree; must reuse the existing client
- `opentelemetry/go-weather` — Go; no wrapper SDK exists, so the posthog-go OTel bridge

The four weather apps implement the identical `get_weather` round trip from
The five weather apps implement the identical `get_weather` round trip from
[Anthropic's tool-use docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview#how-tool-use-works),
so a diff between any two isolates one variable: the SDK, the language, or the
conversation structure.
Expand All @@ -37,7 +38,8 @@ conversation structure.
`posthog_trace_id`, and `posthog_properties`, as shown in the docs. OTel is
acceptable only where the same structure lands. Exceptions:
`openai-agents` (tracing processor), `vercel-ai` (`experimental_telemetry`),
`manual-capture` (no SDK to wrap).
`manual-capture` (no SDK to wrap),
`opentelemetry/go-weather` (Go has no wrapper SDK; the posthog-go OTel bridge).
- **Every app gets a session** — single-trace apps included. The graded
property is **cardinality**: one id shared by the traces that belong
together. A fresh id per call groups nothing and is worse than none.
Expand Down
39 changes: 39 additions & 0 deletions apps/ai-observability/opentelemetry/go-weather/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# wb-aio-opentelemetry-go-weather

Weather assistant, official OpenAI Go SDK (`openai-go`), two-turn
conversation. PostHog-less fixture for `wizard ai-observability`.

Go has no PostHog wrapper SDK. The expected mechanism is the
`github.com/posthog/posthog-go/otel` bridge: register its span processor on a
`TracerProvider`, then hand-author `gen_ai.*` spans around the model calls,
because no Go instrumentation library exists for `openai-go`.

```
thread_abc ← $ai_session_id (Conversation.ThreadID)
├─ Ask("weather in San Francisco?") ← trace
│ ├─ Chat.Completions.New (→ tool_calls) ← generation
│ ├─ get_weather ← span
│ └─ Chat.Completions.New (→ answer) ← generation
└─ Ask("How about Boston?") ← trace (same shape)
```

## Expected outcome

- one session (`thread_abc`), two traces of
`generation → span(get_weather) → generation`, all on `user_123`
- `posthogotel.NewSpanProcessor` on a `TracerProvider`; each model call gets a
span with `gen_ai.*` attributes (operation, provider, model, messages, token
usage) so it lands as `$ai_generation`
- one OTel trace per `Ask`: a root span per turn that itself passes the AI
span filter (name or attribute key in the `gen_ai.` / `ai.` namespaces),
with the generations and the tool span as children
- the tool run captured as a child span with `gen_ai.*` tool attributes, not
left as a plain span the processor drops
- `$ai_session_id` spelled exactly, on every span in the thread;
`posthog.distinct_id` carries `user_123`
- flushed before exit (`ForceFlush` or `Shutdown` on the provider)
- `weather.go` and the tool loop untouched, including the `ToParam()` append

Fail: importing a PostHog wrapper that does not exist in Go; spans that miss
the bridge's AI filter and never reach PostHog; a session id minted per turn;
one trace per model call; no flush.
12 changes: 12 additions & 0 deletions apps/ai-observability/opentelemetry/go-weather/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module wb-aio-opentelemetry-go-weather

go 1.25.0

require github.com/openai/openai-go/v3 v3.54.0

require (
github.com/tidwall/gjson v1.19.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
)
48 changes: 48 additions & 0 deletions apps/ai-observability/opentelemetry/go-weather/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 h1:4gRPBpN1f6xt88yi4WR26m7XaD9OlWtVT6bWPdGUIok=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0/go.mod h1:G7QVLxw1j1JVyrO1MA95S8m8HStaaleDZYTcfGgjB2o=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/aws/aws-sdk-go-v2 v1.43.6 h1:RrmFcqCBxkJuf7g1axVo5krB4jM/AO8r5e5oujrgdoQ=
github.com/aws/aws-sdk-go-v2 v1.43.6/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00=
github.com/aws/aws-sdk-go-v2/config v1.32.37 h1:Ljl7LOJB6ym0liuEl0+TZ3d7f5I8MEZN1Cj9PINlj/g=
github.com/aws/aws-sdk-go-v2/config v1.32.37/go.mod h1:WJ7pe7ZPpmG8Q5kKS53zeypIV4FBGACxmte8Uc6SgUc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.36 h1:84s5xMme6ENYEdKG8rsbSFFg/8+lbHBeM9QYSO0gnDk=
github.com/aws/aws-sdk-go-v2/credentials v1.19.36/go.mod h1:c46BLdagDLIswjgt+GeQOslXgeS0E6wCacs5yZbxPGk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.37 h1:b5tb+CZItBkydC7r3hTNdSO3pszG1R2EtnA+7TePQPk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.37/go.mod h1:ZQ+6SU9X0oz6+7MUCSswv9Mjci4eaqZr21HI2RVy/yA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37 h1:lznzIOvvbqjfe8UAaciCRJgBgJsxuTROKlhZuXQWfv8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37/go.mod h1:otfkzyfQeMMLZAqX59GSXTL3o22BR/l6HFaRzzbWSqA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37 h1:zCEORWo0eU0gDjG+IyApE/2B+ZGG1m+GU7B263XV8ds=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37/go.mod h1:i6c0PEl3TNOWxRbQ++KQcVenPWS/GoQeiklKhNuqzJ8=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38 h1:A3UAuCmx7LyUcrixBTzKJYYIUZ2yTvn6ZhT8PB+7APk=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38/go.mod h1:1PDUYG9Z+JrbbsobsAZHjWOm9QBT/djiK3QbykTL5Z4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 h1:OvYZOB3qA6zvfdRFiRFRzVSiElMYrz3GdntkXZxlp1o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17/go.mod h1:JgR/2Ew50ACfIWau1oeMRX59tMtC0kM+PYQGEaT04cY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37 h1:a3D4AjrOrTrP8+d9ILBthqrElf0z1JNol09Xvnwcys8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37/go.mod h1:ky0gTu+ukvUTuUKFIpp6Wid4oninrkCyvbFkVs0kpHM=
github.com/aws/aws-sdk-go-v2/service/signin v1.5.6 h1:i68sFvXidKlkiSvI7d7Ilc1/UvW4CtBOaivH7jhG4fs=
github.com/aws/aws-sdk-go-v2/service/signin v1.5.6/go.mod h1:/h7Obr9WTtzbjTHGASRQwLN7Bupw+TC3x8x7fyx39hE=
github.com/aws/aws-sdk-go-v2/service/sso v1.33.6 h1:tpfGChmjUmv3W9WlRvy+stwKDTbFFdq8Zk9DbFPrfMU=
github.com/aws/aws-sdk-go-v2/service/sso v1.33.6/go.mod h1:CSjiDzmG/lsKkTOYjbkM+duLmRlW+LOxD64Na44ijnI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.6 h1:49BBtY68A+KJCQ3a2F3eUe6ROsKucxUdfHKoqorc0wI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.6/go.mod h1:ptG2hbs7QltE1GcQY0MpS4bfrc51KCnBXUr7OT1EEfE=
github.com/aws/aws-sdk-go-v2/service/sts v1.45.6 h1:JvExZWabChDM0qJAirQYGfOYo0ndT3edXj+fqSPNjkE=
github.com/aws/aws-sdk-go-v2/service/sts v1.45.6/go.mod h1:XZcaQkV2cItp6yEkrwljyaPOf22RuX7T43jxap/FOmM=
github.com/aws/smithy-go v1.27.8 h1:FR0dxZfIlV7Z8eh2iHfIofdunw382XsDV3Mxt9nUvRY=
github.com/aws/smithy-go v1.27.8/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/openai/openai-go/v3 v3.54.0 h1:qeNvIpFB/wzX7pI8USN1eruvzCS+XqOZ8mx73lGF6Lg=
github.com/openai/openai-go/v3 v3.54.0/go.mod h1:ufI1+K+t0ijRB3gk8eztiw1crcDpsBuxRQL4sbLIrts=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
104 changes: 104 additions & 0 deletions apps/ai-observability/opentelemetry/go-weather/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Weather assistant on the official OpenAI Go SDK. The model may call the
// registered get_weather tool before answering, so one question is either one
// model call or two with a tool execution between them.
package main

import (
"context"
"encoding/json"
"fmt"
"os"

"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
)

const model = "gpt-5-mini"

var tools = []openai.ChatCompletionToolUnionParam{
openai.ChatCompletionFunctionTool(openai.FunctionDefinitionParam{
Name: "get_weather",
Description: openai.String("Get the current weather for a given location."),
Parameters: openai.FunctionParameters{
"type": "object",
"properties": map[string]any{
"location": map[string]string{
"type": "string",
"description": "City and state, e.g. San Francisco, CA",
},
},
"required": []string{"location"},
},
}),
}

// Conversation is one chat thread. Every question asked below belongs to it.
type Conversation struct {
UserID string
ThreadID string
messages []openai.ChatCompletionMessageParamUnion
}

// Ask answers one question, running the tool if the model asks for it.
func (c *Conversation) Ask(ctx context.Context, client openai.Client, question string) (string, error) {
c.messages = append(c.messages, openai.UserMessage(question))

response, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Model: model,
Messages: c.messages,
Tools: tools,
ParallelToolCalls: openai.Bool(false),
})
if err != nil {
return "", err
}
message := response.Choices[0].Message

if len(message.ToolCalls) == 0 {
c.messages = append(c.messages, openai.AssistantMessage(message.Content))
return message.Content, nil
}

call := message.ToolCalls[0]
var args struct {
Location string `json:"location"`
}
if err := json.Unmarshal([]byte(call.Function.Arguments), &args); err != nil {
return "", err
}
result := getWeather(args.Location)

c.messages = append(c.messages, message.ToParam())
c.messages = append(c.messages, openai.ToolMessage(result, call.ID))

followup, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Model: model,
Messages: c.messages,
Tools: tools,
ParallelToolCalls: openai.Bool(false),
})
if err != nil {
return "", err
}
answer := followup.Choices[0].Message.Content
c.messages = append(c.messages, openai.AssistantMessage(answer))
return answer, nil
}

func main() {
ctx := context.Background()
client := openai.NewClient(option.WithAPIKey(os.Getenv("OPENAI_API_KEY")))

thread := &Conversation{UserID: "user_123", ThreadID: "thread_abc"}
for _, question := range []string{
"What's the weather in San Francisco?",
"How about Boston?",
} {
answer, err := thread.Ask(ctx, client, question)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(answer)
}
}
14 changes: 14 additions & 0 deletions apps/ai-observability/opentelemetry/go-weather/weather.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Backing implementation for the get_weather tool. No model call involved.
package main

var forecast = map[string]string{
"San Francisco, CA": "15 degrees Celsius, partly cloudy",
"Boston, MA": "4 degrees Celsius, snow showers",
}

func getWeather(location string) string {
if report, ok := forecast[location]; ok {
return report
}
return "No forecast on file for " + location + "."
}
Loading