Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6429125
Add Open Telemetry instrumentation.
atheriel May 22, 2025
93077ef
Merge branch 'main' into otel
schloerke Sep 8, 2025
a710ce2
`usethis::use_tidy_description()`
schloerke Sep 8, 2025
bf51c67
Updates to the latest promises ospans
schloerke Sep 9, 2025
2d5245a
Simplify internal api
schloerke Sep 9, 2025
397052e
Use `promises::local_ospan_promise_domain()`
schloerke Sep 9, 2025
833c308
Import otel as promises does. Remove suggestions on otelsdk and add t…
schloerke Sep 9, 2025
324c955
Use new promises main branch (PR was merged)
schloerke Sep 9, 2025
a923a1a
Copy in tracer retrieval from httr2
schloerke Sep 9, 2025
bc00cc2
Fix runtime error with otelsdk where `spn$end(status="auto")` would fail
schloerke Sep 10, 2025
e59614b
Pass through the parent chat ospan to the generator methods. Add ospa…
schloerke Sep 10, 2025
5e48b66
Apply suggestions from code review
schloerke Nov 6, 2025
0de30c1
Merge branch 'main' into otel
schloerke Nov 6, 2025
5735bc6
Use existing `is_testing()` method
schloerke Nov 6, 2025
30ee794
Use more descriptive otel tracer function for ellmer
schloerke Nov 6, 2025
443439e
Use `defer` standalone
schloerke Nov 6, 2025
74fa875
Apply similar logic to remove withr for `local_tempfile()` for main code
schloerke Nov 6, 2025
688edd6
`activate_and_cleanup_ospan()` -> `setup_otel_span()`
schloerke Nov 6, 2025
eeb0b84
Update fn names. Move some setup code inside method
schloerke Nov 6, 2025
e8cb35c
Bump dev version to 0.3.2.9001
schloerke Nov 6, 2025
0e19f88
Apply suggestion from @shikokuchuo
schloerke Nov 7, 2025
98e3ff6
Remove `coro::` namespace for `await_each`
schloerke Nov 7, 2025
4457769
Merge branch 'main' into otel
schloerke Nov 7, 2025
e92322e
Use local_tempfile() helper method
schloerke Nov 7, 2025
0c91f8f
Remove otelsdk remote and make a Suggests
schloerke Nov 7, 2025
c377dd6
Refactor otel span argument naming for clarity
schloerke Nov 7, 2025
a7852bc
Update chat-tools.R
schloerke Nov 7, 2025
8080dc6
Update otel tracer caching implementation after #848; add early retur…
shikokuchuo Nov 8, 2025
302e752
Merge branch 'main' into otel
shikokuchuo Nov 8, 2025
37e9ca2
Corrections for 8080dc6
shikokuchuo Nov 8, 2025
01f20e6
Remove superfluous promise domain setups
shikokuchuo Nov 8, 2025
913270d
Refactor agent span activation in OpenTelemetry tracing
schloerke Nov 10, 2025
2dc46ba
Relax span count assertions in otel tracing tests
schloerke Nov 10, 2025
2d78a24
Refactor otel spans tests to be relative instead of comparing against…
shikokuchuo Nov 10, 2025
7f653bb
Simplify span kinds and test
shikokuchuo Nov 10, 2025
c03dfde
Move otel to suggests
shikokuchuo Nov 10, 2025
ecf85fd
Add `span_recording()` helper
shikokuchuo Nov 10, 2025
34d8093
Merge branch 'main' into otel
shikokuchuo Nov 14, 2025
128a7b5
Merge branch 'main' into otel
shikokuchuo Nov 17, 2025
35659d1
Merge branch 'main' into otel
shikokuchuo Nov 18, 2025
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: 5 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Imports:
jsonlite,
later (>= 1.4.0),
lifecycle,
promises (>= 1.3.1),
promises (>= 1.5.0),
R6,
rlang (>= 1.1.0),
S7 (>= 0.2.0),
Expand All @@ -45,6 +45,8 @@ Suggests:
knitr,
magick,
openssl,
otel (>= 0.2.0),
otelsdk (>= 0.2.0),
paws.common,
png,
rmarkdown,
Expand Down Expand Up @@ -82,11 +84,13 @@ Collate:
'content-pdf.R'
'content-replay.R'
'httr2.R'
'import-standalone-defer.R'
'import-standalone-obj-type.R'
'import-standalone-purrr.R'
'import-standalone-types-check.R'
'interpolate.R'
'live.R'
'otel.R'
'parallel-chat.R'
'params.R'
'provider-any.R'
Expand Down
26 changes: 20 additions & 6 deletions R/chat-tools.R
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ on_load({
echo = "none",
on_tool_request = function(request) invisible(),
on_tool_result = function(result) invisible(),
yield_request = FALSE
yield_request = FALSE,
otel_span = NULL
) {
tool_requests <- extract_tool_requests(turn)

Expand All @@ -51,7 +52,7 @@ on_load({
next
}

result <- invoke_tool(request)
result <- invoke_tool(request, otel_span = otel_span)

if (promises::is.promise(result@value)) {
cli::cli_abort(
Expand All @@ -78,7 +79,8 @@ on_load({
echo = "none",
on_tool_request = function(request) invisible(),
on_tool_result = function(result) invisible(),
yield_request = FALSE
yield_request = FALSE,
otel_span = NULL
) {
tool_requests <- extract_tool_requests(turn)

Expand All @@ -94,7 +96,7 @@ on_load({
return(rejected)
}

result <- coro::await(invoke_tool_async(request))
result <- coro::await(invoke_tool_async(request, otel_span = otel_span))

maybe_echo_tool(result, echo = echo)
on_tool_result(result)
Expand Down Expand Up @@ -144,7 +146,7 @@ new_tool_result <- function(request, result = NULL, error = NULL) {
}

# Also need to handle edge cases: https://platform.openai.com/docs/guides/function-calling/edge-cases
invoke_tool <- function(request) {
invoke_tool <- function(request, otel_span = NULL) {
if (is.null(request@tool)) {
return(new_tool_result(request, error = "Unknown tool"))
}
Expand All @@ -155,19 +157,25 @@ invoke_tool <- function(request) {
return(args)
}

tool_span <- local_tool_otel_span(
request,
parent = otel_span
)

tryCatch(
{
result <- do.call(request@tool, args)
new_tool_result(request, result)
},
error = function(e) {
record_tool_otel_span_error(tool_span, e)
new_tool_result(request, error = e)
}
)
}

on_load(
invoke_tool_async <- coro::async(function(request) {
invoke_tool_async <- coro::async(function(request, otel_span = NULL) {
if (is.null(request@tool)) {
return(new_tool_result(request, error = "Unknown tool"))
}
Expand All @@ -178,12 +186,18 @@ on_load(
return(args)
}

tool_span <- local_tool_otel_span(
request,
parent = otel_span
)

tryCatch(
{
result <- await(do.call(request@tool, args))
new_tool_result(request, result)
},
error = function(e) {
record_tool_otel_span_error(tool_span, e)
new_tool_result(request, error = e)
}
)
Expand Down
62 changes: 49 additions & 13 deletions R/chat.R
Original file line number Diff line number Diff line change
Expand Up @@ -456,14 +456,17 @@ Chat <- R6::R6Class(
yield_as_content = FALSE
) {
tool_errors <- list()
withr::defer(warn_tool_errors(tool_errors))
defer(warn_tool_errors(tool_errors))

agent_span <- local_agent_otel_span(private$provider, activate = FALSE)

while (!is.null(user_turn)) {
assistant_chunks <- private$submit_turns(
user_turn,
stream = stream,
echo = echo,
yield_as_content = yield_as_content
yield_as_content = yield_as_content,
otel_span = agent_span
)
for (chunk in assistant_chunks) {
yield(chunk)
Expand All @@ -478,7 +481,8 @@ Chat <- R6::R6Class(
echo = echo,
on_tool_request = private$callback_on_tool_request$invoke,
on_tool_result = private$callback_on_tool_result$invoke,
yield_request = yield_as_content
yield_request = yield_as_content,
otel_span = agent_span
)

tool_results <- list()
Expand Down Expand Up @@ -515,14 +519,17 @@ Chat <- R6::R6Class(
yield_as_content = FALSE
) {
tool_errors <- list()
withr::defer(warn_tool_errors(tool_errors))
defer(warn_tool_errors(tool_errors))

agent_span <- local_agent_otel_span(private$provider, activate = FALSE)

while (!is.null(user_turn)) {
assistant_chunks <- private$submit_turns_async(
user_turn,
stream = stream,
echo = echo,
yield_as_content = yield_as_content
yield_as_content = yield_as_content,
otel_span = agent_span
)
for (chunk in await_each(assistant_chunks)) {
yield(chunk)
Expand All @@ -537,11 +544,12 @@ Chat <- R6::R6Class(
echo = echo,
on_tool_request = private$callback_on_tool_request$invoke_async,
on_tool_result = private$callback_on_tool_result$invoke_async,
yield_request = yield_as_content
yield_request = yield_as_content,
otel_span = agent_span
)
if (tool_mode == "sequential") {
tool_results <- list()
for (tool_step in coro::await_each(tool_calls)) {
for (tool_step in await_each(tool_calls)) {
if (yield_as_content) {
yield(tool_step)
}
Expand Down Expand Up @@ -587,19 +595,27 @@ Chat <- R6::R6Class(
stream,
echo,
type = NULL,
yield_as_content = FALSE
yield_as_content = FALSE,
otel_span = NULL
) {
if (echo == "all") {
cat_line(format(user_turn), prefix = "> ")
}

chat_span <- local_chat_otel_span(
private$provider,
parent = otel_span
)

response <- chat_perform(
provider = private$provider,
mode = if (stream) "stream" else "value",
turns = c(private$.turns, list(user_turn)),
tools = if (is.null(type)) private$tools,
type = type
type = type,
otel_span = chat_span
)

emit <- emitter(echo)
any_text <- FALSE

Expand All @@ -619,9 +635,15 @@ Chat <- R6::R6Class(

result <- stream_merge_chunks(private$provider, result, chunk)
}
turn <- value_turn(private$provider, result, has_type = !is.null(type))
record_chat_otel_span_status(chat_span, result)
turn <- value_turn(
private$provider,
result,
has_type = !is.null(type)
)
turn <- match_tools(turn, private$tools)
} else {
record_chat_otel_span_status(chat_span, response)
turn <- value_turn(
private$provider,
resp_body_json(response),
Expand Down Expand Up @@ -673,15 +695,23 @@ Chat <- R6::R6Class(
stream,
echo,
type = NULL,
yield_as_content = FALSE
yield_as_content = FALSE,
otel_span = NULL
) {
chat_span <- local_chat_otel_span(
private$provider,
parent = otel_span
)

response <- chat_perform(
provider = private$provider,
mode = if (stream) "async-stream" else "async-value",
turns = c(private$.turns, list(user_turn)),
tools = if (is.null(type)) private$tools,
type = type
type = type,
otel_span = chat_span
)

emit <- emitter(echo)
any_text <- FALSE

Expand All @@ -701,10 +731,16 @@ Chat <- R6::R6Class(

result <- stream_merge_chunks(private$provider, result, chunk)
}
turn <- value_turn(private$provider, result, has_type = !is.null(type))
record_chat_otel_span_status(chat_span, result)
turn <- value_turn(
private$provider,
result,
has_type = !is.null(type)
)
} else {
result <- await(response)

record_chat_otel_span_status(chat_span, result)
turn <- value_turn(
private$provider,
resp_body_json(result),
Expand Down
33 changes: 28 additions & 5 deletions R/httr2.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ chat_perform <- function(
mode = c("value", "stream", "async-stream", "async-value"),
turns,
tools = NULL,
type = NULL
type = NULL,
otel_span = NULL
) {
mode <- arg_match(mode)
stream <- mode %in% c("stream", "async-stream")
tools <- tools %||% list()

setup_active_promise_otel_span(otel_span)

req <- chat_request(
provider = provider,
turns = turns,
Expand All @@ -23,14 +26,28 @@ chat_perform <- function(
switch(
mode,
"value" = req_perform(req),
"stream" = chat_perform_stream(provider, req),
"stream" = chat_perform_stream(
provider,
req,
otel_span = otel_span
),
"async-value" = req_perform_promise(req),
"async-stream" = chat_perform_async_stream(provider, req)
"async-stream" = chat_perform_async_stream(
provider,
req,
otel_span = otel_span
)
)
}

on_load(
chat_perform_stream <- coro::generator(function(provider, req) {
chat_perform_stream <- coro::generator(function(
provider,
req,
otel_span = NULL
) {
setup_active_promise_otel_span(otel_span)

resp <- req_perform_connection(req)
on.exit(close(resp))

Expand All @@ -47,7 +64,13 @@ on_load(
)

on_load(
chat_perform_async_stream <- coro::async_generator(function(provider, req) {
chat_perform_async_stream <- coro::async_generator(function(
provider,
req,
otel_span = NULL
) {
setup_active_promise_otel_span(otel_span)

resp <- req_perform_connection(req, blocking = FALSE)
on.exit(close(resp))

Expand Down
35 changes: 35 additions & 0 deletions R/import-standalone-defer.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Standalone file: do not edit by hand
# Source: https://github.com/r-lib/withr/blob/HEAD/R/standalone-defer.R
# Generated by: usethis::use_standalone("r-lib/withr", "defer")
# ----------------------------------------------------------------------
#
# ---
# repo: r-lib/withr
# file: standalone-defer.R
# last-updated: 2024-01-15
# license: https://unlicense.org
# ---
#
# `defer()` is similar to `on.exit()` but with a better default for
# `add` (hardcoded to `TRUE`) and `after` (`FALSE` by default).
# It also supports adding handlers to other frames which is useful
# to implement `local_` functions.
#
#
# ## Changelog
#
# 2024-01-15:
# * Rewritten to be pure base R.
#
# nocov start

defer <- function(expr, envir = parent.frame(), after = FALSE) {
thunk <- as.call(list(function() expr))
do.call(
on.exit,
list(thunk, add = TRUE, after = after),
envir = envir
)
}

# nocov end
Loading