85742ab165
CPU Test / Lint - next (push) Waiting to run
Dashboard / Chromatic (push) Waiting to run
CPU Test / Lint - fast (push) Waiting to run
CPU Test / Build documentation (push) Waiting to run
CPU Test / Test (Store, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Weave, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Others, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Store, latest, Python 3.13) (push) Waiting to run
CPU Test / Lint - slow (push) Waiting to run
CPU Test / Lint - JavaScript (push) Waiting to run
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Others, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Weave, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (JavaScript) (push) Waiting to run
Deploy Documentation / deploy (push) Has been cancelled
220 lines
10 KiB
Markdown
220 lines
10 KiB
Markdown
# Using Emitters
|
|
|
|
[](){ #using-emitter }
|
|
|
|
While returning a single float for the final reward is sufficient for many algorithm-agent combinations, some advanced scenarios require richer feedback. For instance, an algorithm might learn more effectively if it receives intermediate rewards throughout a multi-step task, or if the agent needs to emit additional spans for debugging or analysis.
|
|
|
|
Agent-lightning provides an **emitter** module for recording custom spans inside your agent logic. Just as [Tracer][agentlightning.Tracer] automatically instruments common operations (for example, LLM calls), each emitter helper sends a [Span][agentlightning.Span] that captures Agent-lightning-specific work so downstream algorithms can query it later. See [Working with Traces](./traces.md) for more details.
|
|
|
|
For multi-step routines such as function calls, tools, or adapters, wrap code with [`operation`][agentlightning.operation] — either as a decorator or a context manager — to capture inputs, outputs, and metadata on a dedicated [`operation`][agentlightning.operation] span. This makes it easier to correlate downstream annotations (like rewards or messages) with the higher-level work that produced them.
|
|
|
|
You can find the emitter functions in [`agentlightning.emitter`](../reference/agent.md).
|
|
|
|
## Emitting Rewards, Messages, and More
|
|
|
|
Here are the primary emitter functions:
|
|
|
|
* [`emit_reward(value: float)`][agentlightning.emit_reward]: Records an intermediate/final reward, which is a convenient wrapper of [`emit_annotation`][agentlightning.emit_annotation].
|
|
* [`emit_annotation(attributes: Dict[str, Any])`][agentlightning.emit_annotation]: Records arbitrary metadata as a span.
|
|
* [`emit_message(message: str)`][agentlightning.emit_message]: Records a simple log message as a span.
|
|
* [`emit_exception(exception: BaseException)`][agentlightning.emit_exception]: Records a Python exception, including its type, message, and stack trace.
|
|
* [`emit_object(obj: Any)`][agentlightning.emit_object]: Records any JSON-serializable object, perfect for structured data.
|
|
|
|
Let's first see an example of an agent using these emitters to provide detailed feedback.
|
|
|
|
```python
|
|
import agentlightning as agl
|
|
|
|
@agl.rollout
|
|
def multi_step_agent(task: dict, prompt_template: PromptTemplate) -> float:
|
|
try:
|
|
# Step 1: Initial planning
|
|
agl.emit_message("Starting planning phase.")
|
|
plan = generate_plan(task, prompt_template)
|
|
agl.emit_object({"plan_steps": len(plan), "first_step": plan[0]})
|
|
|
|
# Award a small reward for a valid plan
|
|
plan_reward = grade_plan(plan)
|
|
agl.emit_reward(plan_reward)
|
|
|
|
# Step 2: Execute the plan
|
|
agl.emit_message(f"Executing {len(plan)}-step plan.")
|
|
execution_result = execute_plan(plan)
|
|
|
|
# Step 3: Final evaluation
|
|
final_reward = custom_grade_final_result(execution_result, task["expected_output"])
|
|
|
|
# The return value is treated as the final reward for the rollout
|
|
return final_reward
|
|
|
|
except ValueError as e:
|
|
# Record the specific error and return a failure reward
|
|
agl.emit_exception(e)
|
|
return 0.0
|
|
```
|
|
|
|
Each helper accepts nested `attributes` (or keyword arguments for [`operation`][agentlightning.operation]) and automatically flattens/sanitizes them into dotted OpenTelemetry keys. This means you can pass ordinary dictionaries/lists without pre-processing and still get consistent attribute names such as `meta.any_attribute` across all emitter operations. Agent-lightning does not restrict the attributes you supply, but it is best to consult [OpenTelemetry's semantic conventions](https://opentelemetry.io/docs/specs/semconv/) for recommended names. Agent-lightning also defines [specific semconv](../reference/semconv.md) for its own use cases. The pattern looks like this:
|
|
|
|
```python
|
|
from opentelemetry.semconv.attributes import server_attributes
|
|
from agentlightning import emit_object
|
|
|
|
emit_object({
|
|
"name": "John Doe",
|
|
"age": 30,
|
|
"email": "john.doe@example.com",
|
|
}, attributes={
|
|
server_attributes.SERVER_ADDRESS: "127.0.0.1",
|
|
server_attributes.SERVER_PORT: 8080,
|
|
})
|
|
```
|
|
|
|
Running the above code sends the following span to the backend if you have a tracer active:
|
|
|
|
```text
|
|
Span(
|
|
name='agentlightning.object',
|
|
attributes={
|
|
'agentlightning.object.type': 'dict',
|
|
'agentlightning.object.json': '{"name": "John Doe", "age": 30, "email": "john.doe@example.com"}',
|
|
'server.address': '127.0.0.1',
|
|
'server.port': 8080
|
|
}
|
|
)
|
|
```
|
|
|
|
!!! tip
|
|
|
|
If you don't have a tracer active, the above code will raise the following error:
|
|
|
|
```text
|
|
RuntimeError: No active tracer found. Cannot emit object span.
|
|
```
|
|
|
|
By default, emitter helpers delegate to the active tracer to create and export spans (specifically via [`Tracer.create_span`][agentlightning.Tracer.create_span]). If you want to emit spans without an active tracer, set `propagate=False` to keep the span local — a useful option for offline tests. The default `True` streams spans through the active tracer/exporters.
|
|
|
|
When working with [agentlightning.semconv](../reference/semconv.md), you typically use utilities such as [`make_tag_attributes`][agentlightning.utils.otel.make_tag_attributes] and [`make_link_attributes`][agentlightning.utils.otel.make_link_attributes] to build the attributes dictionary. For example:
|
|
|
|
```python
|
|
from agentlightning.utils.otel import make_tag_attributes
|
|
|
|
emit_annotation(make_tag_attributes(["tool", "calculator", "fast", "good"]))
|
|
```
|
|
|
|
The above code will send a span with the following attributes to the backend:
|
|
|
|
```json
|
|
{
|
|
"agentlightning.tag.0": "tool",
|
|
"agentlightning.tag.1": "calculator",
|
|
"agentlightning.tag.2": "fast",
|
|
"agentlightning.tag.3": "good"
|
|
}
|
|
```
|
|
|
|
A counterpart utility function [`extract_tags_from_attributes`][agentlightning.utils.otel.extract_tags_from_attributes] is also available to extract the tags from the attributes dictionary.
|
|
|
|
## Operations
|
|
|
|
The [`operation`][agentlightning.operation] helper tracks logical units of work within your agent, capturing inputs, outputs, timing, and success/failure status. Unlike point-in-time emitters, operations create a span representing a time interval. Use operations for tool calls, multi-step workflows, debugging, and performance monitoring. [`operation`][agentlightning.operation] works as either a decorator or a context manager.
|
|
|
|
The decorator automatically captures function arguments as inputs and the return value as output:
|
|
|
|
```python
|
|
import agentlightning as agl
|
|
|
|
@agl.operation
|
|
def search_documents(query: str, max_results: int = 10) -> list[dict]:
|
|
results = perform_search(query, max_results)
|
|
return results
|
|
|
|
@agl.operation(category="tool", priority="high")
|
|
def execute_calculation(expression: str) -> float:
|
|
return eval_safely(expression)
|
|
```
|
|
|
|
The example above emits a span with `{"category": "tool", "priority": "high"}` attributes. It also records the function input and output via [OPERATION_INPUT][agentlightning.semconv.LightningSpanAttributes.OPERATION_INPUT] and [OPERATION_OUTPUT][agentlightning.semconv.LightningSpanAttributes.OPERATION_OUTPUT]. It works with async functions too:
|
|
|
|
```python
|
|
@agl.operation
|
|
async def async_api_call(endpoint: str, payload: dict) -> dict:
|
|
response = await http_client.post(endpoint, json=payload)
|
|
return response.json()
|
|
```
|
|
|
|
Override the operation name if needed:
|
|
|
|
```python
|
|
@agl.operation(name="custom-name")
|
|
def any_weird_name_i_dont_want():
|
|
pass
|
|
```
|
|
|
|
For more control, [`operation`][agentlightning.operation] can also be used as a context manager to explicitly record inputs and outputs:
|
|
|
|
```python
|
|
with agl.operation(tool_name="web_search") as op:
|
|
op.set_input(query="latest AI research", filters={"date": "2024"})
|
|
results = search_web("latest AI research", {"date": "2024"})
|
|
op.set_output({"result_count": len(results), "top_result": results[0]})
|
|
```
|
|
|
|
The `propagate=False` flag also applies to [`operation`][agentlightning.operation] when you want to keep operations local without requiring an active tracer:
|
|
|
|
```python
|
|
@agl.operation(propagate=False)
|
|
def local_test():
|
|
return "Not sent to backend"
|
|
```
|
|
|
|
## Linking to Other Spans
|
|
|
|
Sometimes a span should explicitly point back to another span that produced the input it is working on (for example, linking a reward annotation to the [`agentlightning.operation`][agentlightning.operation] span that generated a response). Agent-lightning encodes these relationships through flattened link attributes. The helper [`make_link_attributes`][agentlightning.utils.otel.make_link_attributes] converts a dictionary of keys such as `trace_id`, `span_id`, or any custom attribute into the `"agentlightning.link.*"` ([LightningSpanAttributes.LINK][agentlightning.semconv.LightningSpanAttributes.LINK]) fields expected by the backend. Later, [`query_linked_spans`][agentlightning.utils.otel.query_linked_spans] can recover the original span(s) from those link descriptors.
|
|
|
|
```python
|
|
import opentelemetry.trace as trace_api
|
|
from agentlightning import emit_annotation, operation
|
|
from agentlightning.utils.otel import make_link_attributes, make_tag_attributes
|
|
|
|
with operation(conversation_id="chat-42") as op:
|
|
# ... perform the work ...
|
|
link_attrs = make_link_attributes({
|
|
"conversation_id": "chat-42",
|
|
})
|
|
|
|
emit_annotation(
|
|
{
|
|
**link_attrs,
|
|
**make_tag_attributes(["reward", "good"]),
|
|
}
|
|
)
|
|
```
|
|
|
|
When analyzing in adapters, pass the extracted link models to [`query_linked_spans`][agentlightning.utils.otel.query_linked_spans] to retrieve the matching span(s):
|
|
|
|
```python
|
|
from agentlightning.utils.otel import extract_links_from_attributes, query_linked_spans
|
|
|
|
annotation_span = ... # Span from your trace store
|
|
operation_spans = [...] # list of spans you want to search
|
|
|
|
link_models = extract_links_from_attributes(annotation_span.attributes)
|
|
matches = query_linked_spans(operation_spans, link_models)
|
|
assert matches # Contains the original operation span
|
|
```
|
|
|
|
!!! tip "Correlating Rewards with LLM Requests"
|
|
|
|
[Tracer](./traces.md) instruments each request/response as its own span. You can link to the [`gen_ai.response.id`](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events/) attribute, which comes from the LLM response ID.
|
|
|
|
```python
|
|
from agentlightning import emit_reward
|
|
from agentlightning.utils.otel import make_link_attributes
|
|
|
|
result = call_llm(prompt)
|
|
reward_links = make_link_attributes({"gen_ai.response.id": result.id})
|
|
emit_reward(0.9, attributes=reward_links)
|
|
```
|
|
|
|
Later, use the same `gen_ai.response.id` key inside `query_linked_spans` to find the reward(s) that reference that specific LLM request span.
|