diff --git a/packages/components/credentials/OpikApi.credential.ts b/packages/components/credentials/OpikApi.credential.ts
new file mode 100644
index 000000000..db5d66077
--- /dev/null
+++ b/packages/components/credentials/OpikApi.credential.ts
@@ -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 Opik documentation on how to configure Opik credentials'
+ this.inputs = [
+ {
+ label: 'API Key',
+ name: 'opikApiKey',
+ type: 'password',
+ placeholder: ''
+ },
+ {
+ 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 }
diff --git a/packages/components/nodes/analytic/Opik/Opik.ts b/packages/components/nodes/analytic/Opik/Opik.ts
new file mode 100644
index 000000000..c620bdcc1
--- /dev/null
+++ b/packages/components/nodes/analytic/Opik/Opik.ts
@@ -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 }
diff --git a/packages/components/nodes/analytic/Opik/opik.png b/packages/components/nodes/analytic/Opik/opik.png
new file mode 100644
index 000000000..20de0c39d
Binary files /dev/null and b/packages/components/nodes/analytic/Opik/opik.png differ
diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts
index 48236f2be..a4cafe19b 100644
--- a/packages/components/src/handler.ts
+++ b/packages/components/src/handler.ts
@@ -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() {
try {
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)
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
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: {},
langWatch: {},
arize: {},
- phoenix: {}
+ phoenix: {},
+ opik: {}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
@@ -870,6 +956,40 @@ export class AnalyticHandler {
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
}
@@ -947,6 +1067,16 @@ export class AnalyticHandler {
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) {
@@ -1132,6 +1262,25 @@ export class AnalyticHandler {
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
}
@@ -1197,6 +1346,16 @@ export class AnalyticHandler {
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) {
@@ -1261,6 +1420,16 @@ export class AnalyticHandler {
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) {
@@ -1270,7 +1439,8 @@ export class AnalyticHandler {
lunary: {},
langWatch: {},
arize: {},
- phoenix: {}
+ phoenix: {},
+ opik: {}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
@@ -1369,6 +1539,25 @@ export class AnalyticHandler {
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
}
@@ -1434,6 +1623,16 @@ export class AnalyticHandler {
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) {
@@ -1498,6 +1697,16 @@ export class AnalyticHandler {
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()
+ }
+ }
}
}
diff --git a/packages/ui/src/assets/images/opik.png b/packages/ui/src/assets/images/opik.png
new file mode 100644
index 000000000..20de0c39d
Binary files /dev/null and b/packages/ui/src/assets/images/opik.png differ
diff --git a/packages/ui/src/ui-component/extended/AnalyseFlow.jsx b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx
index d9001368e..de162e51a 100644
--- a/packages/ui/src/ui-component/extended/AnalyseFlow.jsx
+++ b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx
@@ -30,6 +30,7 @@ import lunarySVG from '@/assets/images/lunary.svg'
import langwatchSVG from '@/assets/images/langwatch.svg'
import arizePNG from '@/assets/images/arize.png'
import phoenixPNG from '@/assets/images/phoenix.png'
+import opikPNG from '@/assets/images/opik.png'
// store
import useNotifier from '@/utils/useNotifier'
@@ -188,6 +189,33 @@ const analyticProviders = [
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
+ }
+ ]
}
]