diff --git a/.env.example b/.env.example index d7792de730e..f4e8c3164bd 100644 --- a/.env.example +++ b/.env.example @@ -873,7 +873,6 @@ ETHSTORAGE_ADDRESS=0x64003adbdf3014f7E38FC6BE752EB047b95da89A ETHSTORAGE_RPC_URL=https://rpc.beta.testnet.l2.quarkchain.io:8545 - # Email Automation Plugin Configuration RESEND_API_KEY= # Your Resend API key DEFAULT_TO_EMAIL= # Default recipient @@ -902,3 +901,6 @@ DCAP_MODE= # Options: OFF, PLUGIN-SGX, PLUGIN-TEE, MOCK # QuickIntel Token Security API QUICKINTEL_API_KEY= # Your QuickIntel API key for token security analysis + +# News API Key +NEWS_API_KEY= # News API KEY from https://newsapi.org/ diff --git a/agent/package.json b/agent/package.json index cdd84c21752..30c640ea797 100644 --- a/agent/package.json +++ b/agent/package.json @@ -70,6 +70,7 @@ "@elizaos/plugin-mind-network": "workspace:*", "@elizaos/plugin-movement": "workspace:*", "@elizaos/plugin-massa": "workspace:*", + "@elizaos/plugin-news": "workspace:*", "@elizaos/plugin-nft-generation": "workspace:*", "@elizaos/plugin-node": "workspace:*", "@elizaos/plugin-quick-intel": "workspace:*", diff --git a/agent/src/index.ts b/agent/src/index.ts index ec03b757d79..14080125a80 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -84,6 +84,7 @@ import { lensPlugin } from "@elizaos/plugin-lensNetwork" import { mindNetworkPlugin } from "@elizaos/plugin-mind-network"; import { multiversxPlugin } from "@elizaos/plugin-multiversx" import { nearPlugin } from "@elizaos/plugin-near" +import { newsPlugin } from "@elizaos/plugin-news"; import createNFTCollectionsPlugin from "@elizaos/plugin-nft-collections" import { nftGenerationPlugin } from "@elizaos/plugin-nft-generation" import { createNodePlugin } from "@elizaos/plugin-node" diff --git a/packages/plugin-news/.npmignore b/packages/plugin-news/.npmignore new file mode 100644 index 00000000000..078562eceab --- /dev/null +++ b/packages/plugin-news/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-news/README.md b/packages/plugin-news/README.md new file mode 100644 index 00000000000..f6f0786d916 --- /dev/null +++ b/packages/plugin-news/README.md @@ -0,0 +1,171 @@ +# @elizaos/plugin-news + +A plugin for fetching and handling real-time news data through NewsAPI integration. + +## Overview + +This plugin provides functionality to: +- Fetch latest news articles from NewsAPI +- Search news by specific topics or keywords +- Get article summaries including titles, descriptions, and URLs +- Limit results to most recent and relevant content + +## Installation + +```bash +npm install @elizaos/plugin-news +``` + +## Configuration + +The plugin requires the following environment variable: + +```env +NEWS_API_KEY=your_newsapi_key # Required for accessing NewsAPI +``` + +## Usage + +Import and register the plugin in your Eliza configuration: + +```typescript +import { newsPlugin } from "@elizaos/plugin-news"; + +export default { + plugins: [newsPlugin], + // ... other configuration +}; +``` + +## Features + +### Current News Action + +The plugin provides a `CURRENT_NEWS` action that responds to various news-related queries: + +```typescript +// Example queries the action responds to: +"what's the latest news about <searchTerm>?" +"can you show me the latest news about <searchTerm>?" +"what's in the <searchTerm> news today?" +"show me current events about <searchTerm>?" +"what's going on in the world of <searchTerm>?" +"give me the latest headlines about <searchTerm>?" +"show me news updates about <searchTerm>?" +"what are today's top stories about <searchTerm>?" +``` + +The action returns up to 5 recent articles, including: +- Article title +- Description +- URL +- Content preview (up to 1000 characters) + +## Development + +### Building + +```bash +npm run build +``` + +### Development Mode + +```bash +npm run dev +``` + +### Linting + +```bash +npm run lint +``` + +### Project Structure + +``` +plugin-news/ +├── src/ +│ ├── actions/ # Action implementations +│ │ ├── news.ts # Current news action +│ │ └── index.ts # Action exports +│ └── index.ts # Main plugin export +├── package.json +└── tsconfig.json +``` + +## Dependencies + +- `@ai16z/eliza`: Core Eliza framework +- `tsup`: Build tool for TypeScript packages +- Other standard dependencies listed in package.json + +## API Reference + +### Actions + +- `CURRENT_NEWS`: Main action for fetching news + - Aliases: `["NEWS", "GET_NEWS", "GET_CURRENT_NEWS"]` + - Automatically extracts search terms from user messages + - Returns formatted news articles with titles, descriptions, and URLs + +### Response Format + +```typescript +interface NewsResponse { + title: string; + description: string; + url: string; + content: string; // Limited to 1000 characters +} +``` + +## Future Enhancements + +1. **Additional News Sources** + - Integration with multiple news APIs + - RSS feed support + - Social media news aggregation + +2. **Content Analysis** + - Sentiment analysis of news articles + - Topic categorization + - Trend detection + - Fact-checking integration + +3. **Customization Options** + - User preferences for news sources + - Custom filtering rules + - Personalized news feeds + - Language preferences + +4. **Advanced Search** + - Date range filtering + - Source filtering + - Category-based search + - Advanced query syntax + +5. **Performance Improvements** + - Caching layer + - Rate limiting optimization + - Response compression + - Batch processing + +We welcome community feedback and contributions to help prioritize these enhancements. + +## Contributing + +Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information. + + +## License + +This plugin is part of the Eliza project. See the main project repository for license information. + +## Credits + +This plugin integrates with and builds upon several key technologies: + +- [NewsAPI](https://newsapi.org/): News data provider + +Plugin generated from Eliza coding tutorial [Agent Dev School Part 2](https://www.youtube.com/watch?v=XenGeAcPAQo) diff --git a/packages/plugin-news/eslint.config.mjs b/packages/plugin-news/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/plugin-news/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-news/package.json b/packages/plugin-news/package.json new file mode 100644 index 00000000000..558c23443ab --- /dev/null +++ b/packages/plugin-news/package.json @@ -0,0 +1,19 @@ +{ + "name": "@elizaos/plugin-news", + "version": "0.1.5-alpha.5", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "tsup": "8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint . --fix" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-news/src/actions/index.ts b/packages/plugin-news/src/actions/index.ts new file mode 100644 index 00000000000..a2e178b192f --- /dev/null +++ b/packages/plugin-news/src/actions/index.ts @@ -0,0 +1,2 @@ +export * from "./news.ts"; + diff --git a/packages/plugin-news/src/actions/news.ts b/packages/plugin-news/src/actions/news.ts new file mode 100644 index 00000000000..c6d20f473bf --- /dev/null +++ b/packages/plugin-news/src/actions/news.ts @@ -0,0 +1,219 @@ +import { + ActionExample, + Content, + generateText, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action, +} from "@elizaos/core"; + + +export const currentNewsAction: Action = { + name: "CURRENT_NEWS", + similes: ["NEWS", "GET_NEWS", "GET_CURRENT_NEWS"], + validate: async (_runtime: IAgentRuntime, _message: Memory) => { + const apiKey = process.env.NEWS_API_KEY; + if (!apiKey) { + throw new Error('NEWS_API_KEY environment variable is not set'); + } + return true; + }, + description: + "Get the latest news about a specific topic if asked by the user.", + handler: async ( + _runtime: IAgentRuntime, + _message: Memory, + _state: State, + _options: { [key: string]: unknown; }, + _callback: HandlerCallback, + ): Promise<boolean> => { + async function getCurrentNews(searchTerm: string) { + try { + // Add quotes and additional context terms + const enhancedSearchTerm = encodeURIComponent(`"${searchTerm}" AND (Spain OR Spanish OR Madrid OR Felipe)`); + + const [everythingResponse, headlinesResponse] = await Promise.all([ + fetch( + `https://newsapi.org/v2/everything?` + + `q=${enhancedSearchTerm}&` + + `sortBy=relevancy&` + + `language=en&` + + `pageSize=50&` + + `apiKey=${process.env.NEWS_API_KEY}` + ), + fetch( + `https://newsapi.org/v2/top-headlines?` + + `q=${searchTerm}&` + + `country=es&` + + `language=en&` + + `pageSize=50&` + + `apiKey=${process.env.NEWS_API_KEY}` + ) + ]); + + const [everythingData, headlinesData] = await Promise.all([ + everythingResponse.json(), + headlinesResponse.json() + ]); + + // Combine and filter articles + const allArticles = [ + ...(headlinesData.articles || []), + ...(everythingData.articles || []) + ].filter(article => + article.title && + article.description && + (article.title.toLowerCase().includes(searchTerm.toLowerCase()) || + article.description.toLowerCase().includes(searchTerm.toLowerCase())) + ); + + // Remove duplicates and get up to 15 articles + const uniqueArticles = Array.from( + new Map(allArticles.map(article => [article.title, article])).values() + ).slice(0, 15); + + if (!uniqueArticles.length) { + return "No news articles found."; + } + + return uniqueArticles.map((article, index) => { + const content = article.description || "No content available"; + const urlDomain = article.url ? new URL(article.url).hostname : ""; + return `📰 Article ${index + 1}\n` + + `━━━━━━━━━━━━━━━━━━━━━━\n` + + `📌 **${article.title || "No title"}**\n\n` + + `📝 ${content}\n\n` + + `🔗 Read more at: ${urlDomain}\n`; + }).join("\n"); + } catch (error) { + console.error("Error fetching news:", error); + return "Sorry, there was an error fetching the news."; + } + } + + const context = `What is the specific topic or subject the user wants news about? Extract ONLY the search term from this message: "${_message.content.text}". Return just the search term with no additional text, punctuation, or explanation.` + + const searchTerm = await generateText({ + runtime: _runtime, + context, + modelClass: ModelClass.SMALL, + stop: ["\n"], + }); + + // For debugging + console.log("Search term extracted:", searchTerm); + + const currentNews = await getCurrentNews(searchTerm); + const responseText = ` *protocol droid noises*\n\n${currentNews}`; + + + const newMemory: Memory = { + userId: _message.agentId, + agentId: _message.agentId, + roomId: _message.roomId, + content: { + text: responseText, + action: "CURRENT_NEWS_RESPONSE", + source: _message.content?.source, + } as Content, + }; + + await _runtime.messageManager.createMemory(newMemory); + + _callback(newMemory.content); + return true; + + }, + examples: [ + [ + { + user: "{{user1}}", + content: { text: "what's the latest news about <searchTerm>?" }, + }, + { + user: "{{user2}}", + content: { text: "", action: "CURRENT NEWS" }, + }, + ], + + [ + { + user: "{{user1}}", + content: { text: "can you show me the latest news about <searchTerm>?" }, + }, + { + user: "{{user2}}", + content: { text: "", action: "CURRENT NEWS" }, + }, + ], + + [ + { + user: "{{user1}}", + content: { text: "what's in the <searchTerm> news today?" }, + }, + { + user: "{{user2}}", + content: { text: "", action: "CURRENT NEWS" }, + }, + ], + + [ + { + user: "{{user1}}", + content: { text: "show me current events about <searchTerm>?" }, + }, + { + user: "{{user2}}", + content: { text: "", action: "CURRENT NEWS" }, + }, + ], + + [ + { + user: "{{user1}}", + content: { text: "what's going on in the world of <searchTerm>?" }, + }, + { + user: "{{user2}}", + content: { text: "", action: "CURRENT NEWS" }, + }, + ], + + [ + { + user: "{{user1}}", + content: { text: "give me the latest headlines about <searchTerm>?" }, + }, + { + user: "{{user2}}", + content: { text: "", action: "CURRENT NEWS" }, + }, + ], + + [ + { + user: "{{user1}}", + content: { text: "show me news updates about <searchTerm>?" }, + }, + { + user: "{{user2}}", + content: { text: "", action: "CURRENT NEWS" }, + }, + ], + + [ + { + user: "{{user1}}", + content: { text: "what are today's top stories about <searchTerm>?" }, + }, + { + user: "{{user2}}", + content: { text: "", action: "CURRENT NEWS" }, + }, + ], + ] as ActionExample[][], +} as Action; \ No newline at end of file diff --git a/packages/plugin-news/src/index.ts b/packages/plugin-news/src/index.ts new file mode 100644 index 00000000000..3419dc3f2cb --- /dev/null +++ b/packages/plugin-news/src/index.ts @@ -0,0 +1,10 @@ +import { Plugin } from "@elizaos/core"; +import { currentNewsAction } from "./actions/news"; + +export const newsPlugin: Plugin = { + name: "newsPlugin", + description: "Get the latest news about a specific topic if asked by the user.", + actions: [currentNewsAction], +}; + +export default newsPlugin; diff --git a/packages/plugin-news/tsconfig.json b/packages/plugin-news/tsconfig.json new file mode 100644 index 00000000000..834c4dce269 --- /dev/null +++ b/packages/plugin-news/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-news/tsup.config.ts b/packages/plugin-news/tsup.config.ts new file mode 100644 index 00000000000..e42bf4efeae --- /dev/null +++ b/packages/plugin-news/tsup.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + // Add other modules you want to externalize + ], +});