diff --git a/packages/agents-a365-observability/src/tracing/scopes/ExecuteToolScope.ts b/packages/agents-a365-observability/src/tracing/scopes/ExecuteToolScope.ts index a238c8ac..d787285a 100644 --- a/packages/agents-a365-observability/src/tracing/scopes/ExecuteToolScope.ts +++ b/packages/agents-a365-observability/src/tracing/scopes/ExecuteToolScope.ts @@ -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'; /** @@ -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 + ): 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 + ) { super( SpanKind.INTERNAL, OpenTelemetryConstants.EXECUTE_TOOL_OPERATION_NAME, @@ -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); + // Set endpoint information if provided if (endpoint) { diff --git a/packages/agents-a365-observability/src/tracing/scopes/InferenceScope.ts b/packages/agents-a365-observability/src/tracing/scopes/InferenceScope.ts index cdf1e4a7..6f95527f 100644 --- a/packages/agents-a365-observability/src/tracing/scopes/InferenceScope.ts +++ b/packages/agents-a365-observability/src/tracing/scopes/InferenceScope.ts @@ -8,7 +8,8 @@ import { OpenTelemetryConstants } from '../constants'; import { InferenceDetails, AgentDetails, - TenantDetails + TenantDetails, + SourceMetadata } from '../contracts'; /** @@ -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 + ): 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 + ) { super( SpanKind.CLIENT, details.operationName.toString(), @@ -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); } /** diff --git a/tests/observability/core/scopes.test.ts b/tests/observability/core/scopes.test.ts index 50e8ff58..83cd160b 100644 --- a/tests/observability/core/scopes.test.ts +++ b/tests/observability/core/scopes.test.ts @@ -240,6 +240,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(); + }); }); @@ -291,6 +320,44 @@ describe('Scopes', () => { scope?.dispose(); }); + it('should set conversationId 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, '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', () => {