Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { SpanKind } from '@opentelemetry/api';
import { OpenTelemetryScope } from './OpenTelemetryScope';
import { ToolCallDetails, AgentDetails, TenantDetails } from '../contracts';
import { ToolCallDetails, AgentDetails, TenantDetails, SourceMetadata } from '../contracts';
import { OpenTelemetryConstants } from '../constants';

/**
Expand All @@ -16,13 +16,27 @@ export class ExecuteToolScope extends OpenTelemetryScope {
* @param details The tool call details
* @param agentDetails The agent details
* @param tenantDetails The tenant details
* @param conversationId Optional conversation id to tag on the span (`gen_ai.conversation.id`).
* @param sourceMetadata Optional source metadata; only `name` (channel name) and `description` (channel link/URL) are used for tagging.
* @returns A new ExecuteToolScope instance.
*/
public static start(details: ToolCallDetails, agentDetails: AgentDetails, tenantDetails: TenantDetails): ExecuteToolScope {
return new ExecuteToolScope(details, agentDetails, tenantDetails);
public static start(
details: ToolCallDetails,
agentDetails: AgentDetails,
tenantDetails: TenantDetails,
conversationId?: string,
sourceMetadata?: Pick<SourceMetadata, "name" | "description">
Comment thread
juliomenendez marked this conversation as resolved.
Comment thread
fpfp100 marked this conversation as resolved.
): ExecuteToolScope {
return new ExecuteToolScope(details, agentDetails, tenantDetails, conversationId, sourceMetadata);
}

private constructor(details: ToolCallDetails, agentDetails: AgentDetails, tenantDetails: TenantDetails) {
private constructor(
details: ToolCallDetails,
agentDetails: AgentDetails,
tenantDetails: TenantDetails,
conversationId?: string,
sourceMetadata?: Pick<SourceMetadata, "name" | "description">
Comment thread
juliomenendez marked this conversation as resolved.
) {
super(
SpanKind.INTERNAL,
OpenTelemetryConstants.EXECUTE_TOOL_OPERATION_NAME,
Expand All @@ -39,6 +53,10 @@ export class ExecuteToolScope extends OpenTelemetryScope {
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_TOOL_TYPE_KEY, toolType);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_TOOL_CALL_ID_KEY, toolCallId);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_TOOL_DESCRIPTION_KEY, description);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, conversationId);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_NAME_KEY, sourceMetadata?.name);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, sourceMetadata?.description);
Comment thread
fpfp100 marked this conversation as resolved.


Comment thread
fpfp100 marked this conversation as resolved.
// Set endpoint information if provided
if (endpoint) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { OpenTelemetryConstants } from '../constants';
import {
InferenceDetails,
AgentDetails,
TenantDetails
TenantDetails,
SourceMetadata
} from '../contracts';

/**
Expand All @@ -20,13 +21,27 @@ export class InferenceScope extends OpenTelemetryScope {
* @param details The inference call details
* @param agentDetails The agent details
* @param tenantDetails The tenant details
* @param conversationId Optional conversation id to tag on the span (`gen_ai.conversation.id`).
* @param sourceMetadata Optional source metadata; only `name` (channel name) and `description` (channel link/URL) are used for tagging.
* @returns A new InferenceScope instance
*/
public static start(details: InferenceDetails, agentDetails: AgentDetails, tenantDetails: TenantDetails): InferenceScope {
return new InferenceScope(details, agentDetails, tenantDetails);
public static start(
details: InferenceDetails,
agentDetails: AgentDetails,
tenantDetails: TenantDetails,
conversationId?: string,
sourceMetadata?: Pick<SourceMetadata, "name" | "description">
Comment thread
juliomenendez marked this conversation as resolved.
): InferenceScope {
return new InferenceScope(details, agentDetails, tenantDetails, conversationId, sourceMetadata);
}

private constructor(details: InferenceDetails, agentDetails: AgentDetails, tenantDetails: TenantDetails) {
private constructor(
details: InferenceDetails,
agentDetails: AgentDetails,
tenantDetails: TenantDetails,
conversationId?: string,
sourceMetadata?: Pick<SourceMetadata, "name" | "description">
Comment thread
juliomenendez marked this conversation as resolved.
) {
super(
SpanKind.CLIENT,
details.operationName.toString(),
Expand All @@ -43,6 +58,9 @@ export class InferenceScope extends OpenTelemetryScope {
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_USAGE_OUTPUT_TOKENS_KEY, details.outputTokens?.toString());
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_RESPONSE_FINISH_REASONS_KEY, details.finishReasons?.join(','));
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_RESPONSE_ID_KEY, details.responseId);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, conversationId);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_NAME_KEY, sourceMetadata?.name);
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, sourceMetadata?.description);
Comment thread
fpfp100 marked this conversation as resolved.
}

/**
Expand Down
67 changes: 67 additions & 0 deletions tests/observability/core/scopes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,35 @@ describe('Scopes', () => {
expect(() => scope?.recordResponse('Tool result')).not.toThrow();
scope?.dispose();
});

it('should set conversationId when provided', () => {
const spy = jest.spyOn(OpenTelemetryScope.prototype as any, 'setTagMaybe');
const scope = (ExecuteToolScope as unknown as any).start({ toolName: 'test-tool' }, testAgentDetails, testTenantDetails, 'conv-tool-123');
expect(scope).toBeInstanceOf(ExecuteToolScope);

const calls = spy.mock.calls.map(args => ({ key: args[0], val: args[1] }));
expect(calls).toEqual(expect.arrayContaining([
expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, val: 'conv-tool-123' })
]));

scope?.dispose();
spy.mockRestore();
});

it('should set source metadata tags when provided', () => {
const spy = jest.spyOn(OpenTelemetryScope.prototype as any, 'setTagMaybe');
const scope = (ExecuteToolScope as unknown as any).start({ toolName: 'test-tool' }, testAgentDetails, testTenantDetails, undefined, { name: 'ChannelTool', description: 'https://channel/tool' });
expect(scope).toBeInstanceOf(ExecuteToolScope);

const calls = spy.mock.calls.map(args => ({ key: args[0], val: args[1] }));
expect(calls).toEqual(expect.arrayContaining([
expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_NAME_KEY, val: 'ChannelTool' }),
expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, val: 'https://channel/tool' })
]));

scope?.dispose();
spy.mockRestore();
});
});


Expand Down Expand Up @@ -232,6 +261,44 @@ describe('Scopes', () => {
scope?.dispose();
});

it('should set conversationId when provided', () => {
Comment thread
fpfp100 marked this conversation as resolved.
const spy = jest.spyOn(OpenTelemetryScope.prototype as any, 'setTagMaybe');
const inferenceDetails: InferenceDetails = {
operationName: InferenceOperationType.CHAT,
model: 'gpt-4'
};

const scope = (InferenceScope as unknown as any).start(inferenceDetails, testAgentDetails, testTenantDetails, 'conv-inf-123');
expect(scope).toBeInstanceOf(InferenceScope);

const calls = spy.mock.calls.map(args => ({ key: args[0], val: args[1] }));
expect(calls).toEqual(expect.arrayContaining([
expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY, val: 'conv-inf-123' })
]));

scope?.dispose();
spy.mockRestore();
});

it('should set source metadata tags when provided', () => {
const spy = jest.spyOn(OpenTelemetryScope.prototype as any, 'setTagMaybe');
const inferenceDetails: InferenceDetails = {
operationName: InferenceOperationType.CHAT,
model: 'gpt-4'
};

const scope = (InferenceScope as unknown as any).start(inferenceDetails, testAgentDetails, testTenantDetails, undefined, { name: 'ChannelInf', description: 'https://channel/inf' });
expect(scope).toBeInstanceOf(InferenceScope);

const calls = spy.mock.calls.map(args => ({ key: args[0], val: args[1] }));
expect(calls).toEqual(expect.arrayContaining([
expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_NAME_KEY, val: 'ChannelInf' }),
expect.objectContaining({ key: OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, val: 'https://channel/inf' })
]));

scope?.dispose();
spy.mockRestore();
});
});

describe('Dispose pattern', () => {
Expand Down