@observyze/sdk 0.1.0
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/INSTRUMENTATION_SUMMARY.md +184 -0
- package/README.md +198 -0
- package/examples/auto-instrumentation.ts +210 -0
- package/package.json +43 -0
- package/src/client.ts +578 -0
- package/src/index.ts +21 -0
- package/src/instrumentation/README.md +227 -0
- package/src/instrumentation/anthropic.ts +233 -0
- package/src/instrumentation/index.ts +43 -0
- package/src/instrumentation/openai.ts +193 -0
- package/src/trace.ts +242 -0
- package/src/types.ts +102 -0
- package/tsconfig.json +14 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observyze SDK Client
|
|
3
|
+
* Main entry point for instrumenting AI applications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Trace } from './trace'
|
|
7
|
+
import { TraceStatus } from './types'
|
|
8
|
+
import type { ClientConfig, ResolvedClientConfig } from './types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default configuration values
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_CONFIG: Partial<ClientConfig> = {
|
|
14
|
+
endpoint: 'http://localhost:3001',
|
|
15
|
+
batchSize: 100,
|
|
16
|
+
flushInterval: 5000,
|
|
17
|
+
enableAutoInstrumentation: true,
|
|
18
|
+
debug: false,
|
|
19
|
+
dryRun: false,
|
|
20
|
+
enablePiiRedaction: true,
|
|
21
|
+
hallucinationThreshold: 0.8,
|
|
22
|
+
safetyThreshold: 0.9,
|
|
23
|
+
evalEndpoint: process.env.EVAL_ENDPOINT || (process.env.NODE_ENV === 'production' ? 'https://api.observyze.com' : 'http://localhost:3000'),
|
|
24
|
+
enableCircuitBreaker: true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Main SDK client for Observyze
|
|
29
|
+
*/
|
|
30
|
+
export class ObservyzeClient {
|
|
31
|
+
private config: ResolvedClientConfig
|
|
32
|
+
private traceBuffer: Trace[] = []
|
|
33
|
+
private flushTimer: NodeJS.Timeout | null = null
|
|
34
|
+
private isShuttingDown: boolean = false
|
|
35
|
+
private readonly MAX_QUEUE_SIZE = 1000
|
|
36
|
+
private readonly RETRY_DELAYS = [1000, 2000, 4000, 8000, 16000, 30000] // ms: 1s → 2s → 4s → 8s → 16s → 30s
|
|
37
|
+
|
|
38
|
+
constructor(config: ClientConfig) {
|
|
39
|
+
// Validate required fields
|
|
40
|
+
if (!config.apiKey) {
|
|
41
|
+
throw new Error('Observyze SDK: apiKey is required')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Apply defaults
|
|
45
|
+
this.config = {
|
|
46
|
+
...DEFAULT_CONFIG,
|
|
47
|
+
...config,
|
|
48
|
+
organizationId: config.organizationId || '',
|
|
49
|
+
projectId: config.projectId || ''
|
|
50
|
+
} as ResolvedClientConfig
|
|
51
|
+
|
|
52
|
+
// Start flush timer
|
|
53
|
+
this.startFlushTimer()
|
|
54
|
+
|
|
55
|
+
// Log initialization
|
|
56
|
+
if (this.config.debug) {
|
|
57
|
+
console.log('[Observyze SDK] Initialized with config:', {
|
|
58
|
+
endpoint: this.config.endpoint,
|
|
59
|
+
batchSize: this.config.batchSize,
|
|
60
|
+
flushInterval: this.config.flushInterval,
|
|
61
|
+
dryRun: this.config.dryRun
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Start a new trace
|
|
68
|
+
*/
|
|
69
|
+
startTrace(name: string, metadata?: Record<string, any>): Trace {
|
|
70
|
+
const trace = new Trace(
|
|
71
|
+
name,
|
|
72
|
+
this.config.organizationId,
|
|
73
|
+
this.config.projectId
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if (metadata) {
|
|
77
|
+
trace.setMetadataAll(metadata)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Auto-end and buffer trace when it's ended
|
|
81
|
+
const originalEnd = trace.end.bind(trace)
|
|
82
|
+
trace.end = (status: TraceStatus = TraceStatus.SUCCESS) => {
|
|
83
|
+
originalEnd(status)
|
|
84
|
+
this.bufferTrace(trace)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return trace
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Buffer a completed trace for batch sending
|
|
92
|
+
*/
|
|
93
|
+
private bufferTrace(trace: Trace): void {
|
|
94
|
+
if (!trace.isEnded) {
|
|
95
|
+
if (this.config.debug) {
|
|
96
|
+
console.warn('[Observyze SDK] Attempted to buffer a trace that has not ended')
|
|
97
|
+
}
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if queue is at max capacity
|
|
102
|
+
if (this.traceBuffer.length >= this.MAX_QUEUE_SIZE) {
|
|
103
|
+
if (this.config.debug) {
|
|
104
|
+
console.warn(`[Observyze SDK] Queue at max capacity (${this.MAX_QUEUE_SIZE}), dropping oldest trace`)
|
|
105
|
+
}
|
|
106
|
+
// Drop oldest trace to make room
|
|
107
|
+
this.traceBuffer.shift()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.traceBuffer.push(trace)
|
|
111
|
+
|
|
112
|
+
if (this.config.debug) {
|
|
113
|
+
console.log(`[Observyze SDK] Buffered trace ${trace.id} (${this.traceBuffer.length}/${this.config.batchSize})`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Flush if buffer is full
|
|
117
|
+
if (this.traceBuffer.length >= this.config.batchSize) {
|
|
118
|
+
this.flush().catch(err => {
|
|
119
|
+
console.error('[Observyze SDK] Error flushing buffer:', err)
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Start the auto-flush timer
|
|
126
|
+
*/
|
|
127
|
+
private startFlushTimer(): void {
|
|
128
|
+
if (this.flushTimer) {
|
|
129
|
+
clearInterval(this.flushTimer)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.flushTimer = setInterval(() => {
|
|
133
|
+
if (this.traceBuffer.length > 0) {
|
|
134
|
+
this.flush().catch(err => {
|
|
135
|
+
console.error('[Observyze SDK] Error in auto-flush:', err)
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}, this.config.flushInterval)
|
|
139
|
+
|
|
140
|
+
// Don't keep the process alive just for the timer
|
|
141
|
+
if (this.flushTimer.unref) {
|
|
142
|
+
this.flushTimer.unref()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Flush all buffered traces to the Ingestion Service
|
|
148
|
+
*/
|
|
149
|
+
async flush(): Promise<void> {
|
|
150
|
+
if (this.traceBuffer.length === 0) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const tracesToSend = this.traceBuffer.splice(0, this.config.batchSize)
|
|
155
|
+
|
|
156
|
+
if (this.config.debug) {
|
|
157
|
+
console.log(`[Observyze SDK] Flushing ${tracesToSend.length} traces`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// In dry-run mode, just log and return
|
|
161
|
+
if (this.config.dryRun) {
|
|
162
|
+
if (this.config.debug) {
|
|
163
|
+
console.log('[Observyze SDK] Dry-run mode: traces not sent')
|
|
164
|
+
}
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await this.sendWithRetry(tracesToSend)
|
|
170
|
+
} catch (error) {
|
|
171
|
+
// On final failure, put traces back in buffer (up to max queue size)
|
|
172
|
+
const remainingSpace = this.MAX_QUEUE_SIZE - this.traceBuffer.length
|
|
173
|
+
if (remainingSpace > 0) {
|
|
174
|
+
this.traceBuffer.unshift(...tracesToSend.slice(0, remainingSpace))
|
|
175
|
+
if (this.config.debug) {
|
|
176
|
+
console.log(`[Observyze SDK] Re-queued ${Math.min(tracesToSend.length, remainingSpace)} traces after failure`)
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
if (this.config.debug) {
|
|
180
|
+
console.warn(`[Observyze SDK] Queue full, dropped ${tracesToSend.length} traces`)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.config.debug) {
|
|
185
|
+
console.error('[Observyze SDK] Failed to send traces after retries:', error)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
throw error
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Send traces with exponential backoff retry
|
|
194
|
+
*/
|
|
195
|
+
private async sendWithRetry(traces: Trace[]): Promise<void> {
|
|
196
|
+
let lastError: Error | null = null
|
|
197
|
+
|
|
198
|
+
for (let attempt = 0; attempt < this.RETRY_DELAYS.length + 1; attempt++) {
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch(`${this.config.endpoint}/api/v1/ingest/batch`, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: {
|
|
203
|
+
'Content-Type': 'application/json',
|
|
204
|
+
'Authorization': `Bearer ${this.config.apiKey}`
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify({
|
|
207
|
+
traces: traces.map(trace => {
|
|
208
|
+
const json = trace.toJSON();
|
|
209
|
+
if (this.config.enablePiiRedaction) {
|
|
210
|
+
json.spans = this.sanitizePII(json.spans);
|
|
211
|
+
}
|
|
212
|
+
return json;
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
const errorBody = await response.text()
|
|
219
|
+
throw new Error(`Ingestion failed: ${response.status} ${errorBody}`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.config.debug) {
|
|
223
|
+
console.log(`[Observyze SDK] Successfully sent ${traces.length} traces${attempt > 0 ? ` (after ${attempt} retries)` : ''}`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return // Success
|
|
227
|
+
} catch (error) {
|
|
228
|
+
lastError = error as Error
|
|
229
|
+
|
|
230
|
+
// If this is the last attempt, break and throw
|
|
231
|
+
if (attempt >= this.RETRY_DELAYS.length) {
|
|
232
|
+
break
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Calculate delay with exponential backoff
|
|
236
|
+
const delay = this.RETRY_DELAYS[attempt]
|
|
237
|
+
|
|
238
|
+
if (this.config.debug) {
|
|
239
|
+
console.warn(`[Observyze SDK] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, error)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Wait before retrying
|
|
243
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// All retries exhausted
|
|
248
|
+
throw lastError || new Error('Failed to send traces after all retries')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Shutdown the SDK and flush remaining traces
|
|
253
|
+
*/
|
|
254
|
+
async shutdown(): Promise<void> {
|
|
255
|
+
if (this.isShuttingDown) {
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.isShuttingDown = true
|
|
260
|
+
|
|
261
|
+
if (this.config.debug) {
|
|
262
|
+
console.log('[Observyze SDK] Shutting down...')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Stop the flush timer
|
|
266
|
+
if (this.flushTimer) {
|
|
267
|
+
clearInterval(this.flushTimer)
|
|
268
|
+
this.flushTimer = null
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Flush remaining traces
|
|
272
|
+
try {
|
|
273
|
+
await this.flush()
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.error('[Observyze SDK] Error during shutdown flush:', error)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (this.config.debug) {
|
|
279
|
+
console.log('[Observyze SDK] Shutdown complete')
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get current buffer size
|
|
285
|
+
*/
|
|
286
|
+
get bufferSize(): number {
|
|
287
|
+
return this.traceBuffer.length
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get SDK configuration
|
|
292
|
+
*/
|
|
293
|
+
getConfig(): Readonly<ResolvedClientConfig> {
|
|
294
|
+
return { ...this.config }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Wrap an LLM client (OpenAI, Anthropic) to enable auto-instrumentation
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* import OpenAI from 'openai'
|
|
303
|
+
* import { ObservyzeClient } from '@observyze/sdk'
|
|
304
|
+
*
|
|
305
|
+
* const nw = new ObservyzeClient({ apiKey: 'your-api-key' })
|
|
306
|
+
* const openai = new OpenAI({ apiKey: 'openai-key' })
|
|
307
|
+
*
|
|
308
|
+
* // Wrap the client to enable auto-instrumentation
|
|
309
|
+
* nw.wrap(openai)
|
|
310
|
+
*
|
|
311
|
+
* // All calls are now automatically traced
|
|
312
|
+
* const response = await openai.chat.completions.create({
|
|
313
|
+
* model: 'gpt-4',
|
|
314
|
+
* messages: [{ role: 'user', content: 'Hello!' }]
|
|
315
|
+
* })
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
wrap<T>(client: T): T {
|
|
319
|
+
const { wrap: wrapClient } = require('./instrumentation')
|
|
320
|
+
return wrapClient(client, this)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Sync local agent .history file to Observyze cloud
|
|
325
|
+
* Parses JSON/NDJSON agent history and sends to ingestion endpoint.
|
|
326
|
+
*/
|
|
327
|
+
async syncLocalHistory(filePath: string): Promise<void> {
|
|
328
|
+
try {
|
|
329
|
+
if (typeof process === 'undefined' || !process.versions?.node) {
|
|
330
|
+
throw new Error('syncLocalHistory is only available in Node.js environments')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const fs = require('fs')
|
|
334
|
+
const path = require('path')
|
|
335
|
+
const fullPath = path.resolve(process.cwd(), filePath)
|
|
336
|
+
|
|
337
|
+
if (!fs.existsSync(fullPath)) {
|
|
338
|
+
throw new Error(`History file not found: ${fullPath}`)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const content = fs.readFileSync(fullPath, 'utf-8')
|
|
342
|
+
let items: any[] = []
|
|
343
|
+
|
|
344
|
+
// Handle NDJSON or plain JSON array
|
|
345
|
+
try {
|
|
346
|
+
items = JSON.parse(content)
|
|
347
|
+
} catch (e) {
|
|
348
|
+
items = content.split('\n').filter((l: string) => l.trim()).map((l: string) => JSON.parse(l))
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!Array.isArray(items)) {
|
|
352
|
+
items = [items]
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (this.config.debug) {
|
|
356
|
+
console.log(`[Observyze SDK] Syncing ${items.length} traces from ${filePath}`)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Send directly via api utilizing standard format
|
|
360
|
+
for (let i = 0; i < items.length; i += this.config.batchSize) {
|
|
361
|
+
const batch = items.slice(i, i + this.config.batchSize)
|
|
362
|
+
|
|
363
|
+
const response = await fetch(`${this.config.endpoint}/api/v1/ingest/batch`, {
|
|
364
|
+
method: 'POST',
|
|
365
|
+
headers: {
|
|
366
|
+
'Content-Type': 'application/json',
|
|
367
|
+
'Authorization': `Bearer ${this.config.apiKey}`
|
|
368
|
+
},
|
|
369
|
+
body: JSON.stringify({ traces: batch })
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
if (!response.ok) {
|
|
373
|
+
const errorBody = await response.text()
|
|
374
|
+
throw new Error(`Batch sync failed: ${response.status} ${errorBody}`)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (this.config.debug) {
|
|
378
|
+
console.log(`[Observyze SDK] Synced batch of ${batch.length} traces from local history`)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} catch (err) {
|
|
382
|
+
console.error('[Observyze SDK] Failed to sync local history:', err)
|
|
383
|
+
throw err
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Phase 5: PII Redaction (Compliance & RBAC)
|
|
389
|
+
* Recursively sanitize strings to mask common PII before transmitting to the cloud.
|
|
390
|
+
*/
|
|
391
|
+
private sanitizePII(data: any): any {
|
|
392
|
+
if (data === null || data === undefined) return data;
|
|
393
|
+
|
|
394
|
+
if (typeof data === 'string') {
|
|
395
|
+
return data
|
|
396
|
+
.replace(/\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b/gi, '[EMAIL_REDACTED]') // Emails
|
|
397
|
+
.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN_REDACTED]') // SSN
|
|
398
|
+
.replace(/\b(?:\d[ -]*?){13,16}\b/g, '[CC_REDACTED]'); // Credit Cards
|
|
399
|
+
}
|
|
400
|
+
if (Array.isArray(data)) {
|
|
401
|
+
return data.map(item => this.sanitizePII(item));
|
|
402
|
+
}
|
|
403
|
+
if (typeof data === 'object') {
|
|
404
|
+
const sanitized: any = {};
|
|
405
|
+
for (const [k, v] of Object.entries(data)) {
|
|
406
|
+
sanitized[k] = this.sanitizePII(v);
|
|
407
|
+
}
|
|
408
|
+
return sanitized;
|
|
409
|
+
}
|
|
410
|
+
return data;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Phase 4: Autonomous Circuit Breakers (Requirement 4.1)
|
|
415
|
+
* Evaluate a trace or text for hallucination in real-time.
|
|
416
|
+
* If hallucination score > hallucinationThreshold, the SDK blocks execution.
|
|
417
|
+
*/
|
|
418
|
+
async checkGuardrails(content: string | any): Promise<{ pass: boolean, score: number, reason?: string, safetyScore?: number }> {
|
|
419
|
+
if (!this.config.enableCircuitBreaker) {
|
|
420
|
+
return { pass: true, score: 0 }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
if (this.config.debug) {
|
|
425
|
+
console.log(`[Observyze Guardrail] Analyzing payload for hallucination anomalies...`)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const evalEndpoint = this.config.evalEndpoint
|
|
429
|
+
const payload = typeof content === 'string'
|
|
430
|
+
? { text: content, organization_id: this.config.organizationId }
|
|
431
|
+
: { trace: content, organization_id: this.config.organizationId }
|
|
432
|
+
|
|
433
|
+
const controller = new AbortController()
|
|
434
|
+
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
435
|
+
|
|
436
|
+
let evalResult: any = null
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const response = await fetch(`${evalEndpoint}/api/v1/evaluate/hallucination`, {
|
|
440
|
+
method: 'POST',
|
|
441
|
+
headers: {
|
|
442
|
+
'Content-Type': 'application/json',
|
|
443
|
+
'Authorization': `Bearer ${this.config.apiKey}`
|
|
444
|
+
},
|
|
445
|
+
body: JSON.stringify(payload),
|
|
446
|
+
signal: controller.signal
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
clearTimeout(timeout)
|
|
450
|
+
|
|
451
|
+
if (response.ok) {
|
|
452
|
+
evalResult = await response.json()
|
|
453
|
+
} else if (response.status === 404) {
|
|
454
|
+
if (this.config.debug) {
|
|
455
|
+
console.log('[Observyze Guardrail] Eval endpoint not found, using fallback')
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} catch (fetchError: any) {
|
|
459
|
+
clearTimeout(timeout)
|
|
460
|
+
if (fetchError.name === 'AbortError') {
|
|
461
|
+
if (this.config.debug) {
|
|
462
|
+
console.warn('[Observyze Guardrail] Evaluation timed out after 5s')
|
|
463
|
+
}
|
|
464
|
+
} else if (this.config.debug) {
|
|
465
|
+
console.warn('[Observyze Guardrail] Evaluation request failed:', fetchError.message)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
let hallucinationScore = 0
|
|
470
|
+
let safetyScore = 0
|
|
471
|
+
|
|
472
|
+
if (evalResult) {
|
|
473
|
+
hallucinationScore = evalResult.score ?? evalResult.hallucination_score ?? 0
|
|
474
|
+
safetyScore = evalResult.safety_score ?? 0
|
|
475
|
+
} else {
|
|
476
|
+
if (this.config.debug) {
|
|
477
|
+
console.log('[Observyze Guardrail] No eval result, using conservative threshold')
|
|
478
|
+
}
|
|
479
|
+
hallucinationScore = 0.5
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const hallThreshold = this.config.hallucinationThreshold
|
|
483
|
+
const safeThreshold = this.config.safetyThreshold
|
|
484
|
+
|
|
485
|
+
if (hallucinationScore >= hallThreshold) {
|
|
486
|
+
if (this.config.debug) {
|
|
487
|
+
console.warn(`[Observyze Guardrail] Hallucination circuit breached! Score: ${hallucinationScore.toFixed(2)} >= ${hallThreshold}`)
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
pass: false,
|
|
491
|
+
score: hallucinationScore,
|
|
492
|
+
safetyScore,
|
|
493
|
+
reason: `Hallucination score ${hallucinationScore.toFixed(2)} exceeds threshold ${hallThreshold}. Execution blocked for human review.`
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (safetyScore >= safeThreshold) {
|
|
498
|
+
if (this.config.debug) {
|
|
499
|
+
console.warn(`[Observyze Guardrail] Safety circuit breached! Score: ${safetyScore.toFixed(2)} >= ${safeThreshold}`)
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
pass: false,
|
|
503
|
+
score: hallucinationScore,
|
|
504
|
+
safetyScore,
|
|
505
|
+
reason: `Safety score ${safetyScore.toFixed(2)} exceeds threshold ${safeThreshold}. Execution blocked for safety review.`
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return { pass: true, score: hallucinationScore, safetyScore }
|
|
510
|
+
|
|
511
|
+
} catch (err) {
|
|
512
|
+
console.error('[Observyze Guardrail] Failed to evaluate:', err)
|
|
513
|
+
return { pass: true, score: 0 }
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Phase 4: Autonomous Circuit Breakers
|
|
519
|
+
* Execute an agent action wrapped with the Circuit Breaker.
|
|
520
|
+
* Pauses execution if hallucination score >= hallucinationThreshold and requests human review.
|
|
521
|
+
* @throws Error when execution is blocked by circuit breaker
|
|
522
|
+
*/
|
|
523
|
+
async executeWithCircuitBreaker<T>(agentExecution: () => Promise<T>, traceContext?: any): Promise<T> {
|
|
524
|
+
if (!this.config.enableCircuitBreaker) {
|
|
525
|
+
return await agentExecution()
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const guardResult = await this.checkGuardrails(traceContext || 'execution context')
|
|
529
|
+
|
|
530
|
+
if (!guardResult.pass) {
|
|
531
|
+
const error = new Error(`[Observyze] Execution Blocked by Autonomous Circuit Breaker. ` +
|
|
532
|
+
`Hallucination: ${guardResult.score.toFixed(2)}, Safety: ${(guardResult.safetyScore ?? 0).toFixed(2)}. ` +
|
|
533
|
+
`Reason: ${guardResult.reason}. Human approval required before agent can continue.`)
|
|
534
|
+
|
|
535
|
+
if (this.config.debug) {
|
|
536
|
+
console.error('[Observyze CircuitBreaker] Execution blocked:', error.message)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
throw error
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (this.config.debug) {
|
|
543
|
+
console.log(`[Observyze CircuitBreaker] Execution allowed. Hallucination: ${guardResult.score.toFixed(2)}, Safety: ${(guardResult.safetyScore ?? 0).toFixed(2)}`)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return await agentExecution()
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Phase 4: Bug Bounty Protocol (Automated) (Requirement 4.2)
|
|
551
|
+
* Automatically shard persistent failure cases to external security researcher endpoints (e.g. HackerOne wrapper)
|
|
552
|
+
*/
|
|
553
|
+
async reportBugBounty(traceId: string, securityEndpoint: string, failureContext: any): Promise<void> {
|
|
554
|
+
try {
|
|
555
|
+
if (this.config.debug) {
|
|
556
|
+
console.log(`[Observyze SDK] Sharding persistent failure case ${traceId} to Bug Bounty Protocol endpoint...`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Dispatch failure case context to security webhook
|
|
560
|
+
await fetch(securityEndpoint, {
|
|
561
|
+
method: 'POST',
|
|
562
|
+
headers: { 'Content-Type': 'application/json' },
|
|
563
|
+
body: JSON.stringify({
|
|
564
|
+
alert: 'persistent_failure_sharded',
|
|
565
|
+
trace_id: traceId,
|
|
566
|
+
context: failureContext,
|
|
567
|
+
timestamp: new Date().toISOString()
|
|
568
|
+
})
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
if (this.config.debug) {
|
|
572
|
+
console.log(`[Observyze SDK] Bug Bounty payload successfully transmitted.`);
|
|
573
|
+
}
|
|
574
|
+
} catch (err) {
|
|
575
|
+
console.error('[Observyze Bug Bounty] Failed to shard failure case:', err);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observyze Node.js SDK
|
|
3
|
+
* AI Observability & Governance Platform
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { ObservyzeClient } from './client'
|
|
7
|
+
export { Trace, Span } from './trace'
|
|
8
|
+
export { SpanType, TraceStatus } from './types'
|
|
9
|
+
export type { ClientConfig, ResolvedClientConfig } from './types'
|
|
10
|
+
|
|
11
|
+
// Export instrumentation utilities
|
|
12
|
+
export { wrap, wrapOpenAI, wrapAnthropic } from './instrumentation'
|
|
13
|
+
export type { SupportedClient } from './instrumentation'
|
|
14
|
+
|
|
15
|
+
// Re-export types from @observyze/types for convenience
|
|
16
|
+
export type {
|
|
17
|
+
Span as SpanData,
|
|
18
|
+
Trace as TraceData,
|
|
19
|
+
SpanError,
|
|
20
|
+
TokenUsage
|
|
21
|
+
} from '@observyze/types'
|