Skip to content

feat: enhance tracing system with OpenTelemetry semantic conventions #1331

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 9 additions & 6 deletions nemoguardrails/actions/llm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def _infer_model_name(llm: BaseLanguageModel):
async def llm_call(
llm: BaseLanguageModel,
prompt: Union[str, List[dict]],
model_name: Optional[str] = None,
model_provider: Optional[str] = None,
stop: Optional[List[str]] = None,
custom_callback_handlers: Optional[List[AsyncCallbackHandler]] = None,
) -> str:
Expand All @@ -76,7 +78,8 @@ async def llm_call(
llm_call_info = LLMCallInfo()
llm_call_info_var.set(llm_call_info)

llm_call_info.llm_model_name = _infer_model_name(llm)
llm_call_info.llm_model_name = model_name or _infer_model_name(llm)
llm_call_info.llm_provider_name = model_provider

if custom_callback_handlers and custom_callback_handlers != [None]:
all_callbacks = BaseCallbackManager(
Expand Down Expand Up @@ -172,15 +175,15 @@ def get_colang_history(
history += f'user "{event["text"]}"\n'
elif event["type"] == "UserIntent":
if include_texts:
history += f' {event["intent"]}\n'
history += f" {event['intent']}\n"
else:
history += f'user {event["intent"]}\n'
history += f"user {event['intent']}\n"
elif event["type"] == "BotIntent":
# If we have instructions, we add them before the bot message.
# But we only do that for the last bot message.
if "instructions" in event and idx == last_bot_intent_idx:
history += f"# {event['instructions']}\n"
history += f'bot {event["intent"]}\n'
history += f"bot {event['intent']}\n"
elif event["type"] == "StartUtteranceBotAction" and include_texts:
history += f' "{event["script"]}"\n'
# We skip system actions from this log
Expand Down Expand Up @@ -349,9 +352,9 @@ def flow_to_colang(flow: Union[dict, Flow]) -> str:
if "_type" not in element:
raise Exception("bla")
if element["_type"] == "UserIntent":
colang_flow += f'user {element["intent_name"]}\n'
colang_flow += f"user {element['intent_name']}\n"
elif element["_type"] == "run_action" and element["action_name"] == "utter":
colang_flow += f'bot {element["action_params"]["value"]}\n'
colang_flow += f"bot {element['action_params']['value']}\n"

return colang_flow

Expand Down
6 changes: 5 additions & 1 deletion nemoguardrails/logging/explain.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ class LLMCallInfo(LLMCallSummary):
default="unknown",
description="The name of the model use for the LLM call.",
)
llm_provider_name: Optional[str] = Field(
default="unknown",
description="The provider of the model used for the LLM call, e.g. 'openai', 'nvidia'.",
)


class ExplainInfo(BaseModel):
Expand Down Expand Up @@ -100,7 +104,7 @@ def print_llm_calls_summary(self):
for i in range(len(self.llm_calls)):
llm_call = self.llm_calls[i]
msg = (
f"{i+1}. Task `{llm_call.task}` took {llm_call.duration:.2f} seconds "
f"{i + 1}. Task `{llm_call.task}` took {llm_call.duration:.2f} seconds "
+ (
f"and used {llm_call.total_tokens} tokens."
if total_tokens
Expand Down
12 changes: 12 additions & 0 deletions nemoguardrails/rails/llm/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,18 @@ class TracingConfig(BaseModel):
default_factory=lambda: [LogAdapterConfig()],
description="The list of tracing adapters to use. If not specified, the default adapters are used.",
)
span_format: str = Field(
default="opentelemetry",
description="The span format to use. Options are 'flat' (simple metrics) or 'opentelemetry' (OpenTelemetry semantic conventions).",
)
enable_content_capture: bool = Field(
default=False,
description=(
"Capture prompts and responses (user/assistant/tool message content) in tracing/telemetry events. "
"Disabled by default for privacy and alignment with OpenTelemetry GenAI semantic conventions. "
"WARNING: Enabling this may include PII and sensitive data in your telemetry backend."
),
)


class EmbeddingsCacheConfig(BaseModel):
Expand Down
16 changes: 14 additions & 2 deletions nemoguardrails/rails/llm/llmrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ def __init__(
from nemoguardrails.tracing import create_log_adapters

self._log_adapters = create_log_adapters(config.tracing)
else:
self._log_adapters = None

# We run some additional checks on the config
self._validate_config()
Expand Down Expand Up @@ -1149,9 +1151,19 @@ async def generate_async(
# lazy import to avoid circular dependency
from nemoguardrails.tracing import Tracer

# Create a Tracer instance with instantiated adapters
span_format = getattr(
self.config.tracing, "span_format", "opentelemetry"
)
enable_content_capture = getattr(
self.config.tracing, "enable_content_capture", False
)
# Create a Tracer instance with instantiated adapters and span configuration
tracer = Tracer(
input=messages, response=res, adapters=self._log_adapters
input=messages,
response=res,
adapters=self._log_adapters,
span_format=span_format,
enable_content_capture=enable_content_capture,
)
await tracer.export_async()

Expand Down
22 changes: 21 additions & 1 deletion nemoguardrails/tracing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,24 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from .tracer import InteractionLog, Tracer, create_log_adapters
from .interaction_types import InteractionLog, InteractionOutput
from .span_extractors import (
SpanExtractor,
SpanExtractorV1,
SpanExtractorV2,
create_span_extractor,
)
from .spans import SpanEvent, SpanFlat, SpanOpentelemetry
from .tracer import Tracer, create_log_adapters

___all__ = [
SpanExtractor,
SpanExtractorV1,
SpanExtractorV2,
create_span_extractor,
Tracer,
create_log_adapters,
SpanEvent,
SpanFlat,
SpanOpentelemetry,
]
2 changes: 1 addition & 1 deletion nemoguardrails/tracing/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from abc import ABC, abstractmethod
from typing import Optional

from nemoguardrails.eval.models import InteractionLog
from nemoguardrails.tracing.interaction_types import InteractionLog


class InteractionLogAdapter(ABC):
Expand Down
36 changes: 11 additions & 25 deletions nemoguardrails/tracing/adapters/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
from nemoguardrails.tracing import InteractionLog

from nemoguardrails.tracing.adapters.base import InteractionLogAdapter
from nemoguardrails.tracing.span_formatting import format_span_for_filesystem


class FileSystemAdapter(InteractionLogAdapter):
name = "FileSystem"
SCHEMA_VERSION = "2.0"

def __init__(self, filepath: Optional[str] = None):
if not filepath:
Expand All @@ -41,53 +43,37 @@ def transform(self, interaction_log: "InteractionLog"):
spans = []

for span_data in interaction_log.trace:
span_dict = {
"name": span_data.name,
"span_id": span_data.span_id,
"parent_id": span_data.parent_id,
"trace_id": interaction_log.id,
"start_time": span_data.start_time,
"end_time": span_data.end_time,
"duration": span_data.duration,
"metrics": span_data.metrics,
}
span_dict = format_span_for_filesystem(span_data)
spans.append(span_dict)

log_dict = {
"schema_version": self.SCHEMA_VERSION,
"trace_id": interaction_log.id,
"spans": spans,
}

with open(self.filepath, "a") as f:
f.write(json.dumps(log_dict, indent=2) + "\n")
with open(self.filepath, "a", encoding="utf-8") as f:
f.write(json.dumps(log_dict) + "\n")

async def transform_async(self, interaction_log: "InteractionLog"):
try:
import aiofiles
except ImportError:
raise ImportError(
"aiofiles is required for async file writing. Please install it using `pip install aiofiles"
"aiofiles is required for async file writing. Please install it using `pip install aiofiles`"
)

spans = []

for span_data in interaction_log.trace:
span_dict = {
"name": span_data.name,
"span_id": span_data.span_id,
"parent_id": span_data.parent_id,
"trace_id": interaction_log.id,
"start_time": span_data.start_time,
"end_time": span_data.end_time,
"duration": span_data.duration,
"metrics": span_data.metrics,
}
span_dict = format_span_for_filesystem(span_data)
spans.append(span_dict)

log_dict = {
"schema_version": self.SCHEMA_VERSION,
"trace_id": interaction_log.id,
"spans": spans,
}

async with aiofiles.open(self.filepath, "a") as f:
await f.write(json.dumps(log_dict, indent=2) + "\n")
async with aiofiles.open(self.filepath, "a", encoding="utf-8") as f:
await f.write(json.dumps(log_dict) + "\n")
Loading