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 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
@@ -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
+ }