Model is defined by your deployment name.
70 | 71 |├── LICENSE ├── style.css ├── README.md ├── index.html └── script.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Varshil Desai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #6366f1; 3 | --secondary-color: #a78bfa; 4 | --accent-color: #f472b6; 5 | --bg-color-start: #1e1b4b; 6 | --bg-color-end: #312e81; 7 | --text-color: #e5e7eb; 8 | --text-color-dark: #9ca3af; 9 | --card-bg-color: rgba(255, 255, 255, 0.05); 10 | --card-border-color: rgba(255, 255, 255, 0.1); 11 | --input-bg-color: rgba(0, 0, 0, 0.2); 12 | --kql-bg-color: #0f172a; 13 | --kql-text-color: #94a3b8; 14 | --header-text-color: white; 15 | --header-subtext-color: #e5e7eb; 16 | } 17 | 18 | body[data-theme="light"] { 19 | --primary-color: #4f46e5; 20 | --secondary-color: #7c3aed; 21 | --accent-color: #ec4899; 22 | --bg-color-start: #f3f4f6; 23 | --bg-color-end: #e5e7eb; 24 | --text-color: #1f2937; 25 | --text-color-dark: #6b7280; 26 | --card-bg-color: rgba(255, 255, 255, 0.7); 27 | --card-border-color: rgba(0, 0, 0, 0.1); 28 | --input-bg-color: rgba(0, 0, 0, 0.05); 29 | --kql-bg-color: #f8fafc; 30 | --kql-text-color: #334155; 31 | --header-text-color: #111827; 32 | --header-subtext-color: #374151; 33 | } 34 | 35 | body { 36 | font-family: 'Inter', sans-serif; 37 | background-color: var(--bg-color-start); 38 | color: var(--text-color); 39 | margin: 0; 40 | overflow-x: hidden; 41 | transition: background-color 0.3s, color 0.3s; 42 | } 43 | 44 | .background-aurora { 45 | position: fixed; 46 | top: 0; 47 | left: 0; 48 | width: 100%; 49 | height: 100%; 50 | background: radial-gradient(ellipse at top, var(--primary-color), transparent), 51 | radial-gradient(ellipse at bottom, var(--accent-color), transparent); 52 | opacity: 0.3; 53 | z-index: -1; 54 | animation: aurora 20s infinite linear; 55 | transition: opacity 0.3s; 56 | } 57 | 58 | body[data-theme="light"] .background-aurora { 59 | opacity: 0.15; 60 | } 61 | 62 | @keyframes aurora { 63 | from { transform: rotate(0deg); } 64 | to { transform: rotate(360deg); } 65 | } 66 | 67 | /* Custom styles to complement Tailwind */ 68 | .card { 69 | background-color: var(--card-bg-color); 70 | border: 1px solid var(--card-border-color); 71 | backdrop-filter: blur(10px); 72 | -webkit-backdrop-filter: blur(10px); 73 | transition: background-color 0.3s, border-color 0.3s; 74 | } 75 | 76 | .card-title { 77 | color: var(--text-color); 78 | } 79 | 80 | .card-subtitle { 81 | color: var(--text-color-dark); 82 | } 83 | 84 | .btn-primary { 85 | background: linear-gradient(to right, var(--primary-color), var(--secondary-color)); 86 | color: white; 87 | } 88 | .btn-primary:hover:not(:disabled) { 89 | transform: translateY(-2px); 90 | box-shadow: 0 4px 15px rgba(0,0,0,0.2); 91 | } 92 | 93 | .btn-secondary { 94 | background-color: rgba(255, 255, 255, 0.1); 95 | color: var(--text-color); 96 | border: 1px solid var(--card-border-color); 97 | } 98 | 99 | body[data-theme="dark"] .btn-secondary { 100 | color: white; 101 | } 102 | 103 | .btn-secondary:hover:not(:disabled) { 104 | background-color: rgba(255, 255, 255, 0.2); 105 | } 106 | 107 | body[data-theme="light"] .btn-secondary:hover:not(:disabled) { 108 | background-color: rgba(0, 0, 0, 0.05); 109 | } 110 | 111 | .toggle-container { 112 | background-color: var(--input-bg-color); 113 | } 114 | .toggle-btn { 115 | color: var(--text-color-dark); 116 | } 117 | .toggle-btn.active { 118 | background-color: var(--primary-color); 119 | color: white; 120 | box-shadow: 0 2px 5px rgba(0,0,0,0.2); 121 | } 122 | 123 | .input-field { 124 | background-color: var(--input-bg-color); 125 | border: 1px solid var(--card-border-color); 126 | color: var(--text-color); 127 | } 128 | .input-field:focus { 129 | outline: none; 130 | border-color: var(--primary-color); 131 | box-shadow: 0 0 0 2px var(--primary-color); 132 | } 133 | 134 | .kql-query { 135 | background-color: var(--kql-bg-color); 136 | color: var(--kql-text-color); 137 | border: 1px solid var(--card-border-color); 138 | transition: background-color 0.3s, color 0.3s, border-color 0.3s; 139 | } 140 | .kql-query .comment { 141 | color: #64748b; 142 | } 143 | .kql-query .copy-btn { 144 | background-color: #374151; 145 | color: #e5e7eb; 146 | } 147 | 148 | .modal-overlay { 149 | background-color: rgba(0, 0, 0, 0.5); 150 | backdrop-filter: blur(5px); 151 | -webkit-backdrop-filter: blur(5px); 152 | z-index: 100; /* Ensure modal is on top */ 153 | } 154 | 155 | .message-box { 156 | transition: opacity 0.3s, transform 0.3s; 157 | z-index: 101; 158 | } 159 | .message-box:not(.hidden) { 160 | opacity: 1; 161 | transform: translateY(0); 162 | } 163 | .message-box.success { background-color: #10b981; } 164 | .message-box.error { background-color: #ef4444; } 165 | 166 | /* Theme-aware text colors */ 167 | header h1 { 168 | color: var(--header-text-color); 169 | } 170 | header p { 171 | color: var(--header-subtext-color); 172 | } 173 | .input-label { 174 | color: var(--text-color-dark); 175 | } 176 | .modal-content h2, .modal-content h3 { 177 | color: var(--text-color); 178 | } 179 | .prose { 180 | color: var(--text-color); 181 | } 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
10 |
11 |
12 |
13 |
Go from unstructured report to actionable hunt in seconds.
19 |Model is defined by your deployment name.
70 | 71 |// ${title}
173 |${sanitizedQuery}
`; 174 | 175 | const copyBtn = div.querySelector('.copy-btn'); 176 | copyBtn.addEventListener('click', (e) => copyToClipboard(query, e.target)); 177 | 178 | div.addEventListener('mouseover', () => copyBtn.style.opacity = '1'); 179 | div.addEventListener('mouseout', () => copyBtn.style.opacity = '0'); 180 | 181 | return div; 182 | } 183 | 184 | function copyToClipboard(text, buttonElement) { 185 | navigator.clipboard.writeText(text).then(() => { 186 | buttonElement.textContent = 'Copied!'; 187 | showMessage('Copied to clipboard!', 'success'); 188 | setTimeout(() => { buttonElement.textContent = 'Copy'; }, 2000); 189 | }).catch(err => { 190 | console.error('Failed to copy text: ', err); 191 | showMessage('Failed to copy.', 'error'); 192 | }); 193 | } 194 | 195 | function setLoadingState(isLoading, type) { 196 | const elements = { 197 | analyze: { spinner: dom.spinner, btn: dom.analyzeBtn }, 198 | summary: { spinner: dom.summarySpinner, btn: dom.generateSummaryBtn }, 199 | ai: { spinner: dom.aiSpinner, btn: dom.suggestQueriesBtn }, 200 | mitigation: { spinner: dom.mitigationSpinner, btn: dom.suggestMitigationsBtn } 201 | }; 202 | const el = elements[type]; 203 | if (el) { 204 | el.spinner.classList.toggle('hidden', !isLoading); 205 | el.btn.disabled = isLoading; 206 | } 207 | } 208 | 209 | function resetResults() { 210 | dom.resultsSection.classList.add('hidden'); 211 | state.extractedIOCs = {}; 212 | state.lastAnalyzedText = ''; 213 | state.lastSummary = ''; 214 | 215 | [dom.guaranteedQueriesContainer, dom.threatSummaryContainer, dom.aiAssistedContainer, dom.mitigationContainer].forEach(c => c.classList.add('hidden')); 216 | dom.guaranteedQueriesList.innerHTML = ''; 217 | dom.threatSummaryContent.innerHTML = ''; 218 | dom.aiAssistedQueriesList.innerHTML = ''; 219 | dom.mitigationContent.innerHTML = ''; 220 | 221 | dom.summaryControls.classList.remove('hidden'); 222 | dom.mitigationControls.classList.remove('hidden'); 223 | } 224 | 225 | // --- Theme Management --- 226 | function applyInitialTheme() { 227 | const savedTheme = localStorage.getItem('kqlintel-theme') || 'dark'; 228 | setTheme(savedTheme); 229 | } 230 | 231 | function handleThemeToggle() { 232 | const currentTheme = document.body.dataset.theme; 233 | const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; 234 | setTheme(newTheme); 235 | } 236 | 237 | function setTheme(theme) { 238 | document.body.dataset.theme = theme; 239 | localStorage.setItem('kqlintel-theme', theme); 240 | if (theme === 'light') { 241 | dom.themeSunIcon.classList.add('hidden'); 242 | dom.themeMoonIcon.classList.remove('hidden'); 243 | } else { 244 | dom.themeSunIcon.classList.remove('hidden'); 245 | dom.themeMoonIcon.classList.add('hidden'); 246 | } 247 | } 248 | 249 | // --- API Key Management --- 250 | function saveApiKeys() { 251 | state.apiKeys = { 252 | google: dom.googleKeyInput.value, 253 | openai: dom.openaiKeyInput.value, 254 | anthropic: dom.anthropicKeyInput.value, 255 | openrouter: dom.openrouterKeyInput.value, 256 | azure: { 257 | key: dom.azureKeyInput.value, 258 | endpoint: dom.azureEndpointInput.value, 259 | deployment: dom.azureDeploymentInput.value, 260 | } 261 | }; 262 | localStorage.setItem('kqlintel-apikeys', JSON.stringify(state.apiKeys)); 263 | showMessage('API Keys saved!', 'success'); 264 | dom.settingsModal.classList.add('hidden'); 265 | } 266 | 267 | function loadApiKeys() { 268 | const savedKeys = localStorage.getItem('kqlintel-apikeys'); 269 | if (savedKeys) { 270 | state.apiKeys = JSON.parse(savedKeys); 271 | dom.googleKeyInput.value = state.apiKeys.google || ''; 272 | dom.openaiKeyInput.value = state.apiKeys.openai || ''; 273 | dom.anthropicKeyInput.value = state.apiKeys.anthropic || ''; 274 | dom.openrouterKeyInput.value = state.apiKeys.openrouter || ''; 275 | if (state.apiKeys.azure) { 276 | dom.azureKeyInput.value = state.apiKeys.azure.key || ''; 277 | dom.azureEndpointInput.value = state.apiKeys.azure.endpoint || ''; 278 | dom.azureDeploymentInput.value = state.apiKeys.azure.deployment || ''; 279 | } 280 | } 281 | } 282 | 283 | function checkApiKey(provider) { 284 | if (provider === 'azure') { 285 | if (!state.apiKeys.azure || !state.apiKeys.azure.key || !state.apiKeys.azure.endpoint || !state.apiKeys.azure.deployment) { 286 | showMessage(`Azure credentials are incomplete. Please add Key, Endpoint, and Deployment Name in Settings.`, 'error'); 287 | dom.settingsModal.classList.remove('hidden'); 288 | return false; 289 | } 290 | } else if (!state.apiKeys[provider]) { 291 | showMessage(`API Key for ${provider} is missing. Please add it in Settings.`, 'error'); 292 | dom.settingsModal.classList.remove('hidden'); 293 | return false; 294 | } 295 | return true; 296 | } 297 | 298 | // --- Core Logic --- 299 | async function handleAnalysis() { 300 | if (!checkApiKey(dom.providerSelect.value)) return; 301 | 302 | setLoadingState(true, 'analyze'); 303 | resetResults(); 304 | 305 | try { 306 | const isUrlMode = dom.toggleUrlBtn.classList.contains('active'); 307 | let textToAnalyze; 308 | let sourceDomain = null; 309 | 310 | if (isUrlMode) { 311 | const urlValue = dom.urlInput.value.trim(); 312 | if (!urlValue) { 313 | showMessage('Please provide a URL to analyze.', 'error'); 314 | setLoadingState(false, 'analyze'); 315 | return; 316 | } 317 | try { 318 | const url = new URL(urlValue); 319 | sourceDomain = url.hostname; 320 | } catch (e) { 321 | showMessage('Invalid URL provided.', 'error'); 322 | setLoadingState(false, 'analyze'); 323 | return; 324 | } 325 | textToAnalyze = await fetchUrlContent(urlValue); 326 | if (!textToAnalyze) { 327 | setLoadingState(false, 'analyze'); 328 | return; 329 | } 330 | } else { 331 | textToAnalyze = dom.textInput.value.trim(); 332 | if (!textToAnalyze) { 333 | showMessage('Please provide some text to analyze.', 'error'); 334 | setLoadingState(false, 'analyze'); 335 | return; 336 | } 337 | } 338 | 339 | state.lastAnalyzedText = textToAnalyze; 340 | showMessage('Analyzing text with AI...', 'success'); 341 | 342 | let iocPrompt = `You are an expert cybersecurity threat intelligence analyst with a specialization in parsing unstructured reports. Your primary task is to extract ONLY malicious Indicators of Compromise (IOCs) from the provided text. 343 | 344 | **CRITICAL INSTRUCTIONS:** 345 | 1. **Focus on Explicit IOCs:** Give the highest priority to items listed under explicit headings like "Indicators of Compromise", "IOCs", "Malicious Hashes", "C2 Domains", etc. If no such section exists, be extremely cautious. 346 | 2. **Context is Key:** Do NOT extract every domain, URL, or IP address you find. Analyze the surrounding text. An IOC is typically presented as a threat artifact. A URL in a reference link at the bottom of a page is NOT an IOC. 347 | 3. **Ignore Legitimate & Reference Domains:** You MUST ignore common, legitimate domains and URLs unless they are explicitly identified as malicious. This includes: 348 | * The source domain of the report itself (${sourceDomain || 'the source website'}). 349 | * Major tech and security company domains (e.g., microsoft.com, google.com, apple.com, virustotal.com, github.com). 350 | * Social media links (e.g., twitter.com, linkedin.com). 351 | * URLs in footnotes, references, or "further reading" sections. 352 | 4. **Identify True IOCs:** Look for file hashes (MD5, SHA1, SHA256), suspicious IP addresses, command-and-control (C2) domains, malicious filenames (e.g., malware.exe, payload.dll), and URLs pointing directly to malicious content. 353 | 5. **Be Exhaustive:** You MUST extract ALL indicators of each type from the entire text. Do not stop after finding just one. Scrutinize the entire provided text, especially tables and lists, to ensure every single IOC is included in your final JSON response. 354 | 355 | From the text below, extract the IOCs and return them as a valid JSON object with the following keys: "ipv4", "domain", "md5", "sha1", "sha256", "filename", "url". If you find absolutely no items that you can confidently identify as malicious IOCs based on these rules, return a JSON object with empty arrays for each key. 356 | 357 | Text to analyze: 358 | --- 359 | ${textToAnalyze} 360 | ---`; 361 | 362 | const iocSchema = { type: "OBJECT", properties: { "ipv4": { "type": "ARRAY", "items": { "type": "STRING" } }, "domain": { "type": "ARRAY", "items": { "type": "STRING" } }, "md5": { "type": "ARRAY", "items": { "type": "STRING" } }, "sha1": { "type": "ARRAY", "items": { "type": "STRING" } }, "sha256": { "type": "ARRAY", "items": { "type": "STRING" } }, "filename": { "type": "ARRAY", "items": { "type": "STRING" } }, "url": { "type": "ARRAY", "items": { "type": "STRING" } } } }; 363 | 364 | const iocsResult = await callLLM(iocPrompt, iocSchema); 365 | const iocs = JSON.parse(iocsResult); 366 | 367 | const iocsFound = iocs && Object.values(iocs).some(arr => arr.length > 0); 368 | 369 | dom.resultsSection.classList.remove('hidden'); 370 | dom.guaranteedQueriesContainer.classList.remove('hidden'); 371 | displayGuaranteedQueries(iocs); 372 | 373 | if (iocsFound) { 374 | state.extractedIOCs = iocs; 375 | dom.threatSummaryContainer.classList.remove('hidden'); 376 | dom.aiAssistedContainer.classList.remove('hidden'); 377 | dom.mitigationContainer.classList.remove('hidden'); 378 | showMessage("AI analysis successful! Found IOCs.", "success"); 379 | } else { 380 | showMessage("AI analysis complete. No standard IOCs found.", "success"); 381 | } 382 | } catch (error) { 383 | console.error("Analysis failed:", error); 384 | showMessage(`Analysis failed: ${error.message}`, 'error'); 385 | } finally { 386 | setLoadingState(false, 'analyze'); 387 | } 388 | } 389 | 390 | async function handleThreatSummary() { 391 | if (!checkApiKey(dom.providerSelect.value)) return; 392 | if (!state.lastAnalyzedText) { 393 | showMessage("Please run an analysis first.", 'error'); 394 | return; 395 | } 396 | setLoadingState(true, 'summary'); 397 | try { 398 | const prompt = `You are a senior cybersecurity analyst. Based on the following threat intelligence text and its extracted IOCs, provide a concise summary (2-3 paragraphs) for a security operations team. Explain the threat's nature, behavior, and impact. Return only the summary text itself, without any titles, JSON formatting, or other conversational text.\n\nIOCs:\n${JSON.stringify(state.extractedIOCs)}\n\nOriginal Text:\n---\n${state.lastAnalyzedText}\n---`; 399 | 400 | const summary = await callLLM(prompt, null); 401 | state.lastSummary = summary; // Save summary for mitigation step 402 | 403 | dom.threatSummaryContent.innerHTML = `${summary.replace(/\n\n/g, '
').replace(/\n/g, '
')}
${mitigations.replace(/\n\n/g, '
').replace(/\n/g, '
')}
No standard IOCs were found.
'; 479 | } 480 | } 481 | 482 | function displayAdvancedQueries(queries) { 483 | dom.aiAssistedQueriesList.innerHTML = ''; 484 | if (queries?.length > 0) { 485 | queries.forEach(q => dom.aiAssistedQueriesList.appendChild(createKqlQueryElement(q.title, q.query))); 486 | } else { 487 | dom.aiAssistedQueriesList.innerHTML = 'The AI could not suggest any advanced queries.
'; 488 | } 489 | } 490 | 491 | async function fetchUrlContent(url) { 492 | const readerApiUrl = `https://r.jina.ai/${encodeURIComponent(url)}`; 493 | showMessage('Fetching and parsing content with reader API...', 'success'); 494 | try { 495 | const controller = new AbortController(); 496 | const timeoutId = setTimeout(() => controller.abort(), 25000); 497 | 498 | const response = await fetch(readerApiUrl, { 499 | signal: controller.signal, 500 | headers: { 'Accept': 'text/plain' } 501 | }); 502 | 503 | clearTimeout(timeoutId); 504 | 505 | if (!response.ok) { 506 | throw new Error(`Reader API failed with status ${response.status}: ${await response.text()}`); 507 | } 508 | 509 | const textContent = await response.text(); 510 | 511 | if (!textContent || textContent.trim() === '') { 512 | throw new Error("Reader API returned empty content."); 513 | } 514 | 515 | return textContent; 516 | 517 | } catch (error) { 518 | let errorMsg = error.message; 519 | if (error.name === 'AbortError' || error.name === 'TimeoutError') { 520 | errorMsg = "Request to the reader API timed out after 25 seconds."; 521 | } 522 | console.error("URL content fetching failed:", error); 523 | showMessage(`Failed to fetch content: ${errorMsg}`, 'error'); 524 | return null; 525 | } 526 | } 527 | 528 | // --- LLM API Abstraction Layer --- 529 | async function callLLM(prompt, schema) { 530 | const provider = dom.providerSelect.value; 531 | const model = dom.modelSelect.value; 532 | const apiKey = state.apiKeys[provider]; 533 | 534 | const apiHandlers = { 535 | google: callGoogleAPI, 536 | openai: callOpenAIAPI, 537 | anthropic: callAnthropicAPI, 538 | openrouter: callOpenRouterAPI, 539 | azure: callAzureOpenAIAPI, 540 | }; 541 | 542 | if (!apiHandlers[provider]) { 543 | throw new Error(`Unsupported provider: ${provider}`); 544 | } 545 | 546 | return apiHandlers[provider](apiKey, model, prompt, schema); 547 | } 548 | 549 | // --- Remote APIs (Key Required) --- 550 | async function callGoogleAPI(apiKey, model, prompt, schema) { 551 | const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; 552 | 553 | let finalPrompt = prompt; 554 | if (schema) { 555 | finalPrompt = `${prompt}\n\nImportant: Respond with ONLY a valid JSON object that conforms to the following schema. Do not include any other text, comments, or markdown formatting.\n\nSchema: ${JSON.stringify(schema)}`; 556 | } 557 | 558 | const payload = { 559 | contents: [{ role: "user", parts: [{ text: finalPrompt }] }], 560 | generationConfig: { temperature: 0.1 } 561 | }; 562 | 563 | const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); 564 | if (!response.ok) { 565 | const errorBody = await response.json(); 566 | throw new Error(`Google API Error: ${response.statusText} - ${errorBody.error?.message || 'Unknown error'}`); 567 | } 568 | const result = await response.json(); 569 | const textResponse = result.candidates?.[0]?.content?.parts?.[0]?.text; 570 | 571 | if (!textResponse) { 572 | const reason = result.promptFeedback?.blockReason || 'Unknown reason'; 573 | if(reason !== 'Unknown reason') throw new Error(`Request blocked by Google API. Reason: ${reason}`); 574 | throw new Error("Invalid or empty response from Google API."); 575 | } 576 | 577 | if (!schema) { 578 | return textResponse; 579 | } 580 | 581 | const jsonMatch = textResponse.match(/\{[\s\S]*\}|\[[\s\S]*\]/); 582 | if (!jsonMatch) throw new Error("Google API did not return valid JSON."); 583 | return jsonMatch[0]; 584 | } 585 | 586 | async function callOpenAIAPI(apiKey, model, prompt, schema) { 587 | const url = 'https://api.openai.com/v1/chat/completions'; 588 | const payload = { 589 | model: model, 590 | messages: [{ role: "user", content: prompt }], 591 | temperature: 0.1 592 | }; 593 | 594 | if (schema) { 595 | payload.response_format = { "type": "json_object" }; 596 | } 597 | 598 | const response = await fetch(url, { 599 | method: 'POST', 600 | headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, 601 | body: JSON.stringify(payload) 602 | }); 603 | 604 | if (!response.ok) { 605 | const errorBody = await response.json(); 606 | throw new Error(`OpenAI API Error: ${errorBody.error?.message || response.statusText}`); 607 | } 608 | const result = await response.json(); 609 | const textResponse = result.choices?.[0]?.message?.content; 610 | 611 | if (!textResponse) throw new Error("Invalid or empty response from OpenAI API."); 612 | 613 | if (!schema) { 614 | return textResponse; 615 | } 616 | 617 | return textResponse; 618 | } 619 | 620 | async function callAnthropicAPI(apiKey, model, prompt, schema) { 621 | const url = 'https://api.anthropic.com/v1/messages'; 622 | let systemPrompt = "You are a helpful assistant."; 623 | 624 | if (schema) { 625 | systemPrompt = `You are a helpful assistant designed to output JSON. Respond with a valid JSON object that conforms to this schema. Do not add any other text. Schema: ${JSON.stringify(schema)}`; 626 | } 627 | 628 | const payload = { 629 | model: model, 630 | max_tokens: 4096, 631 | system: systemPrompt, 632 | messages: [{ role: "user", content: prompt }], 633 | temperature: 0.2 634 | }; 635 | const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' }, body: JSON.stringify(payload) }); 636 | if (!response.ok) { 637 | const errorBody = await response.json(); 638 | throw new Error(`Anthropic API Error: ${errorBody.error?.message || response.statusText}`); 639 | } 640 | const result = await response.json(); 641 | const textResponse = result.content?.[0]?.text; 642 | 643 | if (!textResponse) throw new Error("Invalid or empty response from Anthropic API."); 644 | 645 | if (!schema) { 646 | return textResponse; 647 | } 648 | 649 | const jsonMatch = textResponse.match(/\{[\s\S]*\}|\[[\s\S]*\]/); 650 | if (!jsonMatch) throw new Error("Anthropic API did not return valid JSON."); 651 | return jsonMatch[0]; 652 | } 653 | 654 | async function callOpenRouterAPI(apiKey, model, prompt, schema) { 655 | const url = 'https://openrouter.ai/api/v1/chat/completions'; 656 | const payload = { 657 | model: model, 658 | messages: [{ role: "user", content: prompt }], 659 | temperature: 0.1 660 | }; 661 | 662 | if (schema) { 663 | payload.response_format = { "type": "json_object" }; 664 | } 665 | 666 | const response = await fetch(url, { 667 | method: 'POST', 668 | headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'HTTP-Referer': `${location.protocol}//${location.host}`, 'X-Title': 'KQLIntel' }, 669 | body: JSON.stringify(payload) 670 | }); 671 | if (!response.ok) { 672 | const errorBody = await response.json(); 673 | throw new Error(`OpenRouter API Error: ${errorBody.error?.message || response.statusText}`); 674 | } 675 | const result = await response.json(); 676 | const textResponse = result.choices?.[0]?.message?.content; 677 | 678 | if (!textResponse) throw new Error("Invalid or empty response from OpenRouter API."); 679 | 680 | if (!schema) { 681 | return textResponse; 682 | } 683 | 684 | return textResponse; 685 | } 686 | 687 | async function callAzureOpenAIAPI(apiCredentials, model, prompt, schema) { 688 | const { key, endpoint, deployment } = apiCredentials; 689 | const sanitizedEndpoint = endpoint.replace(/\/+$/, ""); 690 | const url = `${sanitizedEndpoint}/openai/deployments/${deployment}/chat/completions?api-version=2024-02-01`; 691 | 692 | const payload = { 693 | messages: [{ role: "user", content: prompt }], 694 | temperature: 0.1 695 | }; 696 | 697 | if (schema) { 698 | payload.response_format = { "type": "json_object" }; 699 | } 700 | 701 | const response = await fetch(url, { 702 | method: 'POST', 703 | headers: { 'Content-Type': 'application/json', 'api-key': key }, 704 | body: JSON.stringify(payload) 705 | }); 706 | 707 | if (!response.ok) { 708 | const errorBody = await response.json(); 709 | throw new Error(`Azure API Error: ${errorBody.error?.message || response.statusText}`); 710 | } 711 | const result = await response.json(); 712 | const textResponse = result.choices?.[0]?.message?.content; 713 | 714 | if (!textResponse) throw new Error("Invalid response from Azure API."); 715 | 716 | if (!schema) { 717 | return textResponse; 718 | } 719 | 720 | return textResponse; 721 | } 722 | 723 | 724 | // --- Run Initialization --- 725 | initialize(); 726 | --------------------------------------------------------------------------------