I'm now playing around with this new basic blog template from Next.js examples --> available on Next.js GitHub.

Luciano Lupo Notes.

Building the News Plugin for CypherNomad.eth

Cover Image for Building the News Plugin for CypherNomad.eth

Introduction

After setting up the basic character and personality of CypherNomad.eth (detailed in my previous post), I focused on enhancing its capabilities with a news plugin. This addition allows the agent to search and share relevant news about whatever you ask.

Plugin Architecture

The news plugin is structured into three main components:

src/plugins/plugin-news/
├── actions/
│   ├── searchNewsAction.ts
│   └── topHeadlinesAction.ts
├── services/
│   └── newsService.ts
├── types.ts
├── environment.ts
└── index.ts

Core Plugin Definition

// src/plugins/plugin-news/index.ts
export function createNewsPlugin() {
  return {
    name: "news",
    description: "News plugin providing access to news articles and headlines",
    services: [new NewsService()],
    actions: [searchNews, topHeadlines],
    evaluators: [],
    providers: [],
  } as const satisfies Plugin;
}

News Service Implementation

The NewsService handles all external API interactions and data processing:

// src/plugins/plugin-news/services/newsService.ts
export class NewsService {
  private readonly apiKey: string;
  private readonly baseUrl: string;

  constructor() {
    this.apiKey = process.env.NEWS_API_KEY;
    this.baseUrl = "https://newsapi.org/v2";
  }

  async searchNews(
    query: string,
    options?: SearchOptions
  ): Promise<NewsResponse> {
    const searchParams = new URLSearchParams({
      q: query,
      apiKey: this.apiKey,
      language: options?.language || "en",
      sortBy: options?.sortBy || "relevancy",
      pageSize: String(options?.pageSize || 10),
    });

    const response = await fetch(`${this.baseUrl}/everything?${searchParams}`);
    return this.processResponse(response);
  }

  async getTopHeadlines(category?: string): Promise<NewsResponse> {
    const searchParams = new URLSearchParams({
      apiKey: this.apiKey,
      category: category || "technology",
      language: "en",
    });

    const response = await fetch(
      `${this.baseUrl}/top-headlines?${searchParams}`
    );
    return this.processResponse(response);
  }

  private async processResponse(response: Response): Promise<NewsResponse> {
    if (!response.ok) {
      throw new Error(`News API Error: ${response.statusText}`);
    }
    return response.json();
  }
}

Action Implementations

Search News Action

// src/plugins/plugin-news/actions/searchNewsAction.ts
export const searchNews: Action = {
  name: "searchNews",
  description: "Search for news articles based on a query",
  parameters: {
    type: "object",
    properties: {
      query: {
        type: "string",
        description: "The search query for news articles",
      },
      options: {
        type: "object",
        properties: {
          language: { type: "string" },
          sortBy: { type: "string" },
          pageSize: { type: "number" },
        },
        required: [],
      },
    },
    required: ["query"],
  },

  async execute({ query, options }, context) {
    const newsService = context.services.get(NewsService);
    const articles = await newsService.searchNews(query, options);

    return formatNewsResponse(articles);
  },
};

Top Headlines Action

// src/plugins/plugin-news/actions/topHeadlinesAction.ts
export const topHeadlines: Action = {
  name: "topHeadlines",
  description: "Get top headlines, optionally filtered by category",
  parameters: {
    type: "object",
    properties: {
      category: {
        type: "string",
        enum: ["technology", "business", "science"],
        description: "Category to filter headlines",
      },
    },
    required: [],
  },

  async execute({ category }, context) {
    const newsService = context.services.get(NewsService);
    const headlines = await newsService.getTopHeadlines(category);

    return formatNewsResponse(headlines);
  },
};

Response Formatting

The plugin includes utilities to format news responses in a consistent way:

// src/plugins/plugin-news/utils/formatters.ts
export function formatNewsResponse(
  response: NewsResponse
): FormattedNewsResponse {
  return {
    articles: response.articles.map((article) => ({
      title: article.title,
      description: article.description,
      url: article.url,
      source: article.source.name,
      publishedAt: new Date(article.publishedAt).toISOString(),
      formattedDate: formatDate(article.publishedAt),
    })),
    totalResults: response.totalResults,
  };
}

function formatDate(dateString: string): string {
  const date = new Date(dateString);
  return date.toLocaleDateString("en-US", {
    month: "short",
    day: "numeric",
    year: "numeric",
  });
}

Type Definitions

Strong typing ensures consistency and reliability:

// src/plugins/plugin-news/types.ts
export interface NewsResponse {
  status: string;
  totalResults: number;
  articles: Article[];
}

export interface Article {
  source: {
    id: string | null;
    name: string;
  };
  author: string | null;
  title: string;
  description: string;
  url: string;
  urlToImage: string | null;
  publishedAt: string;
  content: string;
}

export interface SearchOptions {
  language?: string;
  sortBy?: "relevancy" | "popularity" | "publishedAt";
  pageSize?: number;
}

export interface FormattedNewsResponse {
  articles: FormattedArticle[];
  totalResults: number;
}

export interface FormattedArticle {
  title: string;
  description: string;
  url: string;
  source: string;
  publishedAt: string;
  formattedDate: string;
}

Environment Configuration

// src/plugins/plugin-news/environment.ts
export const requiredEnvVars = {
  NEWS_API_KEY: process.env.NEWS_API_KEY,
};

export function validateEnvironment(): void {
  const missing = Object.entries(requiredEnvVars)
    .filter(([, value]) => !value)
    .map(([key]) => key);

  if (missing.length > 0) {
    throw new Error(
      `Missing required environment variables: ${missing.join(", ")}`
    );
  }
}

Usage Examples

Here's how CypherNomad.eth uses these actions:

// Example: Searching for specific news
const cryptoNews = await context.actions.searchNews({
  query: "ethereum AND (cryptography OR blockchain)",
  options: {
    sortBy: "relevancy",
    pageSize: 5,
  },
});

// Example: Getting top tech headlines
const headlines = await context.actions.topHeadlines({
  category: "technology",
});

Future Enhancements

Planned improvements for the news plugin include:

  1. Advanced Filtering

    • Sentiment analysis
    • Source credibility scoring
    • Content categorization
  2. Custom Formatting

    • Markdown support
    • Social media optimized formats
    • Rich media embedding
  3. Caching Layer

    • Redis integration
    • Rate limiting
    • Response caching

This plugin is part of the ongoing development of CypherNomad.eth. Contributions and feedback are welcome!