Skip to content

Commit 6f670e8

Browse files
authored
KG-647. Update OpenTelemetry feature span attributes (#1359)
KG-647. Update OpenTelemetry feature span attributes according to semantic convention. - Refactor span creation to use builder pattern; - Add Strategy Span for tracking strategy-level execution; - Improve test infrastructure with helper methods in TraceStructureTestBase; - Update tests with new attributes checks.
1 parent fc44846 commit 6f670e8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+3817
-1720
lines changed

agents/agents-features/agents-features-opentelemetry/README.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ Key OpenTelemetry concepts
2121

2222
The OpenTelemetry feature in Koog automatically creates spans for various agent events, including:
2323

24-
- Agent execution start and end
24+
- Agent creation and invocation
25+
- Strategy execution
2526
- Node execution
26-
- LLM calls
27+
- LLM calls (inference)
2728
- Tool calls
2829

2930
## Installation
@@ -177,25 +178,36 @@ The OpenTelemetry feature creates different types of spans for various operation
177178
2. **Invoke Agent Span**
178179
- Purpose: One concrete execution (run) of an agent.
179180
- Emitted: At the start of a run. Closed on successful finish or error.
180-
- Parent: Parent: **Create Agent Span**. Children: **Node Execute Span** instances, and, indirectly, LLM and Tool spans within nodes.
181-
- Useful for: Measuring run‑level timing, status, and grouping of all node, tool, and LLM activity for a run.
181+
- Parent: **Create Agent Span**. Children: **Strategy Span** instances, and, indirectly, node, tool, and LLM spans within strategies.
182+
- Useful for: Measuring run‑level timing, status, and grouping of all strategy, node, tool, and LLM activity for a run.
182183
- Key attributes:
183184
- `gen_ai.operation.name` = `invoke_agent`
184185
- `gen_ai.agent.id`
185186
- `gen_ai.conversation.id`
186187
- `gen_ai.system` (LLM provider)
187188
- `gen_ai.response.finish_reasons` (on error)
188189

189-
3. **Node Execute Span**
190+
3. **Strategy Span**
191+
- Purpose: Execution of a strategy within an agent run.
192+
- Emitted: At the start of strategy execution. Closed when the strategy completes or errors.
193+
- Parent: **Invoke Agent Span**. Children: **Node Execute Span** instances.
194+
- Useful for: Tracking strategy-level execution, grouping all node executions within a strategy, and measuring strategy performance.
195+
- Key attributes:
196+
- `gen_ai.conversation.id`
197+
- `koog.strategy.name`
198+
- `koog.event.id`
199+
- Note: This is a custom span type specific to Koog, not defined in the OpenTelemetry Semantic Conventions.
200+
201+
4. **Node Execute Span**
190202
- Purpose: Execution of a single node in the agent strategy.
191203
- Emitted: Immediately before a node runs. Closed after the node completes or errors.
192-
- Parent: **Invoke Agent Span**. Children: **Inference Spans** and **Execute Tool Span** instances created by the node.
204+
- Parent: **Strategy Span**. Children: **Inference Spans** and **Execute Tool Span** instances created by the node.
193205
- Useful for: Understanding strategy flow and attributing LLM or tool work to a specific node.
194206
- Key attributes:
195207
- `gen_ai.conversation.id`
196208
- `koog.node.id`
197209

198-
4. **Inference Span**
210+
5. **Inference Span**
199211
- Purpose: A single LLM call (prompt execution).
200212
- Emitted: Before the LLM is invoked. Closed after responses are received.
201213
- Parent: **Node Execute Span**.
@@ -216,7 +228,7 @@ The OpenTelemetry feature creates different types of spans for various operation
216228
- `gen_ai.usage.total_tokens` (when available)
217229
- `gen_ai.response.finish_reasons` (Stop, ToolCalls, etc.)
218230

219-
5. **Execute Tool Span**
231+
6. **Execute Tool Span**
220232
- Purpose: Execution of a tool or function call triggered by the agent or LLM.
221233
- Emitted: When a tool is called. Closed after the tool returns a result or fails with an error during validation or execution.
222234
- Parent: **Node Execute Span**.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package ai.koog.agents.features.opentelemetry.attribute
2+
3+
import ai.koog.agents.utils.HiddenString
4+
5+
/**
6+
* An internal object that contains nested sealed interfaces and data classes for representing structured attributes
7+
* used within the Koog framework. These attributes facilitate the creation and organization of hierarchical
8+
* key-value pairs, which can be used for event tracking, strategy configuration, node identification,
9+
* and subgraph definitions.
10+
*/
11+
internal object KoogAttributes {
12+
13+
sealed interface Koog : Attribute {
14+
override val key: String
15+
get() = "koog"
16+
17+
sealed interface Event : Koog {
18+
override val key: String
19+
get() = super.key.concatKey("event")
20+
21+
data class Id(private val id: String) : Event {
22+
override val key: String = super.key.concatKey("id")
23+
override val value: String = id
24+
}
25+
}
26+
27+
sealed interface Strategy : Koog {
28+
override val key: String
29+
get() = super.key.concatKey("strategy")
30+
31+
data class Name(private val name: String) : Strategy {
32+
override val key: String = super.key.concatKey("name")
33+
override val value: String = name
34+
}
35+
}
36+
37+
sealed interface Node : Koog {
38+
override val key: String
39+
get() = super.key.concatKey("node")
40+
41+
data class Id(private val id: String) : Node {
42+
override val key: String = super.key.concatKey("id")
43+
override val value: String = id
44+
}
45+
46+
data class Input(private val input: String) : Node {
47+
override val key: String = super.key.concatKey("input")
48+
override val value: HiddenString = HiddenString(input)
49+
}
50+
51+
data class Output(private val output: String) : Node {
52+
override val key: String = super.key.concatKey("output")
53+
override val value: HiddenString = HiddenString(output)
54+
}
55+
}
56+
57+
sealed interface Subgraph : Koog {
58+
override val key: String
59+
get() = super.key.concatKey("subgraph")
60+
61+
data class Id(private val id: String) : Subgraph {
62+
override val key: String = super.key.concatKey("id")
63+
override val value: String = id
64+
}
65+
66+
data class Input(private val input: String) : Subgraph {
67+
override val key: String = super.key.concatKey("input")
68+
override val value: HiddenString = HiddenString(input)
69+
}
70+
71+
data class Output(private val output: String) : Subgraph {
72+
override val key: String = super.key.concatKey("output")
73+
override val value: HiddenString = HiddenString(output)
74+
}
75+
}
76+
}
77+
}

agents/agents-features/agents-features-opentelemetry/src/jvmMain/kotlin/ai/koog/agents/features/opentelemetry/attribute/SpanAttributes.kt

Lines changed: 183 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
package ai.koog.agents.features.opentelemetry.attribute
22

3+
import ai.koog.agents.core.tools.ToolDescriptor
34
import ai.koog.agents.utils.HiddenString
5+
import ai.koog.prompt.llm.LLMProvider
46
import ai.koog.prompt.llm.LLModel
7+
import ai.koog.prompt.message.ContentPart
8+
import ai.koog.prompt.message.Message
9+
import kotlinx.serialization.json.JsonArray
10+
import kotlinx.serialization.json.JsonArrayBuilder
11+
import kotlinx.serialization.json.JsonElement
12+
import kotlinx.serialization.json.JsonObject
13+
import kotlinx.serialization.json.JsonPrimitive
14+
import kotlinx.serialization.json.addJsonObject
15+
import kotlinx.serialization.json.buildJsonObject
16+
import kotlinx.serialization.json.putJsonArray
517

618
/**
719
* The class describe Attributes related to a Spans in GenAI system.
@@ -86,6 +98,18 @@ internal object SpanAttributes {
8698
}
8799
}
88100

101+
// gen_ai.provider
102+
103+
sealed interface Provider : GenAIAttribute {
104+
override val key: String
105+
get() = super.key.concatKey("provider")
106+
107+
data class Name(private val provider: LLMProvider) : Provider {
108+
override val key: String = super.key.concatKey("name")
109+
override val value: String = provider.id
110+
}
111+
}
112+
89113
// gen_ai.conversation
90114
sealed interface Conversation : GenAIAttribute {
91115
override val key: String
@@ -110,6 +134,31 @@ internal object SpanAttributes {
110134
}
111135
}
112136

137+
// gen_ai.input
138+
sealed interface Input : GenAIAttribute {
139+
override val key: String
140+
get() = super.key.concatKey("input")
141+
142+
// gen_ai.input.messages
143+
data class Messages(private val messages: List<Message>) : Input {
144+
override val key: String = super.key.concatKey("messages")
145+
override val value: HiddenString = HiddenString(
146+
JsonArray(
147+
messages.map { message ->
148+
buildJsonObject {
149+
put("role", JsonPrimitive(message.role.name))
150+
putJsonArray("parts") {
151+
message.parts.forEach { part ->
152+
addContentPart(part, message)
153+
}
154+
}
155+
}
156+
}
157+
).toString()
158+
)
159+
}
160+
}
161+
113162
// gen_ai.output
114163
sealed interface Output : GenAIAttribute {
115164
override val key: String
@@ -121,6 +170,25 @@ internal object SpanAttributes {
121170
override val value: String = type.id
122171
}
123172

173+
// gen_ai.output.messages
174+
data class Messages(private val messages: List<Message>) : Output {
175+
override val key: String = super.key.concatKey("messages")
176+
override val value: HiddenString = HiddenString(
177+
JsonArray(
178+
messages.map { message ->
179+
buildJsonObject {
180+
put("role", JsonPrimitive(message.role.name))
181+
putJsonArray("parts") {
182+
message.parts.forEach { part ->
183+
addContentPart(part, message)
184+
}
185+
}
186+
}
187+
}
188+
).toString()
189+
)
190+
}
191+
124192
enum class OutputType(val id: String) {
125193
TEXT("text"),
126194
JSON("json"),
@@ -278,6 +346,18 @@ internal object SpanAttributes {
278346
override val key: String = super.key.concatKey("id")
279347
override val value: String = id
280348
}
349+
350+
// gen_ai.tool.call.arguments
351+
data class Arguments(private val arguments: JsonObject) : Call {
352+
override val key: String = super.key.concatKey("arguments")
353+
override val value: HiddenString = HiddenString(arguments.toString())
354+
}
355+
356+
// gen_ai.tool.call.result
357+
data class Result(private val result: JsonElement) : Call {
358+
override val key: String = super.key.concatKey("result")
359+
override val value: HiddenString = HiddenString(result.toString())
360+
}
281361
}
282362

283363
// gen_ai.tool.description
@@ -292,16 +372,111 @@ internal object SpanAttributes {
292372
override val value: String = name
293373
}
294374

295-
// Custom tool attribute with tool arguments used for tool calls
296-
data class InputValue(private val input: String) : Attribute {
297-
override val key: String = "input.value"
298-
override val value: HiddenString = HiddenString(input)
375+
// gen_ai.tool.definitions
376+
data class Definitions(private val tools: List<ToolDescriptor>) : Tool {
377+
override val key: String = super.key.concatKey("definitions")
378+
override val value: HiddenString = HiddenString(
379+
JsonArray(
380+
tools.map { tool ->
381+
buildJsonObject {
382+
put("type", JsonPrimitive("function"))
383+
put("name", JsonPrimitive(tool.name))
384+
put("description", JsonPrimitive(tool.description))
385+
}
386+
}
387+
).toString()
388+
)
299389
}
390+
}
391+
392+
// gen_ai.system_instructions
393+
data class SystemInstructions(private val messages: List<Message.System>) : GenAIAttribute {
394+
override val key: String = "system_instructions"
395+
override val value: HiddenString = run {
396+
val jsonObjects = messages.flatMap { (parts, metaInfo) ->
397+
parts.map { part ->
398+
JsonObject(
399+
mapOf(
400+
"type" to JsonPrimitive("text"),
401+
"content" to JsonPrimitive(part.text)
402+
)
403+
)
404+
}
405+
}
406+
407+
HiddenString(JsonArray(jsonObjects).toString())
408+
}
409+
}
410+
411+
//region Private Methods
412+
413+
private fun JsonArrayBuilder.addContentPart(part: ContentPart, message: Message) {
414+
when (part) {
415+
is ContentPart.Text -> {
416+
when (message) {
417+
is Message.Tool.Call -> {
418+
addJsonObject {
419+
put("type", JsonPrimitive("tool_call"))
420+
message.id?.let { id -> put("id", JsonPrimitive(id)) }
421+
put("name", JsonPrimitive(message.tool))
422+
put("arguments", message.contentJson)
423+
}
424+
}
425+
426+
is Message.Tool.Result -> {
427+
addJsonObject {
428+
put("type", JsonPrimitive("tool_call_response"))
429+
message.id?.let { id -> put("id", JsonPrimitive(id)) }
430+
put("result", JsonPrimitive(part.text))
431+
}
432+
}
433+
434+
else -> {
435+
addJsonObject {
436+
put("type", JsonPrimitive("text"))
437+
put("content", JsonPrimitive(part.text))
438+
}
439+
}
440+
}
441+
}
300442

301-
// Custom tool attribute with tool execution results used for tool calls
302-
data class OutputValue(private val output: String) : Attribute {
303-
override val key: String = "output.value"
304-
override val value: HiddenString = HiddenString(output)
443+
is ContentPart.Image -> {
444+
addJsonObject {
445+
put("type", JsonPrimitive("image"))
446+
put("format", JsonPrimitive(part.format))
447+
put("mimeType", JsonPrimitive(part.mimeType))
448+
part.fileName?.let { name -> put("fileName", JsonPrimitive(name)) }
449+
}
450+
}
451+
452+
is ContentPart.Video -> {
453+
addJsonObject {
454+
put("type", JsonPrimitive("video"))
455+
put("format", JsonPrimitive(part.format))
456+
put("mimeType", JsonPrimitive(part.mimeType))
457+
part.fileName?.let { name -> put("fileName", JsonPrimitive(name)) }
458+
}
459+
}
460+
461+
is ContentPart.Audio -> {
462+
addJsonObject {
463+
put("type", JsonPrimitive("audio"))
464+
put("format", JsonPrimitive(part.format))
465+
put("mimeType", JsonPrimitive(part.mimeType))
466+
part.fileName?.let { name -> put("fileName", JsonPrimitive(name)) }
467+
}
468+
}
469+
470+
is ContentPart.File -> {
471+
addJsonObject {
472+
put("type", JsonPrimitive("file"))
473+
put("format", JsonPrimitive(part.format))
474+
put("mimeType", JsonPrimitive(part.mimeType))
475+
part.fileName?.let { name -> put("fileName", JsonPrimitive(name)) }
476+
}
477+
}
305478
}
306479
}
480+
481+
//endregion Private Methods
307482
}

0 commit comments

Comments
 (0)