@archetypeai/ds-cli 0.5.7 → 0.6.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.
@@ -1,559 +0,0 @@
1
- ---
2
- name: newton-machine-state-from-file
3
- description: Run a Machine State Lens by streaming sensor data from a CSV file. Use when analyzing time-series CSV data for machine state classification, anomaly detection, or n-shot state recognition from files.
4
- argument-hint: [csv-file-path]
5
- ---
6
-
7
- # Newton Machine State Lens — Stream from CSV File
8
-
9
- Generate a script that streams time-series data from a CSV file to the Archetype AI Machine State Lens for n-shot state classification. Supports both Python and JavaScript/Web.
10
-
11
- ## Frontend Architecture
12
-
13
- Decompose the UI into components. See `@rules/frontend-architecture` for conventions.
14
-
15
- ### Recommended decomposition
16
-
17
- | UI Area | Component | Pattern/Primitives | Key Props |
18
- |---------|-----------|-------------------|-----------|
19
- | File input | `DataInput.svelte` | BackgroundCard, Button, Input | `onselect`, `status` |
20
- | Classification | `StateDisplay.svelte` | BackgroundCard, Badge | `currentState`, `confidence` |
21
- | Time series | Reuse SensorChart pattern | BackgroundCard, Chart | `data[]`, `signals` |
22
- | Results | Use FlatLogItem pattern in ScrollArea | FlatLogItem, ScrollArea | `status`, `message`, `detail` |
23
-
24
- - Use `@skills/create-dashboard` for the page layout
25
- - Extract streaming and session logic into `$lib/api/machine-state.js`
26
-
27
- ---
28
-
29
- ## Python Implementation
30
-
31
- ### Requirements
32
-
33
- - `archetypeai` Python package
34
- - `pandas`, `numpy`
35
- - Environment variables: `ATAI_API_KEY`, optionally `ATAI_API_ENDPOINT`
36
-
37
- ### Architecture
38
-
39
- The script must follow this exact pattern:
40
-
41
- #### 1. API Client Setup
42
-
43
- ```python
44
- from archetypeai.api_client import ArchetypeAI
45
- import os
46
-
47
- api_key = os.getenv("ATAI_API_KEY")
48
- api_endpoint = os.getenv("ATAI_API_ENDPOINT", ArchetypeAI.get_default_endpoint())
49
- client = ArchetypeAI(api_key, api_endpoint=api_endpoint)
50
- ```
51
-
52
- #### 2. Upload N-Shot Example Files
53
-
54
- Upload one CSV per class. The file ID returned is used in the lens YAML config.
55
-
56
- ```python
57
- # Upload example files for each class
58
- # Class name is typically derived from filename stem
59
- resp = client.files.local.upload("path/to/healthy.csv")
60
- healthy_id = resp["file_id"]
61
-
62
- resp = client.files.local.upload("path/to/broken.csv")
63
- broken_id = resp["file_id"]
64
- ```
65
-
66
- #### 3. Lens YAML Configuration
67
-
68
- Build the YAML config string dynamically, inserting file IDs:
69
-
70
- ```yaml
71
- lens_name: Machine State Lens
72
- lens_config:
73
- model_pipeline:
74
- - processor_name: lens_timeseries_state_processor
75
- processor_config: {}
76
- model_parameters:
77
- model_name: OmegaEncoder
78
- model_version: OmegaEncoder::omega_embeddings_01
79
- normalize_input: true
80
- buffer_size: {window_size}
81
- input_n_shot:
82
- NORMAL: {healthy_file_id}
83
- WARNING: {broken_file_id}
84
- csv_configs:
85
- timestamp_column: timestamp
86
- data_columns: ['a1', 'a2', 'a3', 'a4']
87
- window_size: {window_size}
88
- step_size: {step_size}
89
- knn_configs:
90
- n_neighbors: 5
91
- metric: manhattan
92
- weights: distance
93
- algorithm: ball_tree
94
- normalize_embeddings: false
95
- output_streams:
96
- - stream_type: server_sent_events_writer
97
- ```
98
-
99
- **Important**: `input_n_shot` keys become the predicted class labels. Users can define any number of classes (not just two).
100
-
101
- #### 4. Session Callback Pattern
102
-
103
- ```python
104
- def session_callback(session_id, session_endpoint, client, args):
105
- # Create SSE consumer FIRST
106
- sse_reader = client.lens.sessions.create_sse_consumer(
107
- session_id, max_read_time_sec=args["max_run_time_sec"]
108
- )
109
-
110
- # Load CSV with pandas
111
- df = pd.read_csv(args["file_path"])
112
- columns = ["a1", "a2", "a3", "a4"] # or user-specified columns
113
- data = df[columns].values.T.tolist() # Transpose: [channels][samples]
114
-
115
- # Stream data in windows
116
- total_samples = len(df)
117
- start = 0
118
- counter = 0
119
- while start < total_samples:
120
- end = start + window_size
121
- chunk = [series[start:end] for series in data]
122
-
123
- payload = {
124
- "type": "session.update",
125
- "event_data": {
126
- "type": "data.json",
127
- "event_data": {
128
- "sensor_data": chunk,
129
- "sensor_metadata": {
130
- "sensor_timestamp": time.time(),
131
- "sensor_id": f"streamed_sensor_{counter}"
132
- }
133
- }
134
- }
135
- }
136
- client.lens.sessions.process_event(session_id, payload)
137
- start += step_size
138
- counter += 1
139
-
140
- # Listen for results
141
- for event in sse_reader.read(block=True):
142
- etype = event.get("type")
143
- if etype == "inference.result":
144
- result = event["event_data"].get("response")
145
- meta = event["event_data"].get("query_metadata", {})
146
- print(f"[{meta.get('query_timestamp', 'N/A')}] Predicted: {result}")
147
- elif etype == "session.modify.result":
148
- cls = event["event_data"].get("query_metadata", {}).get("class_name")
149
- print(f"[TRAINING] Processed class: {cls}")
150
- ```
151
-
152
- #### 5. Create and Run Lens
153
-
154
- ```python
155
- client.lens.create_and_run_lens(
156
- yaml_config, session_callback,
157
- client=client, args=args
158
- )
159
- ```
160
-
161
- ### CLI Arguments to Include
162
-
163
- ```
164
- --api-key API key (fallback to ATAI_API_KEY env var)
165
- --api-endpoint API endpoint (default from SDK)
166
- --file-path Path to CSV file to analyze (required)
167
- --n-shot-files Paths to n-shot example CSVs (required, nargs='+')
168
- --window-size Window size in samples (default: 100)
169
- --step-size-n-shot Step size for training data (default: 100)
170
- --step-size-inference Step size for inference stream (default: 100)
171
- --max-run-time-sec Max runtime in seconds (default: 500)
172
- ```
173
-
174
- ---
175
-
176
- ## Web / JavaScript Implementation
177
-
178
- Uses direct `fetch` calls to the Archetype AI REST API. Based on the working pattern from `test-stream/src/lib/atai-client.ts`.
179
-
180
- ### Requirements
181
-
182
- - `@microsoft/fetch-event-source` for SSE consumption
183
-
184
- ### API Reference
185
-
186
- | Operation | Method | Endpoint | Body |
187
- |-----------|--------|----------|------|
188
- | Upload file | POST | `/files` | `FormData` |
189
- | Register lens | POST | `/lens/register` | `{ lens_config: config }` |
190
- | Delete lens | POST | `/lens/delete` | `{ lens_id }` |
191
- | Create session | POST | `/lens/sessions/create` | `{ lens_id }` |
192
- | Process event | POST | `/lens/sessions/events/process` | `{ session_id, event }` |
193
- | Destroy session | POST | `/lens/sessions/destroy` | `{ session_id }` |
194
- | SSE consumer | GET | `/lens/sessions/consumer/{sessionId}` | — |
195
-
196
- ### Helper: API fetch wrapper
197
-
198
- ```typescript
199
- const API_ENDPOINT = 'https://api.u1.archetypeai.app/v0.5'
200
-
201
- async function apiPost<T>(path: string, apiKey: string, body: unknown, timeoutMs = 5000): Promise<T> {
202
- const controller = new AbortController()
203
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
204
-
205
- try {
206
- const response = await fetch(`${API_ENDPOINT}${path}`, {
207
- method: 'POST',
208
- headers: {
209
- Authorization: `Bearer ${apiKey}`,
210
- 'Content-Type': 'application/json',
211
- },
212
- body: JSON.stringify(body),
213
- signal: controller.signal,
214
- })
215
-
216
- if (!response.ok) {
217
- const errorBody = await response.json().catch(() => ({}))
218
- throw new Error(`API POST ${path} failed: ${response.status} - ${JSON.stringify(errorBody)}`)
219
- }
220
-
221
- return response.json()
222
- } finally {
223
- clearTimeout(timeoutId)
224
- }
225
- }
226
- ```
227
-
228
- ### Step 1: Upload n-shot CSV files
229
-
230
- ```typescript
231
- const nShotMap: Record<string, string> = {}
232
-
233
- for (const { file, className } of nShotFiles) {
234
- const formData = new FormData()
235
- formData.append('file', file) // File object from <input type="file">
236
-
237
- const response = await fetch(`${API_ENDPOINT}/files`, {
238
- method: 'POST',
239
- headers: { Authorization: `Bearer ${apiKey}` },
240
- body: formData,
241
- })
242
- const result = await response.json()
243
- nShotMap[className.toUpperCase()] = result.file_id
244
- }
245
- ```
246
-
247
- ### Step 2: Build the lens config
248
-
249
- ```typescript
250
- const windowSize = 100
251
- const stepSize = 100
252
-
253
- const lensConfig = {
254
- lens_name: 'machine_state_lens',
255
- lens_config: {
256
- model_pipeline: [
257
- { processor_name: 'lens_timeseries_state_processor', processor_config: {} },
258
- ],
259
- model_parameters: {
260
- model_name: 'OmegaEncoder',
261
- model_version: 'OmegaEncoder::omega_embeddings_01',
262
- normalize_input: true,
263
- buffer_size: windowSize,
264
- input_n_shot: nShotMap, // { HEALTHY: 'file_id', BROKEN: 'file_id' }
265
- csv_configs: {
266
- timestamp_column: 'timestamp',
267
- data_columns: ['a1', 'a2', 'a3', 'a4'],
268
- window_size: windowSize,
269
- step_size: stepSize,
270
- },
271
- knn_configs: {
272
- n_neighbors: 5,
273
- metric: 'manhattan',
274
- weights: 'distance',
275
- algorithm: 'ball_tree',
276
- normalize_embeddings: false,
277
- },
278
- },
279
- output_streams: [
280
- { stream_type: 'server_sent_events_writer' },
281
- ],
282
- },
283
- }
284
- ```
285
-
286
- ### Step 3: Register lens, create session, wait for ready
287
-
288
- ```typescript
289
- // Register lens — NOTE: body must wrap config as { lens_config: config }
290
- const registeredLens = await apiPost<{ lens_id: string }>(
291
- '/lens/register', apiKey, { lens_config: lensConfig }
292
- )
293
- const lensId = registeredLens.lens_id
294
-
295
- // Create session
296
- const session = await apiPost<{ session_id: string; session_endpoint: string }>(
297
- '/lens/sessions/create', apiKey, { lens_id: lensId }
298
- )
299
- const sessionId = session.session_id
300
-
301
- // Optionally delete the lens definition (session keeps running independently)
302
- await apiPost('/lens/delete', apiKey, { lens_id: lensId })
303
-
304
- // Wait for session to be ready (poll until status = running)
305
- async function waitForSessionReady(sessionId: string, maxWaitMs = 30000): Promise<boolean> {
306
- const start = Date.now()
307
- while (Date.now() - start < maxWaitMs) {
308
- const status = await apiPost<{ session_status: string }>(
309
- '/lens/sessions/events/process', apiKey,
310
- { session_id: sessionId, event: { type: 'session.status' } },
311
- 10000
312
- )
313
- if (status.session_status === 'LensSessionStatus.SESSION_STATUS_RUNNING' ||
314
- status.session_status === '3') {
315
- return true
316
- }
317
- if (status.session_status === 'LensSessionStatus.SESSION_STATUS_FAILED' ||
318
- status.session_status === '6') {
319
- return false
320
- }
321
- await new Promise(r => setTimeout(r, 500))
322
- }
323
- return false
324
- }
325
-
326
- const isReady = await waitForSessionReady(sessionId)
327
- if (!isReady) throw new Error('Session failed to start')
328
- ```
329
-
330
- ### Step 4: Stream CSV data in windows
331
-
332
- Parse the CSV client-side and send windowed chunks via `POST /lens/sessions/events/process`:
333
-
334
- ```typescript
335
- // Parse CSV (using PapaParse or similar)
336
- const rows = parsedCsv.data // array of { timestamp, a1, a2, a3, a4 }
337
- const columns = ['a1', 'a2', 'a3', 'a4']
338
-
339
- let start = 0
340
- let counter = 0
341
-
342
- while (start < rows.length) {
343
- const end = Math.min(start + windowSize, rows.length)
344
- const window = rows.slice(start, end)
345
-
346
- // Transpose to channel-first: [[a1_vals], [a2_vals], [a3_vals], [a4_vals]]
347
- const sensorData = columns.map(col =>
348
- window.map(row => Number(row[col]))
349
- )
350
-
351
- await apiPost('/lens/sessions/events/process', apiKey, {
352
- session_id: sessionId,
353
- event: {
354
- type: 'session.update',
355
- event_data: {
356
- type: 'data.json',
357
- event_data: {
358
- sensor_data: sensorData,
359
- sensor_metadata: {
360
- sensor_timestamp: Date.now() / 1000,
361
- sensor_id: `web_sensor_${counter}`,
362
- },
363
- },
364
- },
365
- },
366
- }, 10000)
367
-
368
- start += stepSize
369
- counter++
370
- }
371
- ```
372
-
373
- ### Step 5: Consume SSE results
374
-
375
- ```typescript
376
- import { fetchEventSource } from '@microsoft/fetch-event-source'
377
-
378
- fetchEventSource(`${API_ENDPOINT}/lens/sessions/consumer/${sessionId}`, {
379
- headers: { Authorization: `Bearer ${apiKey}` },
380
- onmessage(event) {
381
- const parsed = JSON.parse(event.data)
382
-
383
- if (parsed.type === 'inference.result') {
384
- const result = parsed.event_data.response
385
- const meta = parsed.event_data.query_metadata
386
- console.log(`[${meta.query_timestamp ?? 'N/A'}] Predicted: ${result}`)
387
- }
388
-
389
- if (parsed.type === 'session.modify.result') {
390
- const cls = parsed.event_data?.query_metadata?.class_name
391
- console.log(`[TRAINING] Processed class: ${cls}`)
392
- }
393
-
394
- if (parsed.type === 'sse.stream.end') {
395
- console.log('Stream complete')
396
- }
397
- },
398
- })
399
- ```
400
-
401
- ### Step 6: Cleanup
402
-
403
- ```typescript
404
- await apiPost('/lens/sessions/destroy', apiKey, { session_id: sessionId })
405
- ```
406
-
407
- ### Web Lifecycle Summary
408
-
409
- ```
410
- 1. Upload n-shot CSVs -> POST /files (FormData, one per class)
411
- 2. Register lens -> POST /lens/register { lens_config: config }
412
- 3. Create session -> POST /lens/sessions/create { lens_id }
413
- 4. Wait for ready -> POST /lens/sessions/events/process { session_id, event: { type: 'session.status' } }
414
- 5. (Optional) Delete lens -> POST /lens/delete { lens_id }
415
- 6. Stream windowed data -> POST /lens/sessions/events/process { session_id, event } (loop)
416
- 7. Consume SSE results -> GET /lens/sessions/consumer/{sessionId}
417
- 8. Destroy session -> POST /lens/sessions/destroy { session_id }
418
- ```
419
-
420
- ---
421
-
422
- ## CSV Format Expected
423
-
424
- ```csv
425
- timestamp,a1,a2,a3,a4
426
- 1700000000.0,100,200,300,374
427
- 1700000000.01,101,199,301,375
428
- ```
429
-
430
- - `timestamp`: UNIX epoch float
431
- - `a1, a2, a3`: Sensor axes (e.g., accelerometer x, y, z)
432
- - `a4`: Magnitude (sqrt(a1² + a2² + a3²))
433
- - Column names are configurable via `csv_configs.data_columns`
434
-
435
- ## Optional: Results Logging
436
-
437
- Save predictions to a timestamped CSV for analysis or visualization.
438
-
439
- ### Python — Results CSV
440
-
441
- ```python
442
- import csv
443
- from pathlib import Path
444
- from datetime import datetime
445
-
446
- # Create results directory and timestamped filename
447
- results_dir = Path("results")
448
- results_dir.mkdir(exist_ok=True)
449
- file_stem = Path(args["file_path"]).stem
450
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
451
- results_file = results_dir / f"{file_stem}_{timestamp}.csv"
452
-
453
- # Write CSV header
454
- with open(results_file, 'w', newline='', encoding='utf-8') as f:
455
- writer = csv.writer(f)
456
- writer.writerow(['read_index', 'predicted_class', 'confidence_scores',
457
- 'file_id', 'window_size', 'total_rows'])
458
-
459
- # Inside the SSE event loop, when handling inference.result:
460
- if etype == "inference.result":
461
- ed = event.get("event_data", {})
462
- result = ed.get("response")
463
- meta = ed.get("query_metadata", {})
464
- query_meta = meta.get("query_metadata", {})
465
-
466
- predicted_class = result[0] if isinstance(result, list) and len(result) > 0 else "unknown"
467
- confidence_scores = result[1] if isinstance(result, list) and len(result) > 1 else {}
468
- read_index = query_meta.get("read_index", "N/A")
469
- file_id = query_meta.get("file_id", "N/A")
470
- window_size = query_meta.get("window_size", "N/A")
471
- total_rows = query_meta.get("total_rows", "N/A")
472
-
473
- print(f"[{read_index}] Predicted: {predicted_class} | Scores: {confidence_scores}")
474
-
475
- with open(results_file, 'a', newline='', encoding='utf-8') as f:
476
- writer = csv.writer(f)
477
- writer.writerow([read_index, predicted_class, str(confidence_scores),
478
- file_id, window_size, total_rows])
479
- ```
480
-
481
- ### Response Structure
482
-
483
- The `inference.result` response contains:
484
- - `response[0]`: predicted class name (string, e.g. `"HEALTHY"`)
485
- - `response[1]`: confidence scores dict (e.g. `{"HEALTHY": 0.95, "BROKEN": 0.05}`)
486
- - `query_metadata.query_metadata.read_index`: window position in the file
487
- - `query_metadata.query_metadata.file_id`: the file being analyzed
488
- - `query_metadata.query_metadata.window_size`: window size used
489
- - `query_metadata.query_metadata.total_rows`: total rows in the file
490
-
491
- ### Web/JS — Results Array + CSV Download
492
-
493
- ```typescript
494
- interface PredictionResult {
495
- readIndex: number | string
496
- predictedClass: string
497
- confidenceScores: Record<string, number>
498
- fileId: string
499
- windowSize: number
500
- totalRows: number
501
- }
502
-
503
- const results: PredictionResult[] = []
504
-
505
- // Inside the SSE onmessage handler:
506
- if (parsed.type === 'inference.result') {
507
- const result = parsed.event_data.response
508
- const meta = parsed.event_data.query_metadata
509
- const queryMeta = meta?.query_metadata ?? {}
510
-
511
- const prediction: PredictionResult = {
512
- readIndex: queryMeta.read_index ?? 'N/A',
513
- predictedClass: Array.isArray(result) && result.length > 0 ? result[0] : 'unknown',
514
- confidenceScores: Array.isArray(result) && result.length > 1 ? result[1] : {},
515
- fileId: queryMeta.file_id ?? 'N/A',
516
- windowSize: queryMeta.window_size ?? 0,
517
- totalRows: queryMeta.total_rows ?? 0,
518
- }
519
-
520
- results.push(prediction)
521
- console.log(`[${prediction.readIndex}] ${prediction.predictedClass}`, prediction.confidenceScores)
522
- }
523
-
524
- // Download results as CSV
525
- function downloadResultsCsv(results: PredictionResult[], filename: string) {
526
- const header = 'read_index,predicted_class,confidence_scores,file_id,window_size,total_rows\n'
527
- const rows = results.map(r =>
528
- `${r.readIndex},${r.predictedClass},"${JSON.stringify(r.confidenceScores)}",${r.fileId},${r.windowSize},${r.totalRows}`
529
- ).join('\n')
530
-
531
- const blob = new Blob([header + rows], { type: 'text/csv' })
532
- const url = URL.createObjectURL(blob)
533
- const a = document.createElement('a')
534
- a.href = url
535
- a.download = filename
536
- a.click()
537
- URL.revokeObjectURL(url)
538
- }
539
- ```
540
-
541
- ### CLI Flag
542
-
543
- Add `--save-results` flag (default: `True`) to enable/disable results logging:
544
-
545
- ```
546
- --save-results Save predictions to CSV in results/ directory (default: True)
547
- ```
548
-
549
- ---
550
-
551
- ## Key Implementation Notes
552
-
553
- - N-shot class names are derived from the filename stem (e.g., `healthy.csv` → class `HEALTHY`)
554
- - The `data_columns` in `csv_configs` must match both the n-shot files and the data file
555
- - `window_size` and `step_size` control the sliding window over the data
556
- - Default `window_size` and `step_size`: **100**
557
- - Use `signal.SIGINT` handler for graceful shutdown (Python) or `AbortController` (Web)
558
- - Always close `sse_reader` in a `finally` block (Python) or destroy session on unmount (Web)
559
- - The SSE reader emits `inference.result` for predictions and `session.modify.result` for training confirmations