elasticdash-test 0.1.14 → 0.1.16

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.
Files changed (36) hide show
  1. package/README.md +36 -5
  2. package/dist/dashboard-server.d.ts +9 -0
  3. package/dist/dashboard-server.d.ts.map +1 -1
  4. package/dist/dashboard-server.js +209 -22
  5. package/dist/dashboard-server.js.map +1 -1
  6. package/dist/html/dashboard.html +158 -8
  7. package/dist/index.cjs +828 -108
  8. package/dist/index.d.ts +3 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/interceptors/telemetry-push.d.ts +47 -0
  13. package/dist/interceptors/telemetry-push.d.ts.map +1 -1
  14. package/dist/interceptors/telemetry-push.js +139 -6
  15. package/dist/interceptors/telemetry-push.js.map +1 -1
  16. package/dist/interceptors/tool.d.ts.map +1 -1
  17. package/dist/interceptors/tool.js +2 -1
  18. package/dist/interceptors/tool.js.map +1 -1
  19. package/dist/interceptors/workflow-ai.d.ts.map +1 -1
  20. package/dist/interceptors/workflow-ai.js +28 -4
  21. package/dist/interceptors/workflow-ai.js.map +1 -1
  22. package/dist/internals/mock-resolver.d.ts +42 -5
  23. package/dist/internals/mock-resolver.d.ts.map +1 -1
  24. package/dist/internals/mock-resolver.js +124 -5
  25. package/dist/internals/mock-resolver.js.map +1 -1
  26. package/dist/workflow-runner-worker.js +8 -2
  27. package/dist/workflow-runner-worker.js.map +1 -1
  28. package/package.json +3 -2
  29. package/src/dashboard-server.ts +86 -17
  30. package/src/html/dashboard.html +158 -8
  31. package/src/index.ts +3 -2
  32. package/src/interceptors/telemetry-push.ts +158 -7
  33. package/src/interceptors/tool.ts +2 -1
  34. package/src/interceptors/workflow-ai.ts +30 -4
  35. package/src/internals/mock-resolver.ts +131 -5
  36. package/src/workflow-runner-worker.ts +23 -2
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Runtime tool mock resolution for module-imported tools.
2
+ * Runtime mock resolution for module-imported tools and AI calls.
3
3
  *
4
- * Tools that are statically imported (not accessed via globalThis) cannot be
5
- * intercepted by the worker's proxy-based mocking. Instead, each tool function
6
- * calls `resolveMock` at its entry point. The worker writes the mock config to
7
- * `__ELASTICDASH_TOOL_MOCKS__` before the workflow runs and clears it after.
4
+ * Tools/AI calls that are statically imported (not accessed via globalThis)
5
+ * cannot be intercepted by the worker's proxy-based mocking. Instead, each
6
+ * wrapped function calls `resolveMock` / `resolveAIMock` at its entry point.
7
+ * The worker writes the mock config to `__ELASTICDASH_TOOL_MOCKS__` /
8
+ * `__ELASTICDASH_AI_MOCKS__` before the workflow runs and clears it after.
8
9
  */
9
10
 
10
11
  interface ToolMockEntry {
@@ -13,6 +14,17 @@ interface ToolMockEntry {
13
14
  mockData?: Record<number, unknown>
14
15
  }
15
16
 
17
+ /** Per-model AI mock configuration (mirrors ToolMockConfig) */
18
+ export interface AIMockEntry {
19
+ mode: 'live' | 'mock-all' | 'mock-specific'
20
+ callIndices?: number[]
21
+ mockData?: Record<number, unknown>
22
+ }
23
+
24
+ export interface AIMockConfig {
25
+ [modelName: string]: AIMockEntry
26
+ }
27
+
16
28
  type MockResult =
17
29
  | { mocked: true; result: unknown }
18
30
  | { mocked: false }
@@ -90,3 +102,117 @@ export function resolveMock(toolName: string): MockResult {
90
102
 
91
103
  return { mocked: false }
92
104
  }
105
+
106
+ /**
107
+ * Extracts the system prompt string from an LLM call input.
108
+ * Handles:
109
+ * - Anthropic style: `{ system: "...", messages: [...] }`
110
+ * - OpenAI style: `{ messages: [{ role: "system", content: "..." }, ...] }`
111
+ * - Plain message array: `[{ role: "system", content: "..." }, ...]`
112
+ * - Separate field: `{ systemPrompt: "...", messages: [...] }` (custom wrapAI callers)
113
+ */
114
+ export function extractSystemPrompt(input: unknown): string | undefined {
115
+ if (!input || typeof input !== 'object') return undefined
116
+ const o = input as Record<string, unknown>
117
+ if (typeof o.system === 'string') return o.system
118
+ if (typeof o.systemPrompt === 'string' && o.systemPrompt.length > 0) return o.systemPrompt
119
+ const msgs = Array.isArray(o.messages) ? o.messages : (Array.isArray(input) ? input as unknown[] : null)
120
+ if (msgs) {
121
+ for (const m of msgs) {
122
+ if (m && typeof m === 'object') {
123
+ const msg = m as Record<string, unknown>
124
+ if (msg.role === 'system' && typeof msg.content === 'string') return msg.content
125
+ }
126
+ }
127
+ }
128
+ return undefined
129
+ }
130
+
131
+ /**
132
+ * Returns a shallow copy of `input` with the system prompt replaced by `newSystemPrompt`.
133
+ */
134
+ export function replaceSystemPrompt(input: unknown, newSystemPrompt: string): unknown {
135
+ if (!input || typeof input !== 'object') return input
136
+ const o = input as Record<string, unknown>
137
+ if (typeof o.system === 'string') return { ...o, system: newSystemPrompt }
138
+ if (typeof o.systemPrompt === 'string') return { ...o, systemPrompt: newSystemPrompt }
139
+ if (Array.isArray(input)) {
140
+ return (input as unknown[]).map(m => {
141
+ if (m && typeof m === 'object') {
142
+ const msg = m as Record<string, unknown>
143
+ if (msg.role === 'system' && typeof msg.content === 'string') return { ...msg, content: newSystemPrompt }
144
+ }
145
+ return m
146
+ })
147
+ }
148
+ if (Array.isArray(o.messages)) {
149
+ return {
150
+ ...o,
151
+ messages: (o.messages as unknown[]).map(m => {
152
+ if (m && typeof m === 'object') {
153
+ const msg = m as Record<string, unknown>
154
+ if (msg.role === 'system' && typeof msg.content === 'string') return { ...msg, content: newSystemPrompt }
155
+ }
156
+ return m
157
+ }),
158
+ }
159
+ }
160
+ return input
161
+ }
162
+
163
+ /**
164
+ * If a prompt mock is configured for the system prompt found in `input`, returns
165
+ * a copy of `input` with the system prompt replaced. Otherwise returns `undefined`.
166
+ *
167
+ * `__ELASTICDASH_PROMPT_MOCKS__` is `Record<string, string>` keyed by the original
168
+ * system prompt text, set by the worker/dashboard before the workflow runs.
169
+ */
170
+ export function resolvePromptMock(input: unknown): unknown | undefined {
171
+ const g = globalThis as Record<string, unknown>
172
+ const mocks = g['__ELASTICDASH_PROMPT_MOCKS__'] as Record<string, string> | undefined
173
+ if (!mocks || Object.keys(mocks).length === 0) return undefined
174
+ const systemPrompt = extractSystemPrompt(input)
175
+ if (systemPrompt === undefined) return undefined
176
+ const newSystemPrompt = mocks[systemPrompt]
177
+ if (newSystemPrompt === undefined) return undefined
178
+ return replaceSystemPrompt(input, newSystemPrompt)
179
+ }
180
+
181
+ /**
182
+ * Resolves whether the current call to `modelName` should be mocked.
183
+ * Mirrors `resolveMock` but reads `__ELASTICDASH_AI_MOCKS__` and
184
+ * `__ELASTICDASH_AI_CALL_COUNTERS__`.
185
+ */
186
+ export function resolveAIMock(modelName: string): MockResult {
187
+ const g = globalThis as Record<string, unknown>
188
+
189
+ const mocks = g['__ELASTICDASH_AI_MOCKS__'] as Record<string, AIMockEntry> | undefined
190
+ if (!mocks) return { mocked: false }
191
+
192
+ const entry = mocks[modelName]
193
+ if (!entry || entry.mode === 'live') return { mocked: false }
194
+
195
+ if (!g['__ELASTICDASH_AI_CALL_COUNTERS__']) {
196
+ g['__ELASTICDASH_AI_CALL_COUNTERS__'] = {} as Record<string, number>
197
+ }
198
+ const counters = g['__ELASTICDASH_AI_CALL_COUNTERS__'] as Record<string, number>
199
+ counters[modelName] = (counters[modelName] ?? 0) + 1
200
+ const callNumber = counters[modelName]
201
+
202
+ if (entry.mode === 'mock-all') {
203
+ const data = entry.mockData ?? {}
204
+ const raw = data[callNumber] !== undefined ? data[callNumber] : data[0]
205
+ return { mocked: true, result: normaliseMockResult(raw) }
206
+ }
207
+
208
+ if (entry.mode === 'mock-specific') {
209
+ const indices = entry.callIndices ?? []
210
+ if (indices.includes(callNumber)) {
211
+ const data = entry.mockData ?? {}
212
+ return { mocked: true, result: normaliseMockResult(data[callNumber]) }
213
+ }
214
+ return { mocked: false }
215
+ }
216
+
217
+ return { mocked: false }
218
+ }
@@ -79,6 +79,17 @@ interface ToolMockConfig {
79
79
  [toolName: string]: ToolMockEntry
80
80
  }
81
81
 
82
+ /** Per-model AI mock configuration (mirrors ToolMockConfig) */
83
+ interface AIMockEntry {
84
+ mode: 'live' | 'mock-all' | 'mock-specific'
85
+ callIndices?: number[]
86
+ mockData?: Record<number, unknown>
87
+ }
88
+
89
+ interface AIMockConfig {
90
+ [modelName: string]: AIMockEntry
91
+ }
92
+
82
93
  /** Minimal inline tool-wrapping — records each tool call to the trace. */
83
94
  async function loadAndWrapTools(
84
95
  toolsModulePath: string,
@@ -210,6 +221,10 @@ async function main() {
210
221
  agentState?: AgentState
211
222
  /** Optional tool mock configuration from the dashboard UI */
212
223
  toolMockConfig?: ToolMockConfig
224
+ /** Optional AI/LLM mock configuration from the dashboard UI */
225
+ aiMockConfig?: AIMockConfig
226
+ /** Optional prompt mock config: replacement system prompts keyed by original system prompt text */
227
+ promptMockConfig?: Record<string, string>
213
228
  }
214
229
  try {
215
230
  payload = JSON.parse(raw)
@@ -219,7 +234,7 @@ async function main() {
219
234
  return
220
235
  }
221
236
 
222
- const { workflowsModulePath, toolsModulePath, workflowName, args, input, replayMode = false, checkpoint = 0, history = [], agentState, toolMockConfig } = payload
237
+ const { workflowsModulePath, toolsModulePath, workflowName, args, input, replayMode = false, checkpoint = 0, history = [], agentState, toolMockConfig, aiMockConfig, promptMockConfig } = payload
223
238
 
224
239
  const { context, finalise } = startTraceSession()
225
240
  setCurrentTrace(context.trace)
@@ -258,9 +273,12 @@ async function main() {
258
273
  let workflowError: Error | undefined
259
274
 
260
275
  try {
261
- // Write mock config to globals so module-imported tools can read it via resolveMock()
276
+ // Write mock configs to globals so module-imported tools/AI can read them via resolveMock/resolveAIMock()
262
277
  ;(globalThis as any).__ELASTICDASH_TOOL_MOCKS__ = toolMockConfig ?? {}
263
278
  ;(globalThis as any).__ELASTICDASH_TOOL_CALL_COUNTERS__ = {}
279
+ ;(globalThis as any).__ELASTICDASH_AI_MOCKS__ = aiMockConfig ?? {}
280
+ ;(globalThis as any).__ELASTICDASH_AI_CALL_COUNTERS__ = {}
281
+ ;(globalThis as any).__ELASTICDASH_PROMPT_MOCKS__ = promptMockConfig ?? {}
264
282
 
265
283
  await installDBAutoInterceptor()
266
284
  installAIInterceptor()
@@ -321,6 +339,9 @@ async function main() {
321
339
  // Clear mock globals
322
340
  delete (globalThis as any).__ELASTICDASH_TOOL_MOCKS__
323
341
  delete (globalThis as any).__ELASTICDASH_TOOL_CALL_COUNTERS__
342
+ delete (globalThis as any).__ELASTICDASH_AI_MOCKS__
343
+ delete (globalThis as any).__ELASTICDASH_AI_CALL_COUNTERS__
344
+ delete (globalThis as any).__ELASTICDASH_PROMPT_MOCKS__
324
345
  }
325
346
 
326
347
  await recorder.flush()