diff --git a/.gitignore b/.gitignore
index 76add87..3c3629e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1 @@
node_modules
-dist
\ No newline at end of file
diff --git a/README.md b/README.md
index 38de553..a731548 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,3 @@
-
-
-
-
-
# JLINC Langchain Tracer
The JLINC Langchain Tracer is the official way to implement the zero-knowledge third-party auditing provided by the [JLINC Server](https://gitea.jlinc.io/jlinc-labs/jlinc-server) inside any Langchain-based infrastructure.
diff --git a/dist/tracer.d.ts b/dist/tracer.d.ts
new file mode 100644
index 0000000..38a8bf4
--- /dev/null
+++ b/dist/tracer.d.ts
@@ -0,0 +1,127 @@
+export type JLINCTracerFields = {
+ dataStoreApiUrl?: string | undefined;
+ dataStoreApiKey?: string | undefined;
+ archiveApiUrl?: string | undefined;
+ archiveApiKey?: string | undefined;
+ systemPrefix?: string | undefined;
+ agreementId?: string | null | undefined;
+ debug?: boolean | undefined;
+};
+export type SerializedRun = {
+ name?: string | undefined;
+ start_time?: number | undefined;
+ end_time?: number | undefined;
+ trace_id?: string | undefined;
+ inputs?: any;
+ outputs?: any;
+ extra?: {
+ metadata?: {
+ ls_provider?: string;
+ ls_model_name?: string;
+ };
+ } | undefined;
+};
+/**
+ * @typedef {Object} JLINCTracerFields
+ * @property {string} [dataStoreApiUrl]
+ * @property {string} [dataStoreApiKey]
+ * @property {string} [archiveApiUrl]
+ * @property {string} [archiveApiKey]
+ * @property {string} [systemPrefix]
+ * @property {string|null} [agreementId]
+ * @property {boolean} [debug]
+ */
+/**
+ * @typedef {Object} SerializedRun
+ * @property {string} [name]
+ * @property {number} [start_time]
+ * @property {number} [end_time]
+ * @property {string} [trace_id]
+ * @property {any} [inputs]
+ * @property {any} [outputs]
+ * @property {{ metadata?: { ls_provider?: string, ls_model_name?: string } }} [extra]
+ */
+/**
+ * @extends {LangChainTracer}
+ */
+export class JLINCTracer extends LangChainTracer {
+ /**
+ * @param {JLINCTracerFields} [fields={}]
+ */
+ constructor(fields?: JLINCTracerFields);
+ /** @type {string} */
+ dataStoreApiUrl: string;
+ /** @type {string} */
+ dataStoreApiKey: string;
+ /** @type {string} */
+ archiveApiUrl: string;
+ /** @type {string} */
+ archiveApiKey: string;
+ /** @type {string} */
+ systemPrefix: string;
+ /** @type {string|null} */
+ agreementId: string | null;
+ /** @type {boolean} */
+ debug: boolean;
+ /** @type {string|null} */
+ domain: string | null;
+ /** @type {Set} */
+ entities: Set;
+ /**
+ * @param {any} run
+ * @returns {Promise}
+ */
+ onRunCreate(run: any): Promise;
+ /**
+ * @param {any} run
+ * @returns {Promise}
+ */
+ onRunUpdate(run: any): Promise;
+ /**
+ * @returns {Promise}
+ */
+ getDefaultProjectName(): Promise;
+ /**
+ * @param {SerializedRun} serialized
+ * @returns {Promise}
+ */
+ onLLMStart(serialized: SerializedRun): Promise;
+ /**
+ * @param {SerializedRun} serialized
+ * @returns {Promise}
+ */
+ onLLMEnd(serialized: SerializedRun): Promise;
+ /**
+ * @param {SerializedRun} serialized
+ * @returns {Promise}
+ */
+ onToolStart(serialized: SerializedRun): Promise;
+ /**
+ * @param {SerializedRun} serialized
+ * @returns {Promise}
+ */
+ onToolEnd(serialized: SerializedRun): Promise;
+ /**
+ * @param {string} dir
+ * @param {any} payload
+ * @returns {Promise}
+ */
+ _postToApi(dir: string, payload: any): Promise;
+ /**
+ * @param {string} entityShortName
+ * @returns {Promise}
+ */
+ _setEntity(entityShortName: string): Promise;
+ /**
+ * @param {string} input
+ * @returns {string}
+ */
+ _sanitize(input: string): string;
+ /**
+ * @param {any} payload
+ * @param {'in' | 'out'} direction
+ * @returns {Promise}
+ */
+ _jlincTrace(payload: any, direction: "in" | "out"): Promise;
+}
+import { LangChainTracer } from "@langchain/core/dist/tracers/tracer_langchain";
diff --git a/dist/tracer.js b/dist/tracer.js
new file mode 100644
index 0000000..ef78b85
--- /dev/null
+++ b/dist/tracer.js
@@ -0,0 +1,241 @@
+const { LangChainTracer } = require("@langchain/core/tracers/tracer_langchain");
+const axios = require("axios");
+
+/**
+ * @typedef {Object} JLINCTracerFields
+ * @property {string} [dataStoreApiUrl]
+ * @property {string} [dataStoreApiKey]
+ * @property {string} [archiveApiUrl]
+ * @property {string} [archiveApiKey]
+ * @property {string} [systemPrefix]
+ * @property {string|null} [agreementId]
+ * @property {boolean} [debug]
+ */
+
+/**
+ * @typedef {Object} SerializedRun
+ * @property {string} [name]
+ * @property {number} [start_time]
+ * @property {number} [end_time]
+ * @property {string} [trace_id]
+ * @property {any} [inputs]
+ * @property {any} [outputs]
+ * @property {{ metadata?: { ls_provider?: string, ls_model_name?: string } }} [extra]
+ */
+
+/**
+ * @extends {LangChainTracer}
+ */
+class JLINCTracer extends LangChainTracer {
+ /**
+ * @param {JLINCTracerFields} [fields={}]
+ */
+ constructor(fields = {}) {
+ super(fields);
+ const defaultDataStoreApiUrl = "https://api-test.jlinc.io";
+ const defaultDataStoreApiKey = "";
+ const defaultArchiveApiUrl = "https://archive-test.jlinc.io";
+ const defaultArchiveApiKey = "";
+ const defaultSystemPrefix = "MyProject";
+
+ /** @type {string} */
+ this.dataStoreApiUrl = fields.dataStoreApiUrl ?? defaultDataStoreApiUrl;
+ /** @type {string} */
+ this.dataStoreApiKey = fields.dataStoreApiKey ?? defaultDataStoreApiKey;
+ /** @type {string} */
+ this.archiveApiUrl = fields.archiveApiUrl ?? defaultArchiveApiUrl;
+ /** @type {string} */
+ this.archiveApiKey = fields.archiveApiKey ?? defaultArchiveApiKey;
+ /** @type {string} */
+ this.systemPrefix = this._sanitize(fields.systemPrefix ?? defaultSystemPrefix);
+ /** @type {string|null} */
+ this.agreementId = fields.agreementId ?? null;
+ /** @type {boolean} */
+ this.debug = fields.debug ?? false;
+
+ /** @type {string|null} */
+ this.domain = null;
+ /** @type {Set} */
+ this.entities = new Set();
+ }
+
+ /**
+ * @param {any} run
+ * @returns {Promise}
+ */
+ async onRunCreate(run) { }
+
+ /**
+ * @param {any} run
+ * @returns {Promise}
+ */
+ async onRunUpdate(run) { }
+
+ /**
+ * @returns {Promise}
+ */
+ async getDefaultProjectName() {
+ return this.systemPrefix;
+ }
+
+ /**
+ * @param {SerializedRun} serialized
+ * @returns {Promise}
+ */
+ async onLLMStart(serialized) {
+ if (this.debug) {
+ console.log(`\nJLINC: onLLMStart`);
+ console.log(`---------------------------------------------`);
+ }
+ await this._jlincTrace({
+ event: "llm_start",
+ name: serialized?.name || "unknown",
+ ts: serialized?.start_time,
+ provider: serialized?.extra?.metadata,
+ traceId: serialized?.trace_id,
+ inputs: serialized?.inputs,
+ }, 'out');
+ }
+
+ /**
+ * @param {SerializedRun} serialized
+ * @returns {Promise}
+ */
+ async onLLMEnd(serialized) {
+ if (this.debug) {
+ console.log(`\nJLINC: onLLMEnd`);
+ console.log(`---------------------------------------------`);
+ }
+ await this._jlincTrace({
+ event: "llm_end",
+ name: serialized?.name || "unknown",
+ ts: serialized?.end_time,
+ provider: serialized?.extra?.metadata,
+ traceId: serialized?.trace_id,
+ outputs: serialized?.outputs,
+ }, 'in');
+ }
+
+ /**
+ * @param {SerializedRun} serialized
+ * @returns {Promise}
+ */
+ async onToolStart(serialized) {
+ if (this.debug) {
+ console.log(`\nJLINC: onToolStart`);
+ console.log(`---------------------------------------------`);
+ }
+ await this._jlincTrace({
+ event: "tool_start",
+ name: serialized?.name || "unknown",
+ ts: serialized?.start_time,
+ provider: serialized?.extra?.metadata,
+ traceId: serialized?.trace_id,
+ inputs: serialized?.inputs,
+ }, 'out');
+ }
+
+ /**
+ * @param {SerializedRun} serialized
+ * @returns {Promise}
+ */
+ async onToolEnd(serialized) {
+ if (this.debug) {
+ console.log(`\nJLINC: onToolEnd`);
+ console.log(`---------------------------------------------`);
+ }
+ await this._jlincTrace({
+ event: "tool_end",
+ name: serialized?.name || "unknown",
+ ts: serialized?.end_time,
+ provider: serialized?.extra?.metadata,
+ traceId: serialized?.trace_id,
+ outputs: serialized?.outputs,
+ }, 'in');
+ }
+
+ /**
+ * @param {string} dir
+ * @param {any} payload
+ * @returns {Promise}
+ */
+ async _postToApi(dir, payload) {
+ const response = await axios.post(
+ `${this.dataStoreApiUrl}/api/v1/data/${dir}`,
+ payload,
+ {
+ headers: {
+ 'Authorization': `Bearer ${this.dataStoreApiKey}`,
+ }
+ }
+ );
+ return response.data;
+ }
+
+ /**
+ * @param {string} entityShortName
+ * @returns {Promise}
+ */
+ async _setEntity(entityShortName) {
+ if (!this.entities.has(entityShortName)) {
+ let recipient = null;
+ try {
+ recipient = await this._postToApi(`entity/get`, { shortName: entityShortName });
+ } catch (e) {
+ recipient = await this._postToApi(`entity/create`, { shortName: entityShortName });
+ }
+ this.entities.add(entityShortName);
+ }
+ }
+
+ /**
+ * @param {string} input
+ * @returns {string}
+ */
+ _sanitize(input) {
+ return input.replace(/[^a-zA-Z0-9._-]/g, '_');
+ }
+
+ /**
+ * @param {any} payload
+ * @param {'in' | 'out'} direction
+ * @returns {Promise}
+ */
+ async _jlincTrace(payload, direction) {
+ try {
+ if (!this.domain) {
+ const domains = await this._postToApi(`entity/domains/get`, {});
+ this.domain = domains[0];
+ }
+ let entityShortName = `${this.systemPrefix}-${this._sanitize(payload.name)}`;
+ payload?.provider?.ls_provider && (entityShortName += `-${this._sanitize(payload.provider.ls_provider)}`);
+ payload?.provider?.ls_model_name && (entityShortName += `-${this._sanitize(payload.provider.ls_model_name)}`);
+ entityShortName += `@${this.domain}`;
+ await this._setEntity(entityShortName);
+ const systemShortName = `${this.systemPrefix}-system@${this.domain}`;
+ await this._setEntity(systemShortName);
+ const recipientShortName = direction === 'in' ? systemShortName : entityShortName;
+ const senderShortName = direction === 'out' ? systemShortName : entityShortName;
+ const data = {
+ type: 'data',
+ senderShortName,
+ recipientShortName,
+ agreementId: this.agreementId,
+ data: payload,
+ archive: {
+ url: this.archiveApiUrl,
+ key: this.archiveApiKey,
+ }
+ };
+ const event = await this._postToApi(`event/produce`, data);
+ if (this.debug) {
+ console.log(JSON.stringify({ event }, null, 2));
+ }
+ } catch (error) {
+ console.error("[Tracer] Failed to log event:", error.message);
+ }
+ }
+
+}
+
+module.exports = { JLINCTracer }
diff --git a/package-lock.json b/package-lock.json
index 80a1fd3..c9bb9e4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "jlinc-tracer",
+ "name": "@jlinc/langchain",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
diff --git a/package.json b/package.json
index cfc46ce..36c7e12 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
- "name": "jlinc-tracer",
+ "name": "@jlinc/langchain",
"version": "0.1.0",
- "description": "A LangChain callback plugin that logs events to the JLINC API.",
+ "description": "A LangChain plugin that logs events to the JLINC API.",
"main": "dist/tracer.js",
"types": "dist/tracer.d.ts",
"scripts": {