@turingpulse/sdk 1.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/.github/dependabot.yml +38 -0
- package/.github/workflows/ci.yml +246 -0
- package/.github/workflows/framework-compat.yml +169 -0
- package/.github/workflows/security.yml +336 -0
- package/CHANGELOG.md +29 -0
- package/LICENSE +13 -0
- package/MIGRATION.md +30 -0
- package/README.md +221 -0
- package/dist/attachments.d.ts +28 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +59 -0
- package/dist/attachments.js.map +1 -0
- package/dist/config.d.ts +72 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +78 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +126 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +163 -0
- package/dist/context.js.map +1 -0
- package/dist/decorators.d.ts +6 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +52 -0
- package/dist/decorators.js.map +1 -0
- package/dist/deploy.d.ts +89 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +203 -0
- package/dist/deploy.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +34 -0
- package/dist/errors.js.map +1 -0
- package/dist/eventBuilder.d.ts +21 -0
- package/dist/eventBuilder.d.ts.map +1 -0
- package/dist/eventBuilder.js +127 -0
- package/dist/eventBuilder.js.map +1 -0
- package/dist/fingerprint.d.ts +158 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +339 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/governance.d.ts +47 -0
- package/dist/governance.d.ts.map +1 -0
- package/dist/governance.js +104 -0
- package/dist/governance.js.map +1 -0
- package/dist/http.d.ts +62 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +181 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.d.ts +40 -0
- package/dist/instrumentation.d.ts.map +1 -0
- package/dist/instrumentation.js +31 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/integrations/mastra.d.ts +64 -0
- package/dist/integrations/mastra.d.ts.map +1 -0
- package/dist/integrations/mastra.js +256 -0
- package/dist/integrations/mastra.js.map +1 -0
- package/dist/kpi.d.ts +21 -0
- package/dist/kpi.d.ts.map +1 -0
- package/dist/kpi.js +83 -0
- package/dist/kpi.js.map +1 -0
- package/dist/llmDetector.d.ts +22 -0
- package/dist/llmDetector.d.ts.map +1 -0
- package/dist/llmDetector.js +269 -0
- package/dist/llmDetector.js.map +1 -0
- package/dist/plugin.d.ts +33 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +312 -0
- package/dist/plugin.js.map +1 -0
- package/dist/registry.d.ts +13 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +18 -0
- package/dist/registry.js.map +1 -0
- package/dist/tracing.d.ts +10 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +30 -0
- package/dist/tracing.js.map +1 -0
- package/dist/triggerState.d.ts +5 -0
- package/dist/triggerState.d.ts.map +1 -0
- package/dist/triggerState.js +19 -0
- package/dist/triggerState.js.map +1 -0
- package/dist/utils.d.ts +27 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +72 -0
- package/dist/utils.js.map +1 -0
- package/package.json +37 -0
- package/packages/anthropic/package.json +16 -0
- package/packages/anthropic/src/index.ts +5 -0
- package/packages/anthropic/src/wrapper.ts +102 -0
- package/packages/anthropic/tsconfig.build.json +20 -0
- package/packages/langchain/package.json +16 -0
- package/packages/langchain/src/index.ts +7 -0
- package/packages/langchain/src/wrapper.ts +51 -0
- package/packages/mastra/package.json +17 -0
- package/packages/mastra/src/index.ts +8 -0
- package/packages/mastra/src/wrapper.ts +301 -0
- package/packages/openai/package.json +16 -0
- package/packages/openai/src/index.ts +8 -0
- package/packages/openai/src/wrapper.ts +103 -0
- package/packages/openai/tsconfig.build.json +20 -0
- package/packages/openclaw/openclaw.plugin.json +100 -0
- package/packages/openclaw/package.json +41 -0
- package/packages/openclaw/src/buffer.ts +99 -0
- package/packages/openclaw/src/config.ts +139 -0
- package/packages/openclaw/src/hooks/governance.ts +267 -0
- package/packages/openclaw/src/hooks/lifecycle.ts +75 -0
- package/packages/openclaw/src/hooks/telemetry.ts +207 -0
- package/packages/openclaw/src/index.ts +91 -0
- package/packages/openclaw/src/mapper.ts +233 -0
- package/packages/openclaw/src/session-tracker.ts +181 -0
- package/packages/openclaw/src/types.ts +220 -0
- package/packages/openclaw/tests/buffer.test.ts +148 -0
- package/packages/openclaw/tests/config.test.ts +122 -0
- package/packages/openclaw/tests/governance.test.ts +232 -0
- package/packages/openclaw/tests/mapper.test.ts +242 -0
- package/packages/openclaw/tests/session-tracker.test.ts +124 -0
- package/packages/openclaw/tsconfig.json +18 -0
- package/packages/openclaw/vitest.config.ts +8 -0
- package/packages/vercel-ai/package.json +16 -0
- package/packages/vercel-ai/src/index.ts +5 -0
- package/packages/vercel-ai/src/wrapper.ts +49 -0
- package/scripts/bump-version.sh +58 -0
- package/scripts/update-readme-compat.mjs +151 -0
- package/src/__tests__/fingerprint.test.ts +328 -0
- package/src/attachments.ts +88 -0
- package/src/config.ts +164 -0
- package/src/context.ts +258 -0
- package/src/decorators.ts +61 -0
- package/src/deploy.ts +260 -0
- package/src/errors.ts +44 -0
- package/src/eventBuilder.ts +153 -0
- package/src/fingerprint.ts +421 -0
- package/src/governance.ts +156 -0
- package/src/http.ts +241 -0
- package/src/index.ts +57 -0
- package/src/instrumentation.ts +68 -0
- package/src/integrations/mastra.ts +335 -0
- package/src/kpi.ts +112 -0
- package/src/llmDetector.ts +330 -0
- package/src/plugin.ts +384 -0
- package/src/registry.ts +27 -0
- package/src/tracing.ts +39 -0
- package/src/triggerState.ts +27 -0
- package/src/utils.ts +78 -0
- package/tests/compat/anthropic.test.ts +61 -0
- package/tests/compat/cohere.test.ts +57 -0
- package/tests/compat/google-genai.test.ts +61 -0
- package/tests/compat/langchain-openai.test.ts +41 -0
- package/tests/compat/langchain.test.ts +64 -0
- package/tests/compat/mistral.test.ts +58 -0
- package/tests/compat/openai.test.ts +71 -0
- package/tests/compat/vercel-ai.test.ts +56 -0
- package/tests/plugins/anthropic-wrapper.test.ts +120 -0
- package/tests/plugins/langchain-wrapper.test.ts +128 -0
- package/tests/plugins/openai-wrapper.test.ts +165 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [ $# -ne 1 ]; then
|
|
5
|
+
echo "Usage: $0 <new-version>"
|
|
6
|
+
echo "Example: $0 1.2.0"
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
NEW_VERSION="$1"
|
|
11
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
if ! [[ "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
14
|
+
echo "Error: version must be in semver format (e.g. 1.2.0)"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
echo "Bumping all packages to version ${NEW_VERSION}..."
|
|
19
|
+
|
|
20
|
+
update_package_json() {
|
|
21
|
+
local file="$1"
|
|
22
|
+
if [ ! -f "$file" ]; then
|
|
23
|
+
echo " SKIP (not found): $file"
|
|
24
|
+
return
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
node -e "
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const pkg = JSON.parse(fs.readFileSync('$file', 'utf8'));
|
|
30
|
+
pkg.version = '${NEW_VERSION}';
|
|
31
|
+
if (pkg.dependencies) {
|
|
32
|
+
for (const [key, val] of Object.entries(pkg.dependencies)) {
|
|
33
|
+
if (key.startsWith('@turingpulse/')) {
|
|
34
|
+
pkg.dependencies[key] = '>=${NEW_VERSION}';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
fs.writeFileSync('$file', JSON.stringify(pkg, null, 2) + '\n');
|
|
39
|
+
"
|
|
40
|
+
echo " Updated: $file"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
update_package_json "${REPO_ROOT}/package.json"
|
|
44
|
+
|
|
45
|
+
for pkg_dir in "${REPO_ROOT}"/packages/*/; do
|
|
46
|
+
if [ -f "${pkg_dir}package.json" ]; then
|
|
47
|
+
update_package_json "${pkg_dir}package.json"
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
echo ""
|
|
52
|
+
echo "Done. All packages bumped to ${NEW_VERSION}."
|
|
53
|
+
echo "Next steps:"
|
|
54
|
+
echo " 1. Review changes: git diff"
|
|
55
|
+
echo " 2. Commit: git add -A && git commit -m 'release: v${NEW_VERSION}'"
|
|
56
|
+
echo " 3. Tag: git tag v${NEW_VERSION}"
|
|
57
|
+
echo " 4. Push: git push origin main v${NEW_VERSION}"
|
|
58
|
+
echo " 5. Create a GitHub Release from the tag to trigger publish"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate Shields.io badges and a compatibility matrix table in README.md.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/update-readme-compat.mjs <results-dir> <readme-path>
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const BADGE_START = '<!-- COMPAT-BADGES:START -->';
|
|
13
|
+
const BADGE_END = '<!-- COMPAT-BADGES:END -->';
|
|
14
|
+
const MATRIX_START = '<!-- COMPAT-MATRIX:START -->';
|
|
15
|
+
const MATRIX_END = '<!-- COMPAT-MATRIX:END -->';
|
|
16
|
+
|
|
17
|
+
const FRAMEWORK_DISPLAY = {
|
|
18
|
+
openai: 'OpenAI',
|
|
19
|
+
anthropic: 'Anthropic',
|
|
20
|
+
'google-genai': 'Google GenAI',
|
|
21
|
+
langchain: 'LangChain',
|
|
22
|
+
'langchain-openai': 'LangChain-OpenAI',
|
|
23
|
+
'vercel-ai': 'Vercel AI',
|
|
24
|
+
cohere: 'Cohere',
|
|
25
|
+
mistral: 'Mistral',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const FRAMEWORK_ORDER = Object.keys(FRAMEWORK_DISPLAY);
|
|
29
|
+
|
|
30
|
+
function loadResults(dir) {
|
|
31
|
+
const results = [];
|
|
32
|
+
const walk = (d) => {
|
|
33
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
34
|
+
const full = path.join(d, entry.name);
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
walk(full);
|
|
37
|
+
} else if (entry.name.endsWith('.json')) {
|
|
38
|
+
try {
|
|
39
|
+
results.push(JSON.parse(fs.readFileSync(full, 'utf-8')));
|
|
40
|
+
} catch {
|
|
41
|
+
// skip bad files
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
walk(dir);
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeBadge(label, passed) {
|
|
51
|
+
const status = passed ? 'passing' : 'failing';
|
|
52
|
+
const color = passed ? 'brightgreen' : 'red';
|
|
53
|
+
const encoded = encodeURIComponent(label);
|
|
54
|
+
return ``;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function generateBadges(results) {
|
|
58
|
+
const fwStatus = {};
|
|
59
|
+
for (const r of results) {
|
|
60
|
+
const fw = r.framework;
|
|
61
|
+
if (!(fw in fwStatus)) fwStatus[fw] = true;
|
|
62
|
+
if (!r.passed) fwStatus[fw] = false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const badges = [];
|
|
66
|
+
for (const fw of FRAMEWORK_ORDER) {
|
|
67
|
+
if (fw in fwStatus) {
|
|
68
|
+
badges.push(makeBadge(FRAMEWORK_DISPLAY[fw] || fw, fwStatus[fw]));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return badges.join(' ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function generateMatrix(results) {
|
|
75
|
+
const grouped = {};
|
|
76
|
+
const nodeVersions = new Set();
|
|
77
|
+
|
|
78
|
+
for (const r of results) {
|
|
79
|
+
const fw = r.framework;
|
|
80
|
+
const ver = r.installed_version || r.requested_version || '?';
|
|
81
|
+
const nv = r.node_version || '?';
|
|
82
|
+
nodeVersions.add(nv);
|
|
83
|
+
|
|
84
|
+
if (!grouped[fw]) grouped[fw] = {};
|
|
85
|
+
if (!grouped[fw][ver]) grouped[fw][ver] = {};
|
|
86
|
+
grouped[fw][ver][nv] = r.passed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sortedNV = [...nodeVersions].sort();
|
|
90
|
+
const nvHeaders = sortedNV.map((nv) => `Node ${nv}`).join(' | ');
|
|
91
|
+
|
|
92
|
+
const lines = [];
|
|
93
|
+
lines.push(`| Framework | Version | ${nvHeaders} |`);
|
|
94
|
+
lines.push(`|---|---| ${sortedNV.map(() => '---').join(' | ')} |`);
|
|
95
|
+
|
|
96
|
+
for (const fw of FRAMEWORK_ORDER) {
|
|
97
|
+
if (!grouped[fw]) continue;
|
|
98
|
+
const display = FRAMEWORK_DISPLAY[fw] || fw;
|
|
99
|
+
for (const ver of Object.keys(grouped[fw]).sort()) {
|
|
100
|
+
const cells = sortedNV.map((nv) => {
|
|
101
|
+
if (nv in grouped[fw][ver]) {
|
|
102
|
+
return grouped[fw][ver][nv] ? 'pass' : 'fail';
|
|
103
|
+
}
|
|
104
|
+
return '--';
|
|
105
|
+
});
|
|
106
|
+
lines.push(`| ${display} | ${ver} | ${cells.join(' | ')} |`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function replaceSection(content, startMarker, endMarker, replacement) {
|
|
113
|
+
const startIdx = content.indexOf(startMarker);
|
|
114
|
+
const endIdx = content.indexOf(endMarker);
|
|
115
|
+
if (startIdx === -1 || endIdx === -1) return content;
|
|
116
|
+
return (
|
|
117
|
+
content.slice(0, startIdx + startMarker.length) +
|
|
118
|
+
'\n' +
|
|
119
|
+
replacement +
|
|
120
|
+
'\n' +
|
|
121
|
+
content.slice(endIdx)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function main() {
|
|
126
|
+
const args = process.argv.slice(2);
|
|
127
|
+
if (args.length !== 2) {
|
|
128
|
+
console.error(`Usage: node ${path.basename(process.argv[1])} <results-dir> <readme-path>`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const [resultsDir, readmePath] = args;
|
|
133
|
+
const results = loadResults(resultsDir);
|
|
134
|
+
if (results.length === 0) {
|
|
135
|
+
console.error('No results found, skipping README update.');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let content = fs.readFileSync(readmePath, 'utf-8');
|
|
140
|
+
|
|
141
|
+
const badges = generateBadges(results);
|
|
142
|
+
content = replaceSection(content, BADGE_START, BADGE_END, badges);
|
|
143
|
+
|
|
144
|
+
const matrix = generateMatrix(results);
|
|
145
|
+
content = replaceSection(content, MATRIX_START, MATRIX_END, matrix);
|
|
146
|
+
|
|
147
|
+
fs.writeFileSync(readmePath, content);
|
|
148
|
+
console.log(`Updated ${readmePath} with ${results.length} result(s).`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
main();
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for fingerprinting and change detection.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
FingerprintBuilder,
|
|
7
|
+
FingerprintConfig,
|
|
8
|
+
DEFAULT_FINGERPRINT_CONFIG,
|
|
9
|
+
extractPromptFromMessages,
|
|
10
|
+
extractConfigSubset,
|
|
11
|
+
} from '../fingerprint';
|
|
12
|
+
import {
|
|
13
|
+
LLMDetector,
|
|
14
|
+
ToolDetector,
|
|
15
|
+
detectNodeType,
|
|
16
|
+
} from '../llmDetector';
|
|
17
|
+
|
|
18
|
+
describe('FingerprintConfig', () => {
|
|
19
|
+
test('default config has expected values', () => {
|
|
20
|
+
const config = DEFAULT_FINGERPRINT_CONFIG;
|
|
21
|
+
expect(config.enabled).toBe(true);
|
|
22
|
+
expect(config.capturePrompts).toBe(true);
|
|
23
|
+
expect(config.captureConfigs).toBe(true);
|
|
24
|
+
expect(config.captureStructure).toBe(true);
|
|
25
|
+
expect(config.sendAsync).toBe(true);
|
|
26
|
+
expect(config.sendOnFailure).toBe(true);
|
|
27
|
+
expect(config.sensitiveConfigKeys).toContain('apiKey');
|
|
28
|
+
expect(config.promptHashAlgorithm).toBe('sha256');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('FingerprintBuilder', () => {
|
|
33
|
+
test('records nodes correctly', () => {
|
|
34
|
+
const builder = new FingerprintBuilder();
|
|
35
|
+
|
|
36
|
+
builder.recordNode('llm_call', 'llm', { model: 'gpt-4' }, 'You are helpful');
|
|
37
|
+
builder.recordNode('search_tool', 'tool', { api: 'google' });
|
|
38
|
+
|
|
39
|
+
const fingerprint = builder.getFingerprint();
|
|
40
|
+
|
|
41
|
+
expect(fingerprint.node_sequence).toContain('llm_call');
|
|
42
|
+
expect(fingerprint.node_sequence).toContain('search_tool');
|
|
43
|
+
expect(fingerprint.node_types['llm']).toBe(1);
|
|
44
|
+
expect(fingerprint.node_types['tool']).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('counts node types correctly', () => {
|
|
48
|
+
const builder = new FingerprintBuilder();
|
|
49
|
+
|
|
50
|
+
builder.recordNode('llm1', 'llm');
|
|
51
|
+
builder.recordNode('llm2', 'llm');
|
|
52
|
+
builder.recordNode('tool1', 'tool');
|
|
53
|
+
|
|
54
|
+
const fingerprint = builder.getFingerprint();
|
|
55
|
+
|
|
56
|
+
expect(fingerprint.node_types['llm']).toBe(2);
|
|
57
|
+
expect(fingerprint.node_types['tool']).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('tracks parent-child edges', () => {
|
|
61
|
+
const builder = new FingerprintBuilder();
|
|
62
|
+
|
|
63
|
+
builder.recordNode('parent', 'function');
|
|
64
|
+
builder.pushParent('parent');
|
|
65
|
+
builder.recordNode('child1', 'llm');
|
|
66
|
+
builder.recordNode('child2', 'tool');
|
|
67
|
+
|
|
68
|
+
const fingerprint = builder.getFingerprint();
|
|
69
|
+
|
|
70
|
+
expect(fingerprint.edges).toContain('parent->child1');
|
|
71
|
+
expect(fingerprint.edges).toContain('parent->child2');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('tracks nested edges correctly', () => {
|
|
75
|
+
const builder = new FingerprintBuilder();
|
|
76
|
+
|
|
77
|
+
builder.recordNode('root', 'function');
|
|
78
|
+
builder.pushParent('root');
|
|
79
|
+
builder.recordNode('middle', 'function');
|
|
80
|
+
builder.pushParent('middle');
|
|
81
|
+
builder.recordNode('leaf', 'llm');
|
|
82
|
+
|
|
83
|
+
const fingerprint = builder.getFingerprint();
|
|
84
|
+
|
|
85
|
+
expect(fingerprint.edges).toContain('root->middle');
|
|
86
|
+
expect(fingerprint.edges).toContain('middle->leaf');
|
|
87
|
+
expect(fingerprint.edges).not.toContain('root->leaf');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('skips duplicate nodes', () => {
|
|
91
|
+
const builder = new FingerprintBuilder();
|
|
92
|
+
|
|
93
|
+
builder.recordNode('same_name', 'llm');
|
|
94
|
+
builder.recordNode('same_name', 'tool'); // Should be skipped
|
|
95
|
+
|
|
96
|
+
const fingerprint = builder.getFingerprint();
|
|
97
|
+
|
|
98
|
+
expect(fingerprint.node_sequence.length).toBe(1);
|
|
99
|
+
expect(fingerprint.node_types['llm']).toBe(1);
|
|
100
|
+
expect(fingerprint.node_types['tool']).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('produces deterministic structure hash', () => {
|
|
104
|
+
const builder1 = new FingerprintBuilder();
|
|
105
|
+
builder1.recordNode('a', 'llm');
|
|
106
|
+
builder1.recordNode('b', 'tool');
|
|
107
|
+
const fp1 = builder1.getFingerprint();
|
|
108
|
+
|
|
109
|
+
const builder2 = new FingerprintBuilder();
|
|
110
|
+
builder2.recordNode('a', 'llm');
|
|
111
|
+
builder2.recordNode('b', 'tool');
|
|
112
|
+
const fp2 = builder2.getFingerprint();
|
|
113
|
+
|
|
114
|
+
expect(fp1.structure_hash).toBe(fp2.structure_hash);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('different structures produce different hashes', () => {
|
|
118
|
+
const builder1 = new FingerprintBuilder();
|
|
119
|
+
builder1.recordNode('a', 'llm');
|
|
120
|
+
builder1.recordNode('b', 'tool');
|
|
121
|
+
const fp1 = builder1.getFingerprint();
|
|
122
|
+
|
|
123
|
+
const builder2 = new FingerprintBuilder();
|
|
124
|
+
builder2.recordNode('a', 'tool'); // Different type
|
|
125
|
+
builder2.recordNode('b', 'llm');
|
|
126
|
+
const fp2 = builder2.getFingerprint();
|
|
127
|
+
|
|
128
|
+
expect(fp1.structure_hash).not.toBe(fp2.structure_hash);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('hashes config for nodes', () => {
|
|
132
|
+
const builder = new FingerprintBuilder();
|
|
133
|
+
builder.recordNode('llm', 'llm', { model: 'gpt-4', temperature: 0.7 });
|
|
134
|
+
|
|
135
|
+
const fingerprint = builder.getFingerprint();
|
|
136
|
+
|
|
137
|
+
expect(fingerprint.node_config_hashes['llm']).toBeDefined();
|
|
138
|
+
expect(fingerprint.node_config_hashes['llm'].length).toBeGreaterThan(0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('hashes prompts for LLM nodes', () => {
|
|
142
|
+
const builder = new FingerprintBuilder();
|
|
143
|
+
builder.recordNode('llm', 'llm', undefined, 'You are a helpful assistant');
|
|
144
|
+
|
|
145
|
+
const fingerprint = builder.getFingerprint();
|
|
146
|
+
|
|
147
|
+
expect(fingerprint.node_prompt_hashes['llm']).toBeDefined();
|
|
148
|
+
expect(fingerprint.node_prompt_hashes['llm'].length).toBeGreaterThan(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('redacts sensitive config keys', () => {
|
|
152
|
+
const builder = new FingerprintBuilder({
|
|
153
|
+
sensitiveConfigKeys: ['secret'],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Record with sensitive key
|
|
157
|
+
builder.recordNode('llm', 'llm', { model: 'gpt-4', secret: 'my-secret' });
|
|
158
|
+
|
|
159
|
+
const fingerprint = builder.getFingerprint();
|
|
160
|
+
|
|
161
|
+
// The hash should exist (meaning config was processed)
|
|
162
|
+
expect(fingerprint.node_config_hashes['llm']).toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('LLMDetector', () => {
|
|
167
|
+
test('detects OpenAI chat completions', () => {
|
|
168
|
+
const result = LLMDetector.detectLLMCall(
|
|
169
|
+
'create',
|
|
170
|
+
'chat.completions.create',
|
|
171
|
+
[],
|
|
172
|
+
{
|
|
173
|
+
model: 'gpt-4',
|
|
174
|
+
temperature: 0.7,
|
|
175
|
+
messages: [
|
|
176
|
+
{ role: 'system', content: 'You are helpful' },
|
|
177
|
+
{ role: 'user', content: 'Hello' },
|
|
178
|
+
],
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(result).toBeDefined();
|
|
183
|
+
expect(result?.provider).toBe('openai');
|
|
184
|
+
expect(result?.model).toBe('gpt-4');
|
|
185
|
+
expect(result?.prompt).toBe('You are helpful');
|
|
186
|
+
expect(result?.config.temperature).toBe(0.7);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('detects Anthropic messages', () => {
|
|
190
|
+
const result = LLMDetector.detectLLMCall(
|
|
191
|
+
'create',
|
|
192
|
+
'messages.create',
|
|
193
|
+
[],
|
|
194
|
+
{
|
|
195
|
+
model: 'claude-3',
|
|
196
|
+
system: 'You are Claude',
|
|
197
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(result).toBeDefined();
|
|
202
|
+
expect(result?.provider).toBe('anthropic');
|
|
203
|
+
expect(result?.prompt).toBe('You are Claude');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('returns undefined for non-LLM calls', () => {
|
|
207
|
+
const result = LLMDetector.detectLLMCall(
|
|
208
|
+
'process_data',
|
|
209
|
+
'DataProcessor.process_data',
|
|
210
|
+
[],
|
|
211
|
+
{}
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(result).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('ToolDetector', () => {
|
|
219
|
+
test('detects search tools', () => {
|
|
220
|
+
const result = ToolDetector.detectToolCall(
|
|
221
|
+
'search_web',
|
|
222
|
+
'Tools.search_web',
|
|
223
|
+
[],
|
|
224
|
+
{}
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(result).toBe('tool');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('detects retrievers', () => {
|
|
231
|
+
const result = ToolDetector.detectToolCall(
|
|
232
|
+
'retrieve',
|
|
233
|
+
'VectorStoreRetriever.retrieve',
|
|
234
|
+
[],
|
|
235
|
+
{}
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(result).toBe('retriever');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('returns undefined for non-tool calls', () => {
|
|
242
|
+
const result = ToolDetector.detectToolCall(
|
|
243
|
+
'calculate',
|
|
244
|
+
'Math.calculate',
|
|
245
|
+
[],
|
|
246
|
+
{}
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
expect(result).toBeUndefined();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('detectNodeType', () => {
|
|
254
|
+
test('detects LLM nodes', () => {
|
|
255
|
+
const [nodeType, llmInfo] = detectNodeType(
|
|
256
|
+
'create',
|
|
257
|
+
'chat.completions.create',
|
|
258
|
+
[],
|
|
259
|
+
{ model: 'gpt-4', messages: [] }
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
expect(nodeType).toBe('llm');
|
|
263
|
+
expect(llmInfo).toBeDefined();
|
|
264
|
+
expect(llmInfo?.provider).toBe('openai');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('detects tool nodes', () => {
|
|
268
|
+
const [nodeType, llmInfo] = detectNodeType(
|
|
269
|
+
'search',
|
|
270
|
+
'tools.search',
|
|
271
|
+
[],
|
|
272
|
+
{}
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
expect(nodeType).toBe('tool');
|
|
276
|
+
expect(llmInfo).toBeUndefined();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('falls back to function type', () => {
|
|
280
|
+
const [nodeType, llmInfo] = detectNodeType(
|
|
281
|
+
'my_function',
|
|
282
|
+
'module.my_function',
|
|
283
|
+
[],
|
|
284
|
+
{}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
expect(nodeType).toBe('function');
|
|
288
|
+
expect(llmInfo).toBeUndefined();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('Helper Functions', () => {
|
|
293
|
+
test('extractPromptFromMessages finds system message', () => {
|
|
294
|
+
const messages = [
|
|
295
|
+
{ role: 'system', content: 'You are helpful' },
|
|
296
|
+
{ role: 'user', content: 'Hello' },
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
const prompt = extractPromptFromMessages(messages);
|
|
300
|
+
|
|
301
|
+
expect(prompt).toBe('You are helpful');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('extractPromptFromMessages returns undefined when no system message', () => {
|
|
305
|
+
const messages = [
|
|
306
|
+
{ role: 'user', content: 'Hello' },
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const prompt = extractPromptFromMessages(messages);
|
|
310
|
+
|
|
311
|
+
expect(prompt).toBeUndefined();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('extractConfigSubset extracts only specified keys', () => {
|
|
315
|
+
const kwargs = {
|
|
316
|
+
model: 'gpt-4',
|
|
317
|
+
temperature: 0.7,
|
|
318
|
+
max_tokens: 100,
|
|
319
|
+
irrelevant: 'value',
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const subset = extractConfigSubset(kwargs, ['model', 'temperature', 'missing']);
|
|
323
|
+
|
|
324
|
+
expect(subset).toEqual({ model: 'gpt-4', temperature: 0.7 });
|
|
325
|
+
expect(subset['irrelevant']).toBeUndefined();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document attachment support for the TuringPulse TypeScript SDK.
|
|
3
|
+
*
|
|
4
|
+
* Provides AttachmentManager for uploading documents to the ingestion service
|
|
5
|
+
* and injecting attachment metadata into the current span.
|
|
6
|
+
*
|
|
7
|
+
* Design principle: NEVER crash customer code. All upload failures are logged
|
|
8
|
+
* and silently ignored.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { TuringPulseConfig } from './config';
|
|
12
|
+
|
|
13
|
+
export interface AttachmentResult {
|
|
14
|
+
attachmentId: string;
|
|
15
|
+
filename: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
sizeBytes: number;
|
|
18
|
+
storagePath: string;
|
|
19
|
+
direction: string;
|
|
20
|
+
deduplicated: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class AttachmentManager {
|
|
24
|
+
private readonly baseUrl: string;
|
|
25
|
+
private readonly config: TuringPulseConfig;
|
|
26
|
+
private readonly enabled: boolean;
|
|
27
|
+
|
|
28
|
+
constructor(config: TuringPulseConfig, enabled = true) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.baseUrl = config.endpoint;
|
|
31
|
+
this.enabled = enabled;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async attachBytes(
|
|
35
|
+
data: Uint8Array | Buffer,
|
|
36
|
+
filename: string,
|
|
37
|
+
direction: 'input' | 'output' = 'input',
|
|
38
|
+
mimeType?: string,
|
|
39
|
+
): Promise<AttachmentResult | undefined> {
|
|
40
|
+
if (!this.enabled) return undefined;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const formData = new FormData();
|
|
44
|
+
const blob = new Blob([data as BlobPart], { type: mimeType ?? 'application/octet-stream' });
|
|
45
|
+
formData.append('file', blob, filename);
|
|
46
|
+
formData.append('direction', direction);
|
|
47
|
+
|
|
48
|
+
const fetchImpl = this.config.fetchImpl ?? globalThis.fetch;
|
|
49
|
+
const response = await fetchImpl(`${this.baseUrl}/api/v1/attachments`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'X-API-Key': this.config.apiKey,
|
|
53
|
+
},
|
|
54
|
+
body: formData,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
// eslint-disable-next-line no-console
|
|
59
|
+
console.warn(`TuringPulse attachment upload failed: ${response.status}`);
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = (await response.json()) as Record<string, unknown>;
|
|
64
|
+
return {
|
|
65
|
+
attachmentId: result.attachment_id as string,
|
|
66
|
+
filename: result.filename as string,
|
|
67
|
+
mimeType: result.mime_type as string,
|
|
68
|
+
sizeBytes: result.size_bytes as number,
|
|
69
|
+
storagePath: result.storage_path as string,
|
|
70
|
+
direction,
|
|
71
|
+
deduplicated: Boolean(result.deduplicated),
|
|
72
|
+
};
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.warn('TuringPulse attachment upload failed:', err instanceof Error ? err.message : String(err));
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async attachText(
|
|
81
|
+
text: string,
|
|
82
|
+
filename = 'document.txt',
|
|
83
|
+
direction: 'input' | 'output' = 'input',
|
|
84
|
+
): Promise<AttachmentResult | undefined> {
|
|
85
|
+
const encoder = new TextEncoder();
|
|
86
|
+
return this.attachBytes(encoder.encode(text), filename, direction, 'text/plain');
|
|
87
|
+
}
|
|
88
|
+
}
|