chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:47 +08:00
commit 7653f56fed
1422 changed files with 359026 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
FIRECRAWL_API_KEY=your_firecrawl_api_key
OPENAI_API_KEY=your_openai_api_key_incase_you_want_to_use_openai_for_llm
TYPEFULLY_API_KEY=your_typefully_api_key
+34
View File
@@ -0,0 +1,34 @@
# Content writing agentic-workflow
This project leverages CrewAI Flow to scrape a website, prepare a social post and publish it, powered by a locally running Llama 3.2!
### Watch Demo on YouTube
[![Watch Demo on YouTube](https://github.com/patchy631/ai-engineering-hub/blob/main/content_planner_flow/resources/thumbnail.png)](https://www.youtube.com/watch?v=Nor6vNl1NPo)
## Installation and setup
**Get API Keys**:
- [FireCrawl](https://docs.firecrawl.dev/introduction)
- [Typefully](https://support.typefully.com/en/articles/8718287-typefully-api)
**Install Dependencies**:
Ensure you have Python 3.11 or later installed.
```bash
pip install crewai crewai-tools
```
---
## 📬 Stay Updated with Our Newsletter!
**Get a FREE Data Science eBook** 📖 with 150+ essential lessons in Data Science when you subscribe to our newsletter! Stay in the loop with the latest tutorials, insights, and exclusive resources. [Subscribe now!](https://join.dailydoseofds.com)
[![Daily Dose of Data Science Newsletter](https://github.com/patchy631/ai-engineering/blob/main/resources/join_ddods.png)](https://join.dailydoseofds.com)
---
## Contribution
Contributions are welcome! Please fork the repository and submit a pull request with your improvements.
@@ -0,0 +1,35 @@
draft_analyzer:
role: >
Draft Analyzer
goal: >
Analyze draft and identify key ideas, sections and technical concepts
backstory: >
You are a technical writer with years of experience writing, editing and
reviewing technical blogs. You have a talent for understanding and
documenting technical concepts.
verbose: false
twitter_thread_planner:
role: >
Twitter Thread Planner
goal: >
Create a Twitter thread plan based on the provided draft analysis
backstory: >
You are a technical writer with years of experience in converting long
technical blogs into Twitter threads. You have a talent for breaking longform
content into bite-sized tweets that are engaging and informative. And identify
relevant urls to media that can be associated with a tweet.
verbose: false
linkedin_post_planner:
role: >
LinkedIn Post Planner
goal: >
Create an engaging LinkedIn post based on the provided draft analysis
backstory: >
You are a technical writer with extensive experience crafting technical LinkedIn
content. You excel at distilling technical concepts into clear, authoritative posts
that resonate with a professional audience while maintaining technical accuracy.
You know how to balance technical depth with accessibility and incorporate relevant
hashtags and mentions to maximize engagement.
verbose: false
@@ -0,0 +1,83 @@
analyze_draft:
description: |
Analyze the markdown file at {draft_path} to create a developer-focused
technical overview
1. Map out the core idea that the blog discusses
2. Identify key sections and what each section is about
3. For each section, extract all URLs that appear inside image markdown syntax ![](image_url)
4. You must associate these identified image urls to their correspoinding sections, so that we can use them with the tweets as media pieces
Focus on details that are important for a comprehensive understanding of
the blog.
expected_output: |
A technical analysis containing:
- Blog title and core concept/idea
- Key technical sections identified with their main points
- Important code examples or technical concepts covered
- Key takeaways for developers
- Relevant urls to media that are associated with the key sections and can be associated with a tweet, this must be done
create_twitter_thread_plan:
description: |
Develop an engaging Twitter thread based on the draft analysis provided and closely follow the writing style prvided in the {path_to_example_threads}
The thread should break down complex technical concepts into digestible, tweet-sized chunks
that maintain technical accuracy while being accessible.
Plan should include:
- A strong hook tweet that captures attention, it should be under 10 words, it must be same as the title of the blog
- Logical flow from basic to advanced concepts
- Code snippets or key technical highlights that fit Twitter's format
- Relevant urls to media that are associated with the key sections and must be associated with their corresponding tweets
- Clear takeaways for engineering audience
Make sure to cover:
- close follow the writing style provided in the {path_to_example_threads}
- The core problem being solved
- Key technical innovations or approaches
- Interesting implementation details
- Real-world applications or benefits
- Call to action for the conclusion
- Add relevant urls to each tweet that can be associated with a tweet
Focus on creating a narrative that technical audiences will find valuable
while keeping each tweet concise, accessible and impactful.
expected_output: |
A Twitter thread with a list of tweets, where each tweet has the following:
- content
- urls to media that are associated with the tweet, whenever possible
- is_hook: true if the tweet is a hook tweet, false otherwise
create_linkedin_post_plan:
description: |
Develop a comprehensive LinkedIn post based on the draft analysis provided
The post should present technical content in a professional, long-form format
while maintaining engagement and readability.
Plan should include:
- An attention-grabbing opening statement, it should be same as the title of the blog
- Well-structured body that breaks down the technical content
- Professional tone suitable for LinkedIn's business audience
- One main blog URL placed strategically at the end of the post
- Strategic use of line breaks and formatting
- Relevant hashtags (3-5 maximum)
Make sure to cover:
- The core technical problem and its business impact
- Key solutions and technical approaches
- Real-world applications and benefits
- Professional insights or lessons learned
- Clear call to action
Focus on creating content that resonates with both technical professionals
and business leaders while maintaining technical accuracy.
expected_output: |
A LinkedIn post plan containing:
- content
- a main blog url that is associated with the post
+534
View File
@@ -0,0 +1,534 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Content Planning and Publishing Crew"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This notebook demonstrates how to create an AI crew for planning and publishing content using CrewAI Flows.\n",
"The crew will take a link to blog post, download content as markdown using firecrawl, analyze it and generate a twitter thread and schedule it on Typefully."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Initialization and Setup\n",
"Initial imports for the CrewAI Flow and Crew and setting up the environment"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [],
"source": [
"# Importing necessary libraries\n",
"import getpass\n",
"import os\n",
"import datetime\n",
"import uuid\n",
"import yaml\n",
"import json\n",
"import subprocess\n",
"from pathlib import Path\n",
"import pydantic\n",
"from pydantic import BaseModel\n",
"from typing import Optional\n",
"\n",
"# Firecrawl SDK\n",
"from firecrawl import FirecrawlApp\n",
"\n",
"# Typefully scheduler\n",
"import scheduler\n",
"\n",
"# Importing Crew related components\n",
"from crewai import Agent, Task, Crew, LLM\n",
"\n",
"# Importing CrewAI Flow related components\n",
"from crewai.flow.flow import Flow, listen, start, router, or_\n",
"\n",
"from dotenv import load_dotenv\n",
"load_dotenv()\n",
"# Apply a patch to allow nested asyncio loops in Jupyter\n",
"import nest_asyncio\n",
"nest_asyncio.apply()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup LLM\n",
"\n",
"Make sure you have ollama installed and running on your machine"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"llm = LLM(\n",
" model=\"ollama/llama3.2\",\n",
" base_url=\"http://localhost:11434\"\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Blog Post URL"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [],
"source": [
"blog_post_url = \"https://blog.dailydoseofds.com/p/5-chunking-strategies-for-rag\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plan for our Flow\n",
"\n",
"1. Scrape the blog post\n",
"2. Decode where to post using a router\n",
"3. Kickoff the right **[Crew of Agents]** to prepare a draft ready to publish\n",
"4. Publish it using typefully"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<img src=\"content_writing_flow.png\" width=\"1000\" height=\"750\"/>"
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"execution_count": 22,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from IPython.display import HTML\n",
"HTML('<img src=\"content_writing_flow.png\" width=\"1000\" height=\"750\"/>')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Twitter Thread Planning Crew\n",
"\n",
"This structure will be used to capture the output of the planning crew which will be used to create the twitter thread and schedule it on Typefully."
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [],
"source": [
"class Tweet(BaseModel):\n",
" \"\"\"Represents an individual tweet in a thread\"\"\"\n",
" content: str\n",
" is_hook: bool = False # Identifies if this is the opening/hook tweet\n",
" media_urls: Optional[list[str]] = [] # Optional media attachments (images, code snippets)\n",
"\n",
"class Thread(BaseModel):\n",
" \"\"\"Represents a Twitter thread\"\"\"\n",
" topic: str # Main topic/subject of the thread\n",
" tweets: list[Tweet] # List of tweets in the thread"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [],
"source": [
"from crewai_tools import (\n",
" DirectoryReadTool,\n",
" FileReadTool,\n",
")\n",
"\n",
"# Load agent and task configurations from YAML files\n",
"with open('config/planner_agents.yaml', 'r') as f:\n",
" agents_config = yaml.safe_load(f)\n",
"\n",
"with open('config/planner_tasks.yaml', 'r') as f:\n",
" tasks_config = yaml.safe_load(f)"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"2024-12-19 14:18:07,847 - 7988681536 - __init__.py-__init__:537 - WARNING: Overriding of current TracerProvider is not allowed\n"
]
}
],
"source": [
"draft_analyzer = Agent(config=agents_config['draft_analyzer'], tools=[\n",
" DirectoryReadTool(),\n",
" FileReadTool()\n",
"], llm=llm)\n",
"twitter_thread_planner = Agent(config=agents_config['twitter_thread_planner'], tools=[\n",
" DirectoryReadTool(),\n",
" FileReadTool()\n",
"], llm=llm)\n",
"\n",
"analyze_draft = Task(\n",
" config=tasks_config['analyze_draft'],\n",
" agent=draft_analyzer\n",
")\n",
"create_twitter_thread_plan = Task(\n",
" config=tasks_config['create_twitter_thread_plan'],\n",
" agent=twitter_thread_planner,\n",
" output_pydantic=Thread\n",
")\n",
"\n",
"planning_crew = Crew(\n",
" agents=[draft_analyzer, twitter_thread_planner],\n",
" tasks=[analyze_draft, create_twitter_thread_plan],\n",
" verbose=False\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# LinkedIn Post Planning Crew"
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {},
"outputs": [],
"source": [
"class LinkedInPost(BaseModel):\n",
" \"\"\"Represents a LinkedIn post\"\"\"\n",
" content: str\n",
" media_url: str # Main image url for the post"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"2024-12-19 14:25:39,453 - 7988681536 - __init__.py-__init__:537 - WARNING: Overriding of current TracerProvider is not allowed\n"
]
}
],
"source": [
"linkedin_post_planner = Agent(config=agents_config['linkedin_post_planner'], llm=llm)\n",
"\n",
"create_linkedin_post_plan = Task(\n",
" config=tasks_config['create_linkedin_post_plan'],\n",
" agent=linkedin_post_planner,\n",
" output_pydantic=LinkedInPost\n",
")\n",
"\n",
"linkedin_planning_crew = Crew(\n",
" agents=[draft_analyzer, linkedin_post_planner],\n",
" tasks=[analyze_draft, create_linkedin_post_plan],\n",
" verbose=False\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Create Content Planning Flow\n",
"\n",
"A Flow to create the content planning for twitter and linkedin using separate crews for twitter and linkedin"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<img src=\"content_writing_flow.png\" width=\"1000\" height=\"750\"/>"
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"execution_count": 37,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from IPython.display import HTML\n",
"HTML('<img src=\"content_writing_flow.png\" width=\"1000\" height=\"750\"/>')"
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {},
"outputs": [],
"source": [
"from crewai.flow.flow import Flow, listen, start, router, or_\n",
"\n",
"class ContentPlanningState(BaseModel):\n",
" \"\"\"\n",
" State for the content planning flow\n",
" \"\"\"\n",
" blog_post_url: str = blog_post_url\n",
" draft_path: Path = \"workdir/\"\n",
" post_type: str = \"twitter\"\n",
" path_to_example_threads: str = \"workdir/example_threads.txt\"\n",
"\n",
"class CreateContentPlanningFlow(Flow[ContentPlanningState]):\n",
" # Scrape the blog post \n",
" # No need for AI Agents on this step, so we just use regular Python code\n",
" @start()\n",
" def scrape_blog_post(self):\n",
" print(f\"# fetching draft from: {self.state.blog_post_url}\")\n",
" app = FirecrawlApp(api_key=os.getenv(\"FIRECRAWL_API_KEY\"))\n",
" scrape_result = app.scrape_url(self.state.blog_post_url, params={'formats': ['markdown', 'html']})\n",
" try:\n",
" title = scrape_result['metadata']['title']\n",
" except Exception as e:\n",
" title = str(uuid.uuid4())\n",
" self.state.draft_path = f'workdir/{title}.md'\n",
" with open(self.state.draft_path, 'w') as f:\n",
" f.write(scrape_result['markdown'])\n",
" return self.state\n",
"\n",
" @router(scrape_blog_post)\n",
" def select_platform(self):\n",
" if self.state.post_type == \"twitter\":\n",
" return \"twitter\"\n",
" elif self.state.post_type == \"linkedin\":\n",
" return \"linkedin\"\n",
"\n",
" @listen(\"twitter\")\n",
" def twitter_draft(self):\n",
" print(f\"# Planning content for: {self.state.draft_path}\")\n",
" result = planning_crew.kickoff(inputs={'draft_path': self.state.draft_path, 'path_to_example_threads': self.state.path_to_example_threads})\n",
" print(f\"# Planned content for {self.state.draft_path}:\")\n",
" for tweet in result.pydantic.tweets:\n",
" print(f\" - {tweet.content}\")\n",
" return result\n",
" \n",
" @listen(\"linkedin\")\n",
" def linkedin_draft(self):\n",
" print(f\"# Planning content for: {self.state.draft_path}\")\n",
" result = linkedin_planning_crew.kickoff(inputs={'draft_path': self.state.draft_path})\n",
" print(f\"# Planned content for {self.state.draft_path}:\")\n",
" print(f\" - {result.pydantic.content}\")\n",
" return result\n",
"\n",
" @listen(or_(twitter_draft, linkedin_draft))\n",
" def save_plan(self, plan):\n",
" with open(f'thread/{self.state.draft_path.split(\"/\")[-1]}_{self.state.post_type}.json', 'w') as f:\n",
" json.dump(plan.pydantic.model_dump(), f, indent=2)\n",
"\n",
" @listen(or_(twitter_draft, linkedin_draft))\n",
" def publish(self, plan):\n",
" print(f\"# Publishing thread for: {self.state.draft_path}\")\n",
" ## Schedule for 1 hour from now \n",
" response = scheduler.schedule(\n",
" thread_model=plan,\n",
" post_type=self.state.post_type\n",
" )\n",
" print(f\"# Thread scheduled for: {self.state.draft_path}\")\n",
" print(f\"Here's the link to scheduled draft: {response['share_url']}\")\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Implementing helper methods to plot and execute the flow in a Jupyter notebook"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Plot saved as crewai_flow.html\n"
]
}
],
"source": [
"# Plot the flow\n",
"flow = CreateContentPlanningFlow()\n",
"flow.plot()\n",
"\n",
"# Display the flow visualization using IFrame\n",
"from IPython.display import IFrame\n",
"\n",
"# Display the flow visualization\n",
"# IFrame(src='./crewai_flow.html', width='100%', height=400)"
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"ContentPlanningState(blog_post_url='https://blog.dailydoseofds.com/p/5-chunking-strategies-for-rag', draft_path='workdir/', post_type='twitter', path_to_example_threads='workdir/example_threads.txt')"
]
},
"execution_count": 44,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"post_type = \"twitter\"\n",
"flow = CreateContentPlanningFlow()\n",
"flow.state.post_type = post_type\n",
"flow.state"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"# fetching draft from: https://blog.dailydoseofds.com/p/5-chunking-strategies-for-rag\n",
"# Planning content for: workdir/5 Chunking Strategies For RAG - by Avi Chawla.md\n",
"# Planned content for workdir/5 Chunking Strategies For RAG - by Avi Chawla.md:\n",
" - 5 Chunking Strategies For RAG\n",
" - What is Retrieval-Augmented Generation (RAG)? 🤔 It's a technique that boosts language models with information retrieval, refining the interaction with large data. Chunking plays a crucial role here!\n",
" - 1️⃣ Fixed-size Chunking: This method splits text into uniform segments. While straightforward, it risks losing contextual meaning. Use with care! 🔍\n",
" - 2️⃣ Semantic Chunking: Here, we create chunks based on meaningful units (like sentences). This ensures better coherence and relevance in information retrieval. 🤝\n",
" - 3️⃣ Recursive Chunking: Breaks text using natural language boundaries, like paragraphs, and refines chunks that are too large. Preserve flow while keeping it efficient! 📏\n",
" - 4️⃣ Document Structure-based Chunking: Leverage document headings and sections for chunk boundaries. This maintains logical structure and aids retrieval accuracy! 📚\n",
" - 5️⃣ LLM-based Chunking: Use large language models to generate semantically-rich chunks. This is powerful but comes with high computational costs. ⚡️\n",
" - Key Takeaway: Each chunking strategy has its own strengths and weaknesses. Select the right method based on your model and content to optimize performance! 🚀\n",
" - If you're diving into RAG, embrace chunking. Explore these strategies and elevate your AI's ability to communicate knowledge effectively! Join the discussion! 💬\n",
"# Publishing thread for: workdir/5 Chunking Strategies For RAG - by Avi Chawla.md\n",
"######## Thread JSON: {'topic': '5 Chunking Strategies For RAG', 'tweets': [{'content': '5 Chunking Strategies For RAG', 'is_hook': True, 'media_urls': []}, {'content': \"What is Retrieval-Augmented Generation (RAG)? 🤔 It's a technique that boosts language models with information retrieval, refining the interaction with large data. Chunking plays a crucial role here!\", 'is_hook': False, 'media_urls': ['https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6878b8fa-5e74-45a1-9a89-5aab92889126_2366x990.gif']}, {'content': '1️⃣ Fixed-size Chunking: This method splits text into uniform segments. While straightforward, it risks losing contextual meaning. Use with care! 🔍', 'is_hook': False, 'media_urls': ['https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c422a0-f0e2-457c-a256-4476a56a601f_943x232.png']}, {'content': '2️⃣ Semantic Chunking: Here, we create chunks based on meaningful units (like sentences). This ensures better coherence and relevance in information retrieval. 🤝', 'is_hook': False, 'media_urls': ['https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6ad83a6-2879-4c77-9e49-393f16577aef_1066x288.gif']}, {'content': '3️⃣ Recursive Chunking: Breaks text using natural language boundaries, like paragraphs, and refines chunks that are too large. Preserve flow while keeping it efficient! 📏', 'is_hook': False, 'media_urls': ['https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4009caa-34fc-48d6-8102-3d0f6f2c1386_1066x316.gif']}, {'content': '4️⃣ Document Structure-based Chunking: Leverage document headings and sections for chunk boundaries. This maintains logical structure and aids retrieval accuracy! 📚', 'is_hook': False, 'media_urls': ['https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8febecd-ee68-42ff-ab06-41a0a3a43cd3_1102x306.gif']}, {'content': '5️⃣ LLM-based Chunking: Use large language models to generate semantically-rich chunks. This is powerful but comes with high computational costs. ⚡️', 'is_hook': False, 'media_urls': ['https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d1b6d60-8956-4030-8525-d899ee61a9d5_1140x198.gif']}, {'content': 'Key Takeaway: Each chunking strategy has its own strengths and weaknesses. Select the right method based on your model and content to optimize performance! 🚀', 'is_hook': False, 'media_urls': []}, {'content': \"If you're diving into RAG, embrace chunking. Explore these strategies and elevate your AI's ability to communicate knowledge effectively! Join the discussion! 💬\", 'is_hook': False, 'media_urls': []}]}\n",
"Thread scheduled successfully!\n",
"# Thread scheduled for: workdir/5 Chunking Strategies For RAG - by Avi Chawla.md\n",
"Here's the link to scheduled draft: https://typefully.com/t/hXG9c1d\n"
]
}
],
"source": [
"\n",
"flow.kickoff()"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {},
"outputs": [],
"source": [
"flow = CreateContentPlanningFlow()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"ContentPlanningState(blog_post_url='https://blog.dailydoseofds.com/p/5-chunking-strategies-for-rag', draft_path='workdir/', post_type='linkedin')"
]
},
"execution_count": 42,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"flow.state"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.15"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+3
View File
@@ -0,0 +1,3 @@
crewai[tools]==0.86.0
tqdm==4.67.1
Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

+112
View File
@@ -0,0 +1,112 @@
import os
import json
import requests
import datetime
from typing import Optional, Dict, Any
from pydantic import BaseModel
# Constants
API_URL = "https://api.typefully.com/v1/drafts/"
API_KEY = os.getenv("TYPEFULLY_API_KEY")
HEADERS = {
"X-API-KEY": f"Bearer {API_KEY}"
}
def json_to_typefully_content(thread_json: Dict[str, Any]) -> str:
"""Convert JSON thread format to Typefully's format with 4 newlines between tweets."""
tweets = thread_json['tweets']
formatted_tweets = []
for tweet in tweets:
tweet_text = tweet['content']
if 'media_urls' in tweet and tweet['media_urls']:
tweet_text += f"\n{tweet['media_urls'][0]}"
formatted_tweets.append(tweet_text)
return '\n\n\n\n'.join(formatted_tweets)
def json_to_linkedin_content(thread_json: Dict[str, Any]) -> str:
"""Convert JSON thread format to Typefully's format."""
content = thread_json['content']
if 'url' in thread_json and thread_json['url']:
content += f"\n{thread_json['url']}"
return content
def schedule_thread(
content: str,
schedule_date: str = "next-free-slot",
threadify: bool = False,
share: bool = False,
auto_retweet_enabled: bool = False,
auto_plug_enabled: bool = False
) -> Optional[Dict[str, Any]]:
"""Schedule a thread on Typefully."""
payload = {
"content": content,
"schedule-date": schedule_date,
"threadify": threadify,
"share": share,
"auto_retweet_enabled": auto_retweet_enabled,
"auto_plug_enabled": auto_plug_enabled
}
payload = {key: value for key, value in payload.items() if value is not None}
try:
response = requests.post(API_URL, json=payload, headers=HEADERS)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
return None
def schedule(
thread_model: BaseModel,
hours_from_now: int = 1,
threadify: bool = False,
share: bool = True,
post_type: str = "twitter"
) -> Optional[Dict[str, Any]]:
"""
Schedule a thread from a Pydantic model.
Args:
thread_model: Pydantic model containing thread data
hours_from_now: Hours from now to schedule the thread (default: 1)
threadify: Whether to let Typefully split the content (default: False)
share: Whether to get a share URL in response (default: True)
Returns:
API response dictionary or None if failed
"""
try:
# Convert Pydantic model to dict
thread_json = thread_model.pydantic.model_dump()
print("######## Thread JSON: ", thread_json)
# Convert to Typefully format
if post_type == "twitter":
thread_content = json_to_typefully_content(thread_json)
elif post_type == "linkedin":
thread_content = json_to_linkedin_content(thread_json)
# Calculate schedule time
schedule_date = (datetime.datetime.utcnow() +
datetime.timedelta(hours=hours_from_now)).isoformat() + "Z"
# Schedule the thread
response = schedule_thread(
content=thread_content,
schedule_date=schedule_date,
threadify=threadify,
share=share
)
if response:
print("Thread scheduled successfully!")
return response
else:
print("Failed to schedule the thread.")
return None
except Exception as e:
print(f"Error: {str(e)}")
return None