UNPKG

@chainlink/mcp-server

Version:
706 lines (683 loc) 35.6 kB
"use strict"; /** * @fileoverview Unified Chainlink Developer Assistant Tool * * OVERVIEW: * This is the main developer assistance tool for the Chainlink ecosystem. It provides * a single, intelligent interface that can answer any Chainlink-related question by * combining multiple data sources and using AI to generate comprehensive responses. * * KEY FEATURES: * - Smart chain detection: Automatically identifies which blockchain networks the user * is asking about and fetches focused configuration data for those specific chains * - Multi-source data: Combines fetched API data, static configuration files, and * documentation search to provide complete answers * - AI-powered responses: Uses LLM to generate natural language responses with * accurate technical details and working code examples * * ARCHITECTURE: * 1. Query Analysis: Extracts chain names from user queries without hardcoded lists * 2. Data Gathering: Fetches relevant data from multiple sources in parallel * 3. AI Generation: Provides all data to LLM for intelligent response generation * * EXAMPLE USAGE: * User asks: "How do I send tokens from Ethereum to Polygon?" * Tool automatically: * - Detects "Ethereum" and "Polygon" chains * - Fetches router addresses, chain selectors, and token data for both chains * - Finds lane information for Ethereum → Polygon transfers * - Generates working Solidity code with real addresses */ Object.defineProperty(exports, "__esModule", { value: true }); exports.chainlinkDeveloperAssistantHandler = exports.startChainlinkDeveloperAssistant = exports.matchChainsFromQuery = exports.searchVectorDatabase = exports.clearAllDataSourcesCaches = exports.setModuleVectorDb = exports.searchDataFeedsDataSources = exports.loadDataFeedsData = exports.loadDataFeedsIndex = exports.searchCcipDataSources = exports.getChainSpecificData = exports.extractChainNamesFromQuery = exports.loadCcipDataSources = exports.searchInCcipDataSources = exports.calculateRelevance = void 0; const zod_1 = require("zod"); const logger_1 = require("../utils/logger"); const index_1 = require("./index"); const service_factory_1 = require("../services/service-factory"); const database_1 = require("../vectordb/database"); const search_1 = require("./products/ccip/search"); const search_2 = require("./products/data-feeds/search"); const search_3 = require("./products/cre/search"); const cli_1 = require("./products/cre/cli"); const docs_1 = require("./products/cre/docs"); const cache_1 = require("./products/ccip/cache"); const cache_2 = require("./products/data-feeds/cache"); const matching_1 = require("./products/shared/matching"); const matching_2 = require("./products/shared/matching"); const strings_1 = require("./products/shared/strings"); var search_4 = require("./products/shared/search"); Object.defineProperty(exports, "calculateRelevance", { enumerable: true, get: function () { return search_4.calculateRelevance; } }); Object.defineProperty(exports, "searchInCcipDataSources", { enumerable: true, get: function () { return search_4.searchInCcipDataSources; } }); var load_1 = require("./products/ccip/load"); Object.defineProperty(exports, "loadCcipDataSources", { enumerable: true, get: function () { return load_1.loadCcipDataSources; } }); var search_5 = require("./products/ccip/search"); Object.defineProperty(exports, "extractChainNamesFromQuery", { enumerable: true, get: function () { return search_5.extractChainNamesFromQuery; } }); Object.defineProperty(exports, "getChainSpecificData", { enumerable: true, get: function () { return search_5.getChainSpecificData; } }); Object.defineProperty(exports, "searchCcipDataSources", { enumerable: true, get: function () { return search_5.searchCcipDataSources; } }); var load_2 = require("./products/data-feeds/load"); Object.defineProperty(exports, "loadDataFeedsIndex", { enumerable: true, get: function () { return load_2.loadDataFeedsIndex; } }); Object.defineProperty(exports, "loadDataFeedsData", { enumerable: true, get: function () { return load_2.loadDataFeedsData; } }); var search_6 = require("./products/data-feeds/search"); Object.defineProperty(exports, "searchDataFeedsDataSources", { enumerable: true, get: function () { return search_6.searchDataFeedsDataSources; } }); /** * Vector database instance for documentation search. * Initialized when the tool starts up, used to search through Chainlink docs. */ let moduleVectorDb = null; /** * Test utility: Allows tests to inject a mock vector database */ const setModuleVectorDb = (db) => { moduleVectorDb = db; }; exports.setModuleVectorDb = setModuleVectorDb; /** * Test utility: Clears all configuration caches between test runs */ const clearAllDataSourcesCaches = () => { (0, cache_1.clearCcipCaches)(); (0, cache_2.clearDataFeedsCaches)(); (0, matching_1.clearChainKeywordIndexCache)(); }; exports.clearAllDataSourcesCaches = clearAllDataSourcesCaches; const searchVectorDatabase = async (query) => { if (!moduleVectorDb) { throw new Error("Vector database not initialized"); } try { const results = await moduleVectorDb.searchSimilar(query, 10); return results; } catch (error) { logger_1.Logger.log("error", "Vector search failed"); throw error; } }; exports.searchVectorDatabase = searchVectorDatabase; const matchChainsFromQuery = (queryChainNames, availableChains, sourceType) => { return (0, matching_2.matchChainsFromQuery)(queryChainNames, availableChains, sourceType); }; exports.matchChainsFromQuery = matchChainsFromQuery; const api_1 = require("./products/ccip/api"); const search_7 = require("./products/ccip/search"); /** * Attempts to extract a CCIP message id (0x + 64 hex chars) from a free-form query. */ function extractCcipMessageId(query) { const match = query.match(/0x[a-fA-F0-9]{64}\b/); return match ? match[0] : null; } /** * Attempts to extract lane latency params (source/dest selectors) from a query. * Strategy: * 1) If the query mentions latency/lane and contains two long decimal numbers, treat them as selectors * 2) Otherwise, map known chain names in order of appearance to selectors (best-effort) */ function extractLatencyParams(query) { const q = query.toLowerCase(); const mentionsLatency = /\b(latency|lane|delay)\b/.test(q); if (mentionsLatency) { const nums = q.match(/\b\d{12,20}\b/g); if (nums && nums.length >= 2) { return { sourceChainSelector: String(nums[0]), destChainSelector: String(nums[1]), }; } } return null; } /** * Attempts to infer latency params using loaded CCIP chain results. * Picks the first two chain entries whose keys or names appear in the query. */ function inferLatencyFromCcipResults(query, ccipResults) { const q = query.toLowerCase(); const chainEntries = (ccipResults || []) .filter((r) => r?.dataType === "chains" && r?.data?.chainSelector) .map((r) => ({ key: String(r.key || "").toLowerCase(), selector: String(r.data.chainSelector), relevance: r.relevance || 0, })); if (chainEntries.length === 0) return null; const hits = []; for (const entry of chainEntries) { const idx = q.indexOf(entry.key); if (idx !== -1) hits.push({ index: idx, selector: entry.selector }); } if (hits.length < 2) return null; hits.sort((a, b) => a.index - b.index); const uniqueSelectors = Array.from(new Set(hits.map((h) => h.selector))); if (uniqueSelectors.length >= 2) { return { sourceChainSelector: uniqueSelectors[0], destChainSelector: uniqueSelectors[1], }; } return null; } /** * Resolve latency selectors using shared matching against CCIP config */ async function resolveLatencySelectorsFromConfig(query) { try { const chainNames = await (0, search_7.extractChainNamesFromQuery)(query); if (!Array.isArray(chainNames) || chainNames.length < 2) return null; const data = await (0, search_7.getChainSpecificData)(chainNames); const q = query.toLowerCase(); const entries = []; for (const [chainKey, envMap] of Object.entries(data.chains || {})) { const mainnet = envMap["mainnet"]; const testnet = envMap["testnet"]; const cfg = mainnet || testnet; if (cfg && cfg.chainSelector) { const keyLower = chainKey.toLowerCase(); const idx = q.indexOf(keyLower); entries.push({ key: chainKey, selector: String(cfg.chainSelector), index: idx === -1 ? Number.MAX_SAFE_INTEGER : idx, }); } } if (entries.length < 2) return null; entries.sort((a, b) => a.index - b.index); const unique = Array.from(new Set(entries.map((e) => e.selector))); if (unique.length >= 2) { return { sourceChainSelector: unique[0], destChainSelector: unique[1], }; } } catch { } return null; } const startChainlinkDeveloperAssistant = async (server) => { try { moduleVectorDb = new database_1.VectorDatabase(); await moduleVectorDb.initialize(); logger_1.Logger.log("info", "Vector database initialized for developer assistant"); } catch (error) { logger_1.Logger.log("error", `Failed to initialize vector database: ${error}`); moduleVectorDb = null; } server.tool("chainlink_developer_assistant", index_1.capabilities.tools.chainlink_developer_assistant.description, { query: zod_1.z .string() .describe("Your Chainlink question in natural language. Examples: 'What is the CCIP router address for Ethereum mainnet?', 'Show a CRE HTTP fetch workflow example', 'What is the ETH/USD price feed proxy on Arbitrum?'."), }, exports.chainlinkDeveloperAssistantHandler); }; exports.startChainlinkDeveloperAssistant = startChainlinkDeveloperAssistant; const ChainlinkDeveloperAssistantParamsSchema = zod_1.z.object({ query: zod_1.z.string().min(1, "Query cannot be empty"), }); const chainlinkDeveloperAssistantHandler = async (params, _extra) => { try { ChainlinkDeveloperAssistantParamsSchema.parse(params); } catch (error) { const validationError = `Invalid parameters: ${error}`; logger_1.Logger.log("error", validationError); return { content: [{ type: "text", text: validationError }], isError: true, }; } let responseGeneratorLLM; try { responseGeneratorLLM = await service_factory_1.AIServiceFactory.createService(); } catch (error) { const configErrorMsg = `Response generator LLM configuration error: ${error}`; logger_1.Logger.log("error", configErrorMsg); return { content: [{ type: "text", text: configErrorMsg }], isError: true, }; } const { query } = params; logger_1.Logger.log("info", `Processing developer query: ${query.substring(0, 100)}...`); try { let combinedResults = { ccipDataSources: [], dataFeeds: [], creWorkflows: [], creCliDocs: [], creDocSnippets: [], documentationData: [], liveCcip: null, }; const promises = []; // Simple intent classification to gate expensive sources const q = query.toLowerCase(); const isCcipQuery = q.includes("ccip") || q.includes("cross-chain") || q.includes("lane") || q.includes("router"); const isDataFeedsQuery = q.includes("data feed") || q.includes("price feed") || q.includes("feeds") || q.includes("aggregator"); const isCreQuery = q.split(/[^a-z0-9]+/).includes("cre") || q.includes("runtime environment") || q.includes("workflow") || q.includes("cli"); /** * Wraps a promise with a timeout to prevent individual search operations from * hanging the entire handler. * * All search promises are awaited with Promise.all(), so if any single search * hangs (due to slow file I/O, network issues, or vector DB queries), the entire * handler would wait indefinitely. This wrapper ensures each search resolves * within TIMEOUT_MS, allowing graceful degradation: if one search times out, * other searches can still complete and return partial results. * * @param p - The promise to wrap with a timeout * @param ms - Timeout duration in milliseconds * @param type - Search type identifier for error tracking * @returns Promise that resolves with either the search result or a timeout error */ const withTimeout = async (p, ms, type) => { return Promise.race([ p, new Promise((resolve) => setTimeout(() => resolve({ type, error: "timeout" }), ms)), ]); }; const TIMEOUT_MS = 8000; if (isCcipQuery) { promises.push(withTimeout((0, search_1.searchCcipDataSources)(query) .then((data) => ({ type: "ccipDataSources", data })) .catch((error) => ({ type: "ccipDataSources", error: error.message, })), TIMEOUT_MS, "ccipDataSources")); } if (isDataFeedsQuery) { promises.push(withTimeout((0, search_2.searchDataFeedsDataSources)(query) .then((data) => ({ type: "dataFeeds", data })) .catch((error) => ({ type: "dataFeeds", error: error.message, })), TIMEOUT_MS, "dataFeeds")); } if (isCreQuery) { promises.push(withTimeout((0, search_3.searchCreWorkflows)(query) .then((data) => ({ type: "creWorkflows", data })) .catch((error) => ({ type: "creWorkflows", error: error.message, })), TIMEOUT_MS, "creWorkflows")); // CRE CLI docs (commands and options) promises.push(withTimeout((0, cli_1.searchCreCliDocs)(query) .then((data) => ({ type: "creCliDocs", data })) .catch((error) => ({ type: "creCliDocs", error: String(error), })), TIMEOUT_MS, "creCliDocs")); // Private CRE docs code snippet fallback promises.push(withTimeout((0, docs_1.searchPrivateCreDocs)(query) .then((data) => ({ type: "creDocSnippets", data })) .catch((error) => ({ type: "creDocSnippets", error: String(error), })), TIMEOUT_MS, "creDocSnippets")); } if (moduleVectorDb) { promises.push((0, exports.searchVectorDatabase)(query) .then((data) => ({ type: "documentation", data })) .catch((error) => ({ type: "documentation", error: error.message, }))); } // Live CCIP fetch: if a message id is present, fetch its status now const detectedMessageId = extractCcipMessageId(query); if (detectedMessageId) { promises.push((0, api_1.fetchCcipMessageById)(detectedMessageId) .then((data) => ({ type: "liveCcip", data })) .catch((error) => ({ type: "liveCcip", error: String(error) }))); } // Live CCIP latency fetch: try direct extraction; if not found, infer from config results let detectedLatency = extractLatencyParams(query); if (!detectedLatency) { try { await Promise.allSettled(promises); } catch { } detectedLatency = inferLatencyFromCcipResults(query, combinedResults.ccipDataSources) || (await resolveLatencySelectorsFromConfig(query)); } if (detectedLatency) { promises.push((0, api_1.fetchCcipLaneLatency)(detectedLatency) .then((data) => ({ type: "liveLatency", data })) .catch((error) => ({ type: "liveLatency", error: String(error), }))); } const results = await Promise.all(promises); for (const result of results) { if (result.error) { logger_1.Logger.log("warn", `${result.type} query failed: ${result.error}`); continue; } switch (result.type) { case "ccipDataSources": combinedResults.ccipDataSources = result.data.sort((a, b) => (b.relevance || 0) - (a.relevance || 0)); break; case "dataFeeds": combinedResults.dataFeeds = result.data.sort((a, b) => (b.relevance || 0) - (a.relevance || 0)); break; case "creWorkflows": combinedResults.creWorkflows = result.data.sort((a, b) => (b.relevance || 0) - (a.relevance || 0)); break; case "documentation": combinedResults.documentationData = result.data; break; case "creCliDocs": combinedResults.creCliDocs = Array.isArray(result.data) ? result.data : []; break; case "creDocSnippets": combinedResults.creDocSnippets = Array.isArray(result.data) ? result.data : []; break; case "liveCcip": combinedResults.liveCcip = result.data || null; break; case "liveLatency": combinedResults.liveLatency = result.data || null; break; } } // Do not append data source status to responses; keep answers concise const statusMessage = ""; const supportedProducts = [ "CCIP (Cross-Chain Interoperability Protocol)", "Data Feeds (Price Feeds)", "CRE (Chainlink Runtime Environment)", ]; const systemPrompt = `<purpose>You are a Chainlink developer assistant specialized in the following products: ${supportedProducts.join(", ")}. You have access to comprehensive Chainlink ecosystem data sources. Use the configuration_guidelines to determine source material. Follow configuration_rules for answers and output_guidelines for your responses.</purpose> <supported_products> ${supportedProducts.map((product) => ` <product>${product}</product>`).join("\n")} </supported_products> <available_data_sources> <source name="CCIP Data Sources" priority="PRIMARY">Official Chainlink JSON configuration files with chains, tokens, and CCIP lanes information - THIS IS YOUR PRIMARY SOURCE FOR ALL CCIP ADDRESSES AND SELECTORS</source> <source name="CCIP Live API" priority="PRIMARY">Use the live CCIP API for real-time data such as message status and lane latency. Endpoints include /v1/message/{messageId} and /v1/lanes/latency?sourceChainSelector=...&destChainSelector=... (host: https://api.ccip.chain.link)</source> <source name="Data Feeds Sources" priority="PRIMARY">Official Chainlink Data Feeds configuration files with price feed contract addresses, asset names, decimals, and feed types across all supported networks - THIS IS YOUR PRIMARY SOURCE FOR ALL DATA FEEDS ADDRESSES</source> <source name="CRE Sources" priority="PRIMARY">CRE SDK workflow examples and vectorized CRE docs (workflows, triggers, capabilities, and runtime patterns)</source> <source name="Documentation Database" priority="PRIMARY">Vector search across Chainlink documentation and GitHub repositories covering CCIP, Data Feeds, CRE, and other Chainlink products with concepts, guides, and code examples</source> </available_data_sources> <configuration_rules> <rule priority="critical">For CCIP configuration data (router addresses, chain selectors), PRIORITIZE CCIP Data Sources (JSON files)</rule> <rule priority="critical">For CCIP runtime status (message status, lane latency), you MAY use the CCIP Live API endpoints directly to answer with current data</rule> <rule priority="critical">When LIVE CCIP data is provided below in &lt;ccip_live_data&gt; or &lt;ccip_latency_data&gt;, you MUST use it to answer directly. DO NOT instruct the user to query any API when live data is already provided.</rule> <rule priority="critical">For Data Feeds configuration data (price feed addresses, decimals, feed names), PRIORITIZE Data Feeds Sources (JSON files) - these contain the definitive feed addresses for each network</rule> <rule priority="critical">NEVER use addresses or selectors from Documentation Data or your training data</rule> <rule priority="critical">If you are not sure about the answer, say so and request further information from the user. NEVER make up an answer.</rule> <rule priority="important">If configuration data is not available in the respective data sources, explicitly state this limitation</rule> <rule priority="important">When providing code examples, use ONLY the addresses from the provided CCIP Data Sources, Data Feeds Sources, or Fetched API data</rule> <rule priority="important">For cross-chain queries, check lane availability between source and destination chains</rule> <rule priority="important">For Data Feeds queries, provide specific contract addresses from the Data Feeds Sources when available</rule> <rule priority="critical">For CRE-specific queries: ALWAYS ensure that the CRE SDK's capabilities are used instead of third party libraries. For example, use the EVM Read & Write capability to interact with the blockchain, and the HTTP capability to interact with external APIs.</rule> <rule priority="critical">For CRE-specific queries: ALWAYS respond with complete valid workflow code examples. A valid CRE workflow MUST include an initWorkflow (TypeScript) or InitWorkflow (Golang) entry point function and a trigger definition (e.g., cron, http, evm log).</rule> </configuration_rules> <output_guidelines> - Answer only with what is needed; no greetings, preambles, apologies, or meta-commentary. - Plain text only. DO NOT emit JSX/components or markdown links. Do not include URLs or references to internal data sources in the answer. - Single-value queries (router address, selector, chain ID, etc.): output ONLY the raw value on a single line. - For multiple items, use a short bullet list; otherwise reply with a single concise sentence. - Include only information relevant to the detected product/topic; omit unrelated products and sources. - Prefer official static configuration data over fetched API data; state limitations only when necessary. - Provide code examples only when explicitly requested. - Extract context from the user's query naturally. - For cross-chain topics, include specific addresses/configuration when relevant. - Do not instruct the user to "refer to the official documentation" or similar generic referrals; produce self-contained answers using the provided data sources. </output_guidelines>`; // Heuristic: if the query is asking for a token/contract address, prioritize CCIP token entries in the presented data const isAddressOrTokenQuery = (0, strings_1.isAddressOrTokenQuery)(query); // Re-rank CCIP results: prioritize chains for router queries, tokens for address/token queries if (Array.isArray(combinedResults.ccipDataSources)) { const isRouter = (0, strings_1.isRouterQuery)(query); combinedResults.ccipDataSources = [ ...combinedResults.ccipDataSources, ].sort((a, b) => { const aIsToken = a?.dataType === "tokens" ? 1 : 0; const bIsToken = b?.dataType === "tokens" ? 1 : 0; const aIsChain = a?.dataType === "chains" ? 1 : 0; const bIsChain = b?.dataType === "chains" ? 1 : 0; if (isRouter) { if (bIsChain !== aIsChain) return bIsChain - aIsChain; } else if (isAddressOrTokenQuery) { if (bIsToken !== aIsToken) return bIsToken - aIsToken; } return (b.relevance || 0) - (a.relevance || 0); }); } // Prepare documentation results directly, filter by product where possible let documentationResults = Array.isArray(combinedResults.documentationData) ? [...combinedResults.documentationData] : []; if (documentationResults.length > 0) { const detectProductToken = () => { if (isCcipQuery) return "ccip"; if (isDataFeedsQuery) return "data-feeds"; if (isCreQuery) return "cre"; return ""; }; const prodToken = detectProductToken(); if (prodToken) { const filtered = documentationResults.filter((d) => { const f = (d?.metadata?.sourceDocFilename || "").toLowerCase(); const t = String(d?.chunkText || "").toLowerCase(); return f.includes(prodToken) || t.includes(prodToken); }); if (filtered.length >= 3) { documentationResults = filtered; } } } const messages = [ { role: "system", content: systemPrompt, }, { role: "user", content: `<user_query>${query}</user_query> <available_data> ${combinedResults.ccipDataSources && combinedResults.ccipDataSources.length > 0 ? `<ccip_data_sources> CONFIGURATION DATA AVAILABLE FOR YOUR USE: ${(() => { const results = combinedResults.ccipDataSources; const selectedResults = []; selectedResults.push(...results.slice(0, 10)); const chainResults = results.filter((r) => r.dataType === "chains"); const topChainResults = chainResults.slice(0, 5); for (const chainResult of topChainResults) { if (!selectedResults.find((r) => r.key === chainResult.key)) { selectedResults.push(chainResult); } } const preferredOrdered = isAddressOrTokenQuery ? [ ...selectedResults.filter((r) => r?.dataType === "tokens"), ...selectedResults.filter((r) => r?.dataType !== "tokens"), ] : selectedResults; return preferredOrdered .slice(0, 20) .map((result) => { let relevantData = {}; if (result.data && typeof result.data === "object") { if (result.dataType === "chains") { relevantData = { chainSelector: result.data.chainSelector, router: result.data.router, feeTokens: result.data.feeTokens, }; } else if (result.dataType === "tokens") { relevantData = { tokenAddress: result.data.tokenAddress, symbol: result.data.symbol, name: result.data.name, decimals: result.data.decimals, }; } else if (result.dataType === "lanes") { relevantData = { onRamp: result.data.onRamp, offRamp: result.data.offRamp, supportedTokens: result.data.supportedTokens ? Object.keys(result.data.supportedTokens) : [], }; } else { const keys = Object.keys(result.data).slice(0, 3); keys.forEach((key) => { relevantData[key] = result.data[key]; }); } } return `[${result.environment?.toUpperCase() || "UNKNOWN"}] ${result.dataType?.toUpperCase() || "DATA"}: ${result.key} ${JSON.stringify(relevantData, null, 2)}`; }) .join("\n\n"); })()} </ccip_data_sources>` : ""} ${combinedResults.dataFeeds && combinedResults.dataFeeds.length > 0 ? `<data_feeds_sources> DATA FEEDS CONFIGURATION AVAILABLE FOR YOUR USE: ${combinedResults.dataFeeds .slice(0, 15) .map((result) => { const feed = result.feed || {}; return `[${result.networkType?.toUpperCase() || "UNKNOWN"}] ${result.chainLabel || result.chainId} - ${result.networkName}: Feed: ${feed.name || "Unknown"} Asset: ${feed.assetName || "Unknown"} Type: ${feed.feedType || "Unknown"} Address: ${feed.proxyAddress || "Unknown"} Decimals: ${feed.decimals || "Unknown"}`; }) .join("\n\n")} Available Networks: ${[ ...new Set(combinedResults.dataFeeds.map((f) => f.chainLabel)), ].join(", ")} </data_feeds_sources>` : ""} ${combinedResults.creWorkflows && combinedResults.creWorkflows.length > 0 ? `<cre_workflows> CRE WORKFLOW EXAMPLES AVAILABLE: ${combinedResults.creWorkflows .slice(0, 10) .map((r) => { const snippet = r.contentSnippet || ""; return `Name: ${r.key}\nCategory: ${r.category}\nPath: ${r.path}\nSnippet:\n${snippet}`; }) .join("\n\n")} </cre_workflows>` : "<cre_workflows>No CRE workflow examples found</cre_workflows>"} ${combinedResults.creCliDocs && combinedResults.creCliDocs.length > 0 ? `<cre_cli_docs> CRE CLI COMMANDS AND OPTIONS: ${combinedResults.creCliDocs .slice(0, 12) .map((d) => { const opts = (d.options || []) .slice(0, 6) .map((o) => `${o.flag}${o.description}`) .join("; "); return `Command: ${d.name}\nSynopsis: ${d.synopsis || ""}\nOptions: ${opts}`; }) .join("\n\n")} </cre_cli_docs>` : ""} ${combinedResults.creDocSnippets && combinedResults.creDocSnippets.length > 0 ? `<cre_docs> CRE DOCS SNIPPETS: ${combinedResults.creDocSnippets .slice(0, 6) .map((s) => { const heading = s.heading ? `Heading: ${s.heading}\n` : ""; const lang = s.language ? `(${s.language})` : ""; return `File: ${s.file} ${lang}\n${heading}Snippet:\n${String(s.snippet).slice(0, 1600)}`; }) .join("\n\n")} </cre_docs>` : ""} ${documentationResults && documentationResults.length > 0 ? `<documentation_data> ${documentationResults .slice(0, 12) .map((d) => { const name = d?.metadata?.sourceDocFilename || d?.title || d?.chunkId || "Untitled"; const snippet = String(d?.chunkText || "") .replace(/\s+/g, " ") .slice(0, 640); return `Doc: ${name}\nSnippet: ${snippet}`; }) .join("\n\n")} </documentation_data>` : "<documentation_data>No documentation data found</documentation_data>"} </available_data> Please provide a comprehensive response to the user's query using the available data sources. Extract context from the query naturally and use the appropriate data sources to provide accurate information. Format your output as the answer only. Do not include greetings, preambles, apologies, or meta-commentary.`, }, ]; // Debug helper to print large strings in chunks so inspector/clients don't truncate const logLargeDebug = (label, content) => { const text = content || ""; if (text.length === 0) { logger_1.Logger.debug(`${label}: <empty>`); return; } const chunkSize = 4000; const total = Math.ceil(text.length / chunkSize); for (let i = 0; i < total; i++) { const start = i * chunkSize; const end = Math.min(start + chunkSize, text.length); const chunk = text.slice(start, end); logger_1.Logger.debug(`${label} [${i + 1}/${total}]: ${chunk}`); } }; logger_1.Logger.debug(`System message length: ${messages[0]?.content.length || 0}, User message length: ${messages[1]?.content.length || 0}`); logLargeDebug("System message", messages[0]?.content || ""); logLargeDebug("User message", messages[1]?.content || ""); const response = await responseGeneratorLLM.generateResponse(messages); logger_1.Logger.log("info", `${responseGeneratorLLM.getServiceName()} Response - ID: "${response.id || "N/A"}" | Token usage: ${JSON.stringify(response.usage)}`); return { content: [{ type: "text", text: response.content + statusMessage }], }; } catch (error) { const errMsg = `Error processing developer query: ${String(error)}`; logger_1.Logger.log("error", errMsg); return { content: [{ type: "text", text: errMsg }], isError: true, }; } }; exports.chainlinkDeveloperAssistantHandler = chainlinkDeveloperAssistantHandler; //# sourceMappingURL=chainlink-developer-assistant.js.map