Skip to content

Commit

Permalink
add forum analyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
ileana-pr committed Feb 2, 2025
1 parent 1fdae0a commit fbe12a6
Show file tree
Hide file tree
Showing 11 changed files with 799 additions and 1 deletion.
2 changes: 1 addition & 1 deletion agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"@elizaos/plugin-bnb": "workspace:*",
"@elizaos/plugin-bootstrap": "workspace:*",
"@elizaos/plugin-di": "workspace:*",
//"@elizaos/plugin-intiface": "workspace:*",
"@elizaos/plugin-coinbase": "workspace:*",
"@elizaos/plugin-coingecko": "workspace:*",
"@elizaos/plugin-coinmarketcap": "workspace:*",
Expand Down Expand Up @@ -151,6 +150,7 @@
"@elizaos/plugin-near": "workspace:*",
"@elizaos/plugin-stargaze": "workspace:*",
"@elizaos/plugin-zksync-era": "workspace:*",
"@elizaos/plugin-forum-analyzer": "workspace:*",
"readline": "1.3.0",
"ws": "8.18.0",
"yargs": "17.7.2"
Expand Down
2 changes: 2 additions & 0 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ import { quickIntelPlugin } from "@elizaos/plugin-quick-intel";

import { trikonPlugin } from "@elizaos/plugin-trikon";
import arbitragePlugin from "@elizaos/plugin-arbitrage";
import { forumAnalyzerPlugin } from "@elizaos/plugin-forum-analyzer";

const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
const __dirname = path.dirname(__filename); // get the name of the directory

Expand Down
116 changes: 116 additions & 0 deletions packages/plugin-forum-analyzer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# @elizaos/plugin-forum-analyzer

A powerful forum analysis plugin for DAOra that helps identify potential governance proposals from various DAO discussion platforms.

## Features

- Multi-platform support:
- Discourse forums (public and private)
- Discord channels
- Commonwealth discussions
- Advanced analysis capabilities:
- Proposal identification
- Sentiment analysis
- Engagement metrics
- Consensus detection
- Key points extraction
- Public forum support without API access
- Configurable analysis thresholds

## Installation

```bash
pnpm add @elizaos/plugin-forum-analyzer
```

## Configuration

Add the plugin to your DAOra character configuration:

```json
{
"name": "DAOra",
"plugins": ["@elizaos/plugin-forum-analyzer"],
"settings": {
"plugins": {
"forum-analyzer": {
"platforms": {
"discourse": {
"usePublicDiscourse": true,
"baseUrl": "https://your-forum.com"
},
"discord": {
"token": "your-bot-token",
"channels": ["channel-id-1", "channel-id-2"]
},
"commonwealth": {
"space": "your-dao-space"
}
},
"analysisOptions": {
"minEngagementThreshold": 0.3,
"proposalThreshold": 0.7,
"includeSentiment": true,
"includeConsensus": true
}
}
}
}
}
```

## Usage

The plugin automatically enhances DAOra's capabilities to:

1. Monitor forum discussions for potential governance proposals
2. Analyze community sentiment and consensus
3. Track engagement metrics
4. Extract key points from discussions

Example interactions:

```
User: Can you analyze recent discussions for potential proposals?
DAOra: I'll scan the configured platforms and analyze the discussions. I'll look for:
- High engagement topics
- Proposal-like content
- Community consensus
- Supporting evidence
```

## API Reference

### ForumAnalyzerPlugin

Main plugin class that implements forum analysis functionality.

```typescript
interface ForumAnalyzerConfig {
platforms: {
discourse?: {
usePublicDiscourse?: boolean;
apiKey?: string;
baseUrl?: string;
};
discord?: {
token?: string;
channels?: string[];
};
commonwealth?: {
apiKey?: string;
space?: string;
};
};
analysisOptions?: {
minEngagementThreshold?: number;
proposalThreshold?: number;
includeSentiment?: boolean;
includeConsensus?: boolean;
};
}
```

## License

MIT
45 changes: 45 additions & 0 deletions packages/plugin-forum-analyzer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@elizaos/plugin-forum-analyzer",
"version": "0.1.0",
"description": "Forum analysis plugin for DAOra to scrape and analyze DAO discussions",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "jest",
"lint": "eslint src --ext .ts",
"clean": "rimraf dist"
},
"dependencies": {
"@elizaos/core": "workspace:*",
"axios": "^1.6.5",
"cheerio": "^1.0.0-rc.12",
"discord.js": "^14.14.1",
"natural": "^6.10.0",
"puppeteer": "^21.7.0"
},
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/cheerio": "^0.22.35",
"@types/natural": "^5.1.5",
"@types/node": "^20.11.0",
"@types/jest": "^29.5.11",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"rimraf": "^5.0.5",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1"
},
"keywords": [
"dao",
"governance",
"forum",
"analysis",
"discourse",
"discord",
"commonwealth"
],
"author": "DAOra Team",
"license": "MIT"
}
185 changes: 185 additions & 0 deletions packages/plugin-forum-analyzer/src/analysis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import natural from 'natural';
import { ForumPost, DiscussionAnalysis } from './types';

const tokenizer = new natural.WordTokenizer();
const TfIdf = natural.TfIdf;
const sentiment = new natural.SentimentAnalyzer('English', natural.PorterStemmer, 'afinn');

// Keywords that indicate potential governance proposals
const PROPOSAL_KEYWORDS = [
'proposal', 'propose', 'governance', 'vote', 'voting', 'decision',
'treasury', 'fund', 'funding', 'budget', 'allocation', 'grant',
'improvement', 'upgrade', 'change', 'modify', 'update', 'implement',
'strategy', 'policy', 'protocol', 'parameter', 'framework'
];

// Keywords that indicate high engagement/importance
const IMPORTANCE_KEYWORDS = [
'urgent', 'important', 'critical', 'crucial', 'significant',
'essential', 'necessary', 'required', 'needed', 'priority'
];

interface AnalysisOptions {
minEngagementThreshold?: number;
proposalThreshold?: number;
includeSentiment?: boolean;
includeConsensus?: boolean;
}

export async function analyzeDiscussion(post: ForumPost, options: AnalysisOptions = {}): Promise<DiscussionAnalysis> {
const tokens = tokenizer.tokenize(post.content.toLowerCase());
const proposalScore = calculateProposalScore(tokens);
const sentimentResult = analyzeSentiment(post.content);
const engagementScore = calculateEngagementScore(post);

return {
post,
sentiment: {
score: sentimentResult.score,
label: getSentimentLabel(sentimentResult.score)
},
engagement: {
participationRate: calculateParticipationRate(post),
uniqueParticipants: getUniqueParticipants(post),
totalInteractions: calculateTotalInteractions(post)
},
proposalPotential: {
score: proposalScore,
confidence: calculateConfidence(proposalScore, engagementScore),
type: determineProposalType(tokens),
keyPoints: extractKeyPoints(post.content)
},
consensus: analyzeConsensus(post)
};
}

function calculateProposalScore(tokens: string[]): number {
let score = 0;
const tfidf = new TfIdf();

tfidf.addDocument(tokens);

// Calculate score based on proposal keywords
PROPOSAL_KEYWORDS.forEach(keyword => {
const measure = tfidf.tfidf(keyword, 0);
score += measure;
});

// Normalize score to 0-1 range
return Math.min(score / (PROPOSAL_KEYWORDS.length * 2), 1);
}

function analyzeSentiment(content: string) {
const words = tokenizer.tokenize(content);
const score = sentiment.getSentiment(words);

return {
score: normalizeScore(score, -5, 5) // Normalize from AFINN range to -1 to 1
};
}

function getSentimentLabel(score: number): 'positive' | 'negative' | 'neutral' {
if (score > 0.1) return 'positive';
if (score < -0.1) return 'negative';
return 'neutral';
}

function calculateEngagementScore(post: ForumPost): number {
const baseScore =
(post.replies || 0) * 2 +
(post.views || 0) / 100 +
(post.reactions?.reduce((sum, r) => sum + r.count, 0) || 0) * 1.5;

return Math.min(baseScore / 1000, 1); // Normalize to 0-1
}

function calculateParticipationRate(post: ForumPost): number {
const uniqueParticipants = getUniqueParticipants(post);
const totalInteractions = calculateTotalInteractions(post);

return totalInteractions > 0 ? uniqueParticipants / totalInteractions : 0;
}

function getUniqueParticipants(post: ForumPost): number {
// This is a placeholder - in a real implementation, we'd track unique participants
// through replies and reactions
return 1; // Minimum is the original poster
}

function calculateTotalInteractions(post: ForumPost): number {
return (
1 + // Original post
(post.replies || 0) +
(post.reactions?.reduce((sum, r) => sum + r.count, 0) || 0)
);
}

function calculateConfidence(proposalScore: number, engagementScore: number): number {
// Weight both scores equally
return (proposalScore + engagementScore) / 2;
}

function determineProposalType(tokens: string[]): 'governance' | 'treasury' | 'technical' | 'social' | 'other' {
const types = {
governance: ['governance', 'vote', 'proposal', 'policy'],
treasury: ['treasury', 'fund', 'budget', 'grant'],
technical: ['technical', 'protocol', 'code', 'implementation'],
social: ['community', 'social', 'communication', 'culture']
};

const scores = Object.entries(types).map(([type, keywords]) => ({
type,
score: keywords.reduce((sum, keyword) =>
sum + tokens.filter(t => t === keyword).length, 0
)
}));

const maxScore = Math.max(...scores.map(s => s.score));
const topType = scores.find(s => s.score === maxScore);

return (topType?.type as 'governance' | 'treasury' | 'technical' | 'social') || 'other';
}

function extractKeyPoints(content: string): string[] {
const sentences = content.split(/[.!?]+/).map(s => s.trim()).filter(Boolean);
const tfidf = new TfIdf();

sentences.forEach(sentence => tfidf.addDocument(sentence));

// Get the most important sentences based on TF-IDF scores
const sentenceScores = sentences.map((sentence, index) => ({
sentence,
score: calculateSentenceImportance(sentence, tfidf, index)
}));

return sentenceScores
.sort((a, b) => b.score - a.score)
.slice(0, 3)
.map(s => s.sentence);
}

function calculateSentenceImportance(sentence: string, tfidf: any, docIndex: number): number {
const words = tokenizer.tokenize(sentence.toLowerCase());
let score = 0;

// Score based on proposal and importance keywords
[...PROPOSAL_KEYWORDS, ...IMPORTANCE_KEYWORDS].forEach(keyword => {
score += tfidf.tfidf(keyword, docIndex);
});

return score;
}

function analyzeConsensus(post: ForumPost) {
// This is a simplified consensus analysis
// In a real implementation, we'd analyze reply sentiment and reaction patterns
return {
level: 0.5, // Default neutral consensus
majorityOpinion: undefined,
dissenting: undefined
};
}

function normalizeScore(score: number, min: number, max: number): number {
return (score - min) / (max - min) * 2 - 1;
}
Loading

0 comments on commit fbe12a6

Please sign in to comment.