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 + } + ] } ]