feat: Add Opik Tracer integration (#4194)
This update adds support for Opik tracer in Flowise. It includes new credential components, analytic nodes, and UI assets for both tracers. The changes enhance observability by allowing users to integrate Opik for better model monitoring, analysis and evaluation.
This commit is contained in:
parent
a07546145d
commit
dd56d03b78
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { INodeParams, INodeCredential } from '../src/Interface'
|
||||||
|
|
||||||
|
class OpikApi implements INodeCredential {
|
||||||
|
label: string
|
||||||
|
name: string
|
||||||
|
version: number
|
||||||
|
description: string
|
||||||
|
inputs: INodeParams[]
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.label = 'Opik API'
|
||||||
|
this.name = 'opikApi'
|
||||||
|
this.version = 1.0
|
||||||
|
this.description =
|
||||||
|
'Refer to <a target="_blank" href="https://www.comet.com/docs/opik/tracing/sdk_configuration">Opik documentation</a> on how to configure Opik credentials'
|
||||||
|
this.inputs = [
|
||||||
|
{
|
||||||
|
label: 'API Key',
|
||||||
|
name: 'opikApiKey',
|
||||||
|
type: 'password',
|
||||||
|
placeholder: '<OPIK_API_KEY>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'URL',
|
||||||
|
name: 'opikUrl',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'https://www.comet.com/opik/api'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Workspace',
|
||||||
|
name: 'opikWorkspace',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'default'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { credClass: OpikApi }
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { INode, INodeParams } from '../../../src/Interface'
|
||||||
|
|
||||||
|
class Opik_Analytic implements INode {
|
||||||
|
label: string
|
||||||
|
name: string
|
||||||
|
version: number
|
||||||
|
description: string
|
||||||
|
type: string
|
||||||
|
icon: string
|
||||||
|
category: string
|
||||||
|
baseClasses: string[]
|
||||||
|
inputs?: INodeParams[]
|
||||||
|
credential: INodeParams
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.label = 'Opik'
|
||||||
|
this.name = 'opik'
|
||||||
|
this.version = 1.0
|
||||||
|
this.type = 'Opik'
|
||||||
|
this.icon = 'opik.png'
|
||||||
|
this.category = 'Analytic'
|
||||||
|
this.baseClasses = [this.type]
|
||||||
|
this.inputs = []
|
||||||
|
this.credential = {
|
||||||
|
label: 'Connect Credential',
|
||||||
|
name: 'credential',
|
||||||
|
type: 'credential',
|
||||||
|
credentialNames: ['opikApi']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { nodeClass: Opik_Analytic }
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -121,6 +121,50 @@ function getPhoenixTracer(options: PhoenixTracerOptions): Tracer | undefined {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OpikTracerOptions {
|
||||||
|
apiKey: string
|
||||||
|
baseUrl: string
|
||||||
|
projectName: string
|
||||||
|
workspace: string
|
||||||
|
sdkIntegration?: string
|
||||||
|
sessionId?: string
|
||||||
|
enableCallback?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpikTracer(options: OpikTracerOptions): Tracer | undefined {
|
||||||
|
const SEMRESATTRS_PROJECT_NAME = 'openinference.project.name'
|
||||||
|
try {
|
||||||
|
const traceExporter = new ProtoOTLPTraceExporter({
|
||||||
|
url: `${options.baseUrl}/v1/private/otel/v1/traces`,
|
||||||
|
headers: {
|
||||||
|
Authorization: options.apiKey,
|
||||||
|
projectName: options.projectName,
|
||||||
|
'Comet-Workspace': options.workspace
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const tracerProvider = new NodeTracerProvider({
|
||||||
|
resource: new Resource({
|
||||||
|
[ATTR_SERVICE_NAME]: options.projectName,
|
||||||
|
[ATTR_SERVICE_VERSION]: '1.0.0',
|
||||||
|
[SEMRESATTRS_PROJECT_NAME]: options.projectName
|
||||||
|
})
|
||||||
|
})
|
||||||
|
tracerProvider.addSpanProcessor(new SimpleSpanProcessor(traceExporter))
|
||||||
|
if (options.enableCallback) {
|
||||||
|
registerInstrumentations({
|
||||||
|
instrumentations: []
|
||||||
|
})
|
||||||
|
const lcInstrumentation = new LangChainInstrumentation()
|
||||||
|
lcInstrumentation.manuallyInstrument(CallbackManagerModule)
|
||||||
|
tracerProvider.register()
|
||||||
|
}
|
||||||
|
return tracerProvider.getTracer(`opik-tracer-${uuidv4().toString()}`)
|
||||||
|
} catch (err) {
|
||||||
|
if (process.env.DEBUG === 'true') console.error(`Error setting up Opik tracer: ${err.message}`)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function tryGetJsonSpaces() {
|
function tryGetJsonSpaces() {
|
||||||
try {
|
try {
|
||||||
return parseInt(getEnvironmentVariable('LOG_JSON_SPACES') ?? '2')
|
return parseInt(getEnvironmentVariable('LOG_JSON_SPACES') ?? '2')
|
||||||
|
|
@ -559,6 +603,28 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO
|
||||||
|
|
||||||
const tracer: Tracer | undefined = getPhoenixTracer(phoenixOptions)
|
const tracer: Tracer | undefined = getPhoenixTracer(phoenixOptions)
|
||||||
callbacks.push(tracer)
|
callbacks.push(tracer)
|
||||||
|
} else if (provider === 'opik') {
|
||||||
|
const opikApiKey = getCredentialParam('opikApiKey', credentialData, nodeData)
|
||||||
|
const opikEndpoint = getCredentialParam('opikUrl', credentialData, nodeData)
|
||||||
|
const opikWorkspace = getCredentialParam('opikWorkspace', credentialData, nodeData)
|
||||||
|
const opikProject = analytic[provider].opikProjectName as string
|
||||||
|
|
||||||
|
let opikOptions: OpikTracerOptions = {
|
||||||
|
apiKey: opikApiKey,
|
||||||
|
baseUrl: opikEndpoint ?? 'https://www.comet.com/opik/api',
|
||||||
|
projectName: opikProject ?? 'default',
|
||||||
|
workspace: opikWorkspace ?? 'default',
|
||||||
|
sdkIntegration: 'Flowise',
|
||||||
|
enableCallback: true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.chatId) opikOptions.sessionId = options.chatId
|
||||||
|
if (nodeData?.inputs?.analytics?.opik) {
|
||||||
|
opikOptions = { ...opikOptions, ...nodeData?.inputs?.analytics?.opik }
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracer: Tracer | undefined = getOpikTracer(opikOptions)
|
||||||
|
callbacks.push(tracer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -673,6 +739,25 @@ export class AnalyticHandler {
|
||||||
const rootSpan: Span | undefined = undefined
|
const rootSpan: Span | undefined = undefined
|
||||||
|
|
||||||
this.handlers['phoenix'] = { client: phoenix, phoenixProject, rootSpan }
|
this.handlers['phoenix'] = { client: phoenix, phoenixProject, rootSpan }
|
||||||
|
} else if (provider === 'opik') {
|
||||||
|
const opikApiKey = getCredentialParam('opikApiKey', credentialData, this.nodeData)
|
||||||
|
const opikEndpoint = getCredentialParam('opikUrl', credentialData, this.nodeData)
|
||||||
|
const opikWorkspace = getCredentialParam('opikWorkspace', credentialData, this.nodeData)
|
||||||
|
const opikProject = analytic[provider].opikProjectName as string
|
||||||
|
|
||||||
|
let opikOptions: OpikTracerOptions = {
|
||||||
|
apiKey: opikApiKey,
|
||||||
|
baseUrl: opikEndpoint ?? 'https://www.comet.com/opik/api',
|
||||||
|
projectName: opikProject ?? 'default',
|
||||||
|
workspace: opikWorkspace ?? 'default',
|
||||||
|
sdkIntegration: 'Flowise',
|
||||||
|
enableCallback: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const opik: Tracer | undefined = getOpikTracer(opikOptions)
|
||||||
|
const rootSpan: Span | undefined = undefined
|
||||||
|
|
||||||
|
this.handlers['opik'] = { client: opik, opikProject, rootSpan }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -688,7 +773,8 @@ export class AnalyticHandler {
|
||||||
lunary: {},
|
lunary: {},
|
||||||
langWatch: {},
|
langWatch: {},
|
||||||
arize: {},
|
arize: {},
|
||||||
phoenix: {}
|
phoenix: {},
|
||||||
|
opik: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
|
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
|
||||||
|
|
@ -870,6 +956,40 @@ export class AnalyticHandler {
|
||||||
returnIds['phoenix'].chainSpan = chainSpanId
|
returnIds['phoenix'].chainSpan = chainSpanId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.handlers, 'opik')) {
|
||||||
|
const tracer: Tracer | undefined = this.handlers['opik'].client
|
||||||
|
let rootSpan: Span | undefined = this.handlers['opik'].rootSpan
|
||||||
|
|
||||||
|
if (!parentIds || !Object.keys(parentIds).length) {
|
||||||
|
rootSpan = tracer ? tracer.startSpan('Flowise') : undefined
|
||||||
|
if (rootSpan) {
|
||||||
|
rootSpan.setAttribute('session.id', this.options.chatId)
|
||||||
|
rootSpan.setAttribute('openinference.span.kind', 'CHAIN')
|
||||||
|
rootSpan.setAttribute('input.value', input)
|
||||||
|
rootSpan.setAttribute('input.mime_type', 'text/plain')
|
||||||
|
rootSpan.setAttribute('output.value', '[Object]')
|
||||||
|
rootSpan.setAttribute('output.mime_type', 'text/plain')
|
||||||
|
rootSpan.setStatus({ code: SpanStatusCode.OK })
|
||||||
|
rootSpan.end()
|
||||||
|
}
|
||||||
|
this.handlers['opik'].rootSpan = rootSpan
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootSpanContext = rootSpan
|
||||||
|
? opentelemetry.trace.setSpan(opentelemetry.context.active(), rootSpan as Span)
|
||||||
|
: opentelemetry.context.active()
|
||||||
|
const chainSpan = tracer?.startSpan(name, undefined, rootSpanContext)
|
||||||
|
if (chainSpan) {
|
||||||
|
chainSpan.setAttribute('openinference.span.kind', 'CHAIN')
|
||||||
|
chainSpan.setAttribute('input.value', JSON.stringify(input))
|
||||||
|
chainSpan.setAttribute('input.mime_type', 'application/json')
|
||||||
|
}
|
||||||
|
const chainSpanId: any = chainSpan?.spanContext().spanId
|
||||||
|
|
||||||
|
this.handlers['opik'].chainSpan = { [chainSpanId]: chainSpan }
|
||||||
|
returnIds['opik'].chainSpan = chainSpanId
|
||||||
|
}
|
||||||
|
|
||||||
return returnIds
|
return returnIds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -947,6 +1067,16 @@ export class AnalyticHandler {
|
||||||
chainSpan.end()
|
chainSpan.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.handlers, 'opik')) {
|
||||||
|
const chainSpan: Span | undefined = this.handlers['opik'].chainSpan[returnIds['opik'].chainSpan]
|
||||||
|
if (chainSpan) {
|
||||||
|
chainSpan.setAttribute('output.value', JSON.stringify(output))
|
||||||
|
chainSpan.setAttribute('output.mime_type', 'application/json')
|
||||||
|
chainSpan.setStatus({ code: SpanStatusCode.OK })
|
||||||
|
chainSpan.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onChainError(returnIds: ICommonObject, error: string | object, shutdown = false) {
|
async onChainError(returnIds: ICommonObject, error: string | object, shutdown = false) {
|
||||||
|
|
@ -1132,6 +1262,25 @@ export class AnalyticHandler {
|
||||||
returnIds['phoenix'].llmSpan = llmSpanId
|
returnIds['phoenix'].llmSpan = llmSpanId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.handlers, 'opik')) {
|
||||||
|
const tracer: Tracer | undefined = this.handlers['opik'].client
|
||||||
|
const rootSpan: Span | undefined = this.handlers['opik'].rootSpan
|
||||||
|
|
||||||
|
const rootSpanContext = rootSpan
|
||||||
|
? opentelemetry.trace.setSpan(opentelemetry.context.active(), rootSpan as Span)
|
||||||
|
: opentelemetry.context.active()
|
||||||
|
const llmSpan = tracer?.startSpan(name, undefined, rootSpanContext)
|
||||||
|
if (llmSpan) {
|
||||||
|
llmSpan.setAttribute('openinference.span.kind', 'LLM')
|
||||||
|
llmSpan.setAttribute('input.value', JSON.stringify(input))
|
||||||
|
llmSpan.setAttribute('input.mime_type', 'application/json')
|
||||||
|
}
|
||||||
|
const llmSpanId: any = llmSpan?.spanContext().spanId
|
||||||
|
|
||||||
|
this.handlers['opik'].llmSpan = { [llmSpanId]: llmSpan }
|
||||||
|
returnIds['opik'].llmSpan = llmSpanId
|
||||||
|
}
|
||||||
|
|
||||||
return returnIds
|
return returnIds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1197,6 +1346,16 @@ export class AnalyticHandler {
|
||||||
llmSpan.end()
|
llmSpan.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.handlers, 'opik')) {
|
||||||
|
const llmSpan: Span | undefined = this.handlers['opik'].llmSpan[returnIds['opik'].llmSpan]
|
||||||
|
if (llmSpan) {
|
||||||
|
llmSpan.setAttribute('output.value', JSON.stringify(output))
|
||||||
|
llmSpan.setAttribute('output.mime_type', 'application/json')
|
||||||
|
llmSpan.setStatus({ code: SpanStatusCode.OK })
|
||||||
|
llmSpan.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onLLMError(returnIds: ICommonObject, error: string | object) {
|
async onLLMError(returnIds: ICommonObject, error: string | object) {
|
||||||
|
|
@ -1261,6 +1420,16 @@ export class AnalyticHandler {
|
||||||
llmSpan.end()
|
llmSpan.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.handlers, 'opik')) {
|
||||||
|
const llmSpan: Span | undefined = this.handlers['opik'].llmSpan[returnIds['opik'].llmSpan]
|
||||||
|
if (llmSpan) {
|
||||||
|
llmSpan.setAttribute('error.value', JSON.stringify(error))
|
||||||
|
llmSpan.setAttribute('error.mime_type', 'application/json')
|
||||||
|
llmSpan.setStatus({ code: SpanStatusCode.ERROR, message: error.toString() })
|
||||||
|
llmSpan.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onToolStart(name: string, input: string | object, parentIds: ICommonObject) {
|
async onToolStart(name: string, input: string | object, parentIds: ICommonObject) {
|
||||||
|
|
@ -1270,7 +1439,8 @@ export class AnalyticHandler {
|
||||||
lunary: {},
|
lunary: {},
|
||||||
langWatch: {},
|
langWatch: {},
|
||||||
arize: {},
|
arize: {},
|
||||||
phoenix: {}
|
phoenix: {},
|
||||||
|
opik: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
|
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
|
||||||
|
|
@ -1369,6 +1539,25 @@ export class AnalyticHandler {
|
||||||
returnIds['phoenix'].toolSpan = toolSpanId
|
returnIds['phoenix'].toolSpan = toolSpanId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.handlers, 'opik')) {
|
||||||
|
const tracer: Tracer | undefined = this.handlers['opik'].client
|
||||||
|
const rootSpan: Span | undefined = this.handlers['opik'].rootSpan
|
||||||
|
|
||||||
|
const rootSpanContext = rootSpan
|
||||||
|
? opentelemetry.trace.setSpan(opentelemetry.context.active(), rootSpan as Span)
|
||||||
|
: opentelemetry.context.active()
|
||||||
|
const toolSpan = tracer?.startSpan(name, undefined, rootSpanContext)
|
||||||
|
if (toolSpan) {
|
||||||
|
toolSpan.setAttribute('openinference.span.kind', 'TOOL')
|
||||||
|
toolSpan.setAttribute('input.value', JSON.stringify(input))
|
||||||
|
toolSpan.setAttribute('input.mime_type', 'application/json')
|
||||||
|
}
|
||||||
|
const toolSpanId: any = toolSpan?.spanContext().spanId
|
||||||
|
|
||||||
|
this.handlers['opik'].toolSpan = { [toolSpanId]: toolSpan }
|
||||||
|
returnIds['opik'].toolSpan = toolSpanId
|
||||||
|
}
|
||||||
|
|
||||||
return returnIds
|
return returnIds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1434,6 +1623,16 @@ export class AnalyticHandler {
|
||||||
toolSpan.end()
|
toolSpan.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.handlers, 'opik')) {
|
||||||
|
const toolSpan: Span | undefined = this.handlers['opik'].toolSpan[returnIds['opik'].toolSpan]
|
||||||
|
if (toolSpan) {
|
||||||
|
toolSpan.setAttribute('output.value', JSON.stringify(output))
|
||||||
|
toolSpan.setAttribute('output.mime_type', 'application/json')
|
||||||
|
toolSpan.setStatus({ code: SpanStatusCode.OK })
|
||||||
|
toolSpan.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onToolError(returnIds: ICommonObject, error: string | object) {
|
async onToolError(returnIds: ICommonObject, error: string | object) {
|
||||||
|
|
@ -1498,6 +1697,16 @@ export class AnalyticHandler {
|
||||||
toolSpan.end()
|
toolSpan.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.handlers, 'opik')) {
|
||||||
|
const toolSpan: Span | undefined = this.handlers['opik'].toolSpan[returnIds['opik'].toolSpan]
|
||||||
|
if (toolSpan) {
|
||||||
|
toolSpan.setAttribute('error.value', JSON.stringify(error))
|
||||||
|
toolSpan.setAttribute('error.mime_type', 'application/json')
|
||||||
|
toolSpan.setStatus({ code: SpanStatusCode.ERROR, message: error.toString() })
|
||||||
|
toolSpan.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -30,6 +30,7 @@ import lunarySVG from '@/assets/images/lunary.svg'
|
||||||
import langwatchSVG from '@/assets/images/langwatch.svg'
|
import langwatchSVG from '@/assets/images/langwatch.svg'
|
||||||
import arizePNG from '@/assets/images/arize.png'
|
import arizePNG from '@/assets/images/arize.png'
|
||||||
import phoenixPNG from '@/assets/images/phoenix.png'
|
import phoenixPNG from '@/assets/images/phoenix.png'
|
||||||
|
import opikPNG from '@/assets/images/opik.png'
|
||||||
|
|
||||||
// store
|
// store
|
||||||
import useNotifier from '@/utils/useNotifier'
|
import useNotifier from '@/utils/useNotifier'
|
||||||
|
|
@ -188,6 +189,33 @@ const analyticProviders = [
|
||||||
optional: true
|
optional: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Opik',
|
||||||
|
name: 'opik',
|
||||||
|
icon: opikPNG,
|
||||||
|
url: 'https://www.comet.com/opik',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
label: 'Connect Credential',
|
||||||
|
name: 'credential',
|
||||||
|
type: 'credential',
|
||||||
|
credentialNames: ['opikApi']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Project Name',
|
||||||
|
name: 'opikProjectName',
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name of your Opik project',
|
||||||
|
placeholder: 'default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'On/Off',
|
||||||
|
name: 'status',
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue