-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathyou.txt
More file actions
590 lines (485 loc) Β· 25.3 KB
/
Copy pathyou.txt
File metadata and controls
590 lines (485 loc) Β· 25.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
================================================================================
LINKEDIN AGENT β COMPLETE SYSTEM EXPLANATION
Everything you need to know about how this project works
================================================================================
--------------------------------------------------------------------------------
OVERVIEW
--------------------------------------------------------------------------------
LinkedIn Agent is an autonomous bot that:
1. Watches your GitHub profile (repos, commits, new repos)
2. Clones every repo, reads its README, and uses an LLM to generate descriptions
3. Drafts LinkedIn posts about each repo and sends them to you on Telegram
4. Waits for you to approve / reject / improve each post before anything is published
5. Also monitors AI/ML tech news (HuggingFace, ArXiv, etc.) and can draft posts about those
6. Shows a live terminal UI in the browser so you can watch everything happen in real time
There is NO automatic LinkedIn posting β every post goes through Telegram approval first.
--------------------------------------------------------------------------------
PROJECT STRUCTURE
--------------------------------------------------------------------------------
linkedin-agent/
β
βββ backend/ β Python FastAPI backend (runs on port 8006)
β βββ main.py β FastAPI app entry point + WebSocket endpoint
β βββ agent.py β The "brain" β orchestrates everything
β βββ github_service.py β GitHub API calls + git clone/pull
β βββ telegram_service.py β Telegram bot (send/receive messages)
β βββ llm_service.py β LLM calls (LongCat / OpenAI-compatible)
β βββ scraper_service.py β RSS feed scraper for tech news
β βββ storage.py β Reads/writes all data files
β βββ requirements.txt β Python dependencies
β βββ .env.example β Template for your config
β
βββ src/ β React + TypeScript frontend
β βββ App.tsx β The terminal-style browser UI
β βββ main.tsx β React entry point
β βββ index.css β Minimal global CSS
β
βββ data/ β Created at runtime, NOT committed
β βββ repos.json β Full repo data (JSON) β primary data store
β βββ repos.md β Human-readable repo list (Markdown)
β βββ memory.md β Log of all approved posts (style reference)
β βββ todo.json β Queue of pending posts to draft
β βββ state.json β Tracks whether first-run init is done
β βββ pending_messages.json β Telegram messages queued while bot was offline
β βββ posts/ β Draft posts (not yet approved)
β β βββ post1-2024-01-15.txt
β β βββ approved/ β Posts that were approved by you
β β βββ post2_2024-01-15.txt
β βββ repos/ β Git-cloned repositories (one folder per repo)
β βββ BTSC-UNet-ViT/
β βββ ...
β
βββ index.html β Vite HTML entry for the frontend
βββ package.json β Node.js deps for the frontend
βββ README.md β Quick-start guide
βββ you.txt β THIS FILE
--------------------------------------------------------------------------------
FILE-BY-FILE BREAKDOWN
--------------------------------------------------------------------------------
βββ backend/main.py βββ
Role: FastAPI application entry point and server.
What it does:
- Loads .env variables at startup via python-dotenv
- Creates the FastAPI app with CORS enabled (any origin can call the API)
- On startup: calls storage.ensure_data_dir() and launches agent.run_agent()
as an asyncio background task
- On shutdown: cancels the agent task and stops the Telegram bot
- Serves these HTTP endpoints:
GET /status β JSON with init state, repo count, task counts, agent status
GET /todo β Full todo.json list
GET /repos β Full repos.json list
GET /repos/md β The contents of repos.md as a string
GET /memory β The contents of memory.md as a string
POST /restart β Cancel and restart the agent background task
GET /health β Simple {"status": "ok"}
- Serves a WebSocket at /ws
The frontend connects here to receive real-time agent log lines
Every time agent.log() is called, it queues the message to all WS clients
The server sends a ping every 30 s to keep the connection alive
- Runs via uvicorn on port 8006 (configurable via PORT env var)
βββ backend/agent.py βββ
Role: The core "brain" of the system. Coordinates all other services.
Key functions:
log(msg)
Writes to Python logger + terminal AND broadcasts to all WebSocket clients.
This is how the browser terminal sees real-time updates.
check_config()
Checks that TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, and LLM API key are set.
Logs a warning if any are missing.
first_run_setup()
Called ONCE when state.json doesn't exist or initialized=False.
Steps:
1. Fetches all GitHub repos via GitHub API
2. For each repo: clones it, reads its README, calls LLM to generate a
2-3 sentence description
3. Saves everything to repos.json and writes repos.md
4. Builds todo.json with one (or 2-3 for starred repos) post tasks per repo
5. Marks state.json as initialized=True
6. Sends a Telegram notification
sync_repos_on_startup()
Called on every restart AFTER first run.
Steps:
1. Fetches current GitHub repo list
2. Compares with repos.json to find new repos
3. Clones new repos, generates descriptions for them
4. Pulls (git pull --ff-only) all existing repos
5. ALWAYS saves repos.json and writes repos.md (even if no new repos)
6. Adds todo tasks for any newly discovered repos
process_next_repo_post()
Picks the next "pending" task from todo.json and drafts a LinkedIn post.
Steps:
1. Finds next pending repo_post task in todo.json
2. Enforces one-post-per-day limit (checks approved/ folder)
3. Loads repo data + README + memory.md context
4. Calls LLM to draft a LinkedIn post (up to 3 attempts)
5. Saves draft to data/posts/
6. Sends draft to Telegram for review
7. Waits up to 24 hours for user decision:
approve β saves to approved/, appends to memory.md, marks todo done
reject β marks todo done, skips
regenerate β calls itself recursively to draft again
improve β sends feedback to LLM, re-sends for approval
timeout β skips for now (will retry next cycle)
check_github_updates()
Looks for interesting new GitHub activity (pushes, new repos).
Steps:
1. Fetches recent public events from GitHub API
2. Groups push events by repo, identifies CreateEvents
3. For each repo with pushes: fetches commits, asks LLM if it's interesting
4. If interesting: notifies user on Telegram, asks if they want a post
5. If yes: drafts and sends post for Telegram approval
post_from_news()
Scrapes tech news and drafts a post about the most interesting article.
Steps:
1. Fetches RSS feeds from multiple sources (see scraper_service.py)
2. Lists top 10 articles to LLM, asks it to pick the most interesting
3. Drafts a LinkedIn post about that article
4. Sends to Telegram for approval
handle_custom_command(text)
Called when the user sends any message to the Telegram bot.
Routes to:
/status β sends agent status summary
/todo β sends list of pending tasks
/repos β sends repos.md content
/post <t> β drafts a custom post on topic t, sends for approval
/skip β (handled directly by telegram_service)
unknown / β sends help message
other text β sends to LLM as casual chat, replies with chatbot response
run_agent()
The main entry point (called as asyncio Task from main.py).
Steps:
1. Logs startup, checks config
2. Registers handle_custom_command as the Telegram message callback
3. Starts Telegram bot (non-blocking, retries in background)
4. If first run: calls first_run_setup()
Otherwise: calls sync_repos_on_startup()
5. Enters infinite loop:
- If todo has pending tasks β process_next_repo_post()
- Else if GitHub has new activity β check_github_updates()
- Else β post_from_news()
- Sleep 1 hour, repeat
βββ backend/github_service.py βββ
Role: All GitHub interactions β API calls and git operations.
Environment variables:
GITHUB_USERNAME (default: H0NEYP0T-466)
REPOS_DIR (default: ./data/repos)
CLONE_DEPTH (default: 100 commits)
Key functions:
fetch_all_repos()
Uses GitHub API to get all public repos for GITHUB_USERNAME.
Paginates until all repos are retrieved (100 per page).
fetch_latest_commits(repo_name, limit=5)
Gets the last N commits for a repo via GitHub API.
fetch_recent_activity()
Gets the last 30 public events (pushes, new repos, etc.) for the user.
clone_repo(repo, log_callback)
Clones a repo to REPOS_DIR/repo_name.
- If the folder already exists: runs git pull --ff-only
- If new: tries git clone --depth=CLONE_DEPTH
- On failure with "fetch-pack" / "index-pack" errors (transient network
issues): automatically retries with --depth=1 as a fallback
Returns True on success, False on failure.
get_repo_readme(repo_name)
Reads the README file from a cloned repo (tries multiple file names).
Returns up to 3000 characters.
get_repo_file_tree(repo_name)
Returns a list of files (max depth 2, max 40 lines) from a cloned repo.
sync_repos(stored_repos, log_callback)
Compares stored repos vs live GitHub repos.
Clones new ones, pulls existing ones.
Returns the updated combined list.
βββ backend/storage.py βββ
Role: All file I/O β reads and writes every data file.
Data directory: DATA_DIR (default ./data)
Files managed:
data/memory.md Append-only log of approved posts
data/repos.md Human-readable repo list (Markdown)
data/repos.json Full repo data with generated_description, posted, cloned
data/todo.json Queue of pending post tasks
data/state.json Tracks initialization state
data/pending_messages.json Telegram messages queued while bot was offline
data/posts/ Draft post text files
data/posts/approved/ Approved post text files
Key functions:
write_repos_md(repos)
Writes a Markdown file listing all repos with their generated_description
(falls back to GitHub description, then "No description").
Shows: URL, language, stars, description, topics, posted status (β
/β³).
save_repos_data(repos) / get_repos_data()
JSON serialization for repos.json β the primary data store.
mark_repo_posted(repo_name)
Sets posted=True in repos.json and rewrites repos.md.
build_initial_todo(repos)
Creates todo.json with one task per repo (2 tasks for repos with >5 stars).
save_post_draft(content, label) / save_approved_post(content, label)
Saves post text to posts/ or posts/approved/ with auto-incremented filename.
Draft pattern: post{N}-{YYYY-MM-DD}[-label].txt
Approved pattern: post{N}_{YYYY-MM-DD}[_label].txt
can_post_today()
Checks posts/approved/ to see if a post was already saved today.
Enforces the one-post-per-day limit.
load_pending_messages() / save_pending_messages() / add_pending_message()
Persist Telegram messages that couldn't be sent (bot offline).
Flushed automatically when the bot comes back online.
βββ backend/telegram_service.py βββ
Role: All Telegram bot interactions β sending and receiving.
Environment variables:
TELEGRAM_BOT_TOKEN Your bot token from @BotFather
TELEGRAM_CHAT_ID Your personal Telegram chat ID (only this chat is trusted)
TELEGRAM_PROXY Optional proxy URL (HTTPS or SOCKS5)
How it works:
- Uses python-telegram-bot library
- Bot is started non-blocking in a background asyncio task
- If bot startup fails (e.g. network issue), it retries every 60 seconds
- Messages that fail to send are queued in memory AND persisted to
pending_messages.json; they are re-sent when the bot connects
Message flow:
User β Telegram β bot receives β _handle_message():
"approve" β puts {"action": "approve"} in _decision_queue
"reject" β puts {"action": "reject"} in _decision_queue
"regenerate" β puts {"action": "regenerate"} in _decision_queue
"improve: <text>" β puts {"action": "improve", "feedback": text} in queue
/status, /todo etc β calls the registered message_callback (handle_custom_command)
anything else β also calls message_callback (casual chat goes to LLM)
Agent β send_post_for_review(post) β sends Markdown message with approve/reject/etc options
Agent β get_user_decision(timeout) β awaits _decision_queue.get() up to timeout seconds
Commands registered with Telegram:
/start β shows bot info and your chat ID
/status β delegates to message_callback
/todo β delegates to message_callback
/post β delegates to message_callback
/repos β delegates to message_callback
/skip β puts {"action": "reject"} in decision queue immediately
βββ backend/llm_service.py βββ
Role: All LLM (AI) calls β text generation.
Uses LongCat's OpenAI-compatible API (or any OpenAI-format endpoint).
Environment variables:
LONGCAT_API_KEY or OPENAI_API_KEY
LONGCAT_BASE_URL (default: https://api.longcat.chat/openai)
LLM_MODEL (default: longcat-flash-lite)
LLM_TIMEOUT_SECONDS (default: 60)
Functions:
generate_text(prompt, temperature)
Core function: sends a chat completion request and returns the text.
generate_repo_description(repo, readme)
Writes a 2-3 sentence description of a GitHub repo.
Input: repo metadata + README content.
generate_linkedin_post(repo, description, readme, memory_context, post_index, total_posts)
Drafts a LinkedIn post about a repo.
- For repos with multiple posts: focuses each on overview / tech details / results
- Uses memory_context (previous posts) to avoid repetition
- Targets 150-300 words with 3-5 hashtags
generate_news_post(article, memory_context)
Drafts a LinkedIn post about a tech news article.
generate_custom_post(topic, repos_md, memory_context)
Drafts a LinkedIn post on a custom topic, mentioning relevant repos if any.
summarize_commit_activity(repo_name, commits)
Summarizes recent commits in 1-2 sentences.
is_activity_worth_posting(summary)
Asks LLM "is this interesting enough to post?" β returns bool.
chat_response(user_message, context)
Friendly chatbot reply for casual Telegram messages.
βββ backend/scraper_service.py βββ
Role: Fetches the latest AI/ML/tech articles from RSS feeds.
Sources (configured in SOURCES list):
- HuggingFace Blog
- ArXiv AI (cs.AI)
- ArXiv ML (cs.LG)
- Google AI Blog
- Papers With Code
- The Gradient
- Towards Data Science
fetch_rss_feed(source)
Fetches one RSS feed, parses it with feedparser, strips HTML from summaries.
Returns up to MAX_ARTICLES_PER_SOURCE (3) articles per source.
fetch_latest_tech_news()
Fetches all sources concurrently (asyncio.gather), deduplicates by URL.
Returns a flat list of unique articles.
fetch_page_with_cloudflare(url)
Optional: uses Cloudflare Browser Rendering API to fetch JS-heavy pages.
Only active if CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are set.
βββ src/App.tsx (Frontend) βββ
Role: Terminal-style browser UI that shows agent logs in real time.
What you see:
- Black background, green monospace text (like a terminal)
- ASCII art "LINKEDIN AGENT" banner at startup
- Status bar at the top: ONLINE/OFFLINE, repo count, pending/done tasks
- Scrolling log of every message the agent emits
- Blinking cursor at the bottom
How it works:
1. On load: connects to the backend WebSocket at /ws
2. Receives JSON messages: {"type": "log", "message": "..."} β appends to log
3. Pings are silently ignored (just keep-alives)
4. If connection drops: auto-reconnects every 5 seconds
5. Polls /status every 30 seconds to update the status bar
6. Auto-scrolls to the bottom as new lines arrive
7. Keeps max 1000 log lines in memory (older lines are dropped)
Configuration:
VITE_BACKEND_WS WebSocket URL (default: ws://localhost:8006/ws)
VITE_BACKEND_URL HTTP URL (default: http://localhost:8006)
--------------------------------------------------------------------------------
HOW THE CHATBOT + AGENT WORK TOGETHER
--------------------------------------------------------------------------------
The Telegram bot and the agent are tightly coupled through two mechanisms:
1. _decision_queue (asyncio.Queue in telegram_service.py)
When the agent sends a post for review (via send_post_for_review()), it
then calls get_user_decision(timeout=86400) which does:
return await asyncio.wait_for(_decision_queue.get(), timeout=86400)
This BLOCKS the agent's current task for up to 24 hours, waiting for the
user to reply on Telegram.
When the user sends "approve", "reject", "regenerate", or "improve: ...",
the telegram _handle_message() puts the decision into _decision_queue,
which unblocks the agent immediately.
2. message_callback (handle_custom_command in agent.py)
All OTHER messages (commands and casual chat) are routed to this callback.
It runs the command or generates a chatbot reply using the LLM.
This is non-blocking β the agent handles the message and keeps going.
Visual flow:
User types on Telegram
β
βΌ
_handle_message() in telegram_service.py
β
βββ "approve" / "reject" / "regenerate" / "improve: ..."
β β
β βΌ
β _decision_queue.put({"action": ...})
β β
β βΌ
β agent's get_user_decision() returns
β β
β βΌ
β agent processes the decision (save, skip, regenerate)
β
βββ any other text (commands, casual chat)
β
βΌ
message_callback(text) = handle_custom_command()
β
βββ /status β read state, reply with status
βββ /todo β read todo.json, reply with list
βββ /repos β read repos.md, reply
βββ /post β generate custom post, send for approval
βββ chat β LLM chat_response(), reply naturally
--------------------------------------------------------------------------------
DATA FLOW: FIRST RUN
--------------------------------------------------------------------------------
Server starts
β
main.py: asyncio.create_task(agent.run_agent())
β
agent.run_agent():
- Checks config (env vars)
- Starts Telegram bot (background, non-blocking)
- storage.is_first_run() β True (no state.json)
β
agent.first_run_setup():
- github_service.fetch_all_repos() β list of repos from GitHub API
- For each repo:
github_service.clone_repo() β git clone to data/repos/<name>/
github_service.get_repo_readme() β read README.md
llm_service.generate_repo_description() β 2-3 sentence description
- storage.save_repos_data(enriched_repos) β writes data/repos.json
- storage.write_repos_md(enriched_repos) β writes data/repos.md
- storage.build_initial_todo(enriched_repos) β writes data/todo.json
- storage.mark_initialized() β writes data/state.json {initialized: true}
- telegram_service.send_message("initialized!") β Telegram notification
β
agent enters main loop
--------------------------------------------------------------------------------
DATA FLOW: SUBSEQUENT RUNS
--------------------------------------------------------------------------------
Server starts
β
agent.run_agent():
- storage.is_first_run() β False (state.json exists)
β
agent.sync_repos_on_startup():
- github_service.fetch_all_repos() β current GitHub list
- github_service.sync_repos():
For NEW repos: git clone
For EXISTING repos: git pull --ff-only
- If new repos found: generate descriptions, add todo tasks
- ALWAYS: storage.save_repos_data() + storage.write_repos_md()
β
agent enters main loop:
ββ pending todo tasks? β process_next_repo_post()
β β
β draft post β send to Telegram β wait for decision β act
β
ββ no pending tasks β check_github_updates()
β β
β interesting commits? β ask user β draft β Telegram β decision
β
ββ nothing interesting β post_from_news()
β
fetch RSS feeds β pick best article β draft β Telegram β decision
[sleep 1 hour, repeat]
--------------------------------------------------------------------------------
COMMON ISSUES AND FIXES
--------------------------------------------------------------------------------
PROBLEM: "fatal: fetch-pack: invalid index-pack output" when cloning
-----------------------------------------------------------------------
This is a transient network error where git loses its connection mid-transfer.
FIX (now in the code): clone_repo() automatically retries with --depth=1
(single commit history) when this error is detected. This is much faster
and more reliable on unstable connections.
PROBLEM: repos.md not showing generated descriptions
-----------------------------------------------------------------------
FIX: write_repos_md() now checks generated_description first, then falls
back to the GitHub description, then "No description".
PROBLEM: repos.md not being updated on subsequent runs
-----------------------------------------------------------------------
Previously repos.md was only rewritten if NEW repos were discovered.
FIX: sync_repos_on_startup() now ALWAYS calls save_repos_data() and
write_repos_md() at the end, ensuring the file is always up to date.
PROBLEM: "TELEGRAM_BOT_TOKEN not set" / Telegram messages not arriving
-----------------------------------------------------------------------
Check your backend/.env file. The bot will keep retrying every 60 seconds
if it can't connect. Messages generated while the bot is offline are queued
in pending_messages.json and sent once the connection is restored.
PROBLEM: LLM calls failing
-----------------------------------------------------------------------
Check LONGCAT_API_KEY in backend/.env. The agent will use repo.get("description")
as fallback if the LLM fails. After 3 failed attempts on post generation, the
task is skipped and the agent moves on.
--------------------------------------------------------------------------------
HOW TO RUN
--------------------------------------------------------------------------------
1. Backend:
cd backend
cp .env.example .env
# Edit .env: set LONGCAT_API_KEY, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
pip install -r requirements.txt
python main.py
2. Frontend:
# From repo root
npm install
npm run dev
# Open http://localhost:5173
3. Get your Telegram chat ID:
- Start your bot on Telegram
- Send /start to the bot
- Watch the backend logs β it will print your chat ID
- Add it to .env as TELEGRAM_CHAT_ID
--------------------------------------------------------------------------------
ENVIRONMENT VARIABLES REFERENCE
--------------------------------------------------------------------------------
Required:
LONGCAT_API_KEY LLM API key (or OPENAI_API_KEY as alternative)
TELEGRAM_BOT_TOKEN From @BotFather on Telegram
TELEGRAM_CHAT_ID Your personal Telegram chat ID
Optional:
GITHUB_USERNAME Default: H0NEYP0T-466
LLM_MODEL Default: longcat-flash-lite
LONGCAT_BASE_URL Default: https://api.longcat.chat/openai
LLM_TIMEOUT_SECONDS Default: 60
DATA_DIR Default: ./data
REPOS_DIR Default: ./data/repos
PORT Default: 8006
TELEGRAM_PROXY SOCKS5 or HTTPS proxy for blocked regions
CLOUDFLARE_API_TOKEN Optional, for JS-heavy page scraping
CLOUDFLARE_ACCOUNT_ID Optional, for JS-heavy page scraping
RELOAD Set to "1" for uvicorn auto-reload (dev only)
--------------------------------------------------------------------------------
================================================================================