Skip to content

Search

Search the internet and get summarized information from top results.

src/skills/search.ts
import { Context } from "@xmtp/message-kit";
import type { Skill } from "@xmtp/message-kit";
import OpenAI from "openai";
 
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});
 
export const search: Skill[] = [
  {
    skill: "search",
    examples: [
      "/search what is the capital of France?",
      "/search latest news about ethereum",
    ],
    handler: handler,
    description:
      "Search the internet and get summarized information from top results.",
    params: {
      query: {
        type: "prompt",
      },
    },
  },
];
 
export async function handler(context: Context) {
  const {
    message: {
      content: {
        params: { query },
      },
    },
  } = context;
 
  try {
    // Search and get top results
    const searchResults = await searchDuckDuckGo(query);
    if (!searchResults.length) {
      return {
        code: 404,
        message: "No results found for your query.",
      };
    }
 
    // Fetch content from each result
    const contents = await Promise.all(
      searchResults.slice(0, 3).map(async (url) => {
        try {
          const response = await fetch(url, {
            headers: {
              "User-Agent":
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
            },
          });
          const html = await response.text();
          return extractContent(html);
        } catch (error) {
          console.error(`Error fetching ${url}:`, error);
          return "";
        }
      }),
    );
 
    // Combine and summarize the content
    const summary = await getAISummary(contents.join("\n"), query);
 
    return {
      code: 200,
      message: summary,
    };
  } catch (error: any) {
    return {
      code: 500,
      message: `Search failed: ${error.message}`,
    };
  }
}
 
async function searchDuckDuckGo(query: string): Promise<string[]> {
  try {
    // DuckDuckGo's lite version is more friendly to programmatic access
    const response = await fetch(
      `https://duckduckgo.com/lite?q=${encodeURIComponent(query)}`,
      {
        headers: {
          "User-Agent":
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
        },
      },
    );
 
    const html = await response.text();
 
    // Extract URLs from the results
    const urlRegex = /<a class="result-link" href="([^"]+)"/g;
    const urls: string[] = [];
    let match;
 
    while ((match = urlRegex.exec(html)) !== null) {
      if (match[1] && !match[1].includes("duckduckgo.com")) {
        urls.push(match[1]);
      }
    }
 
    // If no results from regex, try alternative pattern
    if (urls.length === 0) {
      const altRegex = /rel="nofollow" href="([^"]+)"/g;
      while ((match = altRegex.exec(html)) !== null) {
        if (match[1] && !match[1].includes("duckduckgo.com")) {
          urls.push(match[1]);
        }
      }
    }
 
    return urls;
  } catch (error) {
    console.error("Search failed:", error);
    return [];
  }
}
 
function extractContent(html: string): string {
  // Remove scripts and style elements
  html = html
    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
    .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "");
 
  // Get text content
  const text = html
    .replace(/<[^>]+>/g, " ")
    .replace(/\s+/g, " ")
    .trim()
    .slice(0, 2000); // Get more content for search results
 
  return text;
}
 
async function getAISummary(content: string, query: string): Promise<string> {
  const completion = await openai.chat.completions.create({
    messages: [
      {
        role: "system",
        content:
          "You are a helpful assistant that provides clear, accurate summaries (2-3 sentences) of search results.",
      },
      {
        role: "user",
        content: `Based on the following search results, please provide a comprehensive answer to the query: "${query}"\n\nSearch results:\n${content}`,
      },
    ],
    model: "gpt-4o-mini",
  });
 
  return (
    completion.choices[0].message.content ||
    "Could not generate a summary of the search results."
  );
}