ai-sage-client 0.0.1
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/README.md +85 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +280 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# ai-sage-client
|
|
2
|
+
|
|
3
|
+
The official Node.js SDK for **AI Sage Agent APM**.
|
|
4
|
+
Auto-instrument your AI agents to capture logs, LLM calls, tool usage, and execution traces with a single line of code.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- 🔍 **Auto-Instrumentation**: Automatically captures `console.log`, `OpenAI` calls, and outbound `fetch`/`http` requests.
|
|
9
|
+
- 🧵 **Context Aware**: Uses `AsyncLocalStorage` to keep logs isolated across concurrent user sessions.
|
|
10
|
+
- 🛡️ **Active Governance**: Validate inputs against policies before execution using `aisage.check()`.
|
|
11
|
+
- ⚡ **Zero Config**: Just set your API Key and go.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install ai-sage-client
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### 1. Initialize
|
|
22
|
+
|
|
23
|
+
Call `init()` at the start of your application (e.g., inside `server.js` or `app.ts`).
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import aisage from 'ai-sage-client';
|
|
27
|
+
|
|
28
|
+
aisage.init({
|
|
29
|
+
apiKey: process.env.AISAGE_API_KEY,
|
|
30
|
+
// baseUrl: 'https://your-sage-instance.com' // Optional
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Wrap Agent Execution
|
|
35
|
+
|
|
36
|
+
Wrap your agent's entry point with `aisage.run()`. This establishes the session context.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import aisage from 'ai-sage-client';
|
|
40
|
+
|
|
41
|
+
async function handleUserRequest(userInput: string) {
|
|
42
|
+
return aisage.run('chat_flow', async () => {
|
|
43
|
+
|
|
44
|
+
// 1. Governance Check (Optional)
|
|
45
|
+
const check = await aisage.check(userInput);
|
|
46
|
+
if (!check.allowed) {
|
|
47
|
+
throw new Error(`Blocked: ${check.reason}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Your Agent Logic (Auto-captured!)
|
|
51
|
+
console.log('Processing user input:', userInput);
|
|
52
|
+
|
|
53
|
+
const response = await openai.chat.completions.create({
|
|
54
|
+
model: 'gpt-4',
|
|
55
|
+
messages: [{ role: 'user', content: userInput }]
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 3. Outbound Calls (Auto-captured!)
|
|
59
|
+
await fetch('https://api.weather.com/...');
|
|
60
|
+
|
|
61
|
+
return response.choices[0].message.content;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Auto-Instrumentation
|
|
67
|
+
|
|
68
|
+
To enable OpenAI capture, pass the `openai` instance:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import OpenAI from 'openai';
|
|
72
|
+
const openai = new OpenAI();
|
|
73
|
+
|
|
74
|
+
// Enable auto-capture
|
|
75
|
+
aisage.instrument(openai);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Environment Variables
|
|
79
|
+
|
|
80
|
+
- `AISAGE_API_KEY`: Your Agent API Key from the AI Sage Dashboard.
|
|
81
|
+
- `AISAGE_BASE_URL`: (Optional) URL of the AI Sage ingestion API.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
interface ClientConfig {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
projectId?: string;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
autoInstrument?: {
|
|
6
|
+
console?: boolean;
|
|
7
|
+
openai?: boolean;
|
|
8
|
+
http?: boolean;
|
|
9
|
+
process?: boolean;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
interface LogEntry {
|
|
13
|
+
conversation_id: string;
|
|
14
|
+
type: 'trigger' | 'step' | 'user_input' | 'model_response' | 'tool_call' | 'tool_result' | 'error' | 'final_output';
|
|
15
|
+
content: any;
|
|
16
|
+
timestamp?: string;
|
|
17
|
+
metadata?: any;
|
|
18
|
+
duration_ms?: number;
|
|
19
|
+
}
|
|
20
|
+
declare class AiSageClient {
|
|
21
|
+
private config;
|
|
22
|
+
private logs;
|
|
23
|
+
private storage;
|
|
24
|
+
private isActive;
|
|
25
|
+
private flushInterval;
|
|
26
|
+
constructor();
|
|
27
|
+
init(config: ClientConfig): void;
|
|
28
|
+
stop(): void;
|
|
29
|
+
private startFlushLoop;
|
|
30
|
+
run<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
31
|
+
private checkStatus;
|
|
32
|
+
/**
|
|
33
|
+
* Active Governance Check: Validate input against policies before execution.
|
|
34
|
+
* @param input The user input or prompt to check
|
|
35
|
+
* @returns { allowed: boolean, reason?: string }
|
|
36
|
+
*/
|
|
37
|
+
check(input: string): Promise<{
|
|
38
|
+
allowed: boolean;
|
|
39
|
+
reason?: string;
|
|
40
|
+
}>;
|
|
41
|
+
log(type: LogEntry['type'], content: any, metadata?: any): void;
|
|
42
|
+
flush(): Promise<void>;
|
|
43
|
+
instrument(openaiModule: any): void;
|
|
44
|
+
private interceptConsole;
|
|
45
|
+
private interceptHttp;
|
|
46
|
+
}
|
|
47
|
+
export declare const aisage: AiSageClient;
|
|
48
|
+
export default aisage;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.aisage = void 0;
|
|
4
|
+
const uuid_1 = require("uuid");
|
|
5
|
+
const async_hooks_1 = require("async_hooks");
|
|
6
|
+
class AiSageClient {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.config = {};
|
|
9
|
+
this.logs = [];
|
|
10
|
+
this.storage = new async_hooks_1.AsyncLocalStorage();
|
|
11
|
+
this.isActive = false;
|
|
12
|
+
this.flushInterval = null;
|
|
13
|
+
}
|
|
14
|
+
init(config) {
|
|
15
|
+
this.config = {
|
|
16
|
+
apiKey: config.apiKey || process.env.AISAGE_API_KEY,
|
|
17
|
+
baseUrl: config.baseUrl || "http://localhost:3000",
|
|
18
|
+
autoInstrument: {
|
|
19
|
+
console: true,
|
|
20
|
+
openai: true,
|
|
21
|
+
http: true,
|
|
22
|
+
...config.autoInstrument
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
if (!this.config.apiKey) {
|
|
26
|
+
console.warn('[AiSage] No API Key provided. SDK disabled.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (this.config.autoInstrument?.console) {
|
|
30
|
+
this.interceptConsole();
|
|
31
|
+
}
|
|
32
|
+
if (this.config.autoInstrument?.http) {
|
|
33
|
+
this.interceptHttp();
|
|
34
|
+
}
|
|
35
|
+
this.isActive = true;
|
|
36
|
+
this.startFlushLoop();
|
|
37
|
+
console.log('[AiSage] Initialized');
|
|
38
|
+
}
|
|
39
|
+
stop() {
|
|
40
|
+
if (this.flushInterval) {
|
|
41
|
+
clearInterval(this.flushInterval);
|
|
42
|
+
this.flushInterval = null;
|
|
43
|
+
}
|
|
44
|
+
this.isActive = false;
|
|
45
|
+
}
|
|
46
|
+
startFlushLoop() {
|
|
47
|
+
if (this.flushInterval)
|
|
48
|
+
clearInterval(this.flushInterval);
|
|
49
|
+
this.flushInterval = setInterval(() => this.flush(), 2000);
|
|
50
|
+
}
|
|
51
|
+
async run(name, fn) {
|
|
52
|
+
if (!this.isActive)
|
|
53
|
+
return fn();
|
|
54
|
+
const sessionId = (0, uuid_1.v4)();
|
|
55
|
+
const context = { sessionId };
|
|
56
|
+
return this.storage.run(context, async () => {
|
|
57
|
+
const start = Date.now();
|
|
58
|
+
try {
|
|
59
|
+
await this.checkStatus();
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
console.error(`[AiSage] Blocked: ${e.message}`);
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
this.log('trigger', { name, type: 'manual_run' });
|
|
66
|
+
try {
|
|
67
|
+
const result = await fn();
|
|
68
|
+
this.log('final_output', result, { duration_ms: Date.now() - start });
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
this.log('error', { error: e.message, stack: e.stack }, { duration_ms: Date.now() - start });
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async checkStatus() {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
const fetchFn = fetch; // Use global fetch
|
|
81
|
+
fetchFn(`${this.config.baseUrl}/api/ingest`, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json', 'x-agent-key': this.config.apiKey },
|
|
84
|
+
body: JSON.stringify({ conversation_id: 'ping', logs: [] })
|
|
85
|
+
})
|
|
86
|
+
.then((res) => res.json())
|
|
87
|
+
.then((data) => {
|
|
88
|
+
if (data.agent_status === 'paused' || data.agent_status === 'disabled') {
|
|
89
|
+
reject(new Error(`Agent is ${data.agent_status}`));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
resolve();
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
.catch((err) => {
|
|
96
|
+
console.warn('[AiSage] Status check failed, proceeding anyway:', err.message);
|
|
97
|
+
resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Active Governance Check: Validate input against policies before execution.
|
|
103
|
+
* @param input The user input or prompt to check
|
|
104
|
+
* @returns { allowed: boolean, reason?: string }
|
|
105
|
+
*/
|
|
106
|
+
async check(input) {
|
|
107
|
+
if (!this.isActive || !this.config.apiKey)
|
|
108
|
+
return { allowed: true };
|
|
109
|
+
try {
|
|
110
|
+
// Extract Agent ID from API Key (if encoded) or just use endpoint that resolves key
|
|
111
|
+
// Ideally we assume the endpoint handles key validation.
|
|
112
|
+
// But wait, the route is /api/agents/[agent_id]/check. We might not know agent_id in SDK if not passed.
|
|
113
|
+
// Let's use a generic /api/check endpoint that uses x-agent-key to resolve agent.
|
|
114
|
+
// Or assume baseUrl points to the right place.
|
|
115
|
+
// Better: use /api/check or /api/govern and let backend resolve agent from key.
|
|
116
|
+
// For MVP, since I created /api/agents/[agent_id]/check, I need agent_id.
|
|
117
|
+
// The ClientConfig has propery `projectId` but not `agentId`.
|
|
118
|
+
// Let's change the route I created to be generic or update SDK to support generic check.
|
|
119
|
+
// I'll assume for now I can reach it via a generic path or update the backend path.
|
|
120
|
+
// Actually, I'll update the backend path to be `/api/govern` which is cleaner.
|
|
121
|
+
// Re-using baseUrl/api/ingest style -> baseUrl/api/govern
|
|
122
|
+
// @ts-ignore
|
|
123
|
+
const res = await fetch(`${this.config.baseUrl}/api/govern`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json', 'x-agent-key': this.config.apiKey },
|
|
126
|
+
body: JSON.stringify({ input })
|
|
127
|
+
});
|
|
128
|
+
const data = await res.json();
|
|
129
|
+
return data;
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
console.error('[AiSage] Governance check failed', e);
|
|
133
|
+
return { allowed: true }; // Fail open
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
log(type, content, metadata = {}) {
|
|
137
|
+
const store = this.storage.getStore();
|
|
138
|
+
if (!store) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.logs.push({
|
|
142
|
+
conversation_id: store.sessionId,
|
|
143
|
+
type,
|
|
144
|
+
content,
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
metadata,
|
|
147
|
+
...metadata
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async flush() {
|
|
151
|
+
if (this.logs.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
const batch = [...this.logs];
|
|
154
|
+
this.logs = [];
|
|
155
|
+
try {
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
await fetch(`${this.config.baseUrl}/api/ingest`, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
'x-agent-key': this.config.apiKey
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
conversation_id: batch[0].conversation_id, // Group by session ID ideally, or send mixed batch
|
|
165
|
+
logs: batch
|
|
166
|
+
})
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
console.error('[AiSage] Flush failed', e);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
instrument(openaiModule) {
|
|
174
|
+
if (!openaiModule?.chat?.completions?.create) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const originalCreate = openaiModule.chat.completions.create.bind(openaiModule.chat.completions);
|
|
178
|
+
openaiModule.chat.completions.create = async (body, options) => {
|
|
179
|
+
const start = Date.now();
|
|
180
|
+
try {
|
|
181
|
+
const result = await originalCreate(body, options);
|
|
182
|
+
this.log('model_response', {
|
|
183
|
+
model: body.model,
|
|
184
|
+
input: body.messages,
|
|
185
|
+
output: result.choices?.[0]?.message,
|
|
186
|
+
usage: result.usage,
|
|
187
|
+
duration_ms: Date.now() - start
|
|
188
|
+
}, { duration_ms: Date.now() - start });
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
this.log('error', {
|
|
193
|
+
model: body.model,
|
|
194
|
+
error: e.message
|
|
195
|
+
}, { duration_ms: Date.now() - start });
|
|
196
|
+
throw e;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
console.log('[AiSage] OpenAI Instrumented');
|
|
200
|
+
}
|
|
201
|
+
interceptConsole() {
|
|
202
|
+
const originalLog = console.log;
|
|
203
|
+
const originalError = console.error;
|
|
204
|
+
console.log = (...args) => {
|
|
205
|
+
if (args[0] && typeof args[0] === 'string' && args[0].startsWith('[AiSage]')) {
|
|
206
|
+
originalLog.apply(console, args);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.log('step', { type: 'console_log', content: args.map(String).join(' ') });
|
|
210
|
+
originalLog.apply(console, args);
|
|
211
|
+
};
|
|
212
|
+
console.error = (...args) => {
|
|
213
|
+
if (args[0] && typeof args[0] === 'string' && args[0].startsWith('[AiSage]')) {
|
|
214
|
+
originalError.apply(console, args);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.log('error', { type: 'console_error', content: args.map(String).join(' ') });
|
|
218
|
+
originalError.apply(console, args);
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
interceptHttp() {
|
|
222
|
+
const self = this;
|
|
223
|
+
// Helper to patch request methods
|
|
224
|
+
function patchRequest(module, protocol) {
|
|
225
|
+
const originalRequest = module.request;
|
|
226
|
+
module.request = function (...args) {
|
|
227
|
+
let options;
|
|
228
|
+
let url;
|
|
229
|
+
if (typeof args[0] === 'string' || args[0] instanceof URL) {
|
|
230
|
+
url = args[0];
|
|
231
|
+
options = args[1] || {};
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
options = args[0];
|
|
235
|
+
url = `${protocol}//${options.hostname || options.host || 'localhost'}${options.path || '/'}`;
|
|
236
|
+
}
|
|
237
|
+
const targetHost = (options.hostname || options.host || '').toString();
|
|
238
|
+
// @ts-ignore
|
|
239
|
+
const configHost = new URL(self.config.baseUrl || 'http://localhost').hostname;
|
|
240
|
+
if (targetHost === configHost) {
|
|
241
|
+
return originalRequest.apply(this, args);
|
|
242
|
+
}
|
|
243
|
+
const start = Date.now();
|
|
244
|
+
const method = options.method || 'GET';
|
|
245
|
+
const logName = `http.${method.toLowerCase()}`;
|
|
246
|
+
self.log('tool_call', {
|
|
247
|
+
name: logName,
|
|
248
|
+
args: { url: url.toString(), method }
|
|
249
|
+
});
|
|
250
|
+
const req = originalRequest.apply(this, args);
|
|
251
|
+
req.on('response', (res) => {
|
|
252
|
+
res.on('end', () => {
|
|
253
|
+
self.log('tool_result', {
|
|
254
|
+
name: logName,
|
|
255
|
+
output: { status: res.statusCode, statusText: res.statusMessage },
|
|
256
|
+
duration_ms: Date.now() - start
|
|
257
|
+
}, { duration_ms: Date.now() - start });
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
req.on('error', (e) => {
|
|
261
|
+
self.log('error', {
|
|
262
|
+
name: logName,
|
|
263
|
+
error: e.message
|
|
264
|
+
}, { duration_ms: Date.now() - start });
|
|
265
|
+
});
|
|
266
|
+
return req;
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// Use require to get the mutable CommonJS module, avoiding "only a getter" on ES namespaces
|
|
270
|
+
// @ts-ignore
|
|
271
|
+
const httpModule = require('http');
|
|
272
|
+
// @ts-ignore
|
|
273
|
+
const httpsModule = require('https');
|
|
274
|
+
patchRequest(httpModule, 'http:');
|
|
275
|
+
patchRequest(httpsModule, 'https:');
|
|
276
|
+
console.log('[AiSage] HTTP Instrumented');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
exports.aisage = new AiSageClient();
|
|
280
|
+
exports.default = exports.aisage;
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-sage-client",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "AI Sage Auto-Instrumentation SDK",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "ts-node test/usage.ts",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"observability",
|
|
15
|
+
"apm",
|
|
16
|
+
"llm",
|
|
17
|
+
"agent",
|
|
18
|
+
"monitoring"
|
|
19
|
+
],
|
|
20
|
+
"author": "AI Sage",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/medeepak/ai-sage.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/medeepak/ai-sage/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/medeepak/ai-sage#readme",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.0.0",
|
|
36
|
+
"ts-node": "^10.9.1",
|
|
37
|
+
"@types/node": "^20.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"uuid": "^9.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|