From 08489637e05155e8b5bdb7273fdc497d471c9622 Mon Sep 17 00:00:00 2001 From: muhammedrazalak Date: Thu, 5 Mar 2026 17:41:20 +0530 Subject: [PATCH 1/3] feat: Add utility for prompt management feature (#47) --- package-lock.json | 4 +- src/api/index.ts | 96 ++++++++++++++++++++------------------- src/api/prompts/api.ts | 44 ++++++++++++++++++ src/api/prompts/client.ts | 49 ++++++++++++++++++++ src/api/prompts/index.ts | 7 +++ src/api/prompts/models.ts | 14 ++++++ src/index.ts | 25 ++++++---- 7 files changed, 182 insertions(+), 57 deletions(-) create mode 100644 src/api/prompts/api.ts create mode 100644 src/api/prompts/client.ts create mode 100644 src/api/prompts/index.ts create mode 100644 src/api/prompts/models.ts diff --git a/package-lock.json b/package-lock.json index 2aa92e1..f71d520 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "netra-sdk", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "netra-sdk", - "version": "1.0.4", + "version": "1.0.5", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/src/api/index.ts b/src/api/index.ts index ab29e55..6dc1451 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -5,62 +5,66 @@ // Usage API export { Usage } from "./usage"; export type { - ListSpansParams, - ListTracesParams, - SessionUsageData, - SpansPage, - TenantUsageData, - TraceSpan, - TracesPage, - TraceSummary, + ListSpansParams, + ListTracesParams, + SessionUsageData, + SpansPage, + TenantUsageData, + TraceSpan, + TracesPage, + TraceSummary, } from "./usage"; // Evaluation API export { - Evaluation, - EntryStatus, - RunStatus, - RunEntryContext, + Evaluation, + EntryStatus, + RunStatus, + RunEntryContext, } from "./evaluation"; export type { - CreateDatasetParams, - Dataset, - DatasetEntry, - DatasetItem, - EvaluationScore, - EvaluatorFunction, - Run, - TaskFunction, - TestSuiteResult, + CreateDatasetParams, + Dataset, + DatasetEntry, + DatasetItem, + EvaluationScore, + EvaluatorFunction, + Run, + TaskFunction, + TestSuiteResult, } from "./evaluation"; // Dashboard API export { - Dashboard, - Aggregation, - ChartType, - DimensionField, - FilterField, - FilterType, - GroupBy, - Measure, - metadataField, - Operator, - Scope, + Dashboard, + Aggregation, + ChartType, + DimensionField, + FilterField, + FilterType, + GroupBy, + Measure, + metadataField, + Operator, + Scope, } from "./dashboard"; export type { - CategoricalDataPoint, - DashboardData, - Dimension, - DimensionValue, - Filter, - FilterConfig, - Metrics, - NumberResponse, - QueryDataParams, - QueryResponse, - TimeRange, - TimeSeriesDataPoint, - TimeSeriesResponse, - TimeSeriesWithDimension, + CategoricalDataPoint, + DashboardData, + Dimension, + DimensionValue, + Filter, + FilterConfig, + Metrics, + NumberResponse, + QueryDataParams, + QueryResponse, + TimeRange, + TimeSeriesDataPoint, + TimeSeriesResponse, + TimeSeriesWithDimension, } from "./dashboard"; + +// Prompts API +export { Prompts } from "./prompts"; +export type { GetPromptParams, PromptResponse } from "./prompts"; diff --git a/src/api/prompts/api.ts b/src/api/prompts/api.ts new file mode 100644 index 0000000..c8438ff --- /dev/null +++ b/src/api/prompts/api.ts @@ -0,0 +1,44 @@ +import { Config } from "../../config"; +import { PromptsHttpClient } from "./client"; +import { GetPromptParams, PromptResponse } from "./models"; + +export class Prompts { + private config: Config; + private client: PromptsHttpClient; + + constructor(config: Config) { + this.config = config; + this.client = new PromptsHttpClient(config); + } + + /** + * Fetch prompt version by name + label + */ + async getPrompt(params: GetPromptParams): Promise { + if (!this.isValidParams(params)) { + throw new TypeError( + "params must contain { name: string, label: string }", + ); + } + + const result = await this.client.getPromptVersion( + params.name, + params.label, + ); + + if (!result) { + return null; + } + + return result.data ?? null; + } + + private isValidParams(value: any): value is GetPromptParams { + return ( + value && + typeof value === "object" && + typeof value.name === "string" && + typeof value.label === "string" + ); + } +} diff --git a/src/api/prompts/client.ts b/src/api/prompts/client.ts new file mode 100644 index 0000000..0eea494 --- /dev/null +++ b/src/api/prompts/client.ts @@ -0,0 +1,49 @@ +/** + * Internal HTTP client for Prompts APIs + */ + +import { Config } from "../../config"; +import { NetraHttpClient } from "../http-client"; + +export class PromptsHttpClient extends NetraHttpClient { + constructor(config: Config) { + super(config, "NETRA_PROMPTS_TIMEOUT", 30.0); + } + + /** + * Fetch prompt version from backend + */ + async getPromptVersion(name: string, label: string): Promise { + if (!this.isInitialized()) { + console.error( + "netra.prompts: Prompts client is not initialized; cannot fetch prompt", + ); + return null; + } + + try { + const payload = { + promptName: name, + label: label, + }; + + const response = await this.post("/sdk/prompts/version", payload); + + if (!response.ok) { + const errorMessage = response.data?.error?.message ?? "Unknown error"; + + console.error(`netra.prompts: Failed to fetch prompt: ${errorMessage}`); + + return null; + } + + return response.data; + } catch (err: any) { + const message = err?.response?.data?.error?.message ?? ""; + + console.error("netra.prompts: Failed to fetch prompt:", message); + + return null; + } + } +} diff --git a/src/api/prompts/index.ts b/src/api/prompts/index.ts new file mode 100644 index 0000000..bedb08f --- /dev/null +++ b/src/api/prompts/index.ts @@ -0,0 +1,7 @@ +/** + * Prompts API exports + */ + +export { Prompts } from "./api"; + +export type { GetPromptParams, PromptResponse } from "./models"; diff --git a/src/api/prompts/models.ts b/src/api/prompts/models.ts new file mode 100644 index 0000000..42b1f7d --- /dev/null +++ b/src/api/prompts/models.ts @@ -0,0 +1,14 @@ +/** + * Prompts API Models + */ + +export interface GetPromptParams { + name: string; + label: string; +} + +/** + * Prompt response is intentionally flexible because + * backend prompt structures may evolve (variables, templates, metadata etc.) + */ +export type PromptResponse = Record; diff --git a/src/index.ts b/src/index.ts index 08b577f..53b2948 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,12 +4,7 @@ * Built on top of OpenTelemetry and Traceloop */ -import { - context, - Span, - SpanKind, - trace -} from "@opentelemetry/api"; +import { context, Span, SpanKind, trace } from "@opentelemetry/api"; import { createRequire } from "module"; import { Dashboard } from "./api/dashboard"; import { Evaluation } from "./api/evaluation"; @@ -30,13 +25,14 @@ import { import { Simulation } from "./simulation"; import { SpanWrapper } from "./span-wrapper"; import { SpanType } from "./types"; +import { Prompts } from "./api"; export { Config, NetraInstruments } from "./config"; export { agent, span, task, workflow } from "./decorators"; export { InstrumentationSpanProcessor, ScrubbingSpanProcessor, - SessionSpanProcessor + SessionSpanProcessor, } from "./processors"; export { ConversationType } from "./session-manager"; export { SpanType } from "./types"; @@ -64,7 +60,9 @@ export { RunStatus, Scope, // Usage API - Usage + Usage, + // Prompts API + Prompts, } from "./api"; export type { @@ -101,7 +99,9 @@ export type { TimeSeriesWithDimension, TracesPage, TraceSpan, - TraceSummary + TraceSummary, + GetPromptParams, + PromptResponse, } from "./api"; // Export simulation types and classes @@ -147,6 +147,12 @@ export class Netra { static simulation: Simulation; + /** + * Prompts API client for prompt versioning + * Available after calling Netra.init() + */ + static prompts: Prompts; + /** * Get the current Netra configuration */ @@ -217,6 +223,7 @@ export class Netra { this.evaluation = new Evaluation(cfg); this.dashboard = new Dashboard(cfg); this.simulation = new Simulation(cfg); + this.prompts = new Prompts(cfg); this._initialized = true; console.info("Netra successfully initialized."); From 202ed958ba794ad69938810691fd7bccd570b2cd Mon Sep 17 00:00:00 2001 From: muhammedrazalak Date: Wed, 11 Mar 2026 16:38:51 +0530 Subject: [PATCH 2/3] fix: conversation gets overrides when tries to add conversation to the same context span --- src/session-manager.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/session-manager.ts b/src/session-manager.ts index 5976c10..f0877ea 100644 --- a/src/session-manager.ts +++ b/src/session-manager.ts @@ -28,6 +28,7 @@ interface EntityContext { spanStack: string[]; spansByName: Map; activeSpans: Span[]; + conversations: Map; } // AsyncLocalStorage for entity context (internal SDK state) @@ -41,6 +42,7 @@ const globalFallbackContext: EntityContext = { spanStack: [], spansByName: new Map(), activeSpans: [], + conversations: new Map(), }; /** @@ -67,6 +69,7 @@ export function runWithEntityContext(fn: () => T): T { spanStack: [], spansByName: new Map(), activeSpans: [], + conversations: new Map(), }; return entityStorage.run(newContext, fn); } @@ -270,16 +273,10 @@ export class SessionManager { return; } - // Get existing conversation - const existing: Array<{ - type: string; - role: string; - content: string | Record; - format: string; - }> = []; + // Get or create conversation history for this span in the current context + const ctx = getOrCreateEntityContext(); + const existing = ctx.conversations.get(span) || []; - // Try to get existing conversation from span attributes - // Note: This is a simplified version - in production you'd need to access span internals const maxLen = Config.CONVERSATION_MAX_LEN; const processedContent = typeof content === "string" @@ -294,6 +291,7 @@ export class SessionManager { }; existing.push(entry); + ctx.conversations.set(span, existing); // Set conversation attribute span.setAttribute("conversation", JSON.stringify(existing)); From 04cac124123f472e2e9a1f2322eede2e779b4f8f Mon Sep 17 00:00:00 2001 From: muhammedrazalak Date: Thu, 12 Mar 2026 13:50:07 +0530 Subject: [PATCH 3/3] fix: clear conversation map while unregistering spans --- src/session-manager.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/session-manager.ts b/src/session-manager.ts index f0877ea..96d4ad5 100644 --- a/src/session-manager.ts +++ b/src/session-manager.ts @@ -134,6 +134,9 @@ export class SessionManager { break; } } + + // Cleanup conversations to prevent memory leaks + ctx.conversations.delete(span); } catch (e) { console.error(`Failed to unregister span '${name}':`, e); }