@asobacloud/nehanda 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/LICENSE.md +57 -0
- package/README.md +404 -2
- package/bin/agent.mjs +1260 -0
- package/bin/nehanda-ui.mjs +824 -0
- package/lib/anthropicHeaders.mjs +8 -0
- package/lib/app.mjs +313 -0
- package/lib/auth.mjs +255 -0
- package/lib/bashguard.mjs +159 -0
- package/lib/compact.mjs +306 -0
- package/lib/energy-middleware.mjs +318 -0
- package/lib/hookShapeOrder.mjs +30 -0
- package/lib/hookplane.mjs +228 -0
- package/lib/instructions.mjs +23 -0
- package/lib/jev_read.mjs +216 -0
- package/lib/jev_write.mjs +253 -0
- package/lib/laya.mjs +296 -0
- package/lib/laya_sidecar.mjs +198 -0
- package/lib/mcp-config.mjs +189 -0
- package/lib/mcp.mjs +169 -0
- package/lib/modelConfig.mjs +152 -0
- package/lib/modelDiscovery.mjs +205 -0
- package/lib/openaiCompat.mjs +147 -0
- package/lib/orchestrate.mjs +1163 -0
- package/lib/paths.mjs +20 -0
- package/lib/permissions.mjs +44 -0
- package/lib/scripts/__pycache__/laya_worker.cpython-314.pyc +0 -0
- package/lib/scripts/audit-code-integrity.py +437 -0
- package/lib/scripts/ensure-laya-env.sh +55 -0
- package/lib/scripts/js-safety-checker.cjs +308 -0
- package/lib/scripts/laya_worker.py +171 -0
- package/lib/scripts/odse-transform.py +97 -0
- package/lib/scripts/python-safety-checker.py +197 -0
- package/lib/scripts/shell-safety-checker.sh +306 -0
- package/lib/settings.mjs +147 -0
- package/lib/store.mjs +278 -0
- package/lib/team.mjs +69 -0
- package/lib/testgen.mjs +146 -0
- package/lib/tools/AuditCodeIntegrity.json +30 -0
- package/lib/tools/JsSafetyChecker.json +29 -0
- package/lib/tools/PythonSafetyChecker.json +29 -0
- package/lib/tools/ShellSafetyChecker.json +29 -0
- package/lib/tools.mjs +907 -0
- package/lib/transcript.mjs +223 -0
- package/lib/ui.mjs +286 -0
- package/lib/workflow.mjs +91 -0
- package/lib/worktree.mjs +56 -0
- package/package.json +50 -4
- package/schema.sql +181 -0
- package/scripts/postinstall.mjs +62 -0
- package/scripts/sdlc-acceptance.sh +477 -0
- package/scripts/verify-sdlc-hook-order.mjs +26 -0
- package/templates/test_e2e.sh +32 -0
- package/templates/test_file.sh +14 -0
- package/templates/test_hook.sh +27 -0
- package/templates/test_http.sh +17 -0
- package/templates/test_phase.sh +23 -0
- package/templates/test_process.sh +14 -0
- package/templates/test_tool.sh +24 -0
- package/templates/validate_test.sh +55 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const vm = require('vm');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const esprima = require('esprima');
|
|
5
|
+
|
|
6
|
+
function checkJSFile(filePath, fileContent) {
|
|
7
|
+
const issues = [];
|
|
8
|
+
|
|
9
|
+
// 1. JAVASCRIPT SYNTAX VALIDATION
|
|
10
|
+
try {
|
|
11
|
+
const scriptMatches = fileContent.match(/<script[^>]*>([\s\S]*?)<\/script>/g) || [];
|
|
12
|
+
// Only match actual JavaScript event handlers in HTML markup.
|
|
13
|
+
// Skip .js files entirely (they don't have real HTML inline handlers).
|
|
14
|
+
// For .html files, strip <script> blocks first so we don't match onclick
|
|
15
|
+
// inside JS string concatenation or template literals.
|
|
16
|
+
let inlineScripts = [];
|
|
17
|
+
if (filePath.endsWith('.html')) {
|
|
18
|
+
const htmlOnly = fileContent.replace(/<script[^>]*>[\s\S]*?<\/script>/g, '');
|
|
19
|
+
inlineScripts = htmlOnly.match(/\s(onclick|onload|onchange|onsubmit|onmouseover|onmouseout|onfocus|onblur|onkeydown|onkeyup)="[^"]*"/g) || [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
scriptMatches.forEach((scriptBlock, index) => {
|
|
23
|
+
const scriptContent = scriptBlock.replace(/<\/?script[^>]*>/g, '');
|
|
24
|
+
if (scriptContent.trim()) {
|
|
25
|
+
try {
|
|
26
|
+
vm.createScript(scriptContent, `script-block-${index}`);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const lineNumber = getLineNumber(fileContent, scriptBlock);
|
|
29
|
+
issues.push(`JavaScript syntax error in script block ${index + 1} at line ${lineNumber}: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
inlineScripts.forEach((inlineScript, index) => {
|
|
35
|
+
const jsCodeMatch = inlineScript.match(/="([^"]*)"/);
|
|
36
|
+
if (jsCodeMatch && jsCodeMatch[1]) {
|
|
37
|
+
const jsCode = jsCodeMatch[1];
|
|
38
|
+
if (jsCode.trim()) {
|
|
39
|
+
try {
|
|
40
|
+
// Wrap in function context to allow return statements
|
|
41
|
+
vm.createScript(`(function(){${jsCode}})`, `inline-script-${index}`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const lineNumber = getLineNumber(fileContent, inlineScript);
|
|
44
|
+
issues.push(`JavaScript syntax error in inline script at line ${lineNumber}: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
} catch (error) {
|
|
51
|
+
issues.push(`JavaScript parsing failed: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. DUPLICATE DEFINITIONS - Use AST-based detection for accuracy
|
|
55
|
+
const scriptBlocks = fileContent.match(/<script[^>]*>([\s\S]*?)<\/script>/g) || [];
|
|
56
|
+
const allFunctionNames = [];
|
|
57
|
+
|
|
58
|
+
scriptBlocks.forEach(scriptBlock => {
|
|
59
|
+
const scriptContent = scriptBlock.replace(/<\/?script[^>]*>/g, '');
|
|
60
|
+
try {
|
|
61
|
+
const ast = esprima.parseScript(scriptContent, { tolerant: true });
|
|
62
|
+
function findFunctions(node) {
|
|
63
|
+
if (!node) return;
|
|
64
|
+
if (node.type === 'FunctionDeclaration' && node.id) {
|
|
65
|
+
allFunctionNames.push(node.id.name);
|
|
66
|
+
}
|
|
67
|
+
for (const key in node) {
|
|
68
|
+
if (node.hasOwnProperty(key) && typeof node[key] === 'object' && node[key] !== null) {
|
|
69
|
+
if (Array.isArray(node[key])) {
|
|
70
|
+
node[key].forEach(findFunctions);
|
|
71
|
+
} else {
|
|
72
|
+
findFunctions(node[key]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
findFunctions(ast);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// If AST parsing fails, fall back to regex but be more careful
|
|
80
|
+
const functionDefinitions = scriptContent.match(/^\s*function\s+(\w+)/gm) || [];
|
|
81
|
+
functionDefinitions.forEach(f => {
|
|
82
|
+
const match = f.match(/function\s+(\w+)/);
|
|
83
|
+
if (match) allFunctionNames.push(match[1]);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const duplicateFunctions = allFunctionNames.filter((func, index) => allFunctionNames.indexOf(func) !== index);
|
|
89
|
+
if (duplicateFunctions.length > 0) {
|
|
90
|
+
issues.push(`Duplicate function definitions: ${duplicateFunctions.join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Only check for duplicate IDs in static HTML, ignore dynamic IDs with concatenation
|
|
94
|
+
// Match id="..." but exclude patterns with string concatenation (+ or ${})
|
|
95
|
+
const idMatches = fileContent.match(/id="([^"]+)"/g) || [];
|
|
96
|
+
const ids = idMatches
|
|
97
|
+
.map(match => match.match(/id="([^"]+)"/)[1])
|
|
98
|
+
.filter(id => !id.includes("' +") && !id.includes("+ '") && !id.includes('${') && !id.includes('}'));
|
|
99
|
+
const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index);
|
|
100
|
+
if (duplicateIds.length > 0) {
|
|
101
|
+
issues.push(`Duplicate IDs: ${duplicateIds.join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return issues;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getLineNumber(content, searchString) {
|
|
108
|
+
const index = content.indexOf(searchString);
|
|
109
|
+
return index !== -1 ? content.substring(0, index).split('\n').length : 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findFiles(dir, ext, callback) {
|
|
113
|
+
fs.readdir(dir, (err, files) => {
|
|
114
|
+
if (err) return callback(err);
|
|
115
|
+
|
|
116
|
+
files.forEach(file => {
|
|
117
|
+
const filePath = path.join(dir, file);
|
|
118
|
+
fs.stat(filePath, (err, stat) => {
|
|
119
|
+
if (err) return callback(err);
|
|
120
|
+
|
|
121
|
+
if (stat.isDirectory()) {
|
|
122
|
+
findFiles(filePath, ext, callback);
|
|
123
|
+
} else if (path.extname(filePath) === ext) {
|
|
124
|
+
callback(null, filePath);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function analyzeJS(jsContent) {
|
|
132
|
+
const declaredFunctions = new Set();
|
|
133
|
+
const declaredClasses = new Set();
|
|
134
|
+
const calledFunctions = new Set();
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const ast = esprima.parseScript(jsContent, { tolerant: true });
|
|
138
|
+
|
|
139
|
+
function traverse(node) {
|
|
140
|
+
if (!node) return;
|
|
141
|
+
|
|
142
|
+
// Track function declarations
|
|
143
|
+
if (node.type === 'FunctionDeclaration' && node.id) {
|
|
144
|
+
declaredFunctions.add(node.id.name);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Track arrow function and function expression assignments:
|
|
148
|
+
// const/let/var name = () => {} or const/let/var name = function() {}
|
|
149
|
+
if (node.type === 'VariableDeclaration') {
|
|
150
|
+
node.declarations.forEach(declarator => {
|
|
151
|
+
if (declarator.id && declarator.id.type === 'Identifier' && declarator.init &&
|
|
152
|
+
(declarator.init.type === 'ArrowFunctionExpression' ||
|
|
153
|
+
declarator.init.type === 'FunctionExpression')) {
|
|
154
|
+
declaredFunctions.add(declarator.id.name);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Track class declarations
|
|
160
|
+
if (node.type === 'ClassDeclaration' && node.id) {
|
|
161
|
+
declaredClasses.add(node.id.name);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Track function calls (e.g., myFunction())
|
|
165
|
+
if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
|
|
166
|
+
calledFunctions.add(node.callee.name);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Track constructor calls (e.g., new MyClass())
|
|
170
|
+
if (node.type === 'NewExpression' && node.callee.type === 'Identifier') {
|
|
171
|
+
calledFunctions.add(node.callee.name);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const key in node) {
|
|
175
|
+
if (node.hasOwnProperty(key)) {
|
|
176
|
+
const child = node[key];
|
|
177
|
+
if (typeof child === 'object' && child !== null) {
|
|
178
|
+
if (Array.isArray(child)) {
|
|
179
|
+
child.forEach(traverse);
|
|
180
|
+
} else {
|
|
181
|
+
traverse(child);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
traverse(ast);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// Ignore parsing errors, they are handled by the vm module
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Combine declared functions and classes for checking
|
|
194
|
+
const allDeclared = new Set([...declaredFunctions, ...declaredClasses]);
|
|
195
|
+
return { declaredFunctions: allDeclared, calledFunctions };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function main() {
|
|
199
|
+
const uiDir = process.argv[2] ? path.resolve(process.argv[2]) : __dirname;
|
|
200
|
+
let allIssues = [];
|
|
201
|
+
let filesToProcess = [];
|
|
202
|
+
const extensions = ['.html', '.js'];
|
|
203
|
+
|
|
204
|
+
// Files and directories to exclude from safety checking
|
|
205
|
+
const excludePatterns = [
|
|
206
|
+
'js-safety-checker.js', // Don't scan the safety checker itself
|
|
207
|
+
'node_modules', // Exclude dependencies
|
|
208
|
+
'dist', // Exclude build outputs
|
|
209
|
+
'build', // Exclude build outputs
|
|
210
|
+
'.git', // Exclude git directory
|
|
211
|
+
'tests' // Exclude test files (Jest globals are not defined in production)
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const fileFinder = (dir, ext) => {
|
|
215
|
+
if (!fs.existsSync(dir)) return;
|
|
216
|
+
const stat = fs.statSync(dir);
|
|
217
|
+
if (stat.isFile()) {
|
|
218
|
+
if (path.extname(dir) === ext) {
|
|
219
|
+
filesToProcess.push(dir);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const files = fs.readdirSync(dir);
|
|
225
|
+
files.forEach(file => {
|
|
226
|
+
const filePath = path.join(dir, file);
|
|
227
|
+
|
|
228
|
+
// Skip excluded files and directories
|
|
229
|
+
const shouldExclude = excludePatterns.some(pattern =>
|
|
230
|
+
file.includes(pattern) || filePath.includes(pattern)
|
|
231
|
+
);
|
|
232
|
+
if (shouldExclude) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const fileStat = fs.statSync(filePath);
|
|
237
|
+
if (fileStat.isDirectory()) {
|
|
238
|
+
fileFinder(filePath, ext);
|
|
239
|
+
} else if (path.extname(filePath) === ext) {
|
|
240
|
+
filesToProcess.push(filePath);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
extensions.forEach(ext => fileFinder(uiDir, ext));
|
|
246
|
+
|
|
247
|
+
const allDeclaredFunctions = new Set();
|
|
248
|
+
const allCalledFunctions = new Set();
|
|
249
|
+
|
|
250
|
+
filesToProcess.forEach(filePath => {
|
|
251
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
252
|
+
const issues = checkJSFile(filePath, fileContent);
|
|
253
|
+
if (issues.length > 0) {
|
|
254
|
+
allIssues.push({ filePath, issues });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const { declaredFunctions, calledFunctions } = analyzeJS(fileContent);
|
|
258
|
+
declaredFunctions.forEach(f => allDeclaredFunctions.add(f));
|
|
259
|
+
calledFunctions.forEach(f => allCalledFunctions.add(f));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Browser and Node.js global functions that should not be flagged as undefined
|
|
263
|
+
const knownGlobals = new Set([
|
|
264
|
+
'fetch', 'alert', 'confirm', 'prompt', 'console', 'setTimeout', 'setInterval',
|
|
265
|
+
'clearTimeout', 'clearInterval', 'document', 'window', 'location', 'localStorage',
|
|
266
|
+
'sessionStorage', 'JSON', 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'encodeURIComponent',
|
|
267
|
+
'decodeURIComponent', 'encodeURI', 'decodeURI', 'escape', 'unescape', 'Date', 'Math',
|
|
268
|
+
'Array', 'Object', 'String', 'Number', 'Boolean', 'RegExp', 'Error', 'require',
|
|
269
|
+
'requestAnimationFrame', 'cancelAnimationFrame', 'requestIdleCallback', 'cancelIdleCallback',
|
|
270
|
+
'Promise', 'Map', 'Set', 'WeakMap', 'WeakSet', 'Symbol', 'Proxy', 'Reflect',
|
|
271
|
+
'CustomEvent', 'Event', 'EventTarget', 'AbortController', 'AbortSignal',
|
|
272
|
+
'FormData', 'URLSearchParams', 'URL', 'Blob', 'File', 'FileReader',
|
|
273
|
+
'XMLHttpRequest', 'WebSocket', 'MutationObserver', 'IntersectionObserver',
|
|
274
|
+
'ResizeObserver', 'PerformanceObserver', 'atob', 'btoa',
|
|
275
|
+
'EventSource', 'TextDecoder', 'TextEncoder'
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
// Common callback parameter names that should not be flagged as undefined
|
|
279
|
+
const commonCallbackNames = new Set([
|
|
280
|
+
'callback', 'cb', 'done', 'next', 'fn', 'handler', 'onSuccess', 'onError',
|
|
281
|
+
'onConfirm', 'onCancel', 'resolve', 'reject', 'complete', 'unsubscribe',
|
|
282
|
+
'listener', 'subscriber', 'observer',
|
|
283
|
+
'onComplete', 'onProgress', 'onChunk', 'onDone'
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
const undefinedFunctions = [...allCalledFunctions].filter(f =>
|
|
287
|
+
!allDeclaredFunctions.has(f) && !knownGlobals.has(f) && !commonCallbackNames.has(f)
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (undefinedFunctions.length > 0) {
|
|
291
|
+
allIssues.push({ filePath: 'Global', issues: [`Undefined function calls: ${undefinedFunctions.join(', ')}`] });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (allIssues.length > 0) {
|
|
295
|
+
console.error('JavaScript Safety Check Failed:');
|
|
296
|
+
allIssues.forEach(fileIssues => {
|
|
297
|
+
console.error(`\n❌ ${fileIssues.filePath}`);
|
|
298
|
+
fileIssues.issues.forEach(issue => {
|
|
299
|
+
console.error(` - ${issue}`);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
process.exit(1);
|
|
303
|
+
} else {
|
|
304
|
+
console.log(`✅ All ${filesToProcess.length} files checked: No issues found`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
main();
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""JSON-lines stdio worker for Laya System-1 decisions.
|
|
3
|
+
|
|
4
|
+
Loads the real convaiinnovations/laya checkpoint on `load` / first predict.
|
|
5
|
+
Never silently invents answers — if Laya cannot load, every request returns ok:false.
|
|
6
|
+
|
|
7
|
+
Protocol (one JSON object per line):
|
|
8
|
+
{"id":1,"method":"ping"}
|
|
9
|
+
{"id":2,"method":"load"} # download + load checkpoint into memory
|
|
10
|
+
{"id":3,"method":"predict","params":{"state":"...","questions":{...}}}
|
|
11
|
+
{"id":4,"method":"shutdown"}
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
import traceback
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
_router = None
|
|
21
|
+
_agent = None
|
|
22
|
+
_loaded = False
|
|
23
|
+
_load_error = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ensure_loaded():
|
|
27
|
+
global _router, _agent, _loaded, _load_error
|
|
28
|
+
if _loaded:
|
|
29
|
+
return
|
|
30
|
+
if _load_error is not None:
|
|
31
|
+
raise RuntimeError(_load_error)
|
|
32
|
+
t0 = time.time()
|
|
33
|
+
try:
|
|
34
|
+
from laya import Router
|
|
35
|
+
_router = Router()
|
|
36
|
+
# Force English checkpoint into memory so warm predict hits ≤50ms path.
|
|
37
|
+
# Router downloads on first predict; prime with a tiny call.
|
|
38
|
+
_router.predict(
|
|
39
|
+
"nehanda system-1 warm start",
|
|
40
|
+
{
|
|
41
|
+
"ready": {
|
|
42
|
+
"type": "noul",
|
|
43
|
+
"instructions": "Is the control plane ready?",
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
model="english",
|
|
47
|
+
)
|
|
48
|
+
_loaded = True
|
|
49
|
+
ms = int((time.time() - t0) * 1000)
|
|
50
|
+
print(f"[laya-worker] loaded english checkpoint in {ms}ms", file=sys.stderr)
|
|
51
|
+
except TypeError:
|
|
52
|
+
# Older Router.predict may not accept model= kwarg
|
|
53
|
+
try:
|
|
54
|
+
from laya import Router
|
|
55
|
+
_router = Router()
|
|
56
|
+
_router.predict(
|
|
57
|
+
"nehanda system-1 warm start",
|
|
58
|
+
{"ready": {"type": "noul", "instructions": "Is the control plane ready?"}},
|
|
59
|
+
)
|
|
60
|
+
_loaded = True
|
|
61
|
+
ms = int((time.time() - t0) * 1000)
|
|
62
|
+
print(f"[laya-worker] loaded via Router in {ms}ms", file=sys.stderr)
|
|
63
|
+
except Exception:
|
|
64
|
+
try:
|
|
65
|
+
import laya
|
|
66
|
+
_agent = laya.load("convaiinnovations/laya")
|
|
67
|
+
_agent.predict(
|
|
68
|
+
"nehanda system-1 warm start",
|
|
69
|
+
{"ready": {"type": "noul", "instructions": "Is the control plane ready?"}},
|
|
70
|
+
)
|
|
71
|
+
_loaded = True
|
|
72
|
+
ms = int((time.time() - t0) * 1000)
|
|
73
|
+
print(f"[laya-worker] loaded laya.load() in {ms}ms", file=sys.stderr)
|
|
74
|
+
except Exception as e2:
|
|
75
|
+
_load_error = f"laya load failed: {e2}"
|
|
76
|
+
raise RuntimeError(_load_error) from e2
|
|
77
|
+
except Exception as e:
|
|
78
|
+
_load_error = f"laya load failed: {e}"
|
|
79
|
+
raise RuntimeError(_load_error) from e
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def predict(state, questions):
|
|
83
|
+
_ensure_loaded()
|
|
84
|
+
if _router is not None:
|
|
85
|
+
try:
|
|
86
|
+
return _router.predict(state, questions, model="english")
|
|
87
|
+
except TypeError:
|
|
88
|
+
return _router.predict(state, questions)
|
|
89
|
+
return _agent.predict(state, questions)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _to_dict(result):
|
|
93
|
+
if result is None:
|
|
94
|
+
return {}
|
|
95
|
+
if isinstance(result, dict):
|
|
96
|
+
# answers may contain nested objects
|
|
97
|
+
out = {}
|
|
98
|
+
for k, v in result.items():
|
|
99
|
+
if hasattr(v, "keys") and not isinstance(v, dict):
|
|
100
|
+
try:
|
|
101
|
+
out[k] = dict(v)
|
|
102
|
+
continue
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
if isinstance(v, dict):
|
|
106
|
+
nested = {}
|
|
107
|
+
for nk, nv in v.items():
|
|
108
|
+
if hasattr(nv, "keys") and not isinstance(nv, dict):
|
|
109
|
+
try:
|
|
110
|
+
nested[nk] = dict(nv)
|
|
111
|
+
continue
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
nested[nk] = nv
|
|
115
|
+
out[k] = nested
|
|
116
|
+
else:
|
|
117
|
+
out[k] = v
|
|
118
|
+
return out
|
|
119
|
+
if hasattr(result, "keys"):
|
|
120
|
+
return dict(result)
|
|
121
|
+
return {"raw": str(result)}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def handle(msg):
|
|
125
|
+
method = msg.get("method")
|
|
126
|
+
mid = msg.get("id")
|
|
127
|
+
params = msg.get("params") or {}
|
|
128
|
+
if method == "ping":
|
|
129
|
+
return {"id": mid, "ok": True, "result": {"pong": True, "loaded": _loaded}}
|
|
130
|
+
if method == "load":
|
|
131
|
+
_ensure_loaded()
|
|
132
|
+
return {"id": mid, "ok": True, "result": {"loaded": True}}
|
|
133
|
+
if method == "shutdown":
|
|
134
|
+
return {"id": mid, "ok": True, "result": {"bye": True}, "_exit": True}
|
|
135
|
+
if method == "predict":
|
|
136
|
+
result = predict(params.get("state", ""), params.get("questions") or {})
|
|
137
|
+
return {"id": mid, "ok": True, "result": _to_dict(result)}
|
|
138
|
+
return {"id": mid, "ok": False, "error": f"unknown method: {method}"}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main():
|
|
142
|
+
for line in sys.stdin:
|
|
143
|
+
line = line.strip()
|
|
144
|
+
if not line:
|
|
145
|
+
continue
|
|
146
|
+
try:
|
|
147
|
+
msg = json.loads(line)
|
|
148
|
+
except json.JSONDecodeError as e:
|
|
149
|
+
sys.stdout.write(json.dumps({"ok": False, "error": f"bad json: {e}"}) + "\n")
|
|
150
|
+
sys.stdout.flush()
|
|
151
|
+
continue
|
|
152
|
+
try:
|
|
153
|
+
resp = handle(msg)
|
|
154
|
+
do_exit = resp.pop("_exit", False)
|
|
155
|
+
sys.stdout.write(json.dumps(resp, default=str) + "\n")
|
|
156
|
+
sys.stdout.flush()
|
|
157
|
+
if do_exit:
|
|
158
|
+
return 0
|
|
159
|
+
except Exception as e:
|
|
160
|
+
traceback.print_exc(file=sys.stderr)
|
|
161
|
+
sys.stdout.write(json.dumps({
|
|
162
|
+
"id": msg.get("id"),
|
|
163
|
+
"ok": False,
|
|
164
|
+
"error": str(e),
|
|
165
|
+
}) + "\n")
|
|
166
|
+
sys.stdout.flush()
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
odse-transform.py — ODSE energy normalization bridge for nehanda-cli.
|
|
4
|
+
|
|
5
|
+
Reads a raw OEM energy payload from stdin, transforms it to ODS-E
|
|
6
|
+
normalized JSON using the pip-installed `odse` package, and writes
|
|
7
|
+
the result to stdout as a JSON array.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
echo '<payload>' | python3 odse-transform.py --source huawei
|
|
11
|
+
echo '<payload>' | python3 odse-transform.py --source solaredge --asset-id SITE-001
|
|
12
|
+
|
|
13
|
+
Exit codes:
|
|
14
|
+
0 — success, normalized JSON written to stdout
|
|
15
|
+
1 — unknown source / transform error (caller should pass payload through)
|
|
16
|
+
2 — usage / import error
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> int:
|
|
25
|
+
parser = argparse.ArgumentParser(description="Transform OEM energy data to ODS-E")
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--source",
|
|
28
|
+
required=True,
|
|
29
|
+
help="OEM source key (e.g. huawei, solaredge, enphase, sungrow)",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--asset-id",
|
|
33
|
+
default=None,
|
|
34
|
+
dest="asset_id",
|
|
35
|
+
help="Optional asset identifier to embed in output records",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--timezone",
|
|
39
|
+
default=None,
|
|
40
|
+
help="Optional timezone for timestamp conversion (e.g. Africa/Johannesburg)",
|
|
41
|
+
)
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
|
|
44
|
+
# Read full payload from stdin
|
|
45
|
+
try:
|
|
46
|
+
payload = sys.stdin.read()
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
print(f"[odse-transform] stdin read error: {exc}", file=sys.stderr)
|
|
49
|
+
return 2
|
|
50
|
+
|
|
51
|
+
if not payload.strip():
|
|
52
|
+
print("[odse-transform] empty payload", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
# Import odse — fail fast with a clear message if not installed
|
|
56
|
+
try:
|
|
57
|
+
from odse.transformer import transform # noqa: PLC0415
|
|
58
|
+
except ImportError:
|
|
59
|
+
print(
|
|
60
|
+
"[odse-transform] odse package not found. Run: pip install odse",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
return 2
|
|
64
|
+
|
|
65
|
+
# Attempt transformation
|
|
66
|
+
try:
|
|
67
|
+
kwargs: dict = {}
|
|
68
|
+
if args.timezone:
|
|
69
|
+
kwargs["timezone"] = args.timezone
|
|
70
|
+
|
|
71
|
+
records = transform(
|
|
72
|
+
payload,
|
|
73
|
+
source=args.source,
|
|
74
|
+
asset_id=args.asset_id,
|
|
75
|
+
**kwargs,
|
|
76
|
+
)
|
|
77
|
+
except ValueError as exc:
|
|
78
|
+
# Unknown source key — caller interprets exit 1 as "pass through"
|
|
79
|
+
print(f"[odse-transform] unknown source '{args.source}': {exc}", file=sys.stderr)
|
|
80
|
+
return 1
|
|
81
|
+
except Exception as exc: # noqa: BLE001
|
|
82
|
+
# Transform failed (malformed payload, etc.) — pass through
|
|
83
|
+
print(f"[odse-transform] transform error: {exc}", file=sys.stderr)
|
|
84
|
+
return 1
|
|
85
|
+
|
|
86
|
+
# Write normalized records as JSON array to stdout
|
|
87
|
+
try:
|
|
88
|
+
print(json.dumps(records, default=str))
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
print(f"[odse-transform] serialization error: {exc}", file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
sys.exit(main())
|