Skip to content

Commit

Permalink
Merge pull request elizaOS#973 from bkellgren/main
Browse files Browse the repository at this point in the history
LinkedIn Client
  • Loading branch information
lalalune authored Dec 11, 2024
2 parents ca885a8 + c5e4141 commit 95e6ae7
Show file tree
Hide file tree
Showing 8 changed files with 801 additions and 0 deletions.
55 changes: 55 additions & 0 deletions packages/client-linkedin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# @ai16z/client-linkedin

LinkedIn client integration for AI16Z agents. This package provides functionality for AI agents to interact with LinkedIn, including:

- Automated post creation and scheduling
- Professional interaction management
- Message and comment handling
- Connection management
- Activity tracking

## Installation

```bash
pnpm add @ai16z/client-linkedin
```

## Configuration

Set the following environment variables:

```env
LINKEDIN_USERNAME=your.email@example.com
LINKEDIN_PASSWORD=your_password
LINKEDIN_DRY_RUN=false
POST_INTERVAL_MIN=24
POST_INTERVAL_MAX=72
```

## Usage

```typescript
import { LinkedInClientInterface } from '@ai16z/client-linkedin';

// Initialize the client
const manager = await LinkedInClientInterface.start(runtime);

// The client will automatically:
// - Generate and schedule posts
// - Respond to messages and comments
// - Manage connections
// - Track activities
```

## Features

- Professional content generation
- Rate-limited API interactions
- Conversation history tracking
- Connection management
- Activity monitoring
- Cache management

## License

MIT
28 changes: 28 additions & 0 deletions packages/client-linkedin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@ai16z/client-linkedin",
"version": "0.1.0-alpha.1",
"description": "LinkedIn client integration for AI16Z agents",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "jest",
"lint": "eslint src --ext .ts"
},
"dependencies": {
"@ai16z/eliza": "workspace:*",
"linkedin-api": "^1.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.0.0",
"jest": "^29.0.0",
"typescript": "^5.0.0"
},
"peerDependencies": {
"@ai16z/eliza": "workspace:*"
}
}
198 changes: 198 additions & 0 deletions packages/client-linkedin/src/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { EventEmitter } from 'events';
import { Client as LinkedInClient } from 'linkedin-api';
import { elizaLogger } from '@ai16z/eliza';
import { stringToUuid, embeddingZeroVector } from '@ai16z/eliza';

class RequestQueue {
private queue: (() => Promise<any>)[] = [];
private processing = false;

async add<T>(request: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const result = await request();
resolve(result);
} catch (error) {
reject(error);
}
});
this.processQueue();
});
}

private async processQueue() {
if (this.processing || this.queue.length === 0) {
return;
}

this.processing = true;
while (this.queue.length > 0) {
const request = this.queue.shift();
try {
await request();
} catch (error) {
console.error('Error processing request:', error);
this.queue.unshift(request);
await this.exponentialBackoff(this.queue.length);
}
await this.randomDelay();
}
this.processing = false;
}

private async exponentialBackoff(retryCount: number) {
const delay = Math.pow(2, retryCount) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}

private async randomDelay() {
const delay = Math.floor(Math.random() * 2000) + 1500;
await new Promise(resolve => setTimeout(resolve, delay));
}
}

export class ClientBase extends EventEmitter {
private static _linkedInClient: LinkedInClient;
protected linkedInClient: LinkedInClient;
protected runtime: any;
protected profile: any;
protected requestQueue: RequestQueue = new RequestQueue();

constructor(runtime: any) {
super();
this.runtime = runtime;

if (ClientBase._linkedInClient) {
this.linkedInClient = ClientBase._linkedInClient;
} else {
this.linkedInClient = new LinkedInClient();
ClientBase._linkedInClient = this.linkedInClient;
}
}

async init() {
const username = this.runtime.getSetting('LINKEDIN_USERNAME');
const password = this.runtime.getSetting('LINKEDIN_PASSWORD');

if (!username || !password) {
throw new Error('LinkedIn credentials not configured');
}

elizaLogger.log('Logging into LinkedIn...');

try {
await this.linkedInClient.login(username, password);
this.profile = await this.fetchProfile();

if (this.profile) {
elizaLogger.log('LinkedIn profile loaded:', JSON.stringify(this.profile, null, 2));
this.runtime.character.linkedInProfile = {
id: this.profile.id,
username: this.profile.username,
fullName: this.profile.fullName,
headline: this.profile.headline,
summary: this.profile.summary
};
} else {
throw new Error('Failed to load LinkedIn profile');
}

await this.loadInitialState();
} catch (error) {
elizaLogger.error('LinkedIn login failed:', error);
throw error;
}
}

async fetchProfile() {
const cachedProfile = await this.getCachedProfile();
if (cachedProfile) return cachedProfile;

try {
const profile = await this.requestQueue.add(async () => {
const profileData = await this.linkedInClient.getProfile();
return {
id: profileData.id,
username: profileData.username,
fullName: profileData.firstName + ' ' + profileData.lastName,
headline: profileData.headline,
summary: profileData.summary
};
});

await this.cacheProfile(profile);
return profile;
} catch (error) {
console.error('Error fetching LinkedIn profile:', error);
return undefined;
}
}

async loadInitialState() {
await this.populateConnections();
await this.populateRecentActivity();
}

async populateConnections() {
const connections = await this.requestQueue.add(async () => {
return await this.linkedInClient.getConnections();
});

for (const connection of connections) {
const roomId = stringToUuid(`linkedin-connection-${connection.id}`);
await this.runtime.ensureConnection(
stringToUuid(connection.id),
roomId,
connection.username,
connection.fullName,
'linkedin'
);
}
}

async populateRecentActivity() {
const activities = await this.requestQueue.add(async () => {
return await this.linkedInClient.getFeedPosts();
});

for (const activity of activities) {
const roomId = stringToUuid(`linkedin-post-${activity.id}`);
await this.saveActivity(activity, roomId);
}
}

private async saveActivity(activity: any, roomId: string) {
const content = {
text: activity.text,
url: activity.url,
source: 'linkedin',
type: activity.type
};

await this.runtime.messageManager.createMemory({
id: stringToUuid(`${activity.id}-${this.runtime.agentId}`),
userId: activity.authorId === this.profile.id ?
this.runtime.agentId :
stringToUuid(activity.authorId),
content,
agentId: this.runtime.agentId,
roomId,
embedding: embeddingZeroVector,
createdAt: activity.timestamp
});
}

private async getCachedProfile() {
return await this.runtime.cacheManager.get(
`linkedin/${this.runtime.getSetting('LINKEDIN_USERNAME')}/profile`
);
}

private async cacheProfile(profile: any) {
await this.runtime.cacheManager.set(
`linkedin/${profile.username}/profile`,
profile
);
}
}
33 changes: 33 additions & 0 deletions packages/client-linkedin/src/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { z } from 'zod';

export const linkedInEnvSchema = z.object({
LINKEDIN_USERNAME: z.string().min(1, 'LinkedIn username is required'),
LINKEDIN_PASSWORD: z.string().min(1, 'LinkedIn password is required'),
LINKEDIN_DRY_RUN: z.string().transform(val => val.toLowerCase() === 'true'),
POST_INTERVAL_MIN: z.string().optional(),
POST_INTERVAL_MAX: z.string().optional()
});

export async function validateLinkedInConfig(runtime: any) {
try {
const config = {
LINKEDIN_USERNAME: runtime.getSetting('LINKEDIN_USERNAME') || process.env.LINKEDIN_USERNAME,
LINKEDIN_PASSWORD: runtime.getSetting('LINKEDIN_PASSWORD') || process.env.LINKEDIN_PASSWORD,
LINKEDIN_DRY_RUN: runtime.getSetting('LINKEDIN_DRY_RUN') || process.env.LINKEDIN_DRY_RUN,
POST_INTERVAL_MIN: runtime.getSetting('POST_INTERVAL_MIN') || process.env.POST_INTERVAL_MIN,
POST_INTERVAL_MAX: runtime.getSetting('POST_INTERVAL_MAX') || process.env.POST_INTERVAL_MAX
};

return linkedInEnvSchema.parse(config);
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.errors
.map(err => `${err.path.join('.')}: ${err.message}`)
.join('\n');
throw new Error(
`LinkedIn configuration validation failed:\n${errorMessages}`
);
}
throw error;
}
}
37 changes: 37 additions & 0 deletions packages/client-linkedin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { elizaLogger } from '@ai16z/eliza';
import { ClientBase } from './base';
import { LinkedInPostClient } from './post';
import { LinkedInInteractionClient } from './interactions';
import { validateLinkedInConfig } from './environment';

class LinkedInManager {
client: ClientBase;
post: LinkedInPostClient;
interaction: LinkedInInteractionClient;

constructor(runtime: any) {
this.client = new ClientBase(runtime);
this.post = new LinkedInPostClient(this.client, runtime);
this.interaction = new LinkedInInteractionClient(this.client, runtime);
}
}

export const LinkedInClientInterface = {
async start(runtime: any) {
await validateLinkedInConfig(runtime);
elizaLogger.log('LinkedIn client started');

const manager = new LinkedInManager(runtime);
await manager.client.init();
await manager.post.start();
await manager.interaction.start();

return manager;
},

async stop(runtime: any) {
elizaLogger.warn('LinkedIn client stop not implemented yet');
}
};

export default LinkedInClientInterface;
Loading

0 comments on commit 95e6ae7

Please sign in to comment.