@taodigital/agents 0.0.0-stage → 1.2.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.
Files changed (76) hide show
  1. package/README.md +99 -2
  2. package/dist/api-test-agent.d.ts +126 -0
  3. package/dist/api-test-agent.js +267 -0
  4. package/dist/artifact-intel-agent.d.ts +11 -0
  5. package/dist/artifact-intel-agent.js +152 -0
  6. package/dist/coverage-gap-agent.d.ts +25 -0
  7. package/dist/coverage-gap-agent.js +269 -0
  8. package/dist/coverage-map-agent.d.ts +15 -0
  9. package/dist/coverage-map-agent.js +255 -0
  10. package/dist/crawl-diff-agent.d.ts +27 -0
  11. package/dist/crawl-diff-agent.js +412 -0
  12. package/dist/data-agent.d.ts +101 -0
  13. package/dist/data-agent.js +2284 -0
  14. package/dist/document-agent.d.ts +10 -0
  15. package/dist/document-agent.js +32 -0
  16. package/dist/enhancement-agent.d.ts +48 -0
  17. package/dist/enhancement-agent.js +342 -0
  18. package/dist/field-detector-agent.d.ts +35 -0
  19. package/dist/field-detector-agent.js +220 -0
  20. package/dist/flow-agent.d.ts +130 -0
  21. package/dist/flow-agent.js +169 -0
  22. package/dist/github-agent.d.ts +18 -0
  23. package/dist/github-agent.js +39 -0
  24. package/dist/github-write-agent.d.ts +67 -0
  25. package/dist/github-write-agent.js +249 -0
  26. package/dist/grouping-agent.d.ts +18 -0
  27. package/dist/grouping-agent.js +338 -0
  28. package/dist/healing-agent.d.ts +73 -0
  29. package/dist/healing-agent.js +137 -0
  30. package/dist/index.d.ts +36 -0
  31. package/dist/index.js +67 -0
  32. package/dist/integration-test-agent.d.ts +118 -0
  33. package/dist/integration-test-agent.js +165 -0
  34. package/dist/label-assignment-agent.d.ts +15 -0
  35. package/dist/label-assignment-agent.js +97 -0
  36. package/dist/llm-config.d.ts +38 -0
  37. package/dist/llm-config.js +287 -0
  38. package/dist/llm-mock.d.ts +77 -0
  39. package/dist/llm-mock.js +192 -0
  40. package/dist/llm.d.ts +58 -0
  41. package/dist/llm.js +438 -0
  42. package/dist/nlp-agent.d.ts +208 -0
  43. package/dist/nlp-agent.js +340 -0
  44. package/dist/orchestrator.d.ts +285 -0
  45. package/dist/orchestrator.js +141 -0
  46. package/dist/plan-agent.d.ts +4 -0
  47. package/dist/plan-agent.js +200 -0
  48. package/dist/repo-scan-agent.d.ts +189 -0
  49. package/dist/repo-scan-agent.js +2032 -0
  50. package/dist/requirements-agent.d.ts +59 -0
  51. package/dist/requirements-agent.js +641 -0
  52. package/dist/scenario-agent.d.ts +20 -0
  53. package/dist/scenario-agent.js +123 -0
  54. package/dist/script-agent.d.ts +447 -0
  55. package/dist/script-agent.js +11813 -0
  56. package/dist/security-test-agent.d.ts +111 -0
  57. package/dist/security-test-agent.js +141 -0
  58. package/dist/skill-file-agent.d.ts +264 -0
  59. package/dist/skill-file-agent.js +3470 -0
  60. package/dist/step-coverage.d.ts +46 -0
  61. package/dist/step-coverage.js +288 -0
  62. package/dist/step-utils.d.ts +2 -0
  63. package/dist/step-utils.js +62 -0
  64. package/dist/test-case-agent.d.ts +206 -0
  65. package/dist/test-case-agent.js +2141 -0
  66. package/dist/test-import-agent.d.ts +148 -0
  67. package/dist/test-import-agent.js +3235 -0
  68. package/dist/test-script-agent.d.ts +16 -0
  69. package/dist/test-script-agent.js +48 -0
  70. package/dist/token-pricing.d.ts +14 -0
  71. package/dist/token-pricing.js +62 -0
  72. package/dist/types.d.ts +232 -0
  73. package/dist/types.js +7 -0
  74. package/dist/visual-agent.d.ts +114 -0
  75. package/dist/visual-agent.js +261 -0
  76. package/package.json +26 -4
package/dist/llm.js ADDED
@@ -0,0 +1,438 @@
1
+ "use strict";
2
+ /**
3
+ * Multi-provider LLM module.
4
+ *
5
+ * Providers using their own SDK: Anthropic.
6
+ * Providers via OpenAI-compatible API (single openai package): OpenAI, Gemini,
7
+ * Grok (xAI), OpenRouter, Ollama (local), Nvidia NIM.
8
+ *
9
+ * Config is resolved per-tenant only (set in Workspace → Settings → AI Provider).
10
+ * No platform-level API key fallback — every workspace must configure its own provider.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.LLMProviderError = exports.DEFAULT_MODELS = void 0;
17
+ exports.deterministicSeed = deterministicSeed;
18
+ exports.sanitizeProviderError = sanitizeProviderError;
19
+ exports.askTrackedWithImages = askTrackedWithImages;
20
+ exports.askWithImages = askWithImages;
21
+ exports.askTracked = askTracked;
22
+ exports.ask = ask;
23
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
24
+ const openai_1 = __importDefault(require("openai"));
25
+ const core_1 = require("@taodigital/core");
26
+ const token_pricing_1 = require("./token-pricing");
27
+ const llm_mock_js_1 = require("./llm-mock.js");
28
+ function deterministicSeed(input) {
29
+ let hash = 0x811c9dc5;
30
+ for (let i = 0; i < input.length; i++) {
31
+ hash ^= input.charCodeAt(i);
32
+ hash = Math.imul(hash, 0x01000193);
33
+ }
34
+ return hash >>> 1;
35
+ }
36
+ // Per-request cap. Without this the SDKs default to 10 minutes, so one hung
37
+ // provider call can silently stall a whole generation batch.
38
+ // Default 120s — long enough for large prompts on free/slow LLMs, short enough to fail fast.
39
+ const LLM_REQUEST_TIMEOUT_MS = Math.max(10_000, Number(process.env.LLM_REQUEST_TIMEOUT_MS ?? 120_000));
40
+ const APP_PUBLIC_URL = process.env.APP_PUBLIC_URL ?? 'https://zeta-ai.io';
41
+ const APP_TITLE = process.env.APP_TITLE ?? 'ZeTa-AI';
42
+ const PROVIDER_BASE_URLS = {
43
+ anthropic: '',
44
+ openai: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1',
45
+ gemini: process.env.GEMINI_BASE_URL ?? 'https://generativelanguage.googleapis.com/v1beta/openai/',
46
+ grok: process.env.GROK_BASE_URL ?? 'https://api.x.ai/v1',
47
+ openrouter: process.env.OPENROUTER_BASE_URL ?? 'https://openrouter.ai/api/v1',
48
+ ollama: process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434/v1',
49
+ nvidia: process.env.NVIDIA_BASE_URL ?? 'https://integrate.api.nvidia.com/v1',
50
+ groq: process.env.GROQ_BASE_URL ?? 'https://api.groq.com/openai/v1',
51
+ mistral: process.env.MISTRAL_BASE_URL ?? 'https://api.mistral.ai/v1',
52
+ deepseek: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com/v1',
53
+ perplexity: process.env.PERPLEXITY_BASE_URL ?? 'https://api.perplexity.ai',
54
+ togetherai: process.env.TOGETHERAI_BASE_URL ?? 'https://api.together.xyz/v1',
55
+ bedrock: '',
56
+ vertex: '',
57
+ azure: '',
58
+ };
59
+ exports.DEFAULT_MODELS = {
60
+ anthropic: 'claude-sonnet-4-6',
61
+ openai: 'gpt-4o',
62
+ gemini: 'gemini-2.5-pro',
63
+ grok: 'grok-3',
64
+ openrouter: 'anthropic/claude-3-5-sonnet:beta',
65
+ ollama: 'llama3',
66
+ nvidia: 'meta/llama-3.1-70b-instruct',
67
+ groq: 'llama-3.3-70b-versatile',
68
+ mistral: 'mistral-large-latest',
69
+ deepseek: 'deepseek-chat',
70
+ perplexity: 'llama-3.1-sonar-large-128k-online',
71
+ togetherai: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
72
+ bedrock: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
73
+ vertex: 'gemini-2.5-pro',
74
+ azure: 'gpt-4o',
75
+ };
76
+ /**
77
+ * Thrown in place of a raw provider SDK error. Provider error messages
78
+ * sometimes echo back a snippet of the request/prompt content (e.g. a
79
+ * content-policy rejection quoting the offending text), which may include a
80
+ * customer's crawled screen content or other sensitive data - safe to log
81
+ * server-side, not safe to return verbatim to whoever's looking at the API
82
+ * response. sanitizeProviderError() below preserves the actionable signal
83
+ * (rate limited vs. bad auth vs. bad request) without the raw text.
84
+ */
85
+ class LLMProviderError extends Error {
86
+ /** True when the error is not transient and retrying will not help. */
87
+ nonRetryable;
88
+ constructor(message = 'AI service temporarily unavailable. Please try again.', nonRetryable = false) {
89
+ super(message);
90
+ this.name = 'LLMProviderError';
91
+ this.nonRetryable = nonRetryable;
92
+ }
93
+ }
94
+ exports.LLMProviderError = LLMProviderError;
95
+ function sanitizeProviderError(e) {
96
+ console.error('[llm] provider error (sanitized before returning to caller):', e);
97
+ // Re-throw LLMProviderErrors we deliberately raised (e.g. "key not configured") without
98
+ // replacing their message with the generic fallback.
99
+ if (e instanceof LLMProviderError)
100
+ return e;
101
+ const status = e?.status ?? e?.statusCode;
102
+ const causeCode = e?.cause?.code ?? e?.code;
103
+ if (causeCode === 'ENOTFOUND')
104
+ return new LLMProviderError('AI provider endpoint is unreachable. Check Workspace → Settings → AI Provider.', true);
105
+ if (status === 429)
106
+ return new LLMProviderError('AI provider rate limit reached. Please try again shortly.');
107
+ if (status === 401 || status === 403)
108
+ return new LLMProviderError('AI provider rejected the configured API key. Check Workspace → Settings → AI Provider.');
109
+ if (status === 404)
110
+ return new LLMProviderError('Model not found on this provider. Check the model ID is correct and available on the production API (not just the playground).');
111
+ if (status === 400)
112
+ return new LLMProviderError('The AI provider rejected this request (invalid input or content policy).');
113
+ if (status === 502 || status === 503 || status === 504)
114
+ return new LLMProviderError('AI provider is temporarily unavailable. Please try again shortly.');
115
+ return new LLMProviderError();
116
+ }
117
+ /** Mock-aware entry point (see llm-mock.ts). With ZETA_LLM_MOCK unset this is a pass-through. */
118
+ async function askTrackedWithImages(system, user, images, llmCfg, options) {
119
+ const mode = (0, llm_mock_js_1.getMockMode)();
120
+ if (mode === 'off') {
121
+ const real = await _askTrackedWithImagesReal(system, user, images, llmCfg, options);
122
+ (0, llm_mock_js_1.recordLlmCall)(real.usage, (0, llm_mock_js_1.mockKey)(system, user, 'vision'), 'real');
123
+ return real;
124
+ }
125
+ const key = (0, llm_mock_js_1.mockKey)(system, user, 'vision');
126
+ const out = (0, llm_mock_js_1.resolveMock)(mode, system, user, { kind: 'vision' });
127
+ if (out.kind === 'result') {
128
+ (0, llm_mock_js_1.recordLlmCall)(out.res.usage, key, out.source);
129
+ return out.res;
130
+ }
131
+ if (out.kind === 'error')
132
+ throw sanitizeProviderError({ status: out.status });
133
+ const real = await _askTrackedWithImagesReal(system, user, images, llmCfg, options);
134
+ (0, llm_mock_js_1.saveFixture)(key, real);
135
+ (0, llm_mock_js_1.recordLlmCall)(real.usage, key, 'recorded');
136
+ return real;
137
+ }
138
+ async function _askTrackedWithImagesReal(system, user, images, llmCfg, options) {
139
+ const provider = llmCfg?.provider ?? 'openai';
140
+ const model = llmCfg?.model ?? (core_1.config.llmModel || exports.DEFAULT_MODELS[provider]);
141
+ const apiKey = llmCfg?.apiKey ?? (provider === 'openai' ? (process.env.OPENAI_API_KEY ?? '') : '');
142
+ const baseUrl = llmCfg?.baseUrl ?? PROVIDER_BASE_URLS[provider];
143
+ if (provider === 'anthropic') {
144
+ if (!apiKey) {
145
+ throw new LLMProviderError('No AI provider configured. Go to Workspace → Settings → AI Provider to add your API key.');
146
+ }
147
+ try {
148
+ return await _askAnthropicWithImages(system, user, images, apiKey, model, options);
149
+ }
150
+ catch (e) {
151
+ throw sanitizeProviderError(e);
152
+ }
153
+ }
154
+ try {
155
+ return await _askOpenAICompatWithImages(system, user, images, provider, model, apiKey, baseUrl, options);
156
+ }
157
+ catch (e) {
158
+ throw sanitizeProviderError(e);
159
+ }
160
+ }
161
+ async function askWithImages(system, user, images, llmCfg) {
162
+ const res = await askTrackedWithImages(system, user, images, llmCfg);
163
+ return res.text;
164
+ }
165
+ async function _askAnthropicWithImages(system, user, images, apiKey, model, askOptions) {
166
+ const client = new sdk_1.default({ apiKey, timeout: LLM_REQUEST_TIMEOUT_MS, maxRetries: 1 });
167
+ const content = images.map((img) => ({
168
+ type: 'image',
169
+ source: { type: 'base64', media_type: img.mediaType, data: img.base64 },
170
+ }));
171
+ content.push({ type: 'text', text: user });
172
+ const visionMaxTokens = parseInt(process.env.VISION_MAX_TOKENS ?? '') || 4096;
173
+ const res = await client.messages.create({
174
+ model,
175
+ max_tokens: visionMaxTokens,
176
+ ...(askOptions?.temperature != null ? { temperature: askOptions.temperature } : {}),
177
+ // @ts-ignore: cache_control is supported in beta
178
+ system: [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }],
179
+ messages: [{ role: 'user', content }],
180
+ });
181
+ const text = res.content.map((b) => (b.type === 'text' ? b.text : '')).join('\n').trim();
182
+ const u = res.usage;
183
+ const provider = 'anthropic';
184
+ const providerKey = `${provider}/${model}`;
185
+ const cacheReadTokens = u.cache_read_input_tokens ?? 0;
186
+ const cacheWriteTokens = u.cache_creation_input_tokens ?? 0;
187
+ const usage = {
188
+ provider, model,
189
+ promptTokens: u.input_tokens,
190
+ completionTokens: u.output_tokens,
191
+ cacheReadTokens,
192
+ cacheWriteTokens,
193
+ totalTokens: u.input_tokens + u.output_tokens,
194
+ estimatedCostUsd: (0, token_pricing_1.estimateCost)({ provider, model, promptTokens: u.input_tokens, completionTokens: u.output_tokens, cacheReadTokens, cacheWriteTokens, totalTokens: u.input_tokens + u.output_tokens }, providerKey),
195
+ };
196
+ return { text, usage };
197
+ }
198
+ async function _askOpenAICompatWithImages(system, user, images, provider, model, apiKey, baseUrl, askOptions) {
199
+ const effectiveKey = apiKey || (provider === 'ollama' ? 'ollama' : '');
200
+ if (!effectiveKey && provider !== 'ollama') {
201
+ throw new LLMProviderError(`API key for provider "${provider}" is not configured. Go to Workspace → Settings → AI Provider and re-enter your key.`);
202
+ }
203
+ const client = new openai_1.default({
204
+ apiKey: effectiveKey,
205
+ baseURL: baseUrl,
206
+ timeout: LLM_REQUEST_TIMEOUT_MS,
207
+ maxRetries: 1,
208
+ ...(provider === 'openrouter' && { defaultHeaders: { 'HTTP-Referer': APP_PUBLIC_URL, 'X-Title': APP_TITLE } }),
209
+ });
210
+ const imageContent = images.map((img) => ({
211
+ type: 'image_url',
212
+ image_url: { url: `data:${img.mediaType};base64,${img.base64}` },
213
+ }));
214
+ const systemContent = provider === 'openrouter'
215
+ ? [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }]
216
+ : system;
217
+ const visionMaxTokens = parseInt(process.env.VISION_MAX_TOKENS ?? '') || 4096;
218
+ const res = await client.chat.completions.create({
219
+ model,
220
+ max_tokens: visionMaxTokens,
221
+ ...(askOptions?.temperature != null ? { temperature: askOptions.temperature } : {}),
222
+ ...(Number.isInteger(askOptions?.seed) ? { seed: askOptions?.seed } : {}),
223
+ messages: [
224
+ { role: 'system', content: systemContent },
225
+ { role: 'user', content: [...imageContent, { type: 'text', text: user }] },
226
+ ],
227
+ });
228
+ const text = res.choices[0]?.message?.content?.trim() ?? '';
229
+ const u = res.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
230
+ const usage = {
231
+ provider, model,
232
+ promptTokens: u.prompt_tokens,
233
+ completionTokens: u.completion_tokens,
234
+ cacheReadTokens: 0,
235
+ cacheWriteTokens: 0,
236
+ totalTokens: u.total_tokens,
237
+ estimatedCostUsd: (0, token_pricing_1.estimateCost)({ provider, model, promptTokens: u.prompt_tokens, completionTokens: u.completion_tokens, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: u.total_tokens }, `${provider}/${model}`),
238
+ };
239
+ return { text, usage };
240
+ }
241
+ /** Mock-aware entry point (see llm-mock.ts). With ZETA_LLM_MOCK unset this is a pass-through. */
242
+ async function askTracked(system, user, llmCfg, maxTokens = parseInt(process.env.AGENT_MAX_TOKENS ?? '4096', 10), schema, options) {
243
+ const mode = (0, llm_mock_js_1.getMockMode)();
244
+ if (mode === 'off') {
245
+ const real = await _askTrackedReal(system, user, llmCfg, maxTokens, schema, options);
246
+ (0, llm_mock_js_1.recordLlmCall)(real.usage, (0, llm_mock_js_1.mockKey)(system, user), 'real');
247
+ return real;
248
+ }
249
+ const key = (0, llm_mock_js_1.mockKey)(system, user);
250
+ const out = (0, llm_mock_js_1.resolveMock)(mode, system, user, { schema });
251
+ if (out.kind === 'result') {
252
+ (0, llm_mock_js_1.recordLlmCall)(out.res.usage, key, out.source);
253
+ return out.res;
254
+ }
255
+ if (out.kind === 'error')
256
+ throw sanitizeProviderError({ status: out.status });
257
+ const real = await _askTrackedReal(system, user, llmCfg, maxTokens, schema, options);
258
+ (0, llm_mock_js_1.saveFixture)(key, real);
259
+ (0, llm_mock_js_1.recordLlmCall)(real.usage, key, 'recorded');
260
+ return real;
261
+ }
262
+ async function _askTrackedReal(system, user, llmCfg, maxTokens = parseInt(process.env.AGENT_MAX_TOKENS ?? '4096', 10), schema, options) {
263
+ const provider = llmCfg?.provider ?? 'openai';
264
+ const model = llmCfg?.model ?? (core_1.config.llmModel || exports.DEFAULT_MODELS[provider]);
265
+ const apiKey = llmCfg?.apiKey ?? (provider === 'openai' ? (process.env.OPENAI_API_KEY ?? '') : '');
266
+ const baseUrl = llmCfg?.baseUrl ?? PROVIDER_BASE_URLS[provider];
267
+ if (provider === 'anthropic') {
268
+ if (!apiKey) {
269
+ throw new LLMProviderError('No AI provider configured. Go to Workspace → Settings → AI Provider to add your API key.');
270
+ }
271
+ try {
272
+ return await _askAnthropic(system, user, apiKey, model, maxTokens, schema, options);
273
+ }
274
+ catch (e) {
275
+ throw sanitizeProviderError(e);
276
+ }
277
+ }
278
+ if ((provider === 'bedrock' || provider === 'vertex') && !llmCfg?.baseUrl) {
279
+ throw new Error(`Provider "${provider}" requires a proxy base URL. Set the Base URL field in Workspace → Settings → AI Provider to a compatible endpoint (e.g., LiteLLM).`);
280
+ }
281
+ // Non-Anthropic providers: flatten multi-block system to a single string
282
+ const systemStr = Array.isArray(system) ? system.map(b => b.text).join('\n\n') : system;
283
+ try {
284
+ return await _askOpenAICompat(systemStr, user, provider, model, apiKey, baseUrl, maxTokens, schema, options);
285
+ }
286
+ catch (e) {
287
+ throw sanitizeProviderError(e);
288
+ }
289
+ }
290
+ async function ask(system, user, llmCfg, maxTokens = parseInt(process.env.AGENT_MAX_TOKENS ?? '4096', 10)) {
291
+ const res = await askTracked(system, user, llmCfg, maxTokens);
292
+ return res.text;
293
+ }
294
+ async function withRetry(fn, maxAttempts = parseInt(process.env.LLM_MAX_RETRIES ?? '') || 3) {
295
+ const retryBaseMs = parseInt(process.env.LLM_RETRY_BASE_MS ?? '') || 1000;
296
+ let lastErr;
297
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
298
+ try {
299
+ return await fn();
300
+ }
301
+ catch (e) {
302
+ const status = e?.status ?? e?.statusCode ?? 0;
303
+ const isRateLimit = status === 429 || status === 503;
304
+ // DNS failures (ENOTFOUND) are not transient — skip retries immediately.
305
+ const isDnsFailure = e?.cause?.code === 'ENOTFOUND' || e?.code === 'ENOTFOUND';
306
+ // APIConnectionTimeoutError = model took too long — don't retry (would waste another timeout window).
307
+ // Only retry on transient network errors (ETIMEDOUT = socket-level, not request-level) and rate limits.
308
+ const isConnErr = !isDnsFailure && status === 0 && (e?.code === 'ETIMEDOUT' || e?.cause?.code === 'ETIMEDOUT' || e?.constructor?.name === 'APIConnectionError');
309
+ if ((isRateLimit || isConnErr) && attempt < maxAttempts - 1) {
310
+ const delay = isRateLimit
311
+ ? Math.pow(2, attempt) * retryBaseMs + Math.random() * 500
312
+ : retryBaseMs * 2 + Math.random() * 1000;
313
+ await new Promise(r => setTimeout(r, delay));
314
+ lastErr = e;
315
+ continue;
316
+ }
317
+ throw e;
318
+ }
319
+ }
320
+ throw lastErr;
321
+ }
322
+ async function _askAnthropic(system, user, apiKey, model, maxTokens = parseInt(process.env.AGENT_MAX_TOKENS ?? '4096', 10), schema, askOptions) {
323
+ const client = new sdk_1.default({ apiKey, timeout: LLM_REQUEST_TIMEOUT_MS, maxRetries: 1 });
324
+ const systemBlocks = Array.isArray(system)
325
+ ? system.map(b => b.cache
326
+ ? { type: 'text', text: b.text, cache_control: { type: 'ephemeral' } }
327
+ : { type: 'text', text: b.text })
328
+ : [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }];
329
+ const options = {
330
+ model,
331
+ max_tokens: maxTokens,
332
+ ...(askOptions?.temperature != null ? { temperature: askOptions.temperature } : {}),
333
+ ...(askOptions?.topP != null ? { top_p: askOptions.topP } : {}),
334
+ // @ts-ignore: cache_control is supported in beta
335
+ system: systemBlocks,
336
+ messages: [{ role: 'user', content: user }],
337
+ };
338
+ if (schema) {
339
+ options.tools = [{
340
+ name: 'extract_data',
341
+ description: 'Extract or derive structured data',
342
+ input_schema: schema,
343
+ }];
344
+ options.tool_choice = { type: 'tool', name: 'extract_data' };
345
+ }
346
+ const res = (await withRetry(() => client.messages.create(options)));
347
+ let text = '';
348
+ if (schema) {
349
+ const toolCall = res.content.find((b) => b.type === 'tool_use');
350
+ if (toolCall) {
351
+ text = JSON.stringify(toolCall.input);
352
+ }
353
+ else {
354
+ text = res.content.map((b) => (b.type === 'text' ? b.text : '')).join('\n').trim();
355
+ }
356
+ }
357
+ else {
358
+ text = res.content.map((b) => (b.type === 'text' ? b.text : '')).join('\n').trim();
359
+ }
360
+ const u = res.usage;
361
+ const provider = 'anthropic';
362
+ const providerKey = `${provider}/${model}`;
363
+ const cacheReadTokens = u.cache_read_input_tokens ?? 0;
364
+ const cacheWriteTokens = u.cache_creation_input_tokens ?? 0;
365
+ const usage = {
366
+ provider, model,
367
+ promptTokens: u.input_tokens,
368
+ completionTokens: u.output_tokens,
369
+ cacheReadTokens,
370
+ cacheWriteTokens,
371
+ totalTokens: u.input_tokens + u.output_tokens,
372
+ estimatedCostUsd: (0, token_pricing_1.estimateCost)({ provider, model, promptTokens: u.input_tokens, completionTokens: u.output_tokens, cacheReadTokens, cacheWriteTokens, totalTokens: u.input_tokens + u.output_tokens }, providerKey),
373
+ };
374
+ return { text, usage };
375
+ }
376
+ async function _askOpenAICompat(system, user, provider, model, apiKey, baseUrl, maxTokens = parseInt(process.env.AGENT_MAX_TOKENS ?? '4096', 10), schema, askOptions) {
377
+ const effectiveKey = apiKey || (provider === 'ollama' ? 'ollama' : '');
378
+ if (!effectiveKey && provider !== 'ollama') {
379
+ throw new LLMProviderError(`API key for provider "${provider}" is not configured. Go to Workspace → Settings → AI Provider and re-enter your key.`);
380
+ }
381
+ const client = new openai_1.default({
382
+ apiKey: effectiveKey,
383
+ baseURL: baseUrl,
384
+ timeout: LLM_REQUEST_TIMEOUT_MS,
385
+ maxRetries: 1,
386
+ ...(provider === 'openrouter' && { defaultHeaders: { 'HTTP-Referer': APP_PUBLIC_URL, 'X-Title': APP_TITLE } }),
387
+ });
388
+ const systemContent = provider === 'openrouter'
389
+ ? [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }]
390
+ : system;
391
+ const options = {
392
+ model,
393
+ max_tokens: maxTokens,
394
+ ...(askOptions?.temperature != null ? { temperature: askOptions.temperature } : {}),
395
+ ...(askOptions?.topP != null ? { top_p: askOptions.topP } : {}),
396
+ ...(Number.isInteger(askOptions?.seed) ? { seed: askOptions?.seed } : {}),
397
+ messages: [
398
+ { role: 'system', content: systemContent },
399
+ { role: 'user', content: user },
400
+ ],
401
+ };
402
+ if (schema) {
403
+ options.tools = [{
404
+ type: 'function',
405
+ function: {
406
+ name: 'extract_data',
407
+ description: 'Extract or derive structured data',
408
+ parameters: schema,
409
+ }
410
+ }];
411
+ options.tool_choice = { type: 'function', function: { name: 'extract_data' } };
412
+ }
413
+ const res = await withRetry(() => client.chat.completions.create(options));
414
+ let text = '';
415
+ if (schema) {
416
+ const toolCall = res.choices[0]?.message?.tool_calls?.[0];
417
+ if (toolCall?.function?.arguments) {
418
+ text = toolCall.function.arguments;
419
+ }
420
+ else {
421
+ text = res.choices[0]?.message?.content?.trim() ?? '';
422
+ }
423
+ }
424
+ else {
425
+ text = res.choices[0]?.message?.content?.trim() ?? '';
426
+ }
427
+ const u = res.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
428
+ const usage = {
429
+ provider, model,
430
+ promptTokens: u.prompt_tokens,
431
+ completionTokens: u.completion_tokens,
432
+ cacheReadTokens: 0,
433
+ cacheWriteTokens: 0,
434
+ totalTokens: u.total_tokens,
435
+ estimatedCostUsd: (0, token_pricing_1.estimateCost)({ provider, model, promptTokens: u.prompt_tokens, completionTokens: u.completion_tokens, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: u.total_tokens }, `${provider}/${model}`),
436
+ };
437
+ return { text, usage };
438
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * ZeTa-AI NLP Test Input Agent
3
+ *
4
+ * Converts plain-English test descriptions into structured, grounded test cases.
5
+ * Unlike competitors (testRigor, Functionize) this grounds the output in the
6
+ * Zeta Core — only uses elements that ACTUALLY EXIST on the real screen.
7
+ *
8
+ * Input: "When a user logs in with valid credentials, they should see the dashboard"
9
+ * Output: A structured TestCase grounded in real elements, ready for review.
10
+ */
11
+ import type { TestKind } from '@taodigital/core';
12
+ export declare function convertNlpToTest(opts: {
13
+ tenantId: string;
14
+ projectId: string;
15
+ description: string;
16
+ suiteId?: string;
17
+ }): Promise<{
18
+ error: string;
19
+ description: string;
20
+ id?: undefined;
21
+ ref?: undefined;
22
+ title?: undefined;
23
+ steps?: undefined;
24
+ expectedResult?: undefined;
25
+ confidence?: undefined;
26
+ understoodIntent?: undefined;
27
+ unmatchedTerms?: undefined;
28
+ testCase?: undefined;
29
+ saved?: undefined;
30
+ warning?: undefined;
31
+ } | {
32
+ id: any;
33
+ ref: any;
34
+ title: string;
35
+ steps: string[];
36
+ expectedResult: string;
37
+ confidence: number;
38
+ understoodIntent: string;
39
+ unmatchedTerms: string[];
40
+ testCase: {
41
+ id: any;
42
+ ref: any;
43
+ screenId: string;
44
+ title: string;
45
+ kind: TestKind;
46
+ steps: string[];
47
+ expectedResult: string;
48
+ groundedOn: string[];
49
+ };
50
+ saved: {
51
+ suite: {
52
+ tenantId: string;
53
+ id: string;
54
+ projectId: string;
55
+ name: string;
56
+ createdAt: Date;
57
+ genModel: string | null;
58
+ category: import(".prisma/client").$Enums.SuiteCategory | null;
59
+ parentId: string | null;
60
+ ownerUserId: string | null;
61
+ genTotalTokens: number | null;
62
+ genTotalCostUsd: number | null;
63
+ genAt: Date | null;
64
+ genScreenCount: number | null;
65
+ coverageReport: import("@prisma/client/runtime/library").JsonValue | null;
66
+ };
67
+ testCases: {
68
+ tenantId: string;
69
+ screenId: string | null;
70
+ id: string;
71
+ projectId: string;
72
+ source: string;
73
+ status: string;
74
+ createdAt: Date;
75
+ updatedAt: Date;
76
+ steps: import("@prisma/client/runtime/library").JsonValue;
77
+ kind: string;
78
+ tags: import("@prisma/client/runtime/library").JsonValue;
79
+ approvedBy: string | null;
80
+ suiteId: string;
81
+ ref: string | null;
82
+ title: string;
83
+ priority: string;
84
+ preconditions: string | null;
85
+ expectedResult: string | null;
86
+ gherkin: string | null;
87
+ approvedAt: Date | null;
88
+ groundedOn: import("@prisma/client/runtime/library").JsonValue;
89
+ caseRefs: import("@prisma/client/runtime/library").JsonValue;
90
+ viewports: import("@prisma/client/runtime/library").JsonValue;
91
+ viewportConstraint: string;
92
+ techniqueOfRecord: string | null;
93
+ traceTo: string | null;
94
+ traceToExtra: import("@prisma/client/runtime/library").JsonValue;
95
+ testDataInline: import("@prisma/client/runtime/library").JsonValue | null;
96
+ dataNote: string | null;
97
+ scriptNote: string | null;
98
+ structuredSteps: import("@prisma/client/runtime/library").JsonValue | null;
99
+ artifactId: string | null;
100
+ scenarioId: string | null;
101
+ suspect: boolean;
102
+ suspectReason: string | null;
103
+ flakyScore: number;
104
+ consecutiveFlakes: number;
105
+ lastFlakyAt: Date | null;
106
+ quarantined: boolean;
107
+ quarantinedAt: Date | null;
108
+ quarantineReason: string | null;
109
+ genPromptTokens: number | null;
110
+ genCompletionTokens: number | null;
111
+ genCostUsd: number | null;
112
+ genModel: string | null;
113
+ dataSetId: string | null;
114
+ repoTargetPath: string | null;
115
+ stepsKey: string | null;
116
+ gherkinKey: string | null;
117
+ contentHash: string | null;
118
+ groundingHash: string | null;
119
+ locatorsJson: import("@prisma/client/runtime/library").JsonValue | null;
120
+ locatorsExtractedAt: Date | null;
121
+ captureStepScreenshots: import("@prisma/client/runtime/library").JsonValue | null;
122
+ }[];
123
+ } | {
124
+ tenantId: string;
125
+ screenId: string | null;
126
+ id: string;
127
+ projectId: string;
128
+ source: string;
129
+ status: string;
130
+ createdAt: Date;
131
+ updatedAt: Date;
132
+ steps: import("@prisma/client/runtime/library").JsonValue;
133
+ kind: string;
134
+ tags: import("@prisma/client/runtime/library").JsonValue;
135
+ approvedBy: string | null;
136
+ suiteId: string;
137
+ ref: string | null;
138
+ title: string;
139
+ priority: string;
140
+ preconditions: string | null;
141
+ expectedResult: string | null;
142
+ gherkin: string | null;
143
+ approvedAt: Date | null;
144
+ groundedOn: import("@prisma/client/runtime/library").JsonValue;
145
+ caseRefs: import("@prisma/client/runtime/library").JsonValue;
146
+ viewports: import("@prisma/client/runtime/library").JsonValue;
147
+ viewportConstraint: string;
148
+ techniqueOfRecord: string | null;
149
+ traceTo: string | null;
150
+ traceToExtra: import("@prisma/client/runtime/library").JsonValue;
151
+ testDataInline: import("@prisma/client/runtime/library").JsonValue | null;
152
+ dataNote: string | null;
153
+ scriptNote: string | null;
154
+ structuredSteps: import("@prisma/client/runtime/library").JsonValue | null;
155
+ artifactId: string | null;
156
+ scenarioId: string | null;
157
+ suspect: boolean;
158
+ suspectReason: string | null;
159
+ flakyScore: number;
160
+ consecutiveFlakes: number;
161
+ lastFlakyAt: Date | null;
162
+ quarantined: boolean;
163
+ quarantinedAt: Date | null;
164
+ quarantineReason: string | null;
165
+ genPromptTokens: number | null;
166
+ genCompletionTokens: number | null;
167
+ genCostUsd: number | null;
168
+ genModel: string | null;
169
+ dataSetId: string | null;
170
+ repoTargetPath: string | null;
171
+ stepsKey: string | null;
172
+ gherkinKey: string | null;
173
+ contentHash: string | null;
174
+ groundingHash: string | null;
175
+ locatorsJson: import("@prisma/client/runtime/library").JsonValue | null;
176
+ locatorsExtractedAt: Date | null;
177
+ captureStepScreenshots: import("@prisma/client/runtime/library").JsonValue | null;
178
+ };
179
+ warning: string | undefined;
180
+ error?: undefined;
181
+ description?: undefined;
182
+ }>;
183
+ export interface ExtractedLocator {
184
+ key: string;
185
+ expression: string;
186
+ elementType: string;
187
+ confidence: 'high' | 'medium' | 'low';
188
+ source: string;
189
+ description: string;
190
+ }
191
+ /**
192
+ * Use an LLM to extract Playwright locators from test case steps of any format.
193
+ * Handles structured (QA Wolf), plain-English (AI-generated), or mixed step formats.
194
+ * Falls back to empty array if no LLM is configured or extraction fails.
195
+ */
196
+ export declare function extractLocatorsWithLLM(opts: {
197
+ tenantId: string;
198
+ steps: Array<Record<string, unknown> | string>;
199
+ dataFields?: Array<{
200
+ fieldKey: string;
201
+ exampleValue: string;
202
+ }>;
203
+ tcContext?: {
204
+ title?: string;
205
+ gherkin?: string;
206
+ preconditions?: string;
207
+ };
208
+ }): Promise<ExtractedLocator[]>;