Spaces:
Sleeping
Sleeping
NewsAgent Pro v2
#1
by EATosin - opened
- .gitattributes +35 -0
- .github/workflows/quality_gate.yml +0 -32
- .gitignore +0 -5
- CONTRIBUTING.md +0 -10
- Dockerfile +5 -15
- LICENSE +0 -21
- README.md +61 -80
- app.py +214 -0
- requirements.txt +5 -8
- src/__init__.py +0 -1
- src/agents/critic.py +0 -60
- src/agents/designer.py +0 -66
- src/agents/planner.py +0 -42
- src/agents/researcher.py +0 -20
- src/agents/writer.py +0 -56
- src/app.py +0 -151
- src/main.py +0 -60
- src/prompts/critic.yaml +0 -50
- src/prompts/planner.yaml +0 -68
- src/prompts/researcher.yaml +0 -44
- src/prompts/writer_linkedin.yaml +0 -66
- src/prompts/writer_twitter.yaml +0 -80
- src/schema.py +0 -57
- src/tools/research.py +0 -27
- src/utils/config.py +0 -36
- src/utils/prompt_loader.py +0 -14
.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.
|
| 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 |
-
|
| 19 |
-
|
|
|
|
| 20 |
|
| 21 |
-
|
| 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
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
|
|
|
| 9 |
<div align="center">
|
| 10 |
|
| 11 |
-
# 🗞️ NewsAgent Pro
|
| 12 |
-
### *
|
| 13 |
|
| 14 |
-
[](https://huggingface.co/black-forest-labs/FLUX.1-schnell)
|
| 19 |
|
| 20 |
-
[View Live Demo](#-live-demo) • [
|
| 21 |
|
| 22 |
</div>
|
| 23 |
|
| 24 |
---
|
| 25 |
|
| 26 |
-
## ⚡ The Problem: "
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
## 🧠 The Solution: Agentic Workflow
|
| 31 |
-
|
| 32 |
-
It reads the internet, plans a strategy, drafts content, **critiques its own work**, and designs branded visuals—all in 60 seconds.
|
| 33 |
|
| 34 |
-
###
|
| 35 |
-
* **
|
| 36 |
-
* **
|
| 37 |
-
* *
|
| 38 |
-
* *
|
|
|
|
| 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
|
| 49 |
-
B -->|
|
| 50 |
-
C -->
|
| 51 |
-
D --> E
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 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 |
-
## 🚀
|
| 72 |
-
|
| 73 |
-
**Try the Production Build on Hugging Face Spaces:**
|
| 74 |
|
| 75 |
-
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
---
|
| 80 |
-
|
| 81 |
-
## 📦 Installation (Local & Cloud)
|
| 82 |
-
|
| 83 |
-
### 1. Clone & Setup
|
| 84 |
```bash
|
| 85 |
-
|
|
|
|
| 86 |
cd NewsAgent-Pro
|
| 87 |
-
pip install -r requirements.txt
|
| 88 |
-
```
|
| 89 |
|
| 90 |
-
#
|
| 91 |
-
|
| 92 |
-
```env
|
| 93 |
-
GROQ_API_KEY=gsk_...
|
| 94 |
-
GEMINI_API_KEY=AIza...
|
| 95 |
-
TAVILY_API_KEY=tvly-...
|
| 96 |
-
HF_TOKEN=hf_...
|
| 97 |
-
```
|
| 98 |
|
| 99 |
-
#
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
|
| 104 |
-
#
|
| 105 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
---
|
| 118 |
|
| 119 |
## 👨💻 Author
|
| 120 |
**Owadokun Tosin Tobi**
|
| 121 |
-
*
|
| 122 |
|
| 123 |
* **Portfolio:** [GitHub](https://github.com/eatosin)
|
| 124 |
-
* **Connect:** [LinkedIn](https://www.linkedin.com/in/owadokun-tosin-tobi
|
| 125 |
|
| 126 |
---
|
| 127 |
-
*
|
|
|
|
| 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 |
+
[](https://streamlit.io/)
|
| 18 |
+
[](https://ai.google.dev/)
|
| 19 |
+
[](https://tavily.com/)
|
| 20 |
+
[](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
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
langchain-google-genai
|
| 6 |
tavily-python
|
| 7 |
huggingface_hub
|
| 8 |
pillow
|
| 9 |
-
pydantic
|
| 10 |
python-dotenv
|
| 11 |
-
|
| 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}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|