Skip to content
58 changes: 54 additions & 4 deletions packages/code-assist/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,50 @@ import {
import { ragEndpoint, DEFAULT_CONTEXTS, SOURCE } from './config.js';
import axios from 'axios';

// Cache for products list
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit brittle since it depends on the semi-unstructured XML response from the /instructions api endpoint for Code Assist middleware. What's a more robust design or any requirements for the upstream API structure or content?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, parsing the XML/Markdown structure is brittle. To mitigate this, I've implemented a fail-safe design:

  1. Hardcoded Fallback: The server initializes with a comprehensive DEFAULT_CONTEXTS list (in config.ts).
  2. Graceful Failure: If the structure changes or parsing fails, we log the error and continue using the hardcoded list. Service is never interrupted.

Ideal Upstream Change: A more robust design would be for the RAG service to return a structured JSON array (e.g., api_products) alongside the system instructions, which would eliminate the need for scraping the text.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideal Upstream Change: A more robust design would be for the RAG service to return a structured JSON array (e.g., api_products) alongside the system instructions, which would eliminate the need for scraping the text.

We can extend our /instructions endpoint to return a list of api_products but currently we're using XML as the format, we could have a ```json block inside of it with the list of products. If that helps.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a bad suggestion ☝️ @caio1985 @mmisim to consider how we always provide a consistent list of the available products via Code Assist

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. The current implementation ensures consistency via the hardcoded fallback in config.ts, while the dynamic fetch offers best-effort freshness.

let cachedProducts: string[] = [...DEFAULT_CONTEXTS];
let productsFetched = false;

// Function to fetch and parse products from instructions
async function fetchProductsFromInstructions() {
if (productsFetched) return;

try {
const response = await axios.get(ragEndpoint.concat("/instructions"), {
params: { source: SOURCE }
});

const systemInstructions = response.data?.systemInstructions;
if (!systemInstructions) return;

// Extract content between <product_overview> tags
const match = systemInstructions.match(/<product_overview>([\s\S]*?)<\/product_overview>/);
if (!match) return;

const content = match[1];
// Regex to find product names in format: * **Product Name**
const productRegex = /\*\s+\*\*([^*]+)\*\*/g;
const newProducts: string[] = [];

let productMatch;
while ((productMatch = productRegex.exec(content)) !== null) {
if (productMatch[1]) {
newProducts.push(productMatch[1].trim());
}
}

if (newProducts.length > 0) {
// Merge with default contexts, removing duplicates
const uniqueProducts = new Set([...cachedProducts, ...newProducts]);
cachedProducts = Array.from(uniqueProducts);
productsFetched = true;
console.log(`Fetched ${newProducts.length} products from instructions.`);
}
} catch (error) {
console.error("Failed to fetch products from instructions:", error);
}
}

// MCP Streamable HTTP compliance: Accept header validation
function validateAcceptHeader(req: Request): boolean {
const acceptHeader = req.headers.accept;
Expand Down Expand Up @@ -230,9 +274,14 @@ export async function handleCompletion(request: CompleteRequest, server: Server)
request.params.ref.name === "retrieve-google-maps-platform-docs" &&
request.params.argument.name === "search_context") {

// Try to refresh products if not yet fetched (background)
if (!productsFetched) {
fetchProductsFromInstructions().catch(e => console.error(e));
}

const currentInput = request.params.argument.value.toLowerCase();
// Filter DEFAULT_CONTEXTS based on input
const matches = DEFAULT_CONTEXTS.filter(ctx => ctx.toLowerCase().includes(currentInput));
// Filter cachedProducts based on input
const matches = cachedProducts.filter(ctx => ctx.toLowerCase().includes(currentInput));

return {
completion: {
Expand Down Expand Up @@ -303,8 +352,9 @@ export async function handleCallTool(request: CallToolRequest, server: Server) {
let prompt: string = request.params.arguments?.prompt as string;
let searchContext: string[] = request.params.arguments?.search_context as string[];

// Merge searchContext with DEFAULT_CONTEXTS and remove duplicates
const mergedContexts = new Set([...DEFAULT_CONTEXTS, ...(searchContext || [])]);
// Merge searchContext with cachedProducts and remove duplicates.
// Note: We use the cached product list here as the base context, not just the static DEFAULT_CONTEXTS.
const mergedContexts = new Set([...cachedProducts, ...(searchContext || [])]);
const contexts = Array.from(mergedContexts);

// Log user request for debugging purposes
Expand Down
Loading