forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Security solution] Knowledge base entry telemetry (elastic#199225)
- Loading branch information
1 parent
0b70791
commit 78cf7a9
Showing
16 changed files
with
578 additions
and
29 deletions.
There are no files selected for viewing
8 changes: 8 additions & 0 deletions
8
x-pack/packages/kbn-langchain/server/tracers/telemetry/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
export { TelemetryTracer } from './telemetry_tracer'; |
204 changes: 204 additions & 0 deletions
204
x-pack/packages/kbn-langchain/server/tracers/telemetry/telemetry_tracer.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import { AnalyticsServiceSetup, Logger } from '@kbn/core/server'; | ||
import { TelemetryTracer, TelemetryParams } from './telemetry_tracer'; | ||
import { Run } from 'langsmith/schemas'; | ||
import { loggerMock } from '@kbn/logging-mocks'; | ||
|
||
const mockRun = { | ||
inputs: { | ||
responseLanguage: 'English', | ||
conversationId: 'db8f74c5-7dca-43a3-b592-d56f219dffab', | ||
llmType: 'openai', | ||
isStream: false, | ||
isOssModel: false, | ||
}, | ||
outputs: { | ||
input: | ||
'Generate an ESQL query to find documents with `host.name` that contains my favorite color', | ||
lastNode: 'agent', | ||
steps: [ | ||
{ | ||
action: { | ||
tool: 'KnowledgeBaseRetrievalTool', | ||
toolInput: { | ||
query: "user's favorite color", | ||
}, | ||
}, | ||
observation: | ||
'"[{\\"pageContent\\":\\"favorite color is blue\\",\\"metadata\\":{\\"source\\":\\"conversation\\",\\"required\\":false,\\"kbResource\\":\\"user\\"}},{\\"pageContent\\":\\"favorite food is pizza\\",\\"metadata\\":{\\"source\\":\\"conversation\\",\\"required\\":false,\\"kbResource\\":\\"user\\"}}]"', | ||
}, | ||
{ | ||
action: { | ||
tool: 'NaturalLanguageESQLTool', | ||
toolInput: { | ||
question: 'Generate an ESQL query to find documents with host.name that contains blue', | ||
}, | ||
}, | ||
observation: | ||
'"To find documents with `host.name` that contains \\"blue\\", you can use the `LIKE` operator with wildcards. Here is the ES|QL query:\\n\\n```esql\\nFROM your_index\\n| WHERE host.name LIKE \\"*blue*\\"\\n```\\n\\nReplace `your_index` with the actual name of your index. This query will filter documents where the `host.name` field contains the substring \\"blue\\"."', | ||
}, | ||
{ | ||
action: { | ||
tool: 'KnowledgeBaseRetrievalTool', | ||
toolInput: { | ||
query: "user's favorite food", | ||
}, | ||
}, | ||
observation: | ||
'"[{\\"pageContent\\":\\"favorite color is blue\\",\\"metadata\\":{\\"source\\":\\"conversation\\",\\"required\\":false,\\"kbResource\\":\\"user\\"}},{\\"pageContent\\":\\"favorite food is pizza\\",\\"metadata\\":{\\"source\\":\\"conversation\\",\\"required\\":false,\\"kbResource\\":\\"user\\"}}]"', | ||
}, | ||
{ | ||
action: { | ||
tool: 'CustomIndexTool', | ||
toolInput: { | ||
query: 'query about index', | ||
}, | ||
}, | ||
observation: '"Wow this is totally cool."', | ||
}, | ||
{ | ||
action: { | ||
tool: 'CustomIndexTool', | ||
toolInput: { | ||
query: 'query about index', | ||
}, | ||
}, | ||
observation: '"Wow this is totally cool."', | ||
}, | ||
{ | ||
action: { | ||
tool: 'CustomIndexTool', | ||
toolInput: { | ||
query: 'query about index', | ||
}, | ||
}, | ||
observation: '"Wow this is totally cool."', | ||
}, | ||
], | ||
hasRespondStep: false, | ||
agentOutcome: { | ||
returnValues: { | ||
output: | ||
'To find documents with `host.name` that contains your favorite color "blue", you can use the `LIKE` operator with wildcards. Here is the ES|QL query:\n\n```esql\nFROM your_index\n| WHERE host.name LIKE "*blue*"\n```\n\nReplace `your_index` with the actual name of your index. This query will filter documents where the `host.name` field contains the substring "blue".', | ||
}, | ||
log: 'To find documents with `host.name` that contains your favorite color "blue", you can use the `LIKE` operator with wildcards. Here is the ES|QL query:\n\n```esql\nFROM your_index\n| WHERE host.name LIKE "*blue*"\n```\n\nReplace `your_index` with the actual name of your index. This query will filter documents where the `host.name` field contains the substring "blue".', | ||
}, | ||
messages: [], | ||
chatTitle: 'Welcome', | ||
llmType: 'openai', | ||
isStream: false, | ||
isOssModel: false, | ||
conversation: { | ||
timestamp: '2024-11-07T17:37:07.400Z', | ||
createdAt: '2024-11-07T17:37:07.400Z', | ||
users: [ | ||
{ | ||
id: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', | ||
name: 'elastic', | ||
}, | ||
], | ||
title: 'Welcome', | ||
category: 'assistant', | ||
apiConfig: { | ||
connectorId: 'my-gpt4o-ai', | ||
actionTypeId: '.gen-ai', | ||
}, | ||
isDefault: true, | ||
messages: [ | ||
{ | ||
timestamp: '2024-11-07T22:47:45.994Z', | ||
content: | ||
'Generate an ESQL query to find documents with `host.name` that contains my favorite color', | ||
role: 'user', | ||
}, | ||
], | ||
updatedAt: '2024-11-08T17:01:21.958Z', | ||
replacements: {}, | ||
namespace: 'default', | ||
id: 'db8f74c5-7dca-43a3-b592-d56f219dffab', | ||
}, | ||
conversationId: 'db8f74c5-7dca-43a3-b592-d56f219dffab', | ||
responseLanguage: 'English', | ||
}, | ||
end_time: 1731085297190, | ||
start_time: 1731085289113, | ||
} as unknown as Run; | ||
const elasticTools = [ | ||
'AlertCountsTool', | ||
'NaturalLanguageESQLTool', | ||
'KnowledgeBaseRetrievalTool', | ||
'KnowledgeBaseWriteTool', | ||
'OpenAndAcknowledgedAlertsTool', | ||
'SecurityLabsKnowledgeBaseTool', | ||
]; | ||
const mockLogger = loggerMock.create(); | ||
|
||
describe('TelemetryTracer', () => { | ||
let telemetry: AnalyticsServiceSetup; | ||
let logger: Logger; | ||
let telemetryParams: TelemetryParams; | ||
let telemetryTracer: TelemetryTracer; | ||
const reportEvent = jest.fn(); | ||
beforeEach(() => { | ||
telemetry = { | ||
reportEvent, | ||
} as unknown as AnalyticsServiceSetup; | ||
logger = mockLogger; | ||
telemetryParams = { | ||
eventType: 'INVOKE_AI_SUCCESS', | ||
assistantStreamingEnabled: true, | ||
actionTypeId: '.gen-ai', | ||
isEnabledKnowledgeBase: true, | ||
model: 'test_model', | ||
}; | ||
telemetryTracer = new TelemetryTracer( | ||
{ | ||
elasticTools, | ||
telemetry, | ||
telemetryParams, | ||
totalTools: 9, | ||
}, | ||
logger | ||
); | ||
}); | ||
|
||
it('should initialize correctly', () => { | ||
expect(telemetryTracer.name).toBe('telemetry_tracer'); | ||
expect(telemetryTracer.elasticTools).toEqual(elasticTools); | ||
expect(telemetryTracer.telemetry).toBe(telemetry); | ||
expect(telemetryTracer.telemetryParams).toBe(telemetryParams); | ||
expect(telemetryTracer.totalTools).toBe(9); | ||
}); | ||
|
||
it('should not log and report event on chain end if parent_run_id exists', async () => { | ||
await telemetryTracer.onChainEnd({ ...mockRun, parent_run_id: '123' }); | ||
|
||
expect(logger.get().debug).not.toHaveBeenCalled(); | ||
expect(telemetry.reportEvent).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should log and report event on chain end', async () => { | ||
await telemetryTracer.onChainEnd(mockRun); | ||
|
||
expect(logger.get().debug).toHaveBeenCalledWith(expect.any(Function)); | ||
expect(telemetry.reportEvent).toHaveBeenCalledWith('INVOKE_AI_SUCCESS', { | ||
assistantStreamingEnabled: true, | ||
actionTypeId: '.gen-ai', | ||
isEnabledKnowledgeBase: true, | ||
model: 'test_model', | ||
isOssModel: false, | ||
durationMs: 8077, | ||
toolsInvoked: { | ||
KnowledgeBaseRetrievalTool: 2, | ||
NaturalLanguageESQLTool: 1, | ||
CustomTool: 3, | ||
}, | ||
}); | ||
}); | ||
}); |
94 changes: 94 additions & 0 deletions
94
x-pack/packages/kbn-langchain/server/tracers/telemetry/telemetry_tracer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import { BaseCallbackHandlerInput } from '@langchain/core/callbacks/base'; | ||
import type { Run } from 'langsmith/schemas'; | ||
import { BaseTracer } from '@langchain/core/tracers/base'; | ||
import { AnalyticsServiceSetup, Logger } from '@kbn/core/server'; | ||
|
||
export interface TelemetryParams { | ||
assistantStreamingEnabled: boolean; | ||
actionTypeId: string; | ||
isEnabledKnowledgeBase: boolean; | ||
eventType: string; | ||
model?: string; | ||
} | ||
export interface LangChainTracerFields extends BaseCallbackHandlerInput { | ||
elasticTools: string[]; | ||
telemetry: AnalyticsServiceSetup; | ||
telemetryParams: TelemetryParams; | ||
totalTools: number; | ||
} | ||
interface ToolRunStep { | ||
action: { | ||
tool: string; | ||
}; | ||
} | ||
/** | ||
* TelemetryTracer is a tracer that uses event based telemetry to track LangChain events. | ||
*/ | ||
export class TelemetryTracer extends BaseTracer implements LangChainTracerFields { | ||
name = 'telemetry_tracer'; | ||
logger: Logger; | ||
elasticTools: string[]; | ||
telemetry: AnalyticsServiceSetup; | ||
telemetryParams: TelemetryParams; | ||
totalTools: number; | ||
constructor(fields: LangChainTracerFields, logger: Logger) { | ||
super(fields); | ||
this.logger = logger.get('telemetryTracer'); | ||
this.elasticTools = fields.elasticTools; | ||
this.telemetry = fields.telemetry; | ||
this.telemetryParams = fields.telemetryParams; | ||
this.totalTools = fields.totalTools; | ||
} | ||
|
||
async onChainEnd(run: Run): Promise<void> { | ||
if (!run.parent_run_id) { | ||
const { eventType, ...telemetryParams } = this.telemetryParams; | ||
const toolsInvoked = | ||
run?.outputs && run?.outputs.steps.length | ||
? run.outputs.steps.reduce((acc: { [k: string]: number }, event: ToolRunStep | never) => { | ||
if ('action' in event && event?.action?.tool) { | ||
if (this.elasticTools.includes(event.action.tool)) { | ||
return { | ||
...acc, | ||
...(event.action.tool in acc | ||
? { [event.action.tool]: acc[event.action.tool] + 1 } | ||
: { [event.action.tool]: 1 }), | ||
}; | ||
} else { | ||
// Custom tool names are user data, so we strip them out | ||
return { | ||
...acc, | ||
...('CustomTool' in acc | ||
? { CustomTool: acc.CustomTool + 1 } | ||
: { CustomTool: 1 }), | ||
}; | ||
} | ||
} | ||
return acc; | ||
}, {}) | ||
: {}; | ||
const telemetryValue = { | ||
...telemetryParams, | ||
durationMs: (run.end_time ?? 0) - (run.start_time ?? 0), | ||
toolsInvoked, | ||
...(telemetryParams.actionTypeId === '.gen-ai' | ||
? { isOssModel: run.inputs.isOssModel } | ||
: {}), | ||
}; | ||
this.logger.debug( | ||
() => `Invoke ${eventType} telemetry:\n${JSON.stringify(telemetryValue, null, 2)}` | ||
); | ||
this.telemetry.reportEvent(eventType, telemetryValue); | ||
} | ||
} | ||
|
||
// everything below is required for type only | ||
protected async persistRun(_run: Run): Promise<void> {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.