forked from elizaOS/eliza
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request elizaOS#973 from bkellgren/main
LinkedIn Client
- Loading branch information
Showing
8 changed files
with
801 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:*" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.