netra-sdk 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20,6 +20,8 @@ var core = require('@opentelemetry/core');
20
20
  var http = require('http');
21
21
  var https = require('https');
22
22
  var otlpTransformer = require('@opentelemetry/otlp-transformer');
23
+ var pLimit = require('p-limit');
24
+ var axios = require('axios');
23
25
 
24
26
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
25
27
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -51,6 +53,8 @@ var dotenv__namespace = /*#__PURE__*/_interopNamespace(dotenv);
51
53
  var shimmer__default = /*#__PURE__*/_interopDefault(shimmer);
52
54
  var http__namespace = /*#__PURE__*/_interopNamespace(http);
53
55
  var https__namespace = /*#__PURE__*/_interopNamespace(https);
56
+ var pLimit__default = /*#__PURE__*/_interopDefault(pLimit);
57
+ var axios__default = /*#__PURE__*/_interopDefault(axios);
54
58
 
55
59
  var __create = Object.create;
56
60
  var __defProp = Object.defineProperty;
@@ -18511,6 +18515,8 @@ var Measure = /* @__PURE__ */ ((Measure2) => {
18511
18515
  Measure2["TOTAL_COST"] = "Total Cost";
18512
18516
  Measure2["VIOLATIONS"] = "Violations";
18513
18517
  Measure2["TOTAL_TOKENS"] = "Total Tokens";
18518
+ Measure2["AUDIO_DURATION"] = "Audio Duration";
18519
+ Measure2["CHARACTER_COUNT"] = "Character Count";
18514
18520
  return Measure2;
18515
18521
  })(Measure || {});
18516
18522
  var Aggregation = /* @__PURE__ */ ((Aggregation2) => {
@@ -26984,6 +26990,445 @@ function uninstrumentAll() {
26984
26990
  console.debug("Failed to uninstrument TypeORM:", e);
26985
26991
  }
26986
26992
  }
26993
+ var LOG_PREFIX = "netra.simulation";
26994
+ var DEFAULT_TIMEOUT = 1e4;
26995
+ var SimulationHttpClient = class {
26996
+ constructor(config2) {
26997
+ this.client = null;
26998
+ this.client = this._createClient(config2);
26999
+ }
27000
+ /**
27001
+ * Create and configure the HTTP client.
27002
+ */
27003
+ _createClient(config2) {
27004
+ const endpoint = (config2.otlpEndpoint || "").trim();
27005
+ if (!endpoint) {
27006
+ console.error(`${LOG_PREFIX}: NETRA_OTLP_ENDPOINT is required`);
27007
+ return null;
27008
+ }
27009
+ const baseURL = this._resolveBaseUrl(endpoint);
27010
+ const headers = this._buildHeaders(config2);
27011
+ const timeout = this._getTimeout();
27012
+ try {
27013
+ return axios__default.default.create({
27014
+ baseURL,
27015
+ headers,
27016
+ timeout
27017
+ });
27018
+ } catch (error) {
27019
+ console.error(`${LOG_PREFIX}: Failed to create HTTP client:`, error);
27020
+ return null;
27021
+ }
27022
+ }
27023
+ /**
27024
+ * Extract base URL, removing telemetry suffix if present.
27025
+ */
27026
+ _resolveBaseUrl(endpoint) {
27027
+ let baseUrl = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
27028
+ if (baseUrl.endsWith("/telemetry")) {
27029
+ baseUrl = baseUrl.slice(0, -"/telemetry".length);
27030
+ }
27031
+ return baseUrl;
27032
+ }
27033
+ /**
27034
+ * Build request headers from configuration.
27035
+ */
27036
+ _buildHeaders(config2) {
27037
+ const headers = { ...config2.headers };
27038
+ if (config2.apiKey) {
27039
+ headers["x-api-key"] = config2.apiKey;
27040
+ }
27041
+ return headers;
27042
+ }
27043
+ /**
27044
+ * Get timeout from environment or use default.
27045
+ */
27046
+ _getTimeout() {
27047
+ const timeoutStr = process.env.NETRA_SIMULATION_TIMEOUT;
27048
+ if (!timeoutStr) {
27049
+ return DEFAULT_TIMEOUT;
27050
+ }
27051
+ const timeout = parseFloat(timeoutStr);
27052
+ if (isNaN(timeout)) {
27053
+ console.warn(
27054
+ `${LOG_PREFIX}: Invalid timeout '${timeoutStr}', using default ${DEFAULT_TIMEOUT}ms`
27055
+ );
27056
+ return DEFAULT_TIMEOUT;
27057
+ }
27058
+ return timeout * 1e3;
27059
+ }
27060
+ /**
27061
+ * Create a new simulation run for the specified dataset.
27062
+ */
27063
+ async createRun(name, datasetId, context14) {
27064
+ if (!this.client) {
27065
+ console.error(`${LOG_PREFIX}: Client not initialized`);
27066
+ return null;
27067
+ }
27068
+ try {
27069
+ const url = "/evaluations/test_run/multi-turn";
27070
+ const payload = {
27071
+ name,
27072
+ datasetId,
27073
+ context: context14 || {}
27074
+ };
27075
+ const response = await this.client.post(url, payload);
27076
+ const data = response.data;
27077
+ const responseData = data.data || {};
27078
+ const userMessages = responseData.userMessages || [];
27079
+ if (userMessages.length === 0) {
27080
+ console.warn(`${LOG_PREFIX}: No user messages returned from create_run`);
27081
+ return null;
27082
+ }
27083
+ const runId = responseData.id || "";
27084
+ const simulationItems = userMessages.map(
27085
+ (msg) => ({
27086
+ runItemId: msg.testRunItemId || "",
27087
+ message: msg.userMessage || "",
27088
+ turnId: msg.turnId || ""
27089
+ })
27090
+ );
27091
+ return {
27092
+ runId,
27093
+ simulationItems
27094
+ };
27095
+ } catch (error) {
27096
+ const errorMsg = this._extractErrorMessage(error);
27097
+ console.error(`${LOG_PREFIX}: Failed to create simulation run:`, errorMsg);
27098
+ return null;
27099
+ }
27100
+ }
27101
+ /**
27102
+ * Send a conversation turn to the backend and get the next response.
27103
+ */
27104
+ async triggerConversation(message, turnId, sessionId, traceId) {
27105
+ if (!this.client) {
27106
+ console.error(`${LOG_PREFIX}: Client not initialized`);
27107
+ return null;
27108
+ }
27109
+ try {
27110
+ const url = "/evaluations/turn/agent-response";
27111
+ const payload = {
27112
+ turnId,
27113
+ agentResponse: { message },
27114
+ sessionId,
27115
+ traceId
27116
+ };
27117
+ const response = await this.client.post(url, payload);
27118
+ const data = response.data;
27119
+ const responseData = data.data || {};
27120
+ const decision = responseData.decision || "continue";
27121
+ if (decision === "stop") {
27122
+ return {
27123
+ decision,
27124
+ reason: responseData.reason || ""
27125
+ };
27126
+ }
27127
+ const userMessages = responseData.userMessages || [];
27128
+ if (userMessages.length === 0) {
27129
+ console.warn(`${LOG_PREFIX}: No user messages in continue response`);
27130
+ return null;
27131
+ }
27132
+ const nextMsg = userMessages[0];
27133
+ return {
27134
+ decision,
27135
+ nextTurnId: nextMsg.turnId || "",
27136
+ nextUserMessage: nextMsg.userMessage || "",
27137
+ nextRunItemId: nextMsg.testRunItemId || ""
27138
+ };
27139
+ } catch (error) {
27140
+ const errorMsg = this._extractErrorMessage(error);
27141
+ console.error(`${LOG_PREFIX}: Failed to trigger conversation:`, errorMsg);
27142
+ return null;
27143
+ }
27144
+ }
27145
+ /**
27146
+ * Report a task execution failure to the backend.
27147
+ */
27148
+ async reportFailure(runId, runItemId, error) {
27149
+ if (!this.client) {
27150
+ console.error(`${LOG_PREFIX}: Client not initialized`);
27151
+ return;
27152
+ }
27153
+ try {
27154
+ const url = `/evaluations/run/${runId}/item/${runItemId}/status`;
27155
+ const payload = { status: "failed", failureReason: error };
27156
+ await this.client.patch(url, payload);
27157
+ console.info(`${LOG_PREFIX}: Reported failure - ${error}`);
27158
+ } catch (err) {
27159
+ const errorMsg = this._extractErrorMessage(err);
27160
+ console.error(`${LOG_PREFIX}: Failed to report failure:`, errorMsg);
27161
+ }
27162
+ }
27163
+ /**
27164
+ * Submit the run status.
27165
+ */
27166
+ async postRunStatus(runId, status) {
27167
+ if (!this.client) {
27168
+ console.error(
27169
+ `${LOG_PREFIX}: Client not initialized; cannot post run status`
27170
+ );
27171
+ return { success: false };
27172
+ }
27173
+ try {
27174
+ const url = `/evaluations/run/${runId}/status`;
27175
+ const payload = { status };
27176
+ const response = await this.client.post(url, payload);
27177
+ const data = response.data;
27178
+ if (data && typeof data === "object" && "data" in data) {
27179
+ console.info(`${LOG_PREFIX}: Completed test run successfully`);
27180
+ return data.data || {};
27181
+ }
27182
+ return data;
27183
+ } catch (error) {
27184
+ const errorMsg = this._extractErrorMessage(error);
27185
+ console.error(
27186
+ `${LOG_PREFIX}: Failed to post run status for run '${runId}':`,
27187
+ errorMsg
27188
+ );
27189
+ return { success: false };
27190
+ }
27191
+ }
27192
+ /**
27193
+ * Extract error message from response or exception.
27194
+ */
27195
+ _extractErrorMessage(error) {
27196
+ if (axios__default.default.isAxiosError(error)) {
27197
+ const axiosError = error;
27198
+ if (axiosError.response?.data) {
27199
+ const responseData = axiosError.response.data;
27200
+ if (typeof responseData === "object" && responseData.error && typeof responseData.error === "object") {
27201
+ return responseData.error.message || error.message;
27202
+ }
27203
+ }
27204
+ return axiosError.message;
27205
+ }
27206
+ return error?.message || String(error);
27207
+ }
27208
+ };
27209
+
27210
+ // src/simulation/task.ts
27211
+ var BaseTask = class {
27212
+ };
27213
+
27214
+ // src/simulation/utils.ts
27215
+ var LOG_PREFIX2 = "netra.simulation";
27216
+ function validateSimulationInputs(datasetId, task2) {
27217
+ if (!datasetId) {
27218
+ console.error(`${LOG_PREFIX2}: dataset_id is required`);
27219
+ return false;
27220
+ }
27221
+ if (!(task2 instanceof BaseTask)) {
27222
+ console.error(`${LOG_PREFIX2}: task must be a BaseTask instance`);
27223
+ return false;
27224
+ }
27225
+ return true;
27226
+ }
27227
+ async function executeTask2(task2, message, sessionId) {
27228
+ const result = task2.run(message, sessionId);
27229
+ const resolvedResult = result instanceof Promise ? await result : result;
27230
+ if (typeof resolvedResult === "object" && resolvedResult !== null && "message" in resolvedResult && "sessionId" in resolvedResult) {
27231
+ return [resolvedResult.message, resolvedResult.sessionId];
27232
+ }
27233
+ throw new Error(
27234
+ `Task must return TaskResult, got ${typeof resolvedResult}`
27235
+ );
27236
+ }
27237
+
27238
+ // src/simulation/api.ts
27239
+ var LOG_PREFIX3 = "netra.simulation";
27240
+ var SPAN_NAME = "Netra.Simulation.TestRun";
27241
+ var Simulation = class {
27242
+ constructor(config2) {
27243
+ this._config = config2;
27244
+ this._client = new SimulationHttpClient(config2);
27245
+ }
27246
+ /**
27247
+ * Run a multi-turn conversation simulation.
27248
+ *
27249
+ * @param options - Simulation configuration options
27250
+ * @returns Dictionary with simulation results, or null on failure
27251
+ */
27252
+ async runSimulation(options) {
27253
+ const {
27254
+ name,
27255
+ datasetId,
27256
+ task: task2,
27257
+ context: context14,
27258
+ maxConcurrency = 5
27259
+ } = options;
27260
+ if (!validateSimulationInputs(datasetId, task2)) {
27261
+ return null;
27262
+ }
27263
+ const startTime = Date.now();
27264
+ const runResult = await this._client.createRun(name, datasetId, context14);
27265
+ if (!runResult) {
27266
+ return null;
27267
+ }
27268
+ const { runId, simulationItems } = runResult;
27269
+ if (!simulationItems || simulationItems.length === 0) {
27270
+ console.error(`${LOG_PREFIX3}: No items returned from create_run`);
27271
+ return null;
27272
+ }
27273
+ let interrupted = false;
27274
+ const proc = typeof process !== "undefined" ? process : void 0;
27275
+ const finalizeFailure = (signal) => {
27276
+ if (interrupted) {
27277
+ return;
27278
+ }
27279
+ interrupted = true;
27280
+ proc?.removeListener("SIGINT", handleSignal);
27281
+ proc?.removeListener("SIGTERM", handleSignal);
27282
+ proc?.removeListener("uncaughtException", handleException);
27283
+ proc?.removeListener("unhandledRejection", handleRejection);
27284
+ void this._client.postRunStatus(runId, "failed").finally(() => {
27285
+ if (signal) {
27286
+ proc?.kill(proc.pid, signal);
27287
+ }
27288
+ });
27289
+ };
27290
+ const handleSignal = (signal) => {
27291
+ finalizeFailure(signal);
27292
+ };
27293
+ const handleException = () => {
27294
+ finalizeFailure();
27295
+ };
27296
+ const handleRejection = () => {
27297
+ finalizeFailure();
27298
+ };
27299
+ if (proc && typeof proc.once === "function") {
27300
+ proc.once("SIGINT", handleSignal);
27301
+ proc.once("SIGTERM", handleSignal);
27302
+ proc.once("uncaughtException", handleException);
27303
+ proc.once("unhandledRejection", handleRejection);
27304
+ }
27305
+ console.info(
27306
+ `${LOG_PREFIX3}: Starting simulation with ${simulationItems.length} items`
27307
+ );
27308
+ try {
27309
+ const result = await this._runSimulationAsync(
27310
+ runId,
27311
+ simulationItems,
27312
+ task2,
27313
+ maxConcurrency
27314
+ );
27315
+ const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
27316
+ console.info(
27317
+ `${LOG_PREFIX3}: Simulation completed in ${elapsedTime} seconds`
27318
+ );
27319
+ return result;
27320
+ } catch (error) {
27321
+ console.error(`${LOG_PREFIX3}: Run simulation failed`);
27322
+ await this._client.postRunStatus(runId, "failed");
27323
+ throw error;
27324
+ } finally {
27325
+ if (proc && typeof proc.removeListener === "function") {
27326
+ proc.removeListener("SIGINT", handleSignal);
27327
+ proc.removeListener("SIGTERM", handleSignal);
27328
+ proc.removeListener("uncaughtException", handleException);
27329
+ proc.removeListener("unhandledRejection", handleRejection);
27330
+ }
27331
+ }
27332
+ }
27333
+ /**
27334
+ * Async implementation of run_simulation with concurrency control.
27335
+ */
27336
+ async _runSimulationAsync(runId, runItems, task2, maxConcurrency) {
27337
+ const results = {
27338
+ success: true,
27339
+ completed: [],
27340
+ failed: [],
27341
+ totalItems: runItems.length
27342
+ };
27343
+ let processedCount = 0;
27344
+ const limit = pLimit__default.default(Math.min(5, maxConcurrency));
27345
+ const promises = runItems.map(
27346
+ (runItem) => limit(async () => {
27347
+ const result = await this._executeConversation(runId, runItem, task2);
27348
+ if (result.success) {
27349
+ results.completed.push(result);
27350
+ } else {
27351
+ results.failed.push(result);
27352
+ }
27353
+ processedCount++;
27354
+ console.info(
27355
+ `${LOG_PREFIX3}: ${processedCount}/${runItems.length} processed (run_item_id=${runItem.runItemId})`
27356
+ );
27357
+ return result;
27358
+ })
27359
+ );
27360
+ await Promise.all(promises);
27361
+ console.info(
27362
+ `${LOG_PREFIX3}: Completed=${results.completed.length}, Failed=${results.failed.length}`
27363
+ );
27364
+ await this._client.postRunStatus(runId, "completed");
27365
+ return results;
27366
+ }
27367
+ /**
27368
+ * Execute a multi-turn conversation for a single simulation item.
27369
+ */
27370
+ async _executeConversation(runId, runItem, task2) {
27371
+ const { runItemId, message: initialMessage, turnId: initialTurnId } = runItem;
27372
+ let message = initialMessage;
27373
+ let turnId = initialTurnId;
27374
+ let sessionId = null;
27375
+ while (true) {
27376
+ try {
27377
+ const span2 = new SpanWrapper(SPAN_NAME, {}, LOG_PREFIX3);
27378
+ span2.start();
27379
+ const traceId = span2.getCurrentSpan()?.spanContext().traceId ?? "";
27380
+ const [responseMessage, taskSessionId] = await executeTask2(
27381
+ task2,
27382
+ message,
27383
+ sessionId
27384
+ );
27385
+ if (taskSessionId) {
27386
+ sessionId = taskSessionId;
27387
+ }
27388
+ span2.end();
27389
+ const response = await this._client.triggerConversation(
27390
+ responseMessage,
27391
+ turnId,
27392
+ sessionId || "",
27393
+ traceId
27394
+ );
27395
+ if (response === null) {
27396
+ const errorMsg = "Failed to get conversation response";
27397
+ return {
27398
+ runItemId,
27399
+ success: false,
27400
+ error: errorMsg,
27401
+ turnId
27402
+ };
27403
+ }
27404
+ if (response.decision === "stop") {
27405
+ console.info(
27406
+ `${LOG_PREFIX3}: Completed run_item_id=${runItemId} reason=${response.reason}`
27407
+ );
27408
+ return {
27409
+ runItemId,
27410
+ success: true,
27411
+ finalTurnId: turnId
27412
+ };
27413
+ }
27414
+ message = response.nextUserMessage;
27415
+ turnId = response.nextTurnId;
27416
+ } catch (error) {
27417
+ const errorMsg = error instanceof Error ? error.message : String(error);
27418
+ console.error(
27419
+ `${LOG_PREFIX3}: Task failed run_item_id=${runItemId}, turn_id=${turnId}: ${errorMsg}`
27420
+ );
27421
+ await this._client.reportFailure(runId, runItemId, errorMsg);
27422
+ return {
27423
+ runItemId,
27424
+ success: false,
27425
+ error: errorMsg,
27426
+ turnId
27427
+ };
27428
+ }
27429
+ }
27430
+ }
27431
+ };
26987
27432
  function serializeValue(value) {
26988
27433
  try {
26989
27434
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) {
@@ -27179,6 +27624,15 @@ function span(targetOrOptions, options) {
27179
27624
  // src/index.ts
27180
27625
  var _rootSpan;
27181
27626
  var Netra = class {
27627
+ /**
27628
+ * Get the current Netra configuration
27629
+ */
27630
+ static getConfig() {
27631
+ if (!this._config) {
27632
+ throw new Error("Netra SDK not initialized. Call Netra.init() first.");
27633
+ }
27634
+ return this._config;
27635
+ }
27182
27636
  /**
27183
27637
  * Check if Netra has been initialized
27184
27638
  */
@@ -27224,6 +27678,7 @@ var Netra = class {
27224
27678
  this.usage = new Usage(cfg);
27225
27679
  this.evaluation = new Evaluation(cfg);
27226
27680
  this.dashboard = new Dashboard(cfg);
27681
+ this.simulation = new Simulation(cfg);
27227
27682
  this._initialized = true;
27228
27683
  console.info("Netra successfully initialized.");
27229
27684
  if (cfg.debugMode) {
@@ -27513,6 +27968,7 @@ Netra.withBlockedSpansLocal = withBlockedSpansLocal;
27513
27968
  var index_default = Netra;
27514
27969
 
27515
27970
  exports.Aggregation = Aggregation;
27971
+ exports.BaseTask = BaseTask;
27516
27972
  exports.ChartType = ChartType;
27517
27973
  exports.Config = Config;
27518
27974
  exports.ConversationType = ConversationType;
@@ -27534,6 +27990,7 @@ exports.RunStatus = RunStatus;
27534
27990
  exports.Scope = Scope;
27535
27991
  exports.ScrubbingSpanProcessor = ScrubbingSpanProcessor;
27536
27992
  exports.SessionSpanProcessor = SessionSpanProcessor;
27993
+ exports.Simulation = Simulation;
27537
27994
  exports.SpanType = SpanType;
27538
27995
  exports.TrialAwareOTLPExporter = TrialAwareOTLPExporter;
27539
27996
  exports.Usage = Usage;