Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LinkedIn Client #973

Merged
merged 1 commit into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
[email protected]
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