Repository navigation
Export offline evaluation spans through the SDK exporter - #170
Draft
ccschmitz-launchdarkly wants to merge 10 commits into
Draft
ccschmitz-launchdarkly wants to merge 10 commits into
ccschmitz-launchdarkly wants to merge 10 commits into
Conversation
…porter Stamp the shared UUID5 conversation id and evaluation-run attributes on handler spans, and record a verdict-free gen_ai.evaluation.result on an evaluate span so a plain function still exports through the SDK-key provider init_client already registered. Co-authored-by: christopher.schmitz <christopher.schmitz@launchdarkly.com>
ToolInvocation gains start and end times, excluded from equality, so the offline runner can place execute_tool spans. observe_handler_spans counts the SDK spans a handler opens, so the runner can tell a provider handler that traced itself from a plain function that did not. Co-authored-by: christopher.schmitz <christopher.schmitz@launchdarkly.com>
Each dataset row gets a launchdarkly.dataset_row root, and its evaluate spans parent under it, so generation and judging share one trace. When the handler is a plain function that opened no SDK span, the runner writes what it handed the function and what came back: a chat span with the rendered system instructions, input, output (tool_call parts first), model, and usage; an execute_tool span per recorded call; and a judge chat span under evaluate with the rendered judge prompt and completion. evaluate carries the judge model and OK or ERROR status. The offline-evals custom events are unchanged and still carry no transcript or tool payload. Co-authored-by: christopher.schmitz <christopher.schmitz@launchdarkly.com>
…l-conversation-spans-17b3 # Conflicts: # packages/client/src/launchdarkly_ai_server/lifecycle.py Co-authored-by: christopher.schmitz <christopher.schmitz@launchdarkly.com>
TESTING.md §8.10 now matches §8.8.6: a row whose handler raised is never
scored, so it has no evaluate span. The span test also pins the judge chat
transcript with a messages-mode judge whose user turn expands
{{message_history}}.
Co-authored-by: christopher.schmitz <christopher.schmitz@launchdarkly.com>
… trace Spans are stamped from timestamps captured around the call, so a child's window sits inside its parent's and the tool call's inside the generation chat's. Containment is the invariant; a duration floor would assert the machine's speed instead. Co-authored-by: christopher.schmitz <christopher.schmitz@launchdarkly.com>
…l-conversation-spans-17b3 Co-authored-by: christopher.schmitz <christopher.schmitz@launchdarkly.com>
Core now exposes two feature-neutral span primitives, stamp_span_attributes and count_sdk_spans, and the processor's span-start path has no evaluation branch. The evaluation run keys, the ai-evaluator UUID5 namespace, the row-id derivation, bind_evaluation_row and record_evaluation_result move to evaluations/spans.py (TESTING.md §0.3, §8.10). Co-authored-by: christopher.schmitz <christopher.schmitz@launchdarkly.com>
The process capture_content default stays false. Synthetic evaluation spans keep writing the transcript, and a test shows a normal handler span written with that resolved default still omits it. Co-authored-by: christopher.schmitz <christopher.schmitz@launchdarkly.com>
Keep highlight.project_id set to the same SDK key so older readers still match. The new name is the SDK key, not the project's BSON id. Co-authored-by: christopher.schmitz <christopher.schmitz@launchdarkly.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
launchdarkly/python-ai-sdk
EvaluationsModule.runexports each dataset row as one trace, through the tracer providerinit_clientalready registers. That provider'slaunchdarkly.project_idandhighlight.project_idare both the SDK key, not the project's BSON id.$ld:ai:offline-evals:generationand:criterionare unchanged and still carry no transcript or tool payload.Spec: launchdarkly/ai-sdks-monorepo#60 (
TESTING.md§8.10).Frontend: launchdarkly/gonfalon#74117.
Staging evaluation you can open
Project
default, environmentai-o-11-y. The test-only AI Config isoffline-eval-trace-verifyand the judge isoffline-eval-trace-verify-judge. The judge has ausermessage with{{message_history}}after its system rubric, which is how library judges are written. The row is the canned "What is the capital of France?" prompt with alookup_capitaltool. Custom events went tohttps://events-stg.launchdarkly.com, the events URI in gonfalon's staging e2e config (packages/e2e/playwright/config.staging.json). The run summary reachedpassed=1, which only happens after those events are ingested.https://ld-stg.launchdarkly.com/projects/default/ai/evaluations/178760f1-2873-440a-ab43-91f5dd17cc0b/runs/a7a2bd3d-8f1a-4194-8187-7353aabd79dd?env=ai-o-11-y
Preview of gonfalon#74117 on that same run (
frontend-versionis the PR head,b6338c27c):https://ld-stg.launchdarkly.com/projects/default/ai/evaluations/178760f1-2873-440a-ab43-91f5dd17cc0b/runs/a7a2bd3d-8f1a-4194-8187-7353aabd79dd?env=ai-o-11-y&frontend-version=b6338c27c
a7a2bd3d-8f1a-4194-8187-7353aabd79ddconversationIdbe7c123c-c9bb-50ed-aee1-88af53750ca7, status COMPLETE, judge score 0.9 (LaunchDarkly ruled it a pass)Turn the spans flag on, or the panel shows a different waterfall
enable-evaluation-run-conversationshows the panel.enable-evaluation-run-conversation-spansgates whether it queries the exported spans, and it is off indefault/staging. With it off the panel rendersevaluationResultToSpans, which rebuilds a transcript from the stored evaluation result instead. That fallback mints one trace id per phase (<conversationId>:generationand<conversationId>:evaluation, so the header reads "2 traces") and takes each duration from a stored millisecond latency. On this canned run those aregenerationLatencyMs: 1and the criterion'smeta.usage.latencyMs: 0, whichformatDurationrenders as1ms,0ns, and0ns. None of those numbers come from the exported spans.With the flag on, the backend's
trace_conversationquery returns what the SDK actually sent:One trace id, the parenting §8.10 specifies, and each span's window inside its parent's: the tool call sits inside the generation
chat, and the judgechatinsideevaluate. The trace-count chip does not render at all, because it only appears above one trace.The durations are microseconds because the handler is a canned in-memory function that makes no model call. The spans are stamped from timestamps captured around the call, and nothing is padded to make the waterfall look busier. A real provider call produces real wall time in the same spans.
What each row exports
launchdarkly.dataset_rowis the row's root span. It carries the shared UUID5gen_ai.conversation.idpluslaunchdarkly.evaluation_run.idand.row_index. It has nogen_aioperation, so the conversation view treats it as an envelope. Its status is OK, or ERROR when the handler raised.chat {model}span is a child of the root. It carries the renderedgen_ai.system_instructions,gen_ai.input.messages, andgen_ai.output.messages(atool_callpart for each recorded call, then the answer), plus provider, model, usage, and tool definitions.execute_tool {name}is a child of the root, one per recorded tool call.evaluate {criterion}is a child of the root and carries agen_ai.evaluation.resultwith no verdict. A row whose handler raised is never scored (§8.8.6), so it has noevaluatespan.chat {model}is a child ofevaluate, which keeps it out of the transcript. It carries the judge's system instructions, then its other messages rendered with the judge variables, then the judged output.The runner writes the
chatandexecute_toolspans only when the handler opened no SDK span of its own, so a provider handler that traces itself gets no duplicates.Where the code lives (§0.3)
Core
conversation.pydoesn't know offline evaluations exist. Its span processor exposes two feature-neutral primitives.stamp_span_attributeswrites a caller-supplied str/int attribute set, write-if-absent, onto SDK spans started in the block.count_sdk_spanscounts SDK spans started in the block. Thelaunchdarkly.evaluation_run.*keys, the ai-evaluator UUID5 namespace,conversation_id_for_evaluation_row,bind_evaluation_row, andrecord_evaluation_resultall live inevaluations/spans.py. The runner usescount_sdk_spansto decide whether to describe a plain function's call. This layout doesn't change behavior, so the staging run and links still apply.capture_contentstays false (SDKOptions.capture_content, introduced by Alexis Georges). Synthetic evaluation spans still export the transcript: row input and messages, rendered system and judge instructions, model output, tool arguments and results, and the judge completion and explanation. The public warning is inpackages/client/README.md, under "Run an evaluation from code". A normal handler span written with the resolved default still omits that content.test_offline_spans_export_content_when_capture_content_is_falsecovers both sides. Staging was not rerun.This change does not touch the
generation_incompletecriterion event that Python still emits for a failed row. §8.8.6 tracks that as a separate divergence. Here the runner just stops wrapping it in a span.Known view-side gap: gonfalon shows
gen_ai.system_instructionsas its raw JSON part array, because it reads that attribute as a plain string.Neither repo has a pull request template (
pull_request_templateis unset). This stays a draft.