.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.github/workflows/quality_gate.yml DELETED
@@ -1,32 +0,0 @@
1
- name: Production Quality Gate
2
-
3
- on:
4
- push:
5
- branches: [ "main" ]
6
- pull_request:
7
- branches: [ "main" ]
8
-
9
- jobs:
10
- build-and-audit:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - uses: actions/checkout@v3
15
-
16
- - name: Set up Python 3.10
17
- uses: actions/setup-python@v3
18
- with:
19
- python-version: "3.10"
20
-
21
- - name: Install Dependencies
22
- run: |
23
- python -m pip install --upgrade pip
24
- pip install flake8
25
- if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
26
-
27
- - name: Lint with Flake8 (Style Enforcer)
28
- run: |
29
- # stop the build if there are Python syntax errors or undefined names
30
- flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
31
- # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
32
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore DELETED
@@ -1,5 +0,0 @@
1
- venv/
2
- __pycache__/
3
- *.pyc
4
- .env
5
- .DS_Store
 
 
 
 
 
 
CONTRIBUTING.md DELETED
@@ -1,10 +0,0 @@
1
- # Contributing to NewsAgent Pro
2
-
3
- ## Architecture
4
- * **Orchestrator:** Python/Streamlit
5
- * **AI Models:** Gemini 2.5 (Text), Flux.1-schnell (Vision)
6
- * **Search:** Tavily API
7
-
8
- ## Standards
9
- * All PRs must pass the `quality_gate` workflow.
10
- * No hardcoded API keys. Use `os.getenv`.
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,22 +1,12 @@
1
- FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
-
5
- # Install system dependencies (needed for Pillow/Graphics)
6
- RUN apt-get update && apt-get install -y --no-install-recommends \
7
- build-essential \
8
- libglib2.0-0 \
9
- && rm -rf /var/lib/apt/lists/*
10
-
11
- # Install Python deps
12
  COPY requirements.txt .
13
  RUN pip install --no-cache-dir -r requirements.txt
14
-
15
- # Copy source code
16
  COPY . .
17
 
18
- # Set Python path so 'src' is discoverable
19
- ENV PYTHONPATH=/app
 
20
 
21
- # Run app (Handles Render $PORT and HF 7860)
22
- CMD sh -c "streamlit run src/app.py --server.port=${PORT:-7860} --server.address=0.0.0.0 --server.enableCORS=false --server.enableXsrfProtection=false"
 
1
+ FROM python:3.10-slim
2
 
3
  WORKDIR /app
 
 
 
 
 
 
 
 
4
  COPY requirements.txt .
5
  RUN pip install --no-cache-dir -r requirements.txt
 
 
6
  COPY . .
7
 
8
+ RUN useradd -m -u 1000 user
9
+ USER user
10
+ ENV PATH="/home/user/.local/bin:$PATH"
11
 
12
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0", "--server.enableCORS=false", "--server.enableXsrfProtection=false"]
 
LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Owadokun Tosin Tobi
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,127 +1,108 @@
1
  ---
2
- title: NewsAgent Pro v2
3
- emoji: 🗞️
4
- colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
  pinned: false
 
 
8
  ---
 
9
  <div align="center">
10
 
11
- # 🗞️ NewsAgent Pro v2
12
- ### *The Autonomous, Self-Correcting AI Newsroom*
13
 
14
- [![Python](https://img.shields.io/badge/Python-3.11-3776AB?style=for-the-badge&logo=python&logoColor=white)](https://python.org)
15
- [![Streamlit](https://img.shields.io/badge/Streamlit-FF4B4B?style=for-the-badge&logo=streamlit&logoColor=white)](https://streamlit.io)
16
- [![LangGraph](https://img.shields.io/badge/Orchestrator-LangGraph-1C1C1C?style=for-the-badge)](https://langchain-ai.github.io/langgraph/)
17
- [![Groq](https://img.shields.io/badge/Inference-Groq_Llama_3.3-F55036?style=for-the-badge&logo=groq&logoColor=white)](https://groq.com)
18
- [![Flux](https://img.shields.io/badge/Visuals-FLUX.1_Schnell-000?style=for-the-badge&logo=huggingface)](https://huggingface.co/black-forest-labs/FLUX.1-schnell)
19
 
20
- [View Live Demo](#-live-demo) • [System Architecture](#-system-architecture) • [Deploy Now](#-deployment)
21
 
22
  </div>
23
 
24
  ---
25
 
26
- ## ⚡ The Problem: "Content Fatigue"
27
- To run a high-quality media channel today, you need a **Researcher** to find facts, a **Writer** to draft hooks, an **Editor** to fix mistakes, and a **Designer** to make thumbnails.
28
- Doing this manually takes hours. Most "AI Writers" just hallucinate generic slop.
 
 
 
 
 
29
 
30
  ## 🧠 The Solution: Agentic Workflow
31
- **NewsAgent Pro** is not a chatbot. It is a **Multi-Agent Swarm** that mimics a real newsroom.
32
- It reads the internet, plans a strategy, drafts content, **critiques its own work**, and designs branded visuals—all in 60 seconds.
33
 
34
- ### Key Innovations (v2.0)
35
- * **🔄 Self-Correction Loop:** The **Critic Agent** reads the draft and grades it (1-10). If the score is low, it sends it back to the Writer with specific feedback. It iterates until perfection.
36
- * ** Hyper-Fast Inference:** Powered by **Groq (Llama 3.3)** for sub-second logic and planning.
37
- * **🕵️‍♂️ Real-Time Truth:** Uses **Tavily API** to scrape live news (last 48 hours), preventing hallucinations.
38
- * **🎨 AI Graphic Design:** Uses **Flux.1-Schnell** to generate cinematic backgrounds, then uses **Python Pillow** to programmatically overlay "Newsflash" headlines.
 
39
 
40
  ---
41
 
42
  ## ⚙️ System Architecture
43
 
44
- The system uses a **Stateful Graph** (LangGraph) with conditional routing.
45
-
46
  ```mermaid
47
  graph LR
48
- A[User Input] --> B(🧠 Planner)
49
- B -->|Strategy| C(🕵️‍♂️ Researcher)
50
- C -->|Facts| D(✍️ Writer)
51
- D --> E{⚖️ Critic}
52
- E -->|Score < 8| D
53
- E -->|Score > 8| F(🎨 Designer)
54
- F -->|Visuals| G[Final Output]
55
-
56
- style E fill:#ff9999,stroke:#333,stroke-width:2px,color:black
57
- style F fill:#99ff99,stroke:#333,stroke-width:2px,color:black
58
  ```
59
 
60
- ### The "Newsroom" Staff
61
- | Role | Model / Tool | Function |
62
- | :--- | :--- | :--- |
63
- | **Planner** | **Llama 3.3 (Groq)** | Analyzes the topic and determines the "Viral Angle." |
64
- | **Researcher** | **Tavily API** | Scrapes the web for facts/quotes from the last 48 hours. |
65
- | **Writer** | **Gemini 2.5 / Groq** | Drafts platform-specific content (Threads vs Posts). |
66
- | **Critic** | **Llama 3.3 (Groq)** | **The Gatekeeper.** Rejects low-quality drafts and forces rewrites. |
67
- | **Designer** | **Flux.1-Schnell** | Generates 16:9 cinematic cover art in <4 steps. |
68
-
69
  ---
70
 
71
- ## 🚀 Live Demo
72
-
73
- **Try the Production Build on Hugging Face Spaces:**
74
 
75
- [![Hugging Face Spaces](https://img.shields.io/badge/🤗%20Launch%20App-NewsAgent_Pro-yellow?style=for-the-badge&logo=huggingface)](https://huggingface.co/spaces/EATosin/NewsAgent-Pro)
 
 
76
 
77
- > *Try searching: "DeepSeek vs OpenAI" or "SpaceX Starship Launch"*
78
-
79
- ---
80
-
81
- ## 📦 Installation (Local & Cloud)
82
-
83
- ### 1. Clone & Setup
84
  ```bash
85
- git clone https://github.com/Eatosin/NewsAgent-Pro.git
 
86
  cd NewsAgent-Pro
87
- pip install -r requirements.txt
88
- ```
89
 
90
- ### 2. Configure Environment
91
- Create a `.env` file with your keys (Get Groq for free speed!):
92
- ```env
93
- GROQ_API_KEY=gsk_...
94
- GEMINI_API_KEY=AIza...
95
- TAVILY_API_KEY=tvly-...
96
- HF_TOKEN=hf_...
97
- ```
98
 
99
- ### 3. Run the App
100
- ```bash
101
- streamlit run src/app.py
102
- ```
103
 
104
- ### 4. Docker (Production)
105
- We use a custom multi-stage build to handle system dependencies (Fonts, Pillow):
106
- ```bash
107
- docker build -t newsagent .
108
- docker run -p 7860:7860 --env-file .env newsagent
109
  ```
110
 
111
- ---
112
-
113
- ## 📈 Star History
114
 
115
- [![Star History Chart](https://api.star-history.com/svg?repos=Eatosin/NewsAgent-Pro&type=Date)](https://star-history.com/#Eatosin/NewsAgent-Pro&Date)
 
 
 
116
 
117
  ---
118
 
119
  ## 👨‍💻 Author
120
  **Owadokun Tosin Tobi**
121
- *Senior AI Engineer & Product Builder*
122
 
123
  * **Portfolio:** [GitHub](https://github.com/eatosin)
124
- * **Connect:** [LinkedIn](https://www.linkedin.com/in/owadokun-tosin-tobi/)
125
 
126
  ---
127
- *Built with the Lexpertz R&D Stack.*
 
1
  ---
2
+ title: NewsAgent Pro
3
+ emoji: 💻
4
+ colorFrom: purple
5
  colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
+ short_description: 'Your autonomous AI newsroom '
10
  ---
11
+
12
  <div align="center">
13
 
14
+ # 🗞️ NewsAgent Pro: Autonomous Content Engine
15
+ ### *Multi-Modal AI Agent that Researches, Writes, and Designs Viral News.*
16
 
17
+ [![Streamlit](https://img.shields.io/badge/Streamlit-FF4B4B?style=for-the-badge&logo=streamlit&logoColor=white)](https://streamlit.io/)
18
+ [![Gemini](https://img.shields.io/badge/Google%20Gemini-8E75B2?style=for-the-badge&logo=googlebard&logoColor=white)](https://ai.google.dev/)
19
+ [![Tavily](https://img.shields.io/badge/Tavily-Search_API-000?style=for-the-badge&logo=googlechrome&logoColor=white)](https://tavily.com/)
20
+ [![Flux](https://img.shields.io/badge/Flux.1-Image_Gen-blue?style=for-the-badge)](https://huggingface.co/black-forest-labs/FLUX.1-dev)
 
21
 
22
+ [View Live Demo](#-live-demo) • [Architecture](#-system-architecture) • [Setup](#-installation)
23
 
24
  </div>
25
 
26
  ---
27
 
28
+ ## ⚡ The Problem: "The Blank Page"
29
+ Content creation is a bottleneck. To post high-quality news updates, a human must:
30
+ 1. **Research:** Scrape multiple news sites to find facts.
31
+ 2. **Write:** Draft content optimized for different platforms (X vs LinkedIn).
32
+ 3. **Design:** Create a visually appealing image to stop the scroll.
33
+ 4. **Format:** Ensure character limits aren't breached.
34
+
35
+ **NewsAgent Pro automates this entire pipeline.**
36
 
37
  ## 🧠 The Solution: Agentic Workflow
38
+ NewsAgent Pro is not a chatbot. It is a **Multi-Modal Agent** that connects live internet data to state-of-the-art generation models.
 
39
 
40
+ ### Key Capabilities
41
+ * **🕵️‍♂️ Real-Time Research:** Uses **Tavily API** to scrape news from the last 48 hours. It cites sources, ensuring factual accuracy over hallucination.
42
+ * **✍️ Adaptive Copywriting:** Uses **Gemini 2.5 Flash** to write platform-specific content.
43
+ * *Twitter Mode:* Generates threaded tweets (<280 chars) with hooks.
44
+ * *LinkedIn Mode:* Generates long-form professional insights.
45
+ * **🎨 AI Graphic Design:** Uses **Flux.1 (via Hugging Face)** to generate cinematic background art, then uses **Python Pillow** to programmatically overlay "Newsflash" style headlines.
46
 
47
  ---
48
 
49
  ## ⚙️ System Architecture
50
 
 
 
51
  ```mermaid
52
  graph LR
53
+ A[User Input] --> B{Researcher Node}
54
+ B -->|Tavily API| C[Live News Data]
55
+ C --> D{Writer Node}
56
+ D -->|Gemini 2.5| E[Draft Copy]
57
+ A --> F{Visual Node}
58
+ F -->|Flux.1| G[Background Image]
59
+ G -->|Pillow| H[Branded Asset]
60
+ E --> I[Final Output]
61
+ H --> I
 
62
  ```
63
 
 
 
 
 
 
 
 
 
 
64
  ---
65
 
66
+ ## 🚀 Installation
 
 
67
 
68
+ ### Prerequisites
69
+ * Python 3.10+
70
+ * API Keys: Google Gemini, Tavily, Hugging Face Token.
71
 
72
+ ### Local Setup
 
 
 
 
 
 
73
  ```bash
74
+ # 1. Clone the repository
75
+ git clone https://github.com/eatosin/NewsAgent-Pro.git
76
  cd NewsAgent-Pro
 
 
77
 
78
+ # 2. Install dependencies
79
+ pip install -r requirements.txt
 
 
 
 
 
 
80
 
81
+ # 3. Create .env file
82
+ echo "GEMINI_API_KEY=your_key" >> .env
83
+ echo "TAVILY_API_KEY=your_key" >> .env
84
+ echo "HF_TOKEN=your_key" >> .env
85
 
86
+ # 4. Run the App
87
+ streamlit run app.py
 
 
 
88
  ```
89
 
90
+ ### Docker Deployment
91
+ The project includes a production-ready `Dockerfile` for deployment on Hugging Face Spaces or Render.
 
92
 
93
+ ```dockerfile
94
+ # Run on port 7860 (Hugging Face Default)
95
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
96
+ ```
97
 
98
  ---
99
 
100
  ## 👨‍💻 Author
101
  **Owadokun Tosin Tobi**
102
+ *AI Product Engineer*
103
 
104
  * **Portfolio:** [GitHub](https://github.com/eatosin)
105
+ * **Connect:** [LinkedIn](https://www.linkedin.com/in/owadokun-tosin-tobi-6159091a3?utm_source=share&utm_campaign=share_via&utm_content=profile&utm_medium=android_app)
106
 
107
  ---
108
+ *Powered by the Lexpertz AI Engineering Stack.*
app.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import io
4
+ import requests
5
+ import textwrap
6
+ from dotenv import load_dotenv
7
+ from tavily import TavilyClient
8
+ import google.generativeai as genai
9
+ from huggingface_hub import InferenceClient
10
+ from PIL import Image, ImageDraw, ImageFont
11
+
12
+ # Load environment variables
13
+ load_dotenv()
14
+
15
+ # Configuration
16
+ PAGE_TITLE = "NewsAgent Pro"
17
+ # SAVE TO /tmp TO FIX PERMISSION ERROR
18
+ FONT_PATH = "/tmp/font.ttf"
19
+ FONT_URL = "https://github.com/google/fonts/raw/main/ofl/anton/Anton-Regular.ttf"
20
+ IMAGE_MODEL = "black-forest-labs/FLUX.1-schnell"
21
+ LLM_MODEL = "gemini-2.5-flash"
22
+
23
+ st.set_page_config(page_title=PAGE_TITLE, layout="wide", page_icon="🗞️")
24
+
25
+ # Initialize Clients
26
+ try:
27
+ tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
28
+ genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
29
+ editor_model = genai.GenerativeModel(LLM_MODEL)
30
+ image_client = InferenceClient(IMAGE_MODEL, token=os.getenv("HF_TOKEN"))
31
+ except Exception as e:
32
+ st.error(f"Configuration Error: {e}")
33
+ st.stop()
34
+
35
+ def get_font(size=80):
36
+ """
37
+ Loads a font from /tmp. Downloads if not present.
38
+ """
39
+ try:
40
+ return ImageFont.truetype(FONT_PATH, size)
41
+ except OSError:
42
+ try:
43
+ response = requests.get(FONT_URL)
44
+ if response.status_code == 200:
45
+ with open(FONT_PATH, "wb") as f:
46
+ f.write(response.content)
47
+ return ImageFont.truetype(FONT_PATH, size)
48
+ except Exception as e:
49
+ print(f"Font download failed: {e}")
50
+ return ImageFont.load_default()
51
+
52
+ def research_topic(topic):
53
+ """
54
+ Retrieves latest news context + sources.
55
+ Returns: (Context String, List of Source Links)
56
+ """
57
+ try:
58
+ search_result = tavily.search(query=topic, topic="news", days=2)
59
+ results = search_result.get('results', [])
60
+
61
+ context = []
62
+ sources = []
63
+
64
+ for res in results[:3]:
65
+ context.append(f"Title: {res['title']}\nSummary: {res['content']}")
66
+ sources.append(f"🔗 [{res['title']}]({res['url']})")
67
+
68
+ return "\n\n".join(context), sources
69
+ except Exception as e:
70
+ return f"Research failed: {str(e)}", []
71
+
72
+ def generate_content(platform, topic, research_data):
73
+ if "Twitter" in platform:
74
+ system_prompt = (
75
+ "You are a social media ghostwriter. Write a Twitter thread based on the research provided. "
76
+ "Split tweets using the delimiter '|||'. "
77
+ "Ensure the first tweet is a strong hook and the last is a call to action. "
78
+ "Keep each section under 280 characters."
79
+ )
80
+ else:
81
+ system_prompt = (
82
+ "You are a professional content strategist. Write a LinkedIn post based on the research provided. "
83
+ "Focus on business impact, strategic insights, and professional tone. "
84
+ "Use appropriate line breaks for readability."
85
+ )
86
+
87
+ user_prompt = f"""
88
+ TOPIC: {topic}
89
+ RESEARCH DATA: {research_data}
90
+
91
+ SYSTEM INSTRUCTION: {system_prompt}
92
+ """
93
+
94
+ try:
95
+ response = editor_model.generate_content(user_prompt)
96
+ return response.text
97
+ except Exception as e:
98
+ return f"Generation failed: {str(e)}"
99
+
100
+ def generate_visual_asset(topic, platform):
101
+ prompt = (
102
+ f"Abstract 3D render representing {topic}, dark navy and black gradient background, "
103
+ "glass texture, soft studio lighting, minimalist, 8k resolution, negative space, "
104
+ "high definition, no text, no chaotic details"
105
+ )
106
+
107
+ try:
108
+ image = image_client.text_to_image(prompt)
109
+
110
+ draw = ImageDraw.Draw(image)
111
+ width, height = image.size
112
+
113
+ font_size = 90
114
+ font = get_font(font_size)
115
+
116
+ lines = textwrap.wrap(topic.upper(), width=15)
117
+ wrapped_text = "\n".join(lines)
118
+
119
+ bbox = draw.textbbox((0, 0), wrapped_text, font=font)
120
+ text_height = bbox[3] - bbox[1]
121
+
122
+ padding = 50
123
+ box_height = text_height + (padding * 3)
124
+ box_y = height - box_height - 100
125
+
126
+ draw.rectangle(
127
+ [(0, box_y), (width, height)],
128
+ fill=(0, 0, 0, 240)
129
+ )
130
+
131
+ text_y = box_y + padding
132
+ draw.text((padding, text_y), wrapped_text, font=font, fill="white")
133
+
134
+ small_font = get_font(30)
135
+ draw.text((padding, height - 60), f"GENERATED FOR {platform.upper()}", font=small_font, fill="#00ff00")
136
+
137
+ return image, None
138
+ except Exception as e:
139
+ return None, str(e)
140
+
141
+ def main():
142
+ st.title("NewsAgent Pro")
143
+ st.markdown("Autonomous Multi-Modal Content Engine")
144
+
145
+ with st.sidebar:
146
+ st.header("Configuration")
147
+ platform_choice = st.selectbox("Target Platform", ["Twitter (Thread)", "LinkedIn (Post)"])
148
+
149
+ col1, col2 = st.columns([1, 1])
150
+
151
+ with col1:
152
+ st.subheader("Briefing")
153
+ topic_input = st.text_input("Topic", placeholder="Enter news topic or keyword...")
154
+
155
+ if st.button("Generate Content", type="primary"):
156
+ if not topic_input:
157
+ st.warning("Please enter a topic.")
158
+ return
159
+
160
+ status = st.status("Initializing Agent Workflow...", expanded=True)
161
+
162
+ # Step 1: Research
163
+ status.write("Agent: Researching topic...")
164
+ research_data, sources = research_topic(topic_input)
165
+
166
+ # SHOW SOURCES (RESTORED)
167
+ if sources:
168
+ st.markdown("### 📚 Live Sources Found:")
169
+ for s in sources:
170
+ st.markdown(s)
171
+
172
+ # Step 2: Content Generation
173
+ status.write("Agent: Drafting copy...")
174
+ content_draft = generate_content(platform_choice, topic_input, research_data)
175
+
176
+ # Step 3: Visual Generation
177
+ status.write("Agent: Designing assets...")
178
+ visual_asset, error = generate_visual_asset(topic_input, platform_choice)
179
+
180
+ if error:
181
+ st.error(f"Image Gen Failed: {error}")
182
+
183
+ status.update(label="Workflow Complete", state="complete")
184
+
185
+ st.session_state['content'] = content_draft
186
+ st.session_state['image'] = visual_asset
187
+
188
+ with col2:
189
+ st.subheader("Production Output")
190
+
191
+ if 'image' in st.session_state and st.session_state['image']:
192
+ st.image(st.session_state['image'], use_column_width=True, caption="Generated Asset")
193
+
194
+ buf = io.BytesIO()
195
+ st.session_state['image'].save(buf, format="PNG")
196
+ st.download_button(
197
+ label="Download Image",
198
+ data=buf.getvalue(),
199
+ file_name="news_asset.png",
200
+ mime="image/png"
201
+ )
202
+
203
+ if 'content' in st.session_state:
204
+ raw_content = st.session_state['content']
205
+
206
+ if "|||" in raw_content:
207
+ tweets = raw_content.split("|||")
208
+ for i, tweet in enumerate(tweets):
209
+ st.text_area(f"Tweet {i+1}", value=tweet.strip(), height=120)
210
+ else:
211
+ st.text_area("Post Content", value=raw_content, height=400)
212
+
213
+ if __name__ == "__main__":
214
+ main()
requirements.txt CHANGED
@@ -1,12 +1,9 @@
1
- streamlit>=1.40.0
2
- langgraph>=0.1.0
3
- langchain>=0.2.0
4
- langchain-groq
5
- langchain-google-genai
6
  tavily-python
7
  huggingface_hub
8
  pillow
9
- pydantic
10
  python-dotenv
11
- langchain-community
12
- langchain-google-genai
 
1
+ streamlit
2
+ google-generativeai
3
+ langgraph
4
+ langchain_core
 
5
  tavily-python
6
  huggingface_hub
7
  pillow
 
8
  python-dotenv
9
+ anthropic
 
src/__init__.py DELETED
@@ -1 +0,0 @@
1
-
 
 
src/agents/critic.py DELETED
@@ -1,60 +0,0 @@
1
- import json
2
- from langchain_core.messages import SystemMessage, HumanMessage
3
- from src.utils.config import get_llm
4
- from src.utils.prompt_loader import load_prompt
5
- from src.schema import HybridState, AgentState
6
-
7
- def critic_node(state: AgentState):
8
- """
9
- Critic Agent: Reviews content quality and enforces editorial standards.
10
- Uses external YAML prompts for easy tuning.
11
- """
12
- # Initialize Hybrid Access (Safety Wrapper)
13
- state_wrapper = HybridState(state)
14
-
15
- draft = state_wrapper.get("draft")
16
- platform = state_wrapper.get("platform")
17
- revision_count = state_wrapper.get("revision_count", 0)
18
-
19
- print(f"⚖️ Critic is reviewing draft (Revision {revision_count})...")
20
-
21
- system_prompt = load_prompt("critic.yaml")
22
-
23
- # Construct Context for the Critic
24
- user_msg = f"""
25
- TARGET PLATFORM: {platform}
26
-
27
- CURRENT DRAFT:
28
- {draft}
29
-
30
- TASK: Score this content and provide specific feedback for improvement.
31
- """
32
-
33
- messages = [
34
- SystemMessage(content=system_prompt),
35
- HumanMessage(content=user_msg)
36
- ]
37
-
38
- # Use Groq (Planning Model) for fast scoring
39
- llm = get_llm("planning")
40
- response = llm.invoke(messages)
41
-
42
- # Robust JSON Parsing (Handles Markdown blocks)
43
- content = response.content.strip()
44
- if "```json" in content:
45
- content = content.split("```json")[1].split("```")[0]
46
-
47
- try:
48
- data = json.loads(content)
49
- return {
50
- "score": data.get("score", 5),
51
- "critique": data.get("feedback", "Improve clarity and engagement."),
52
- "revision_count": revision_count + 1
53
- }
54
- except:
55
- # Fail-safe if JSON breaks
56
- return {
57
- "score": 5,
58
- "critique": "Format error. Please review structure.",
59
- "revision_count": revision_count + 1
60
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/designer.py DELETED
@@ -1,66 +0,0 @@
1
- import os
2
- import requests
3
- import io
4
- import textwrap
5
- from PIL import Image, ImageDraw, ImageFont
6
- from huggingface_hub import InferenceClient
7
- from src.schema import HybridState, AgentState
8
-
9
- # Initialize Flux Schnell (Faster, Apache 2.0)
10
- hf_token = os.getenv("HF_TOKEN")
11
- client = InferenceClient("black-forest-labs/FLUX.1-schnell", token=hf_token)
12
-
13
- def designer_node(state: AgentState):
14
- state_wrapper = HybridState(state)
15
- topic = state_wrapper.get("topic", "Breaking News")
16
- platform = state_wrapper.get("platform", "twitter")
17
-
18
- prompt = f"Abstract 3D render of {topic}, dark gradient background, minimalist, high tech, 8k resolution, no text"
19
-
20
- try:
21
- # Generate Image
22
- image = client.text_to_image(prompt)
23
-
24
- # Overlay Text
25
- draw = ImageDraw.Draw(image)
26
- width, height = image.size
27
-
28
- # Font Fallback
29
- try:
30
- font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 90)
31
- except:
32
- # Download font if missing (Docker environment)
33
- font_url = "https://github.com/google/fonts/raw/main/ofl/anton/Anton-Regular.ttf"
34
- r = requests.get(font_url)
35
- font = ImageFont.truetype(io.BytesIO(r.content), 90)
36
-
37
- # Wrap Text
38
- lines = textwrap.wrap(topic.upper(), width=15)
39
- text = "\n".join(lines)
40
-
41
- # Draw Box
42
- bbox = draw.textbbox((0, 0), text, font=font)
43
- text_height = bbox[3] - bbox[1]
44
-
45
- box_y = height - text_height - 150
46
- draw.rectangle([(0, box_y), (width, height)], fill=(0, 0, 0, 240))
47
-
48
- # Draw Text
49
- draw.text((50, box_y + 50), text, font=font, fill="white")
50
-
51
- # Save to buffer
52
- img_byte_arr = io.BytesIO()
53
- image.save(img_byte_arr, format='PNG')
54
- img_byte_arr = img_byte_arr.getvalue()
55
-
56
- # Convert to simple path or bytes for UI
57
- # For Streamlit state, bytes are fine, but saving to /tmp is safer for passing
58
- save_path = "/tmp/generated_image.png"
59
- with open(save_path, "wb") as f:
60
- f.write(img_byte_arr)
61
-
62
- return {"image_url": save_path}
63
-
64
- except Exception as e:
65
- print(f"Design failed: {e}")
66
- return {"image_url": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/planner.py DELETED
@@ -1,42 +0,0 @@
1
- import json
2
- from langchain_core.messages import SystemMessage, HumanMessage
3
- from src.utils.config import get_llm
4
- from src.utils.prompt_loader import load_prompt
5
- from src.schema import HybridState, AgentState
6
-
7
- def planner_node(state: AgentState):
8
- state_wrapper = HybridState(state)
9
- topic = state_wrapper.get("topic")
10
-
11
- # Load your existing SOTA prompt
12
- system_prompt = load_prompt("planner.yaml")
13
-
14
- messages = [
15
- SystemMessage(content=system_prompt),
16
- HumanMessage(content=f"Topic: {topic}")
17
- ]
18
-
19
- # Use Groq for speed
20
- llm = get_llm("planning")
21
- response = llm.invoke(messages)
22
-
23
- # Robust JSON parsing
24
- content = response.content.strip()
25
- if "```json" in content:
26
- content = content.split("```json")[1].split("```")[0]
27
-
28
- try:
29
- data = json.loads(content)
30
- outline = data.get("outline", "")
31
- # Ensure outline is string
32
- if isinstance(outline, list):
33
- outline = "\n".join(outline)
34
-
35
- return {
36
- "hook": data.get("hook"),
37
- "outline": outline,
38
- "cta": data.get("cta")
39
- }
40
- except:
41
- # Fallback if JSON fails
42
- return {"outline": content, "hook": f"News: {topic}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/researcher.py DELETED
@@ -1,20 +0,0 @@
1
- from src.tools.research import perform_research
2
- from src.schema import HybridState, AgentState
3
-
4
- def researcher_node(state: AgentState):
5
- """
6
- Executes the research tool and stores the results.
7
- """
8
- # Wrap state for safe access
9
- state_wrapper = HybridState(state)
10
- topic = state_wrapper.get("topic")
11
-
12
- print(f"🕵️‍♂️ Researching: {topic}")
13
-
14
- # Call the Tavily Tool
15
- research_results = perform_research.invoke(topic)
16
-
17
- # Return updates to the state
18
- return {
19
- "research_data": research_results
20
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/writer.py DELETED
@@ -1,56 +0,0 @@
1
- from langchain_core.messages import SystemMessage, HumanMessage
2
- from src.utils.config import get_llm
3
- from src.utils.prompt_loader import load_prompt
4
- from src.schema import HybridState, AgentState
5
-
6
- def writer_node(state: AgentState):
7
- state_wrapper = HybridState(state)
8
- platform = state_wrapper.get("platform", "twitter")
9
- research = state_wrapper.get("research_data")
10
- outline = state_wrapper.get("outline")
11
- critique = state_wrapper.get("critique") # Check for feedback
12
- current_draft = state_wrapper.get("draft")
13
-
14
- # Select prompt based on platform
15
- prompt_file = "writer_twitter.yaml" if "twitter" in platform.lower() else "writer_linkedin.yaml"
16
- system_prompt = load_prompt(prompt_file)
17
-
18
- # Dynamic User Message: First Draft vs Revision
19
- if critique and current_draft:
20
- print("✍️ Writer is revising based on feedback...")
21
- user_msg = f"""
22
- ORIGINAL DRAFT:
23
- {current_draft}
24
-
25
- CRITIQUE TO FIX:
26
- {critique}
27
-
28
- TASK: Rewrite the draft to address the critique. Keep the same format.
29
- """
30
- else:
31
- print("✍️ Writer is drafting fresh content...")
32
- user_msg = f"""
33
- Research: {research}
34
- Outline: {outline}
35
- """
36
-
37
- messages = [
38
- SystemMessage(content=system_prompt),
39
- HumanMessage(content=user_msg)
40
- ]
41
-
42
- llm = get_llm("writing")
43
- response = llm.invoke(messages)
44
- draft = response.content.strip()
45
-
46
- final_thread = []
47
- if "twitter" in platform.lower():
48
- final_thread = [t.strip() for t in draft.split("|||") if t.strip()]
49
- else:
50
- final_thread = [draft]
51
-
52
- return {
53
- "draft": draft,
54
- "final_thread": final_thread,
55
- "revision_count": state.revision_count # Persist count
56
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app.py DELETED
@@ -1,151 +0,0 @@
1
- import streamlit as st
2
- import base64
3
- import os
4
- import time # <-- This was missing!
5
- from io import BytesIO
6
- from src.main import graph
7
- from src.schema import AgentState
8
- # --- PAGE CONFIG ---
9
- st.set_page_config(
10
- page_title="NewsAgent Pro v2",
11
- page_icon="🗞️",
12
- layout="wide",
13
- initial_sidebar_state="expanded"
14
- )
15
-
16
- # --- CUSTOM CSS ---
17
- st.markdown("""
18
- <style>
19
- .stButton>button {width: 100%; border-radius: 8px; font-weight: bold;}
20
- .reportview-container {margin-top: -2em;}
21
- h1 {color: #FF4B4B;}
22
- </style>
23
- """, unsafe_allow_html=True)
24
-
25
- # --- SIDEBAR ---
26
- with st.sidebar:
27
- st.title("🤖 Agent Command")
28
- st.info("System Online v2.0")
29
- st.markdown("### ⚙️ Engine Specs")
30
- st.markdown("- **Planner:** Llama 3.3 (Groq)")
31
- st.markdown("- **Writer:** Gemini 2.5 / Groq")
32
- st.markdown("- **Visuals:** Flux.1 Schnell")
33
-
34
- st.markdown("---")
35
- st.write("Authored by **Lexpertz R&D**")
36
-
37
- # --- MAIN INTERFACE ---
38
- st.title("🗞️ NewsAgent Pro")
39
- st.markdown("### Autonomous Multi-Modal Content Engine")
40
- st.caption("Enter a topic. The AI swarm will Research, Plan, Write, and Design assets automatically.")
41
-
42
- # Input Section
43
- with st.container():
44
- col_input, col_btn = st.columns([3, 1])
45
- with col_input:
46
- topic = st.text_input("Mission Objective (Topic)", placeholder="e.g. DeepSeek vs OpenAI rivalry")
47
- with col_btn:
48
- platform = st.selectbox("Target Platform", ["Twitter", "LinkedIn"])
49
- run_btn = st.button("🚀 Launch Agents", type="primary")
50
-
51
- # --- SESSION STATE INITIALIZATION ---
52
- if "generated_content" not in st.session_state:
53
- st.session_state.generated_content = None
54
- if "generated_image" not in st.session_state:
55
- st.session_state.generated_image = None
56
- if "sources" not in st.session_state:
57
- st.session_state.sources = []
58
-
59
- # --- EXECUTION LOGIC ---
60
- if run_btn and topic:
61
- # Reset State
62
- st.session_state.generated_content = None
63
- st.session_state.generated_image = None
64
-
65
- status_box = st.status("🚀 Initializing Agent Swarm...", expanded=True)
66
-
67
- try:
68
- # Initialize Pydantic State
69
- initial_state = AgentState(
70
- topic=topic,
71
- platform=platform.lower()
72
- )
73
-
74
- # Run Graph
75
- curr_state = initial_state
76
-
77
- # We iterate through the stream updates
78
- for event in graph.stream(initial_state):
79
- for node_name, values in event.items():
80
- # Skip empty updates
81
- if not values:
82
- continue
83
-
84
- # Update status based on active agent
85
- if node_name == "planner":
86
- status_box.write("🧠 **Planner:** Strategy & Hook defined.")
87
- elif node_name == "researcher":
88
- status_box.write(f"🕵️‍♂️ **Researcher:** Gathered data.")
89
- elif node_name == "writer":
90
- status_box.write("✍️ **Writer:** Draft generated.")
91
- elif node_name == "designer":
92
- status_box.write("🎨 **Designer:** Visual asset rendered.")
93
-
94
- # Update local state dict to track progress
95
- # Note: LangGraph returns the *changes*, so we update our tracker
96
- # For simplicity in this UI loop, we grab final artifacts at the end
97
- if "final_thread" in values:
98
- st.session_state.generated_content = values["final_thread"]
99
- if "image_url" in values:
100
- st.session_state.generated_image = values["image_url"]
101
- if "research_data" in values:
102
- # Extract sources for display
103
- # Assuming research_data is a string in the final state or list of dicts
104
- # Adjust based on your researcher.py output
105
- pass
106
-
107
- status_box.update(label="✅ Mission Accomplished", state="complete", expanded=False)
108
-
109
- except Exception as e:
110
- status_box.update(label="❌ Mission Failed", state="error")
111
- st.error(f"Agent Logic Error: {str(e)}")
112
-
113
- # --- RESULTS DISPLAY ---
114
- if st.session_state.generated_content or st.session_state.generated_image:
115
- st.divider()
116
- res_col1, res_col2 = st.columns([1, 1])
117
-
118
- # LEFT: Visuals
119
- with res_col1:
120
- st.subheader("🎨 Visual Asset")
121
- if st.session_state.generated_image:
122
- img_path = st.session_state.generated_image
123
- if os.path.exists(img_path):
124
- st.image(img_path, caption="Viral Cover Image", use_container_width=True)
125
-
126
- # Download Button
127
- with open(img_path, "rb") as file:
128
- btn = st.download_button(
129
- label="⬇️ Download PNG",
130
- data=file,
131
- file_name=f"newsagent_{int(time.time())}.png",
132
- mime="image/png"
133
- )
134
- else:
135
- st.warning("Image file missing (Docker ephemeral storage).")
136
- else:
137
- st.info("No visual generated for this run.")
138
-
139
- # RIGHT: Copy
140
- with res_col2:
141
- st.subheader(f"📝 {platform} Draft")
142
- content = st.session_state.generated_content
143
-
144
- if content:
145
- if isinstance(content, list): # Twitter Thread
146
- for i, tweet in enumerate(content):
147
- st.text_area(f"Tweet {i+1}", value=tweet, height=120)
148
- else: # LinkedIn Post
149
- st.text_area("Post Content", value=content, height=400)
150
- else:
151
- st.info("No text content generated.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/main.py DELETED
@@ -1,60 +0,0 @@
1
- from langgraph.graph import StateGraph, END
2
- from src.schema import AgentState, HybridState
3
-
4
- # Import Agents
5
- from src.agents.planner import planner_node
6
- from src.agents.researcher import researcher_node
7
- from src.agents.writer import writer_node
8
- from src.agents.designer import designer_node
9
- from src.agents.critic import critic_node
10
-
11
- # --- CONDITIONAL LOGIC ---
12
- def should_continue(state: AgentState):
13
- """
14
- Decides: Go back to Writer? Or move to Designer?
15
- """
16
- wrapper = HybridState(state)
17
- score = wrapper.get("score", 0)
18
- revisions = wrapper.get("revision_count", 0)
19
-
20
- # Rule: If score < 8 AND we haven't tried too many times...
21
- if score < 8 and revisions < 2:
22
- print(f"🔄 Quality Check Failed (Score: {score}/10). Revising...")
23
- return "writer"
24
-
25
- if score >= 8:
26
- print(f"✅ Quality Check Passed (Score: {score}/10).")
27
- else:
28
- print("⚠️ Max revisions reached. Moving to publishing.")
29
-
30
- return "designer"
31
-
32
- # --- BUILD GRAPH ---
33
- workflow = StateGraph(AgentState)
34
-
35
- workflow.add_node("planner", planner_node)
36
- workflow.add_node("researcher", researcher_node)
37
- workflow.add_node("writer", writer_node)
38
- workflow.add_node("critic", critic_node)
39
- workflow.add_node("designer", designer_node)
40
-
41
- # Linear flow start
42
- workflow.set_entry_point("planner")
43
- workflow.add_edge("planner", "researcher")
44
- workflow.add_edge("researcher", "writer")
45
-
46
- # The Loop: Writer -> Critic -> (Router)
47
- workflow.add_edge("writer", "critic")
48
-
49
- workflow.add_conditional_edges(
50
- "critic",
51
- should_continue,
52
- {
53
- "writer": "writer", # Loop back
54
- "designer": "designer" # Move forward
55
- }
56
- )
57
-
58
- workflow.add_edge("designer", END)
59
-
60
- graph = workflow.compile()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/prompts/critic.yaml DELETED
@@ -1,50 +0,0 @@
1
- system: |
2
- You are a ruthless, world-class viral content editor with 20+ years experience at top media outlets and growth agencies. Your job is to elevate good drafts into 100k+ impression threads/posts by being brutally honest.
3
-
4
- Evaluation Criteria (score each 1–10, then average for overall):
5
- 1. Hook Strength: Does the opener grab attention instantly? (Shocking stat/question/claim)
6
- 2. Depth & Insight: Does it go beyond surface-level? Unique angles, data, implications?
7
- 3. Engagement Flow: Curiosity gaps, pacing, emotional arc, readability (line breaks, bullets)?
8
- 4. Accuracy & Credibility: Faithful to research/sources? No hallucinations?
9
- 5. Platform Fit: Perfect tone/length/style for Twitter (conversational, punchy) or LinkedIn (professional, value-driven)?
10
- 6. Virality Potential: CTA strength, shareability, controversy without toxicity?
11
-
12
- Process:
13
- - Think step-by-step: Score each criterion with justification.
14
- - Overall score: Weighted average (Hook x2, Engagement x2, rest x1).
15
- - If overall ≥8.5: Approve with minor polish suggestions.
16
- - If <8.5: Mandatory full revised draft incorporating fixes.
17
-
18
- Output STRICTLY JSON only:
19
- {
20
- "reasoning": "Step-by-step critique",
21
- "scores": {"hook": int, "depth": int, ...},
22
- "overall_score": float,
23
- "feedback": "Specific, actionable improvements",
24
- "approved": boolean,
25
- "revised_draft": "Full new draft if not approved, else null"
26
- }
27
-
28
- Few-Shot Examples:
29
-
30
- Example 1 (Weak Draft Critique):
31
- Draft: "AI is advancing quickly. Here are some updates..."
32
- Output: {
33
- "reasoning": "Hook is generic - no grab. Depth shallow. Flow boring...",
34
- "scores": {"hook": 3, "depth": 5, ...},
35
- "overall_score": 4.8,
36
- "feedback": "Rewrite hook with stat like 'AI just surpassed humans in X'. Add implications...",
37
- "approved": false,
38
- "revised_draft": "New full thread here|||..."
39
- }
40
-
41
- Example 2 (Strong Draft Approval):
42
- Draft: "The Venezuela oil crisis just escalated... (thread with deep geo insights)"
43
- Output: {
44
- "reasoning": "Hook strong with controversy. Depth excellent with data...",
45
- "scores": {"hook": 9, "depth": 10, ...},
46
- "overall_score": 9.2,
47
- "feedback": "Minor: Add one more question CTA",
48
- "approved": true,
49
- "revised_draft": null
50
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/prompts/planner.yaml DELETED
@@ -1,68 +0,0 @@
1
- system: |
2
- You are an elite content strategist and outline architect for viral social media in 2026. You specialize in turning raw news/topics into high-engagement thread/post structures that get 10k–100k+ impressions.
3
-
4
- Core Principles:
5
- - Hook First: Always start with a grabber (shocking stat, bold claim, question, controversy, or personal angle).
6
- - Narrative Arc: Build curiosity → deliver value → create emotional peak → end with strong CTA.
7
- - Platform Adaptation:
8
- - Twitter/X: 8–15 points, numbered/sequenced, punchy, curiosity gaps between tweets.
9
- - LinkedIn: 5–10 sections, bullet-heavy, professional insights, thought leadership tone.
10
- - Virality: Include emotional triggers, actionable takeaways, questions to audience.
11
- - Length: Balanced for attention span.
12
-
13
- Input: Topic + platform + (later) research.
14
-
15
- Output STRICTLY JSON only:
16
- {
17
- "hook": "Full hook text (first tweet/post paragraph)",
18
- "sections": ["Section 1 title/description", "Section 2...", ...],
19
- "cta": "Strong closing call-to-action",
20
- "estimated_length": "e.g., 12 tweets" or "1200 chars"
21
- }
22
-
23
- Few-Shot Examples:
24
-
25
- Example 1 (Twitter - Venezuela Oil News):
26
- Input Topic: Venezuela political/oil developments
27
- Output: {
28
- "hook": "The Venezuela plot just thickened—and it could reshape global oil markets overnight. (thread 🧵)",
29
- "sections": [
30
- "1. Venezuela holds 303B barrels—more than Saudi—but production crashed from 3.3M to <1M bpd",
31
- "2. Why? Sanctions + heavy crude challenges",
32
- "3. US refineries now crave heavy crude (70% of imports)",
33
- "4. The real play: Control reserves for supply flood",
34
- "5. Implications for commodities, crypto, stocks"
35
- ],
36
- "cta": "This will move markets hard. Follow for real-time updates—what's your take?",
37
- "estimated_length": "10 tweets"
38
- }
39
-
40
- Example 2 (LinkedIn - AI Agents Trend):
41
- Input Topic: Rise of multi-agent AI systems
42
- Output: {
43
- "hook": "AI agents aren't coming—they're already here, quietly transforming how teams build and ship.",
44
- "sections": [
45
- "• The shift: From single models to orchestrated agents (LangGraph, CrewAI)",
46
- "• Real wins: 40–60% faster prototyping with hybrid routing",
47
- "• Risks: Hallucinations without critique loops",
48
- "• 2026 outlook: Every company will have internal agent tools",
49
- "• How leaders should prepare now"
50
- ],
51
- "cta": "Are you experimenting with agents yet? Share your biggest win or concern in comments 👇 #AI #Leadership",
52
- "estimated_length": "1500 characters"
53
- }
54
-
55
- Example 3 (Twitter - Useful Gadgets List):
56
- Input Topic: Random useful everyday items
57
- Output: {
58
- "hook": "Random stuff that's actually USEFUL in 2026 (save this thread 🧵)",
59
- "sections": [
60
- "1. Magnetic cable organizers that end desk chaos",
61
- "2. Portable SSD with built-in encryption",
62
- "3. Smart water bottle that tracks hydration + glows reminders",
63
- "4. Noise-cancelling earbuds under $50 that rival AirPods",
64
- "5. Multi-tool pen for EDC fans"
65
- ],
66
- "cta": "Which one are you buying first? Reply below!",
67
- "estimated_length": "8 tweets"
68
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/prompts/researcher.yaml DELETED
@@ -1,44 +0,0 @@
1
- system: |
2
- You are a world-class investigative researcher and synthesis expert in 2026. You turn raw news/topics into deep, balanced, LLM-ready research briefs that power viral threads/posts.
3
-
4
- Process:
5
- 1. Plan 3–5 targeted search queries (neutral, opposing views, data/stats, implications).
6
- 2. Think step-by-step: What are the core claims? Controversies? Stakeholders?
7
- 3. Synthesize: Extract key facts, timelines, quotes, stats. Note contradictions.
8
- 4. Balance: Include multiple perspectives. Flag potential biases (media slant, official statements).
9
- 5. Credibility: Prioritize reputable sources. List URLs.
10
-
11
- Output STRICTLY JSON only:
12
- {
13
- "summary": "Concise 300–600 word overview with narrative flow",
14
- "key_facts": ["Bullet 1: Fact + source", "Bullet 2: ..."],
15
- "sources": ["Title - URL", ...],
16
- "controversies": ["Point 1", ...],
17
- "implications": "Forward-looking impacts for markets/people/tech",
18
- "suggested_hook_ideas": ["Hook idea 1", "Hook idea 2"]
19
- }
20
-
21
- You will be provided search results from Tavily tool calls.
22
-
23
- Few-Shot Examples:
24
-
25
- Example 1 (Venezuela Oil Topic):
26
- Search Results: [Various snippets on reserves, sanctions, US refineries]
27
- Output: {
28
- "summary": "Venezuela holds world's largest proven reserves (303B barrels, mostly heavy crude)... production collapse due to sanctions/infrastructure... US dependency shifted to heavy imports... recent political events could unlock supply...",
29
- "key_facts": [
30
- "303B barrels reserves - more than Saudi (OPEC data)",
31
- "Production: 3.3M bpd (2000s) → <1M today (EIA)",
32
- "US heavy crude imports: 70% today vs 20% in 1980 (EIA)"
33
- ],
34
- "sources": [
35
- "OPEC Annual Report - https://opec.org/...",
36
- "EIA Venezuela Analysis - https://eia.gov/..."
37
- ],
38
- "controversies": ["Sanctions effectiveness vs regime corruption blame", "Environmental concerns with heavy crude"],
39
- "implications": "Potential supply flood → lower oil prices, commodity volatility, crypto/energy stock moves",
40
- "suggested_hook_ideas": ["Venezuela controls more oil than Saudi—but can't sell it", "Why the US quietly needs Venezuela's 'dirty' oil"]
41
- }
42
-
43
- Example 2 (AI Agents Trend):
44
- Search Results: [Groq release, LangGraph updates, adoption stats]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/prompts/writer_linkedin.yaml DELETED
@@ -1,66 +0,0 @@
1
- system: |
2
- You are an elite LinkedIn thought leader ghostwriter in 2026. Your posts get 10k–100k+ reactions by delivering deep value, professional insights, and subtle calls to engage.
3
-
4
- Core Rules:
5
- - Hook: Professional but bold — question, stat, personal reflection, or trend observation.
6
- - Style: Authoritative yet approachable. Longer paragraphs ok. Use bullets/lists for scannability. Bold key phrases. No excessive emojis (1–2 max).
7
- - Length: 800–2000 characters (single post) or structured as carousel slides if noted.
8
- - Value: Actionable insights, implications for professionals/business, forward-looking.
9
- - Virality: End with open question or "What do you think?" to drive comments.
10
- - Accuracy: Stick rigidly to research/sources.
11
- - Format: Clean markdown-ready text (no separators needed).
12
-
13
- Use full research, sources, and outline.
14
-
15
- Output ONLY the post text.
16
-
17
- Few-Shot Examples:
18
-
19
- Example 1 (News/Geopolitics Insight):
20
- Venezuela's political developments this weekend aren't just headlines—they're a masterclass in global energy dynamics.
21
-
22
- With 303 billion barrels of proven reserves (mostly heavy crude), Venezuela sits on more oil than Saudi Arabia. Yet production has cratered from 3.3M barrels/day in the 2000s to under 1M today.
23
-
24
- Why this matters for energy markets and professionals:
25
-
26
- • US refineries in Texas/Louisiana are optimized for heavy crude—70% of imports now heavy vs 20% in 1980.
27
- • Sanctions + infrastructure decay created the vacuum.
28
- • Potential policy shifts could flood markets with supply, impacting prices and commodities.
29
-
30
- The real question: How will this reshape global trade, inflation, and investment strategies in 2026?
31
-
32
- What are your thoughts on the energy implications? Share below 👇
33
-
34
- #Energy #Geopolitics #Commodities
35
-
36
- Example 2 (Tech Trend Breakdown):
37
- AI agents are no longer hype—they're quietly transforming how we work.
38
-
39
- Last week alone:
40
- • Groq released blazing-fast inference that rivals paid tiers for free.
41
- • Multi-agent frameworks like LangGraph hit production maturity.
42
-
43
- For leaders and builders:
44
- - Cut development time 40–60% with agentic workflows.
45
- - Risk: Over-reliance without critique loops leads to errors.
46
- - Opportunity: Build internal tools that give teams superpowers.
47
-
48
- I've been experimenting with hybrid Groq/Gemini agents—results are game-changing.
49
-
50
- Are you integrating agents yet? What's your biggest win or concern?
51
-
52
- #AI #Leadership #Productivity
53
-
54
- Example 3 (Career/Insight List):
55
- 7 harsh truths I learned after 10 years in tech (that no one tells you):
56
-
57
- 1. Your code matters less than your communication.
58
- 2. Politics exists everywhere—learn to navigate it early.
59
- 3. Promotions go to those who ship impact, not hours.
60
- ...
61
-
62
- The sooner you internalize these, the faster you grow.
63
-
64
- Which one resonates most? Add #8 in comments.
65
-
66
- #CareerAdvice #Tech
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/prompts/writer_twitter.yaml DELETED
@@ -1,80 +0,0 @@
1
- system: |
2
- You are a world-class viral X/Twitter ghostwriter in 2026 style. Your threads get 10k–100k+ likes by combining deep insights, bold hooks, and perfect pacing.
3
-
4
- Core Rules:
5
- - Start with a killer hook: Shocking claim, question, stat, or controversy.
6
- - Style: Conversational but authoritative. Use line breaks for readability. Emojis sparingly (1-2 max per thread). Bold key phrases with * or CAPS.
7
- - Length: 8–15 tweets, each <280 characters.
8
- - Virality techniques: Numbered or sequenced points, curiosity gaps ("But here's why this matters..."), build narrative arc, end with strong CTA (question, follow invite, or thought-provoker).
9
- - Accuracy: Base strictly on provided research/sources. No hallucinations.
10
- - Format: Split tweets exactly with ||| (nothing else as separator).
11
- - Output ONLY the thread text—no extras, no JSON.
12
-
13
- Use the full research, sources, and outline provided.
14
-
15
- Few-Shot Examples (study these for structure and tone):
16
-
17
- Example 1 (News Explanatory Thread - Hook + Facts + Geopolitics + CTA):
18
- The Venezuela plot thickens:
19
-
20
- While Venezuela holds 303 BILLION barrels of oil reserves, much of this is HEAVY crude oil.
21
-
22
- Texas and Louisiana also *happen* to have 6 of the LARGEST HEAVY crude oil refineries in the world.
23
-
24
- What does this mean? Let us explain.
25
-
26
- (a thread)|||In the early 2000s, Venezuela was a MUCH larger oil producer than the US.
27
-
28
- In fact, Venezuela produced 3 TIMES as much oil, at nearly 3.3 million barrels per day.
29
-
30
- By 2020, Venezuela's production had declined to just 900K/day, while the US hit 5 million/day.
31
-
32
- This is key.|||First, Venezuela has been heavily sanctioned by the US for years.
33
-
34
- This resulted in old infrastructure, hindering the ability to extract HEAVY crude oil.
35
-
36
- Heavy oil is far more expensive to extract than light crude.
37
-
38
- This requires advanced techniques like steam injection.|||The US has become incredibly dependent on heavy crude oil.
39
-
40
- In 1980, just 10-20% of US crude oil imports were heavy.
41
-
42
- Today, the MAJORITY are heavy, at ~70%.
43
-
44
- The US wants more heavy crude and Venezuela has BILLIONS of barrels of it.|||Currently, Venezuela holds more oil reserves than any other country.
45
-
46
- They even hold 20% more than Saudi Arabia.
47
-
48
- What's the "best" way to restore these imports?
49
-
50
- Take control of the country's oil reserves.|||This weekend's events in Venezuela will have major effects on the global economy.
51
-
52
- Stocks, commodities, bonds, and crypto will move.
53
-
54
- Follow @KobeissiLetter for real-time analysis as this develops.
55
-
56
- Example 2 (List-Style Engagement Thread - Hook + Numbered Items):
57
- Offensively UGLY dolls of celebrities (thread)
58
-
59
- 1. Emma Watson as Belle|||2. Marilyn Monroe by Tristar|||3. Beyoncé by Hasbro|||4. Princess Diana by Street Players|||5. Ashley Tisdale by Huckleberry|||6. Spice Girls by Toymax (Sporty & Baby)|||7. Fran (The Nanny) by Street Players|||8. Britney Spears by Yaboom|||9. TLC by Yaboom|||10. B*Witched by Yaboom|||11. Kylie Minogue by Jakks Pacific|||12. Hilary Duff by Dakin & Playmates
60
-
61
- (Which one shocked you most? Reply below 👇)
62
-
63
- Example 3 (Practical Useful List Thread - Hook + Items + Links):
64
- random stuff that actually useful
65
-
66
- a thread|||baona pouch 🖤
67
-
68
- https://s.shopee.co.id/xxx|||squidward tray
69
-
70
- https://s.shopee.co.id/xxx|||box acrylic
71
-
72
- https://s.shopee.co.id/xxx|||storge srbaguna tempel
73
-
74
- https://s.shopee.co.id/xxx|||cermin plus storage 🍑
75
-
76
- https://s.shopee.co.id/xxx|||box penyimpanan bntuk buku
77
-
78
- https://s.shopee.co.id/xxx
79
-
80
- (Save this thread for later 🧵)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/schema.py DELETED
@@ -1,57 +0,0 @@
1
- from pydantic import BaseModel, Field, ConfigDict
2
- from typing import List, Dict, Optional, Any
3
-
4
- class AgentState(BaseModel):
5
- """
6
- The shared memory for the Multi-Agent System.
7
- Includes 'extra=allow' to prevent validation errors on new fields.
8
- """
9
- model_config = ConfigDict(extra='allow')
10
-
11
- # Input
12
- topic: str = ""
13
- platform: str = "twitter"
14
-
15
- # Research & Planning
16
- research_data: Optional[Any] = None
17
- outline: Optional[str] = None
18
- hook: Optional[str] = None
19
-
20
- # Content
21
- draft: Optional[str] = None
22
- critique: Optional[str] = None
23
- score: float = 0.0
24
- revision_count: int = 0
25
-
26
- # Output
27
- final_thread: Optional[List[str]] = Field(default_factory=list)
28
- image_url: Optional[str] = None
29
- sources: Optional[List[str]] = Field(default_factory=list)
30
-
31
- # 🛠️ UTILITY WRAPPER (Paste this here so it's available everywhere)
32
- class HybridState:
33
- """
34
- Universal wrapper to allow both dot-notation (state.topic)
35
- and dict-access (state['topic']).
36
- """
37
- def __init__(self, state):
38
- # Unwrap if it's already a HybridState
39
- if isinstance(state, HybridState):
40
- self.__dict__ = state.__dict__
41
- # Convert Pydantic to dict
42
- elif hasattr(state, 'model_dump'):
43
- self.__dict__.update(state.model_dump())
44
- # Use dict directly
45
- elif isinstance(state, dict):
46
- self.__dict__.update(state)
47
- else:
48
- raise ValueError(f"Unknown state type: {type(state)}")
49
-
50
- def get(self, key, default=None):
51
- return self.__dict__.get(key, default)
52
-
53
- def __getitem__(self, key):
54
- return self.__dict__.get(key)
55
-
56
- def __setitem__(self, key, value):
57
- self.__dict__[key] = value
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/tools/research.py DELETED
@@ -1,27 +0,0 @@
1
- import os
2
- from tavily import TavilyClient
3
- from langchain_core.tools import tool
4
-
5
- @tool
6
- def perform_research(topic: str):
7
- """
8
- Searches the web for recent news using Tavily.
9
- """
10
- api_key = os.getenv("TAVILY_API_KEY")
11
- if not api_key:
12
- return "Error: TAVILY_API_KEY not found."
13
-
14
- client = TavilyClient(api_key=api_key)
15
-
16
- try:
17
- # Search specifically for news
18
- response = client.search(query=topic, topic="news", days=2)
19
- results = response.get("results", [])
20
-
21
- context = []
22
- for r in results[:4]:
23
- context.append(f"Title: {r['title']}\nURL: {r['url']}\nSummary: {r['content']}")
24
-
25
- return "\n\n".join(context)
26
- except Exception as e:
27
- return f"Search failed: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/utils/config.py DELETED
@@ -1,36 +0,0 @@
1
- import os
2
- from dotenv import load_dotenv
3
- from langchain_groq import ChatGroq
4
- from langchain_google_genai import ChatGoogleGenerativeAI
5
-
6
- load_dotenv()
7
-
8
- def get_llm(task_type: str = "general"):
9
- """
10
- Factory to get the best LLM for the job.
11
- - Planning/Logic -> Groq (Llama 3.3) for speed.
12
- - Writing/Context -> Groq or Gemini Fallback.
13
- """
14
- groq_key = os.getenv("GROQ_API_KEY")
15
- gemini_key = os.getenv("GEMINI_API_KEY")
16
-
17
- # PRIMARY: Groq (Llama 3.3 70B)
18
- if groq_key:
19
- try:
20
- return ChatGroq(
21
- temperature=0.7,
22
- model_name="llama-3.3-70b-versatile",
23
- api_key=groq_key
24
- )
25
- except Exception as e:
26
- print(f"⚠️ Groq failed: {e}. Falling back...")
27
-
28
- # FALLBACK: Gemini
29
- if gemini_key:
30
- return ChatGoogleGenerativeAI(
31
- model="gemini-1.5-flash",
32
- google_api_key=gemini_key,
33
- temperature=0.7
34
- )
35
-
36
- raise ValueError("❌ No API Keys found! Please set GROQ_API_KEY or GEMINI_API_KEY.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/utils/prompt_loader.py DELETED
@@ -1,14 +0,0 @@
1
- import os
2
-
3
- def load_prompt(filename):
4
- """Reads a prompt file from src/prompts."""
5
- current_dir = os.path.dirname(__file__)
6
- prompts_dir = os.path.join(os.path.dirname(current_dir), 'prompts')
7
- file_path = os.path.join(prompts_dir, filename)
8
-
9
- try:
10
- with open(file_path, 'r', encoding='utf-8') as f:
11
- return f.read()
12
- except FileNotFoundError:
13
- # Fallback if file is missing
14
- return f"You are a helpful assistant. Task: {filename}"