Commit e0c738d
authored
Extend production-observability example with accumulator pattern (#133)
* Extend production-observability with accumulator pattern
Two pre-release polish items for the v0.12.0 cycle.
CHANGELOG: the Unreleased Changed entry for the spec-pin advance
originally said proposal 0052 "lands in a follow-on PR of this
cycle" and 0054 "lands in a follow-on PR". Both have since landed
(PRs 131 + 132). Rewrites the bullet to factually describe the
final state: three proposals (0048, 0052, 0054) ship as fully
implemented, two (0051, 0053) ship as textual-only.
production-observability example: adds an LlmUsageAccumulator
class plus a terminal persist node that demonstrates the queryable
observer + drain_events_for pattern end-to-end. The accumulator
subscribes to the LLM-namespace event stream, accumulates per-
invocation token totals via current_invocation_id() bucket keys,
and exposes convention-only get_bucket / drop methods. The persist
node calls drain_events_for to synchronize on the deliver loop
before reading the bucket so the rollup reflects every LLM call
in the invocation, drops the bucket per the explicit-cleanup
discipline, and prints a cost summary. The graph grows from
respond -> END to respond -> persist -> END. Module-level
singletons (_accumulator + _compiled_graph) keep the persist
node closure-free and follow the existing _provider_instance
precedent.
Walkthrough doc updates the H1, overview, what-it-teaches list,
captured-output sample, and reading-the-output walkthrough to
cover the new pattern.
* Surface invocation-span attribution attrs in OTel formatter
The example's _format_otel_spans excluded the root
openarmature.invocation span from its captured-output listing
because two issues kept it from landing in the in-memory
exporter and from showing usefully even when it did:
1. The OTel observer's shutdown() was never called, so the root
invocation span stayed open and never moved into the
exporter's finished-spans list. Adds otel_observer.shutdown()
to the finally block after drain(), mirroring the pattern in
the OTel unit tests.
2. The formatter's curated key set didn't include the
invocation-level attributes the new span carries
(openarmature.graph.entry_node, .spec_version,
openarmature.implementation.name + .version). The formatter
now picks the right key set based on span name: the
invocation span surfaces its four invocation-level attrs
only, inner-node spans surface the per-node + cross-cutting
user.* + GenAI semconv attrs. Skipping cross-cutting attrs
on the invocation line avoids repeating data that appears
three more times below.
Net visible change: the captured-OTel-spans block now opens with
a [openarmature.invocation] line carrying
implementation_name='openarmature-python' +
implementation_version + spec_version + entry_node. Operators
filtering traces by library version in Phoenix / Datadog /
Honeycomb / Tempo / HyperDX read these directly from the root
invocation span.
Walkthrough doc's reading-the-output bullet now distinguishes
the three OTel attribute families (invocation-level 5.1,
cross-cutting 5.6, GenAI semconv) and explains why the
invocation span only closes on observer shutdown().
* Tighten accumulator example per PR review
Eight PR review threads, addressing four distinct issues.
state.invocation_id -> current_invocation_id() in the example
module docstring and walkthrough doc. The runnable persist() uses
current_invocation_id() because State has no invocation_id field
by default; the docstring snippets had drifted to the wrong shape.
assert -> RuntimeError in persist(). The three runtime
preconditions (_compiled_graph not None, _accumulator not None,
current_invocation_id() not None) now raise explicit
RuntimeError so the failure mode stays informative under python
-O, which strips asserts and would otherwise produce silent None
dereferences.
InvocationCompletedEvent backstop cleanup in the accumulator.
persist()'s drop is the fast path; if drain_events_for times out
and the deliver loop later processes late-arriving LLM events,
setdefault() would recreate a bucket that nothing ever cleans up.
Adding InvocationCompletedEvent handling at the top of __call__
drops any leftover bucket on invocation completion. The drop is
idempotent so it composes with persist()'s drop without harm.
Defensive total_tokens derivation. LlmEventPayload makes all
three usage fields optional; providers that emit prompt +
completion but no total (anything non-OpenAI in practice) would
leave bucket.total_tokens at zero while the sub-fields are
correct. Now derives total from prompt + completion when total
is None on the payload.
build_graph() self-contained per the demo convention. Previously,
persist() depended on _compiled_graph + _accumulator module
globals that only main() populated, so a copy-pasting reader
doing `graph = build_graph(); await graph.invoke(...)` would
hit RuntimeError at persist time. build_graph() now owns the
accumulator construction, the graph attach, and the global
wiring. main() drops the duplicate construction and just
attaches OTel + Langfuse on top.
* Reconcile docstrings + comments with refactor
Four stale-text findings from a second PR review pass — all
caused by the previous review pass changing behavior without
fully sweeping the surrounding documentation.
Module docstring snippet now shows the full call shape:
``await graph.drain_events_for(current_invocation_id(),
timeout=2.0)``, matching the runnable persist() pattern (the
previous snippet stripped the await + timeout for brevity but
under-described the API).
The accumulator's drop() comment block was rewritten to describe
the actual two-step lifecycle: fast-path explicit drop after read
by the terminal node, plus the InvocationCompletedEvent backstop
that the prior pass added. The old comment claimed "does NOT
auto-drop on InvocationCompletedEvent" which directly contradicted
the implementation.
RuntimeError message in persist() now points readers at
build_graph() instead of main() for the initialization pattern —
the prior pass moved the singleton wiring into build_graph but
left the error message pointing at the old call site.
Walkthrough doc's "three observers attached at compile time"
becomes "three observers attached before invoke", which is honest
for both the build_graph-side accumulator attachment and the
main-side OTel + Langfuse attachments. attach_observer happens
after compile() in the OA API regardless of which function calls
it.1 parent a283e62 commit e0c738d
3 files changed
Lines changed: 339 additions & 37 deletions
File tree
- docs/examples
- examples/production-observability
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
11 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
12 | 15 | | |
13 | 16 | | |
14 | 17 | | |
15 | | - | |
16 | | - | |
17 | | - | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
18 | 23 | | |
19 | 24 | | |
20 | 25 | | |
| |||
77 | 82 | | |
78 | 83 | | |
79 | 84 | | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
80 | 102 | | |
81 | 103 | | |
82 | 104 | | |
| |||
105 | 127 | | |
106 | 128 | | |
107 | 129 | | |
| 130 | + | |
108 | 131 | | |
109 | 132 | | |
110 | 133 | | |
111 | 134 | | |
112 | | - | |
| 135 | + | |
113 | 136 | | |
114 | | - | |
| 137 | + | |
| 138 | + | |
115 | 139 | | |
116 | 140 | | |
117 | 141 | | |
| |||
133 | 157 | | |
134 | 158 | | |
135 | 159 | | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
136 | 167 | | |
137 | 168 | | |
138 | 169 | | |
139 | | - | |
140 | | - | |
141 | | - | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
142 | 187 | | |
143 | 188 | | |
144 | 189 | | |
| |||
0 commit comments