Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add instrumentation for Agno Team, Workflow, and Model response methods.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ Usage
# Instrument Agno
AgnoInstrumentor().instrument()

Supported Operations
--------------------

The instrumentation automatically traces:

* ``Agent.run`` and ``Agent.arun``
* ``Team.run`` and ``Team.arun``
* ``Workflow.run`` and ``Workflow.arun``
* ``Model.response`` and ``Model.aresponse``
* ``FunctionCall.execute`` and ``FunctionCall.aexecute``

Configuration
-------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ dependencies = [
"opentelemetry-api ~= 1.43",
"opentelemetry-instrumentation >= 0.64b0, <1",
"opentelemetry-semantic-conventions >= 0.64b0, <1",
"opentelemetry-util-genai >= 1.0b0, <2",
"opentelemetry-util-genai >= 1.1b0.dev, <2",
]

[project.optional-dependencies]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
from opentelemetry.util.genai.handler import TelemetryHandler
from opentelemetry.util.genai.invocation import (
AgentInvocation,
InferenceInvocation,
ToolInvocation,
WorkflowInvocation,
)
from opentelemetry.util.genai.types import (
InputMessage,
Expand All @@ -31,12 +33,18 @@

_AGNO_MODULE = "agno.agent"
_AGENT_CLASS = "Agent"
_AGNO_TEAM_MODULE = "agno.team"
_TEAM_CLASS = "Team"
_AGNO_TOOLS_MODULE = "agno.tools.function"
_FUNCTION_CALL_CLASS = "FunctionCall"
_AGNO_WORKFLOW_MODULE = "agno.workflow.workflow"
_WORKFLOW_CLASS = "Workflow"
_AGNO_MODELS_MODULE = "agno.models.base"
_MODEL_CLASS = "Model"


def patch_agent(handler: TelemetryHandler) -> None:
"""Apply patches to agno.agent.Agent and agno.tools.function.FunctionCall class methods."""
"""Apply patches to Agno class methods."""
wrap_function_wrapper(
_AGNO_MODULE,
f"{_AGENT_CLASS}.run",
Expand All @@ -47,6 +55,19 @@ def patch_agent(handler: TelemetryHandler) -> None:
f"{_AGENT_CLASS}.arun",
_agent_arun(handler),
)
try:
wrap_function_wrapper(
_AGNO_TEAM_MODULE,
f"{_TEAM_CLASS}.run",
_agent_run(handler),
)
wrap_function_wrapper(
_AGNO_TEAM_MODULE,
f"{_TEAM_CLASS}.arun",
_agent_arun(handler),
)
except (ImportError, AttributeError):
pass
try:
wrap_function_wrapper(
_AGNO_TOOLS_MODULE,
Expand All @@ -60,24 +81,72 @@ def patch_agent(handler: TelemetryHandler) -> None:
)
except (ImportError, AttributeError):
pass
try:
wrap_function_wrapper(
_AGNO_WORKFLOW_MODULE,
f"{_WORKFLOW_CLASS}.run",
_workflow_run(handler),
)
wrap_function_wrapper(
_AGNO_WORKFLOW_MODULE,
f"{_WORKFLOW_CLASS}.arun",
_workflow_arun(handler),
)
Comment thread
DylanRussell marked this conversation as resolved.
except (ImportError, AttributeError):
pass
try:
wrap_function_wrapper(
_AGNO_MODELS_MODULE,
f"{_MODEL_CLASS}.response",
_model_response(handler),
)
wrap_function_wrapper(
_AGNO_MODELS_MODULE,
f"{_MODEL_CLASS}.aresponse",
_model_aresponse(handler),
)
except (ImportError, AttributeError):
pass


def unpatch_agent() -> None:
"""Remove patches from agno.agent.Agent class methods."""
"""Remove patches from Agno class methods."""
try:
import agno.agent # pylint: disable=import-outside-toplevel

unwrap(agno.agent.Agent, "run")
unwrap(agno.agent.Agent, "arun")
except (ImportError, AttributeError):
pass
try:
import agno.team # pylint: disable=import-outside-toplevel

unwrap(agno.team.Team, "run")
unwrap(agno.team.Team, "arun")
except (ImportError, AttributeError):
pass
try:
import agno.tools.function # pylint: disable=import-outside-toplevel

unwrap(agno.tools.function.FunctionCall, "execute")
unwrap(agno.tools.function.FunctionCall, "aexecute")
except (ImportError, AttributeError):
pass
# Workflow depends on optional packages (like fastapi), may fail to import.
try:
import agno.workflow.workflow # pylint: disable=import-outside-toplevel

unwrap(agno.workflow.workflow.Workflow, "run")
unwrap(agno.workflow.workflow.Workflow, "arun")
except (ImportError, AttributeError):
pass
try:
import agno.models.base # pylint: disable=import-outside-toplevel

unwrap(agno.models.base.Model, "response")
unwrap(agno.models.base.Model, "aresponse")
except (ImportError, AttributeError):
pass


def _extract_input_content(input_val: Any) -> str:
Expand Down Expand Up @@ -136,16 +205,12 @@ def _set_tool_invocation_output(


def _set_invocation_input(
invocation: AgentInvocation,
invocation: AgentInvocation | WorkflowInvocation,
instance: Any,
args: tuple[Any, ...],
kwargs: dict[str, Any],
capture_content: bool,
) -> None:
agent_id = getattr(instance, "agent_id", None)
if agent_id:
invocation.agent_id = str(agent_id)

if capture_content and (args or "input" in kwargs):
input_val = args[0] if args else kwargs.get("input")
if input_val is not None:
Expand All @@ -156,20 +221,24 @@ def _set_invocation_input(


def _set_invocation_output(
invocation: Any,
invocation: AgentInvocation | WorkflowInvocation,
result: Any,
capture_content: bool,
) -> None:
if capture_content and result is not None:
output_str = _extract_output_content(result)
invocation.output_messages = [
OutputMessage(
role="assistant",
role=str(getattr(result, "role", "assistant")),
parts=[Text(content=output_str)],
finish_reason="stop",
finish_reason=str(getattr(result, "finish_reason", "stop")),
)
]
if hasattr(result, "session_id") and getattr(result, "session_id"):
if (
isinstance(invocation, AgentInvocation)
and hasattr(result, "session_id")
and getattr(result, "session_id")
):
invocation.conversation_id = str(getattr(result, "session_id"))


Expand All @@ -180,7 +249,7 @@ def _start_agent_invocation(
kwargs: dict[str, Any],
capture_content: bool,
) -> AgentInvocation:
agent_name = getattr(instance, "name", None) or "Agent"
agent_name = getattr(instance, "name", None)
invocation = handler.invoke_local_agent(agent_name=agent_name)
_set_invocation_input(invocation, instance, args, kwargs, capture_content)
invocation.tool_definitions = prepare_tool_definitions(
Expand Down Expand Up @@ -291,3 +360,155 @@ async def traced_method(
return result

return cast(Callable[..., Any], traced_method)


def _start_workflow_invocation(
handler: TelemetryHandler,
instance: Any,
args: tuple[Any, ...],
kwargs: dict[str, Any],
capture_content: bool,
) -> WorkflowInvocation:
workflow_name = getattr(instance, "name", None)
invocation = handler.workflow(name=workflow_name)
_set_invocation_input(invocation, instance, args, kwargs, capture_content)
return invocation


def _start_model_invocation(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to report inference from agno? does it rely on openai / anthropic / google genai to make model calls? if so, why not rely on the corresponding instrumentations to do it?

Otherwise we end up with duplication if inference is recorded by both layers.

handler: TelemetryHandler,
instance: Any,
args: tuple[Any, ...],
kwargs: dict[str, Any],
capture_content: bool,
) -> InferenceInvocation:
provider_name = (
getattr(instance, "provider", None)
or instance.__class__.__name__
or "agno"
)
request_model = getattr(instance, "id", None)
invocation = handler.inference(
provider=str(provider_name),
request_model=str(request_model) if request_model else None,
)
if capture_content and (args or "messages" in kwargs):
messages_val: Any = args[0] if args else kwargs.get("messages")
if messages_val and isinstance(messages_val, (list, tuple)):
input_msgs: list[InputMessage] = []
for msg in cast(list[Any], messages_val):
role = str(getattr(msg, "role", "user"))
content: Any = getattr(msg, "content", "")
content_str = str(content) if content is not None else ""
input_msgs.append(
InputMessage(role=role, parts=[Text(content=content_str)])
)
if input_msgs:
invocation.input_messages = input_msgs
return invocation


def _set_model_invocation_output(
invocation: Any,
result: Any,
capture_content: bool,
) -> None:
if capture_content and result is not None:
content = getattr(result, "content", None)
if content is not None:
output_str = (
str(content) if not isinstance(content, str) else content
)
invocation.output_messages = [
OutputMessage(
role=str(getattr(result, "role", "assistant")),
parts=[Text(content=output_str)],
finish_reason=str(
getattr(result, "finish_reason", "stop")
),
)
]


def _workflow_run(
Comment thread
DylanRussell marked this conversation as resolved.
handler: TelemetryHandler,
) -> Callable[..., Any]:
capture_content = handler.should_capture_content()

def traced_method(
wrapped: Callable[..., Any],
instance: Any,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> Any:
with _start_workflow_invocation(
handler, instance, args, kwargs, capture_content
) as invocation:
result = wrapped(*args, **kwargs)
_set_invocation_output(invocation, result, capture_content)
return result
Comment thread
DylanRussell marked this conversation as resolved.

return traced_method


def _workflow_arun(
handler: TelemetryHandler,
) -> Callable[..., Any]:
capture_content = handler.should_capture_content()

async def traced_method(
wrapped: Callable[..., Awaitable[Any]],
instance: Any,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> Any:
with _start_workflow_invocation(
handler, instance, args, kwargs, capture_content
) as invocation:
result = await wrapped(*args, **kwargs)
_set_invocation_output(invocation, result, capture_content)
return result

return cast(Callable[..., Any], traced_method)


def _model_response(
handler: TelemetryHandler,
) -> Callable[..., Any]:
capture_content = handler.should_capture_content()

def traced_method(
wrapped: Callable[..., Any],
instance: Any,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> Any:
with _start_model_invocation(
handler, instance, args, kwargs, capture_content
) as invocation:
result = wrapped(*args, **kwargs)
_set_model_invocation_output(invocation, result, capture_content)
return result

return traced_method


def _model_aresponse(
handler: TelemetryHandler,
) -> Callable[..., Any]:
capture_content = handler.should_capture_content()

async def traced_method(
wrapped: Callable[..., Awaitable[Any]],
instance: Any,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> Any:
with _start_model_invocation(
handler, instance, args, kwargs, capture_content
) as invocation:
result = await wrapped(*args, **kwargs)
_set_model_invocation_output(invocation, result, capture_content)
return result

return cast(Callable[..., Any], traced_method)
Loading
Loading