Spaces:
Sleeping
Sleeping
Christophe Bourgoin
Claude
commited on
Commit
Β·
a8a231d
1
Parent(s):
b1b4142
feat: Initial deployment of Scientific Content Generation Agent
Browse files- Added multi-agent system with 5 specialized agents
- Gradio UI with 4 tabs (Generate, Profile, History, Settings)
- Google ADK integration with Gemini 2.0 Flash
- Research capabilities (arXiv + DuckDuckGo)
- Multi-platform content generation (Blog, LinkedIn, Twitter)
π€ Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- .env.example +6 -0
- HUGGINGFACE_DEPLOYMENT.md +266 -0
- README.md +60 -5
- app.py +8 -0
- main.py +300 -0
- profile.example.yaml +155 -0
- requirements.txt +9 -0
- src/__init__.py +1 -0
- src/__pycache__/__init__.cpython-311.pyc +0 -0
- src/__pycache__/__init__.cpython-312.pyc +0 -0
- src/__pycache__/agents.cpython-311.pyc +0 -0
- src/__pycache__/agents.cpython-312.pyc +0 -0
- src/__pycache__/config.cpython-311.pyc +0 -0
- src/__pycache__/config.cpython-312.pyc +0 -0
- src/__pycache__/profile.cpython-311.pyc +0 -0
- src/__pycache__/profile.cpython-312.pyc +0 -0
- src/__pycache__/profile_editor.cpython-311.pyc +0 -0
- src/__pycache__/session_manager.cpython-311.pyc +0 -0
- src/__pycache__/tools.cpython-311.pyc +0 -0
- src/__pycache__/tools.cpython-312.pyc +0 -0
- src/agent.py +9 -0
- src/agents.py +441 -0
- src/config.py +42 -0
- src/profile.py +368 -0
- src/profile_editor.py +138 -0
- src/requirements.txt +7 -0
- src/session_manager.py +256 -0
- src/tools.py +811 -0
- ui_app.py +879 -0
.env.example
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Google AI API Key
|
| 2 |
+
# Get your key from: https://aistudio.google.com/app/api_keys
|
| 3 |
+
GOOGLE_API_KEY=your_api_key_here
|
| 4 |
+
|
| 5 |
+
# Gemini Configuration
|
| 6 |
+
GOOGLE_GENAI_USE_VERTEXAI=FALSE
|
HUGGINGFACE_DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deploying to Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
This guide shows you how to deploy the Scientific Content Generation Agent to Hugging Face Spaces for free hosting and a public demo.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
1. **Hugging Face Account**: Sign up at https://huggingface.co/join
|
| 8 |
+
2. **Google API Key**: Get one from https://aistudio.google.com/app/api_keys
|
| 9 |
+
3. **Git**: Installed on your machine
|
| 10 |
+
|
| 11 |
+
## Step-by-Step Deployment
|
| 12 |
+
|
| 13 |
+
### 1. Create a New Space on Hugging Face
|
| 14 |
+
|
| 15 |
+
1. Go to https://huggingface.co/spaces
|
| 16 |
+
2. Click **"Create new Space"**
|
| 17 |
+
3. Fill in the details:
|
| 18 |
+
- **Owner**: Your username
|
| 19 |
+
- **Space name**: `scientific-content-agent` (or your preferred name)
|
| 20 |
+
- **License**: MIT
|
| 21 |
+
- **Select the SDK**: Choose **Gradio**
|
| 22 |
+
- **Space hardware**: CPU basic (free tier is sufficient)
|
| 23 |
+
- **Visibility**: Public (or Private if you prefer)
|
| 24 |
+
4. Click **"Create Space"**
|
| 25 |
+
|
| 26 |
+
### 2. Clone the Space Repository
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
# Clone your newly created Space
|
| 30 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/scientific-content-agent
|
| 31 |
+
cd scientific-content-agent
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
### 3. Copy Files from Your Project
|
| 35 |
+
|
| 36 |
+
Copy the necessary files from your local project:
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
# From the agentic-content-generation directory, copy these files:
|
| 40 |
+
cp -r src/ ../scientific-content-agent/
|
| 41 |
+
cp main.py ../scientific-content-agent/
|
| 42 |
+
cp app.py ../scientific-content-agent/
|
| 43 |
+
cp ui_app.py ../scientific-content-agent/
|
| 44 |
+
cp requirements.txt ../scientific-content-agent/
|
| 45 |
+
cp README_HF_SPACES.md ../scientific-content-agent/README.md
|
| 46 |
+
cp .env.example ../scientific-content-agent/
|
| 47 |
+
|
| 48 |
+
# Optional: Copy profile example
|
| 49 |
+
cp profile.example.yaml ../scientific-content-agent/
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
Or manually copy these files:
|
| 53 |
+
- `src/` (entire directory)
|
| 54 |
+
- `main.py`
|
| 55 |
+
- `app.py`
|
| 56 |
+
- `ui_app.py`
|
| 57 |
+
- `requirements.txt`
|
| 58 |
+
- `README_HF_SPACES.md` β rename to `README.md`
|
| 59 |
+
- `.env.example`
|
| 60 |
+
|
| 61 |
+
### 4. Configure API Key as a Secret
|
| 62 |
+
|
| 63 |
+
**Option A: Via Web Interface (Recommended)**
|
| 64 |
+
|
| 65 |
+
1. Go to your Space settings: `https://huggingface.co/spaces/YOUR_USERNAME/scientific-content-agent/settings`
|
| 66 |
+
2. Click on **"Variables and secrets"** section
|
| 67 |
+
3. Click **"New secret"**
|
| 68 |
+
4. Add:
|
| 69 |
+
- **Name**: `GOOGLE_API_KEY`
|
| 70 |
+
- **Value**: Your Google API key from AI Studio
|
| 71 |
+
5. Click **"Save"**
|
| 72 |
+
|
| 73 |
+
**Option B: Via Environment Variable in Code**
|
| 74 |
+
|
| 75 |
+
Add this to `app.py` if you prefer users to enter their own API key:
|
| 76 |
+
|
| 77 |
+
```python
|
| 78 |
+
import os
|
| 79 |
+
from ui_app import create_ui
|
| 80 |
+
|
| 81 |
+
# For Hugging Face Spaces deployment
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
# Check for API key in environment (from HF Spaces secrets)
|
| 84 |
+
if not os.getenv("GOOGLE_API_KEY"):
|
| 85 |
+
print("β οΈ Warning: GOOGLE_API_KEY not set. Users will need to configure it in Settings.")
|
| 86 |
+
|
| 87 |
+
app = create_ui()
|
| 88 |
+
app.queue()
|
| 89 |
+
app.launch()
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
### 5. Push to Hugging Face
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
cd scientific-content-agent
|
| 96 |
+
|
| 97 |
+
# Add all files
|
| 98 |
+
git add .
|
| 99 |
+
|
| 100 |
+
# Commit
|
| 101 |
+
git commit -m "Initial deployment of Scientific Content Generation Agent"
|
| 102 |
+
|
| 103 |
+
# Push to Hugging Face
|
| 104 |
+
git push origin main
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### 6. Wait for Build
|
| 108 |
+
|
| 109 |
+
1. Go to your Space URL: `https://huggingface.co/spaces/YOUR_USERNAME/scientific-content-agent`
|
| 110 |
+
2. You'll see the build logs in real-time
|
| 111 |
+
3. The build typically takes 2-5 minutes
|
| 112 |
+
4. Once complete, your app will be live!
|
| 113 |
+
|
| 114 |
+
## Verifying Deployment
|
| 115 |
+
|
| 116 |
+
### Test the Space
|
| 117 |
+
|
| 118 |
+
1. **Generate Content Tab**:
|
| 119 |
+
- Enter a topic like "AI Agents and Multi-Agent Systems"
|
| 120 |
+
- Select platforms (Blog, LinkedIn, Twitter)
|
| 121 |
+
- Click "Generate Content"
|
| 122 |
+
- Wait 2-5 minutes for results
|
| 123 |
+
|
| 124 |
+
2. **Profile Editor Tab**:
|
| 125 |
+
- Click "Load Profile"
|
| 126 |
+
- Edit fields as needed
|
| 127 |
+
- Click "Validate Profile"
|
| 128 |
+
- Click "Save Profile"
|
| 129 |
+
|
| 130 |
+
3. **Session History Tab**:
|
| 131 |
+
- Click "Refresh Sessions"
|
| 132 |
+
- View past generations
|
| 133 |
+
|
| 134 |
+
4. **Settings Tab**:
|
| 135 |
+
- If you didn't set a secret, users can enter their API key here
|
| 136 |
+
- Configure model and content preferences
|
| 137 |
+
|
| 138 |
+
## Troubleshooting
|
| 139 |
+
|
| 140 |
+
### Build Fails
|
| 141 |
+
|
| 142 |
+
**Error**: `ModuleNotFoundError`
|
| 143 |
+
- **Solution**: Check that `requirements.txt` includes all dependencies
|
| 144 |
+
- Verify file paths in `app.py` match your structure
|
| 145 |
+
|
| 146 |
+
**Error**: `No space left on device`
|
| 147 |
+
- **Solution**: Your Space may need more storage
|
| 148 |
+
- Upgrade to a larger hardware tier in Settings
|
| 149 |
+
|
| 150 |
+
### App Runs But Can't Generate Content
|
| 151 |
+
|
| 152 |
+
**Error**: `GOOGLE_API_KEY not found`
|
| 153 |
+
- **Solution**: Add the API key as a secret in Space settings
|
| 154 |
+
- Or configure it in the Settings tab
|
| 155 |
+
|
| 156 |
+
**Error**: `404 NOT_FOUND` for model
|
| 157 |
+
- **Solution**: Check `src/config.py` uses a valid model name
|
| 158 |
+
- Should be `gemini-2.0-flash-exp` or another valid Gemini model
|
| 159 |
+
|
| 160 |
+
### Slow Response Time
|
| 161 |
+
|
| 162 |
+
- This is normal! The agent pipeline takes 2-5 minutes
|
| 163 |
+
- Progress bar shows which agent is running
|
| 164 |
+
- Consider using Vertex AI deployment for production speed
|
| 165 |
+
|
| 166 |
+
## Updating Your Space
|
| 167 |
+
|
| 168 |
+
To update your deployed Space:
|
| 169 |
+
|
| 170 |
+
```bash
|
| 171 |
+
cd scientific-content-agent
|
| 172 |
+
|
| 173 |
+
# Make changes to files
|
| 174 |
+
# ...
|
| 175 |
+
|
| 176 |
+
# Commit and push
|
| 177 |
+
git add .
|
| 178 |
+
git commit -m "Update: describe your changes"
|
| 179 |
+
git push origin main
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
Hugging Face will automatically rebuild and redeploy.
|
| 183 |
+
|
| 184 |
+
## Configuration Options
|
| 185 |
+
|
| 186 |
+
### Custom Domain (Pro Feature)
|
| 187 |
+
|
| 188 |
+
Upgrade to HF Pro to use a custom domain:
|
| 189 |
+
1. Go to Space settings
|
| 190 |
+
2. Click "Custom domain"
|
| 191 |
+
3. Follow instructions
|
| 192 |
+
|
| 193 |
+
### Hardware Upgrades
|
| 194 |
+
|
| 195 |
+
For better performance:
|
| 196 |
+
1. Go to Space settings
|
| 197 |
+
2. Under "Hardware", choose:
|
| 198 |
+
- **CPU basic** (free): Works fine for demos
|
| 199 |
+
- **CPU upgrade** (paid): Faster response
|
| 200 |
+
- **GPU** (paid): Not needed for this app
|
| 201 |
+
|
| 202 |
+
### Making Space Private
|
| 203 |
+
|
| 204 |
+
1. Go to Space settings
|
| 205 |
+
2. Under "Visibility", select "Private"
|
| 206 |
+
3. Share access with specific users
|
| 207 |
+
|
| 208 |
+
## Tips for Portfolio Demo
|
| 209 |
+
|
| 210 |
+
### Showcase in Kaggle Submission
|
| 211 |
+
|
| 212 |
+
1. **Take Screenshots**:
|
| 213 |
+
- Main interface with all 4 tabs
|
| 214 |
+
- Generate Content tab with results
|
| 215 |
+
- Profile Editor with your data
|
| 216 |
+
- Session History showing past generations
|
| 217 |
+
|
| 218 |
+
2. **Write Description**:
|
| 219 |
+
- "Live demo available at: https://huggingface.co/spaces/YOUR_USERNAME/scientific-content-agent"
|
| 220 |
+
- "Try it with your own research topics!"
|
| 221 |
+
- "Fully deployed AI agent system with web interface"
|
| 222 |
+
|
| 223 |
+
3. **Add to README**:
|
| 224 |
+
- Link to HF Space in your project README
|
| 225 |
+
- Badge: `[](https://huggingface.co/spaces/YOUR_USERNAME/scientific-content-agent)`
|
| 226 |
+
|
| 227 |
+
### Embed in Website
|
| 228 |
+
|
| 229 |
+
You can embed your Space in any website:
|
| 230 |
+
|
| 231 |
+
```html
|
| 232 |
+
<iframe
|
| 233 |
+
src="https://YOUR_USERNAME-scientific-content-agent.hf.space"
|
| 234 |
+
frameborder="0"
|
| 235 |
+
width="850"
|
| 236 |
+
height="450"
|
| 237 |
+
></iframe>
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
## Cost
|
| 241 |
+
|
| 242 |
+
- **Basic CPU Space**: **FREE** β
|
| 243 |
+
- **Secrets (API keys)**: **FREE** β
|
| 244 |
+
- **Public hosting**: **FREE** β
|
| 245 |
+
|
| 246 |
+
Your Google API key usage is billed separately by Google (generous free tier).
|
| 247 |
+
|
| 248 |
+
## Next Steps
|
| 249 |
+
|
| 250 |
+
After deployment:
|
| 251 |
+
|
| 252 |
+
1. β
Test all features thoroughly
|
| 253 |
+
2. β
Share the link with colleagues for feedback
|
| 254 |
+
3. β
Add to your Kaggle capstone submission (+5 bonus points!)
|
| 255 |
+
4. β
Include in your portfolio/resume
|
| 256 |
+
5. β
Share on LinkedIn/Twitter to showcase your work
|
| 257 |
+
|
| 258 |
+
## Support
|
| 259 |
+
|
| 260 |
+
- **HF Spaces Docs**: https://huggingface.co/docs/hub/spaces
|
| 261 |
+
- **Gradio Docs**: https://gradio.app/docs
|
| 262 |
+
- **Issues**: Report at your GitHub repo
|
| 263 |
+
|
| 264 |
+
---
|
| 265 |
+
|
| 266 |
+
**Congratulations!** π Your AI agent is now publicly accessible and ready to showcase!
|
README.md
CHANGED
|
@@ -1,14 +1,69 @@
|
|
| 1 |
---
|
| 2 |
-
title: Scientific Content Agent
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.0.1
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
-
short_description: A multi-agent system that generates research-backed content
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Scientific Content Generation Agent
|
| 3 |
+
emoji: π¬
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.0.1
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# π¬ Scientific Content Generation Agent
|
| 14 |
+
|
| 15 |
+
An AI-powered multi-agent system that generates research-backed content (blog articles, LinkedIn posts, Twitter threads) from scientific topics. Built with Google's Agent Development Kit (ADK).
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
- π¬ **Deep Research**: Searches academic papers (arXiv) and web sources (DuckDuckGo)
|
| 20 |
+
- π **Multi-Platform Output**: Blog, LinkedIn, and Twitter content
|
| 21 |
+
- π― **Professional Credibility**: SEO-optimized for recruiter visibility
|
| 22 |
+
- π **Proper Citations**: APA-formatted references
|
| 23 |
+
- π€ **User Profiles**: Personalized content based on your expertise
|
| 24 |
+
- πΎ **Session Management**: Resume conversations and track history
|
| 25 |
+
|
| 26 |
+
## How to Use
|
| 27 |
+
|
| 28 |
+
1. **Generate Content Tab**: Enter a research topic and click Generate
|
| 29 |
+
2. **Profile Editor Tab**: Customize your professional profile
|
| 30 |
+
3. **Session History Tab**: View and resume past generations
|
| 31 |
+
4. **Settings Tab**: Configure API key and preferences
|
| 32 |
+
|
| 33 |
+
## Requirements
|
| 34 |
+
|
| 35 |
+
β οΈ **Important**: You need a Google API key to use this app.
|
| 36 |
+
|
| 37 |
+
Get your free API key from: [Google AI Studio](https://aistudio.google.com/app/api_keys)
|
| 38 |
+
|
| 39 |
+
Then add it in the **Settings Tab** or set it as a Space secret named `GOOGLE_API_KEY`.
|
| 40 |
+
|
| 41 |
+
## Architecture
|
| 42 |
+
|
| 43 |
+
Multi-agent pipeline with 5 specialized agents:
|
| 44 |
+
1. **ResearchAgent**: Searches papers and trends
|
| 45 |
+
2. **StrategyAgent**: Plans content approach
|
| 46 |
+
3. **ContentGeneratorAgent**: Creates platform-specific content
|
| 47 |
+
4. **LinkedInOptimizationAgent**: Optimizes for opportunities
|
| 48 |
+
5. **ReviewAgent**: Adds citations and validates
|
| 49 |
+
|
| 50 |
+
## Local Development
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/scientific-content-agent
|
| 54 |
+
cd scientific-content-agent
|
| 55 |
+
pip install -r requirements.txt
|
| 56 |
+
python app.py
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
## About
|
| 60 |
+
|
| 61 |
+
Built for the Google/Kaggle Agents Intensive Week capstone project.
|
| 62 |
+
|
| 63 |
+
- **Framework**: Google Agent Development Kit (ADK)
|
| 64 |
+
- **Model**: Gemini 2.0 Flash
|
| 65 |
+
- **UI**: Gradio 6.0
|
| 66 |
+
|
| 67 |
+
## License
|
| 68 |
+
|
| 69 |
+
MIT License - See LICENSE file for details
|
app.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Entry point for Hugging Face Spaces deployment."""
|
| 2 |
+
|
| 3 |
+
from ui_app import create_ui
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
app = create_ui()
|
| 7 |
+
app.queue() # Enable queueing for concurrent users
|
| 8 |
+
app.launch()
|
main.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Main entry point for the Scientific Content Generation Agent."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import asyncio
|
| 5 |
+
import contextlib
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
import uuid
|
| 9 |
+
|
| 10 |
+
from google.adk.plugins.logging_plugin import LoggingPlugin
|
| 11 |
+
from google.adk.runners import Runner
|
| 12 |
+
from google.adk.sessions import DatabaseSessionService
|
| 13 |
+
from google.genai import types
|
| 14 |
+
|
| 15 |
+
from src.agents import create_content_generation_pipeline
|
| 16 |
+
from src.config import GOOGLE_API_KEY, LOG_FILE, LOG_LEVEL
|
| 17 |
+
from src.profile import (
|
| 18 |
+
DEFAULT_PROFILE,
|
| 19 |
+
PROFILE_DIR,
|
| 20 |
+
PROFILE_PATH,
|
| 21 |
+
load_user_profile,
|
| 22 |
+
save_profile_to_yaml,
|
| 23 |
+
)
|
| 24 |
+
from src.profile_editor import edit_profile_interactive, validate_after_edit
|
| 25 |
+
from src.session_manager import delete_session, format_session_list, list_sessions
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
async def run_content_generation(topic: str, preferences: dict = None, session_id: str = None):
|
| 29 |
+
"""Run the content generation pipeline for a given topic.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
topic: The research topic to generate content about
|
| 33 |
+
preferences: Optional dict with user preferences:
|
| 34 |
+
- platforms: List of platforms (default: ["blog", "linkedin", "twitter"])
|
| 35 |
+
- tone: Preferred tone (default: "professional")
|
| 36 |
+
- target_audience: Target audience description
|
| 37 |
+
- max_papers: Maximum papers to search (default: 5)
|
| 38 |
+
session_id: Optional session ID to resume a conversation
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
Final content for all platforms
|
| 42 |
+
"""
|
| 43 |
+
if not GOOGLE_API_KEY:
|
| 44 |
+
raise ValueError(
|
| 45 |
+
"GOOGLE_API_KEY not found. Please set it in .env file.\n"
|
| 46 |
+
"Get your key from: https://aistudio.google.com/app/api_keys"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
# Set environment variable
|
| 50 |
+
os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY
|
| 51 |
+
|
| 52 |
+
# Load user profile
|
| 53 |
+
profile = load_user_profile()
|
| 54 |
+
print(f"π€ Generating content for: {profile.name} ({profile.target_role})")
|
| 55 |
+
|
| 56 |
+
# Create the agent pipeline
|
| 57 |
+
print("\nπ€ Initializing Scientific Content Generation Agent...\n")
|
| 58 |
+
agent = create_content_generation_pipeline()
|
| 59 |
+
|
| 60 |
+
# Configure logging
|
| 61 |
+
logging.basicConfig(
|
| 62 |
+
level=getattr(logging, LOG_LEVEL),
|
| 63 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 64 |
+
handlers=[
|
| 65 |
+
logging.FileHandler(LOG_FILE),
|
| 66 |
+
# logging.StreamHandler() # Uncomment to see logs in console
|
| 67 |
+
],
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Initialize persistent session service
|
| 71 |
+
db_path = PROFILE_DIR / "sessions.db"
|
| 72 |
+
db_url = f"sqlite:///{db_path}"
|
| 73 |
+
session_service = DatabaseSessionService(db_url=db_url)
|
| 74 |
+
|
| 75 |
+
# Create runner
|
| 76 |
+
app_name = "scientific-content-agent"
|
| 77 |
+
runner = Runner(
|
| 78 |
+
agent=agent, app_name=app_name, session_service=session_service, plugins=[LoggingPlugin()]
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
# Generate or use provided session ID
|
| 82 |
+
if not session_id:
|
| 83 |
+
session_id = str(uuid.uuid4())
|
| 84 |
+
print(f"π Starting new session: {session_id}")
|
| 85 |
+
else:
|
| 86 |
+
print(f"π Resuming session: {session_id}")
|
| 87 |
+
|
| 88 |
+
# Build the user message
|
| 89 |
+
preferences = preferences or {}
|
| 90 |
+
platforms = preferences.get("platforms", ["blog", "linkedin", "twitter"])
|
| 91 |
+
tone = preferences.get("tone", profile.content_tone)
|
| 92 |
+
audience = preferences.get("target_audience", "researchers and professionals")
|
| 93 |
+
|
| 94 |
+
# Inject profile summary into the prompt
|
| 95 |
+
profile_summary = profile.get_profile_summary()
|
| 96 |
+
|
| 97 |
+
user_message = f"""Generate scientific content on the following topic: {topic}
|
| 98 |
+
|
| 99 |
+
Preferences:
|
| 100 |
+
- Target platforms: {", ".join(platforms)}
|
| 101 |
+
- Tone: {tone}
|
| 102 |
+
- Target audience: {audience}
|
| 103 |
+
|
| 104 |
+
User Profile Context:
|
| 105 |
+
{profile_summary}
|
| 106 |
+
|
| 107 |
+
Please create engaging, credible content that:
|
| 108 |
+
1. Incorporates recent research and academic sources
|
| 109 |
+
2. Builds professional credibility on LinkedIn
|
| 110 |
+
3. Demonstrates expertise in the field
|
| 111 |
+
4. Is suitable for scientific research monitoring
|
| 112 |
+
5. Aligns with the user's profile and expertise
|
| 113 |
+
|
| 114 |
+
Generate content for all three platforms: blog article, LinkedIn post, and Twitter thread.
|
| 115 |
+
"""
|
| 116 |
+
|
| 117 |
+
print(f"π Topic: {topic}")
|
| 118 |
+
print(f"π― Target platforms: {', '.join(platforms)}")
|
| 119 |
+
print(f"π₯ Target audience: {audience}\n")
|
| 120 |
+
print("=" * 80)
|
| 121 |
+
print("\nπ Running content generation pipeline...\n")
|
| 122 |
+
print("Step 1: ResearchAgent - Searching for papers and current trends...")
|
| 123 |
+
|
| 124 |
+
final_content = ""
|
| 125 |
+
try:
|
| 126 |
+
# Ensure session exists
|
| 127 |
+
with contextlib.suppress(Exception):
|
| 128 |
+
await session_service.create_session(
|
| 129 |
+
app_name=app_name, user_id=profile.name, session_id=session_id
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# Run the agent
|
| 133 |
+
query = types.Content(role="user", parts=[types.Part(text=user_message)])
|
| 134 |
+
|
| 135 |
+
async for event in runner.run_async(
|
| 136 |
+
user_id=profile.name, session_id=session_id, new_message=query
|
| 137 |
+
):
|
| 138 |
+
# Check for final content in state delta
|
| 139 |
+
if (
|
| 140 |
+
event.actions
|
| 141 |
+
and event.actions.state_delta
|
| 142 |
+
and "final_content" in event.actions.state_delta
|
| 143 |
+
):
|
| 144 |
+
final_content = event.actions.state_delta["final_content"]
|
| 145 |
+
|
| 146 |
+
# Also check if the model returned a text response (fallback)
|
| 147 |
+
if event.content and event.content.parts:
|
| 148 |
+
for part in event.content.parts:
|
| 149 |
+
if part.text:
|
| 150 |
+
# This might be intermediate thought or final answer depending on agent structure
|
| 151 |
+
# For now we rely on state_delta as per original design, but keep this as backup
|
| 152 |
+
pass
|
| 153 |
+
|
| 154 |
+
if not final_content:
|
| 155 |
+
final_content = "No content generated. Please check the logs."
|
| 156 |
+
|
| 157 |
+
print("\nβ
Content generation complete!\n")
|
| 158 |
+
print("=" * 80)
|
| 159 |
+
print("\nπ GENERATED CONTENT:\n")
|
| 160 |
+
print(final_content)
|
| 161 |
+
print("\n" + "=" * 80)
|
| 162 |
+
|
| 163 |
+
return final_content
|
| 164 |
+
|
| 165 |
+
except Exception as e:
|
| 166 |
+
print(f"\nβ Error during content generation: {str(e)}")
|
| 167 |
+
raise
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
async def main():
|
| 171 |
+
"""Main function to demonstrate the agent."""
|
| 172 |
+
parser = argparse.ArgumentParser(description="Scientific Content Generation Agent")
|
| 173 |
+
parser.add_argument(
|
| 174 |
+
"--init-profile",
|
| 175 |
+
action="store_true",
|
| 176 |
+
help="Initialize a default user profile in ~/.agentic-content-generation/profile.yaml",
|
| 177 |
+
)
|
| 178 |
+
parser.add_argument(
|
| 179 |
+
"--validate-profile",
|
| 180 |
+
action="store_true",
|
| 181 |
+
help="Validate the current profile and show warnings/errors",
|
| 182 |
+
)
|
| 183 |
+
parser.add_argument(
|
| 184 |
+
"--edit-profile",
|
| 185 |
+
action="store_true",
|
| 186 |
+
help="Open profile in your default editor",
|
| 187 |
+
)
|
| 188 |
+
parser.add_argument(
|
| 189 |
+
"--list-sessions",
|
| 190 |
+
action="store_true",
|
| 191 |
+
help="List all saved sessions",
|
| 192 |
+
)
|
| 193 |
+
parser.add_argument(
|
| 194 |
+
"--delete-session",
|
| 195 |
+
type=str,
|
| 196 |
+
metavar="SESSION_ID",
|
| 197 |
+
help="Delete a specific session by ID",
|
| 198 |
+
)
|
| 199 |
+
parser.add_argument(
|
| 200 |
+
"--topic",
|
| 201 |
+
type=str,
|
| 202 |
+
default="Large Language Models and AI Agents",
|
| 203 |
+
help="Topic to generate content about",
|
| 204 |
+
)
|
| 205 |
+
parser.add_argument(
|
| 206 |
+
"--session-id",
|
| 207 |
+
type=str,
|
| 208 |
+
help="Session ID to resume a conversation",
|
| 209 |
+
)
|
| 210 |
+
args = parser.parse_args()
|
| 211 |
+
|
| 212 |
+
print("\n" + "=" * 80)
|
| 213 |
+
print("π¬ SCIENTIFIC CONTENT GENERATION AGENT")
|
| 214 |
+
print("=" * 80)
|
| 215 |
+
|
| 216 |
+
if args.init_profile:
|
| 217 |
+
if PROFILE_PATH.exists():
|
| 218 |
+
print(f"β οΈ Profile already exists at {PROFILE_PATH}")
|
| 219 |
+
print("Edit this file to customize your profile.")
|
| 220 |
+
else:
|
| 221 |
+
save_profile_to_yaml(DEFAULT_PROFILE, PROFILE_PATH)
|
| 222 |
+
print(f"β
Created default profile at {PROFILE_PATH}")
|
| 223 |
+
print(
|
| 224 |
+
"π Please edit this file with your personal information before running the agent."
|
| 225 |
+
)
|
| 226 |
+
return
|
| 227 |
+
|
| 228 |
+
if args.validate_profile:
|
| 229 |
+
print("\nπ Validating profile...\n")
|
| 230 |
+
try:
|
| 231 |
+
profile = load_user_profile(validate=True)
|
| 232 |
+
print("β
Profile validation complete!")
|
| 233 |
+
if profile.name != "Your Name":
|
| 234 |
+
print(f"π€ Profile: {profile.name} ({profile.target_role})")
|
| 235 |
+
except ValueError as e:
|
| 236 |
+
print(f"\nβ Validation failed: {e}")
|
| 237 |
+
return
|
| 238 |
+
return
|
| 239 |
+
|
| 240 |
+
if args.edit_profile:
|
| 241 |
+
print("\nπ Opening profile editor...\n")
|
| 242 |
+
if not PROFILE_PATH.exists():
|
| 243 |
+
print("β οΈ No profile found. Creating one first...")
|
| 244 |
+
save_profile_to_yaml(DEFAULT_PROFILE, PROFILE_PATH)
|
| 245 |
+
print(f"β
Created default profile at {PROFILE_PATH}\n")
|
| 246 |
+
|
| 247 |
+
changed = edit_profile_interactive()
|
| 248 |
+
if changed:
|
| 249 |
+
# Validate after editing
|
| 250 |
+
validate_after_edit()
|
| 251 |
+
return
|
| 252 |
+
|
| 253 |
+
if args.list_sessions:
|
| 254 |
+
print("\nπ Listing all sessions...\n")
|
| 255 |
+
sessions = list_sessions()
|
| 256 |
+
if sessions:
|
| 257 |
+
print(format_session_list(sessions))
|
| 258 |
+
print(f"Total: {len(sessions)} session(s)")
|
| 259 |
+
print("\nπ‘ To resume a session: python main.py --session-id <SESSION_ID>")
|
| 260 |
+
print("π‘ To delete a session: python main.py --delete-session <SESSION_ID>")
|
| 261 |
+
else:
|
| 262 |
+
print("No sessions found. Start a new conversation to create one!")
|
| 263 |
+
return
|
| 264 |
+
|
| 265 |
+
if args.delete_session:
|
| 266 |
+
session_id_to_delete = args.delete_session
|
| 267 |
+
print(f"\nποΈ Deleting session: {session_id_to_delete}...")
|
| 268 |
+
result = delete_session(session_id_to_delete)
|
| 269 |
+
if result["status"] == "success":
|
| 270 |
+
print(f"β
{result['message']}")
|
| 271 |
+
else:
|
| 272 |
+
print(f"β {result['message']}")
|
| 273 |
+
return
|
| 274 |
+
|
| 275 |
+
# Example usage
|
| 276 |
+
topic = args.topic
|
| 277 |
+
session_id = args.session_id
|
| 278 |
+
|
| 279 |
+
preferences = {
|
| 280 |
+
"platforms": ["blog", "linkedin", "twitter"],
|
| 281 |
+
# Tone is now loaded from profile by default
|
| 282 |
+
"target_audience": "AI researchers and industry professionals",
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
result = await run_content_generation(topic, preferences, session_id)
|
| 286 |
+
|
| 287 |
+
# Save output to file
|
| 288 |
+
output_dir = "output"
|
| 289 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 290 |
+
|
| 291 |
+
output_file = f"{output_dir}/content_{topic.replace(' ', '_').lower()}.txt"
|
| 292 |
+
with open(output_file, "w", encoding="utf-8") as f:
|
| 293 |
+
f.write(result)
|
| 294 |
+
|
| 295 |
+
print(f"\nπΎ Content saved to: {output_file}")
|
| 296 |
+
print("\n⨠Done!")
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
if __name__ == "__main__":
|
| 300 |
+
asyncio.run(main())
|
profile.example.yaml
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# User Profile Configuration for Scientific Content Generation Agent
|
| 2 |
+
#
|
| 3 |
+
# This file defines your professional profile for personalized content generation.
|
| 4 |
+
# Copy this file to ~/.agentic-content-generation/profile.yaml and customize it.
|
| 5 |
+
#
|
| 6 |
+
# Quick Start:
|
| 7 |
+
# 1. Run: python main.py --init-profile
|
| 8 |
+
# 2. Edit: ~/.agentic-content-generation/profile.yaml
|
| 9 |
+
# 3. Validate: python main.py --validate-profile
|
| 10 |
+
# 4. Generate content: python main.py --topic "Your Topic"
|
| 11 |
+
|
| 12 |
+
# =============================================================================
|
| 13 |
+
# PROFESSIONAL IDENTITY
|
| 14 |
+
# =============================================================================
|
| 15 |
+
|
| 16 |
+
# Your full name (used for attribution and session tracking)
|
| 17 |
+
name: John Doe
|
| 18 |
+
|
| 19 |
+
# Your target professional role (what you want to be known for)
|
| 20 |
+
# Examples: AI Consultant, ML Engineer, Data Scientist, AI Architect,
|
| 21 |
+
# Research Scientist, AI Product Manager, MLOps Engineer
|
| 22 |
+
target_role: AI Consultant
|
| 23 |
+
|
| 24 |
+
# Your areas of expertise (3-5 recommended)
|
| 25 |
+
# These will be emphasized in content generation
|
| 26 |
+
expertise_areas:
|
| 27 |
+
- Machine Learning
|
| 28 |
+
- Natural Language Processing
|
| 29 |
+
- Computer Vision
|
| 30 |
+
- MLOps
|
| 31 |
+
- AI Strategy
|
| 32 |
+
|
| 33 |
+
# =============================================================================
|
| 34 |
+
# PROFESSIONAL GOALS
|
| 35 |
+
# =============================================================================
|
| 36 |
+
|
| 37 |
+
# What you want to achieve with your content
|
| 38 |
+
# Valid options: opportunities, credibility, visibility, thought-leadership, networking
|
| 39 |
+
content_goals:
|
| 40 |
+
- opportunities # Attract freelance/consulting/job opportunities
|
| 41 |
+
- credibility # Build professional credibility
|
| 42 |
+
- visibility # Increase visibility in the field
|
| 43 |
+
|
| 44 |
+
# =============================================================================
|
| 45 |
+
# GEOGRAPHIC & MARKET
|
| 46 |
+
# =============================================================================
|
| 47 |
+
|
| 48 |
+
# Your primary region (affects industry trends and SEO)
|
| 49 |
+
# Examples: Europe, US, Asia, Global, UK, Canada, Australia
|
| 50 |
+
region: Europe
|
| 51 |
+
|
| 52 |
+
# Languages you create content in
|
| 53 |
+
languages:
|
| 54 |
+
- English
|
| 55 |
+
- French
|
| 56 |
+
|
| 57 |
+
# Target industries for your content
|
| 58 |
+
target_industries:
|
| 59 |
+
- Technology
|
| 60 |
+
- Finance
|
| 61 |
+
- Healthcare
|
| 62 |
+
- Consulting
|
| 63 |
+
- E-commerce
|
| 64 |
+
|
| 65 |
+
# =============================================================================
|
| 66 |
+
# PORTFOLIO & ONLINE PRESENCE
|
| 67 |
+
# =============================================================================
|
| 68 |
+
|
| 69 |
+
# Your GitHub username (not the full URL, just username)
|
| 70 |
+
# Example: octocat
|
| 71 |
+
github_username: johndoe
|
| 72 |
+
|
| 73 |
+
# Your LinkedIn profile URL (full URL)
|
| 74 |
+
# Example: https://www.linkedin.com/in/johndoe
|
| 75 |
+
linkedin_url: https://www.linkedin.com/in/johndoe
|
| 76 |
+
|
| 77 |
+
# Your personal portfolio/website URL (full URL)
|
| 78 |
+
# Example: https://johndoe.com
|
| 79 |
+
portfolio_url: https://johndoe.com
|
| 80 |
+
|
| 81 |
+
# Your Kaggle username (not the full URL, just username)
|
| 82 |
+
# Example: johndoe
|
| 83 |
+
kaggle_username: johndoe
|
| 84 |
+
|
| 85 |
+
# =============================================================================
|
| 86 |
+
# NOTABLE PROJECTS
|
| 87 |
+
# =============================================================================
|
| 88 |
+
|
| 89 |
+
# Key projects to mention in your content (3-5 recommended)
|
| 90 |
+
# These help demonstrate your expertise and provide portfolio links
|
| 91 |
+
notable_projects:
|
| 92 |
+
- name: AI-Powered Recommendation Engine
|
| 93 |
+
description: Built a scalable recommendation system serving 1M+ users
|
| 94 |
+
technologies: PyTorch, FastAPI, Redis, Kubernetes
|
| 95 |
+
url: https://github.com/johndoe/recommendation-engine
|
| 96 |
+
|
| 97 |
+
- name: Medical Image Classification System
|
| 98 |
+
description: Deep learning model for detecting pneumonia from X-rays (95% accuracy)
|
| 99 |
+
technologies: TensorFlow, OpenCV, Docker, AWS SageMaker
|
| 100 |
+
url: https://github.com/johndoe/medical-imaging
|
| 101 |
+
|
| 102 |
+
- name: Real-Time Sentiment Analysis API
|
| 103 |
+
description: Production NLP API processing 10k requests/day
|
| 104 |
+
technologies: Transformers, Flask, PostgreSQL, Celery
|
| 105 |
+
url: https://github.com/johndoe/sentiment-api
|
| 106 |
+
|
| 107 |
+
# =============================================================================
|
| 108 |
+
# TECHNICAL SKILLS & TOOLS
|
| 109 |
+
# =============================================================================
|
| 110 |
+
|
| 111 |
+
# Your primary technical skills (top 5-10)
|
| 112 |
+
# These will be used for SEO keywords and skills matching
|
| 113 |
+
primary_skills:
|
| 114 |
+
- Python
|
| 115 |
+
- PyTorch
|
| 116 |
+
- TensorFlow
|
| 117 |
+
- Scikit-learn
|
| 118 |
+
- Transformers
|
| 119 |
+
- FastAPI
|
| 120 |
+
- Docker
|
| 121 |
+
- Kubernetes
|
| 122 |
+
- AWS
|
| 123 |
+
- MLflow
|
| 124 |
+
|
| 125 |
+
# =============================================================================
|
| 126 |
+
# CONTENT PREFERENCES
|
| 127 |
+
# =============================================================================
|
| 128 |
+
|
| 129 |
+
# Tone for your content
|
| 130 |
+
# Valid options: professional-formal, professional-conversational, technical, casual
|
| 131 |
+
content_tone: professional-conversational
|
| 132 |
+
|
| 133 |
+
# Whether to use emojis in LinkedIn posts (true/false)
|
| 134 |
+
use_emojis: true
|
| 135 |
+
|
| 136 |
+
# Your target posting frequency
|
| 137 |
+
# Valid options: daily, 2-3x per week, weekly, biweekly, monthly
|
| 138 |
+
posting_frequency: 2-3x per week
|
| 139 |
+
|
| 140 |
+
# =============================================================================
|
| 141 |
+
# SEO & POSITIONING
|
| 142 |
+
# =============================================================================
|
| 143 |
+
|
| 144 |
+
# Your unique value proposition (1-2 sentences)
|
| 145 |
+
# What makes you different? What specific problem do you solve?
|
| 146 |
+
unique_value_proposition: I help companies bridge the gap between AI research and production by building scalable, reliable ML systems that deliver measurable business value.
|
| 147 |
+
|
| 148 |
+
# Key differentiators (3-5 bullet points)
|
| 149 |
+
# What sets you apart from other professionals in your field?
|
| 150 |
+
key_differentiators:
|
| 151 |
+
- End-to-end ML pipeline design and implementation
|
| 152 |
+
- 5+ years scaling ML systems in production
|
| 153 |
+
- Strong focus on business ROI and practical impact
|
| 154 |
+
- Research-backed approach with real-world pragmatism
|
| 155 |
+
- Expert in both cloud-native and edge ML deployment
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Google Agent Development Kit
|
| 2 |
+
google-adk>=0.1.0
|
| 3 |
+
google-genai>=0.1.0
|
| 4 |
+
|
| 5 |
+
# Additional dependencies
|
| 6 |
+
python-dotenv>=1.0.0
|
| 7 |
+
requests>=2.31.0
|
| 8 |
+
duckduckgo-search>=6.0.0
|
| 9 |
+
google-cloud-aiplatform>=1.38.0
|
src/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Scientific Content Generation Agent System"""
|
src/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (283 Bytes). View file
|
|
|
src/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (272 Bytes). View file
|
|
|
src/__pycache__/agents.cpython-311.pyc
ADDED
|
Binary file (17.5 kB). View file
|
|
|
src/__pycache__/agents.cpython-312.pyc
ADDED
|
Binary file (16.9 kB). View file
|
|
|
src/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (1.37 kB). View file
|
|
|
src/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (1.32 kB). View file
|
|
|
src/__pycache__/profile.cpython-311.pyc
ADDED
|
Binary file (16.9 kB). View file
|
|
|
src/__pycache__/profile.cpython-312.pyc
ADDED
|
Binary file (15.3 kB). View file
|
|
|
src/__pycache__/profile_editor.cpython-311.pyc
ADDED
|
Binary file (5.97 kB). View file
|
|
|
src/__pycache__/session_manager.cpython-311.pyc
ADDED
|
Binary file (9.98 kB). View file
|
|
|
src/__pycache__/tools.cpython-311.pyc
ADDED
|
Binary file (32.4 kB). View file
|
|
|
src/__pycache__/tools.cpython-312.pyc
ADDED
|
Binary file (29.1 kB). View file
|
|
|
src/agent.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Entry point for Agent Engine deployment.
|
| 2 |
+
|
| 3 |
+
This module provides the root_agent instance required by the ADK deployment system.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from .agents import create_content_generation_pipeline
|
| 7 |
+
|
| 8 |
+
# Create the root agent for deployment
|
| 9 |
+
root_agent = create_content_generation_pipeline()
|
src/agents.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Agent definitions for the scientific content generation system."""
|
| 2 |
+
|
| 3 |
+
from google.adk.agents import LlmAgent, SequentialAgent
|
| 4 |
+
from google.adk.models.google_llm import Gemini
|
| 5 |
+
|
| 6 |
+
from .config import (
|
| 7 |
+
CONTENT_GENERATOR_AGENT_NAME,
|
| 8 |
+
DEFAULT_MODEL,
|
| 9 |
+
RESEARCH_AGENT_NAME,
|
| 10 |
+
RETRY_CONFIG,
|
| 11 |
+
REVIEW_AGENT_NAME,
|
| 12 |
+
ROOT_AGENT_NAME,
|
| 13 |
+
STRATEGY_AGENT_NAME,
|
| 14 |
+
)
|
| 15 |
+
from .tools import (
|
| 16 |
+
analyze_content_for_opportunities,
|
| 17 |
+
create_engagement_hooks,
|
| 18 |
+
extract_key_findings,
|
| 19 |
+
format_for_platform,
|
| 20 |
+
generate_citations,
|
| 21 |
+
generate_seo_keywords,
|
| 22 |
+
search_industry_trends,
|
| 23 |
+
search_papers,
|
| 24 |
+
search_web,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def create_research_agent() -> LlmAgent:
|
| 29 |
+
"""Create the ResearchAgent that searches for papers and current information.
|
| 30 |
+
|
| 31 |
+
The ResearchAgent is responsible for:
|
| 32 |
+
- Searching academic papers on the given topic
|
| 33 |
+
- Finding recent trends and discussions
|
| 34 |
+
- Extracting key findings from research
|
| 35 |
+
- Compiling relevant sources for content creation
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
LlmAgent configured for research tasks
|
| 39 |
+
"""
|
| 40 |
+
return LlmAgent(
|
| 41 |
+
name=RESEARCH_AGENT_NAME,
|
| 42 |
+
model=Gemini(model=DEFAULT_MODEL, retry_options=RETRY_CONFIG),
|
| 43 |
+
description="Searches for academic papers, research articles, and current trends on a given topic",
|
| 44 |
+
instruction="""You are a research specialist focused on finding credible, up-to-date information.
|
| 45 |
+
|
| 46 |
+
Your tasks:
|
| 47 |
+
1. **Deep Research Workflow**:
|
| 48 |
+
- First, search for academic papers using search_papers() on the given topic.
|
| 49 |
+
- Second, search for broader context (industry news, blogs, real-world applications) using search_web().
|
| 50 |
+
- Analyze the initial results. If you find gaps or need more specific details, perform follow-up searches.
|
| 51 |
+
|
| 52 |
+
2. **Synthesize Findings**:
|
| 53 |
+
- Combine academic rigor (from papers) with real-world relevance (from web search).
|
| 54 |
+
- Extract key findings using extract_key_findings().
|
| 55 |
+
- Identify current trends based on both research and industry news.
|
| 56 |
+
|
| 57 |
+
3. **Compile Comprehensive Report**:
|
| 58 |
+
- **Academic Papers**: List of papers with titles, authors, and key findings.
|
| 59 |
+
- **Industry Context**: Real-world applications, news, and market data.
|
| 60 |
+
- **Current Trends**: Emerging themes from both research and industry.
|
| 61 |
+
- **Key Insights**: Most important takeaways.
|
| 62 |
+
- **Sources**: All sources (papers and web links) with URLs for proper citation.
|
| 63 |
+
|
| 64 |
+
Focus on scientific credibility AND practical relevance.
|
| 65 |
+
Organize findings clearly for the next agent to use.
|
| 66 |
+
|
| 67 |
+
IMPORTANT: After completing your research, you MUST provide the final report as your text response. This text will be passed to the next agent.
|
| 68 |
+
""",
|
| 69 |
+
tools=[search_papers, extract_key_findings, search_web],
|
| 70 |
+
output_key="research_findings",
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def create_strategy_agent() -> LlmAgent:
|
| 75 |
+
"""Create the StrategyAgent that plans content approach and angles.
|
| 76 |
+
|
| 77 |
+
The StrategyAgent is responsible for:
|
| 78 |
+
- Analyzing research findings
|
| 79 |
+
- Determining the best angles for content
|
| 80 |
+
- Identifying target audience
|
| 81 |
+
- Planning platform-specific approaches
|
| 82 |
+
- Defining key messages
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
LlmAgent configured for content strategy
|
| 86 |
+
"""
|
| 87 |
+
return LlmAgent(
|
| 88 |
+
name=STRATEGY_AGENT_NAME,
|
| 89 |
+
model=Gemini(model=DEFAULT_MODEL, retry_options=RETRY_CONFIG),
|
| 90 |
+
description="Analyzes research and creates content strategy for different platforms",
|
| 91 |
+
instruction="""You are a content strategist specializing in professional positioning and opportunity generation for AI/ML experts.
|
| 92 |
+
|
| 93 |
+
You will receive research findings from the ResearchAgent. Your task is to:
|
| 94 |
+
|
| 95 |
+
1. Analyze the research findings: {research_findings}
|
| 96 |
+
|
| 97 |
+
2. Determine content angles focused on **professional opportunities**:
|
| 98 |
+
- What demonstrates deep expertise and thought leadership?
|
| 99 |
+
- What business problems does this research solve?
|
| 100 |
+
- How can this position the author as an expert consultant/engineer?
|
| 101 |
+
- What will attract recruiters and potential clients on LinkedIn?
|
| 102 |
+
- What's engaging enough for a comprehensive blog?
|
| 103 |
+
- What can create viral Twitter insights?
|
| 104 |
+
|
| 105 |
+
3. Create a content strategy document with:
|
| 106 |
+
|
| 107 |
+
**Primary Angle**: The main hook/message (focus on business value + expertise)
|
| 108 |
+
|
| 109 |
+
**Professional Positioning**:
|
| 110 |
+
- Position author as: AI/ML consultant, expert, thought leader
|
| 111 |
+
- Demonstrate: Deep technical expertise + business acumen
|
| 112 |
+
- Show: Ability to turn research into production solutions
|
| 113 |
+
|
| 114 |
+
**Target Audience**:
|
| 115 |
+
- Primary: Recruiters, hiring managers, potential clients
|
| 116 |
+
- Secondary: Peers, researchers, industry professionals
|
| 117 |
+
- Tertiary: Students, aspiring professionals
|
| 118 |
+
|
| 119 |
+
**Key Messages** (3-5 core points):
|
| 120 |
+
- Lead with business impact and practical value
|
| 121 |
+
- Support with technical depth and research
|
| 122 |
+
- Include pain points this expertise solves
|
| 123 |
+
- Mention relevant skills/technologies
|
| 124 |
+
|
| 125 |
+
**Platform Strategy**:
|
| 126 |
+
* **Blog**: Educational deep-dive establishing authority
|
| 127 |
+
- Comprehensive technical explanation
|
| 128 |
+
- Real-world applications and case studies
|
| 129 |
+
- Position as expert resource
|
| 130 |
+
|
| 131 |
+
* **LinkedIn** (PRIMARY PLATFORM for opportunities):
|
| 132 |
+
- Professional credibility + opportunity magnet
|
| 133 |
+
- Business-focused angle with technical credibility
|
| 134 |
+
- Strong engagement hooks and CTAs
|
| 135 |
+
- SEO keywords for recruiter visibility
|
| 136 |
+
- Portfolio/project mentions
|
| 137 |
+
- Clear invitation to connect/collaborate
|
| 138 |
+
|
| 139 |
+
* **Twitter**: Thought leadership + visibility
|
| 140 |
+
- Provocative insights that spark discussion
|
| 141 |
+
- Demonstrate expertise in bite-sized format
|
| 142 |
+
- Drive traffic to profile
|
| 143 |
+
|
| 144 |
+
**Tone**: Professional-conversational with confident expertise
|
| 145 |
+
|
| 146 |
+
**Opportunity Elements**:
|
| 147 |
+
- Keywords: Identify must-include SEO terms
|
| 148 |
+
- Pain Points: Business problems this expertise addresses
|
| 149 |
+
- Portfolio Opportunities: Where to mention projects/experience
|
| 150 |
+
- CTAs: How to invite professional connections
|
| 151 |
+
|
| 152 |
+
Focus on building credibility that translates to career opportunities.
|
| 153 |
+
Position the author as someone companies want to hire or work with.
|
| 154 |
+
""",
|
| 155 |
+
tools=[], # Strategy agent uses reasoning, not tools
|
| 156 |
+
output_key="content_strategy",
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def create_content_generator_agent() -> LlmAgent:
|
| 161 |
+
"""Create the ContentGeneratorAgent that produces platform-specific content.
|
| 162 |
+
|
| 163 |
+
The ContentGeneratorAgent is responsible for:
|
| 164 |
+
- Creating blog article drafts
|
| 165 |
+
- Writing LinkedIn posts
|
| 166 |
+
- Composing Twitter threads
|
| 167 |
+
- Tailoring tone and length for each platform
|
| 168 |
+
- Incorporating research findings and sources
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
LlmAgent configured for content generation
|
| 172 |
+
"""
|
| 173 |
+
return LlmAgent(
|
| 174 |
+
name=CONTENT_GENERATOR_AGENT_NAME,
|
| 175 |
+
model=Gemini(model=DEFAULT_MODEL, retry_options=RETRY_CONFIG),
|
| 176 |
+
description="Generates platform-specific content based on research and strategy",
|
| 177 |
+
instruction="""You are an expert content creator specializing in scientific and professional communication.
|
| 178 |
+
|
| 179 |
+
You will receive:
|
| 180 |
+
- Research findings: {research_findings}
|
| 181 |
+
- Content strategy: {content_strategy}
|
| 182 |
+
|
| 183 |
+
Your task is to create high-quality content for THREE platforms:
|
| 184 |
+
|
| 185 |
+
1. **BLOG ARTICLE** (1000-2000 words):
|
| 186 |
+
- Title: Compelling and SEO-friendly
|
| 187 |
+
- Introduction: Hook the reader, explain why this matters
|
| 188 |
+
- Main sections: Deep dive into key findings with proper structure (H2/H3 headings)
|
| 189 |
+
- Examples and explanations: Make complex ideas accessible
|
| 190 |
+
- Conclusion: Summarize and provide future outlook
|
| 191 |
+
- References section: Placeholder for citations
|
| 192 |
+
- Tone: Educational, authoritative, accessible
|
| 193 |
+
|
| 194 |
+
2. **LINKEDIN POST** (300-800 words):
|
| 195 |
+
- Hook: Start with an attention-grabbing statement or question
|
| 196 |
+
- Context: Brief background on why this matters
|
| 197 |
+
- Key insights: 3-5 main takeaways with brief explanations
|
| 198 |
+
- Professional angle: How this impacts the field/industry
|
| 199 |
+
- Call-to-action: Engage readers (ask question, invite comments)
|
| 200 |
+
- Hashtags: 3-5 relevant professional hashtags
|
| 201 |
+
- Tone: Professional, conversational, thought-leadership
|
| 202 |
+
|
| 203 |
+
3. **TWITTER THREAD** (8-12 tweets):
|
| 204 |
+
- Tweet 1: Hook + thread overview (include "π§΅ Thread:")
|
| 205 |
+
- Tweets 2-10: One key insight per tweet, numbered (2/12, 3/12, etc.)
|
| 206 |
+
- Use emojis strategically for visual appeal
|
| 207 |
+
- Each tweet must be under 280 characters
|
| 208 |
+
- Final tweet: Conclusion + relevant hashtags
|
| 209 |
+
- Tone: Concise, engaging, insightful
|
| 210 |
+
|
| 211 |
+
For each platform, use format_for_platform() to ensure proper formatting.
|
| 212 |
+
|
| 213 |
+
Important:
|
| 214 |
+
- Reference specific papers/sources naturally in the content
|
| 215 |
+
- Maintain scientific accuracy while being engaging
|
| 216 |
+
- Build author's credibility by demonstrating deep understanding
|
| 217 |
+
- Make content shareable and valuable
|
| 218 |
+
|
| 219 |
+
Output format:
|
| 220 |
+
=== BLOG ARTICLE ===
|
| 221 |
+
[Full blog content]
|
| 222 |
+
|
| 223 |
+
=== LINKEDIN POST ===
|
| 224 |
+
[Full LinkedIn content]
|
| 225 |
+
|
| 226 |
+
=== TWITTER THREAD ===
|
| 227 |
+
[Full Twitter thread]
|
| 228 |
+
""",
|
| 229 |
+
tools=[format_for_platform],
|
| 230 |
+
output_key="generated_content",
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def create_linkedin_optimization_agent() -> LlmAgent:
|
| 235 |
+
"""Create the LinkedInOptimizationAgent that optimizes content for opportunities.
|
| 236 |
+
|
| 237 |
+
The LinkedInOptimizationAgent is responsible for:
|
| 238 |
+
- Optimizing LinkedIn content for SEO and recruiter visibility
|
| 239 |
+
- Adding engagement hooks and calls-to-action
|
| 240 |
+
- Integrating portfolio mentions naturally
|
| 241 |
+
- Emphasizing business value and practical impact
|
| 242 |
+
- Positioning author as expert/consultant
|
| 243 |
+
|
| 244 |
+
Returns:
|
| 245 |
+
LlmAgent configured for LinkedIn optimization
|
| 246 |
+
"""
|
| 247 |
+
return LlmAgent(
|
| 248 |
+
name="LinkedInOptimizationAgent",
|
| 249 |
+
model=Gemini(model=DEFAULT_MODEL, retry_options=RETRY_CONFIG),
|
| 250 |
+
description="Optimizes content for professional opportunities and recruiter visibility",
|
| 251 |
+
instruction="""You are a LinkedIn optimization specialist focused on career opportunities.
|
| 252 |
+
|
| 253 |
+
You will receive:
|
| 254 |
+
- Research findings: {research_findings}
|
| 255 |
+
- Content strategy: {content_strategy}
|
| 256 |
+
- Generated content: {generated_content}
|
| 257 |
+
|
| 258 |
+
Your mission: Optimize the LINKEDIN POST ONLY to maximize professional opportunities.
|
| 259 |
+
|
| 260 |
+
**Optimization Tasks**:
|
| 261 |
+
|
| 262 |
+
1. **SEO Optimization** (use generate_seo_keywords tool):
|
| 263 |
+
- Add keywords recruiters search for (AI Consultant, ML Engineer, etc.)
|
| 264 |
+
- Include hot technical skills (PyTorch, TensorFlow, LangChain, etc.)
|
| 265 |
+
- Weave keywords naturally into the post
|
| 266 |
+
|
| 267 |
+
2. **Engagement Hooks** (use create_engagement_hooks tool):
|
| 268 |
+
- Start with a compelling hook that stops scrolling
|
| 269 |
+
- End with a strong call-to-action inviting connections
|
| 270 |
+
- Add 1-2 questions that spark discussion
|
| 271 |
+
- Include invitation to DM for collaboration
|
| 272 |
+
|
| 273 |
+
3. **Portfolio Integration**:
|
| 274 |
+
- Naturally mention relevant projects or experience
|
| 275 |
+
- Reference GitHub, Kaggle, or specific work (if mentioned in context)
|
| 276 |
+
- Use phrases like "In my recent project..." or "While building..."
|
| 277 |
+
- Don't force it if not relevant
|
| 278 |
+
|
| 279 |
+
4. **Business Value Focus**:
|
| 280 |
+
- Emphasize practical impact over pure theory
|
| 281 |
+
- Use business language: ROI, scale, production, results
|
| 282 |
+
- Show how research translates to real-world solutions
|
| 283 |
+
- Position as consultant/expert who solves problems
|
| 284 |
+
|
| 285 |
+
5. **Professional Positioning**:
|
| 286 |
+
- Use confident, authoritative tone
|
| 287 |
+
- Demonstrate deep expertise
|
| 288 |
+
- Show thought leadership
|
| 289 |
+
- Subtly signal availability for opportunities
|
| 290 |
+
|
| 291 |
+
6. **Industry Trends** (use search_industry_trends if helpful):
|
| 292 |
+
- Connect content to current market demands
|
| 293 |
+
- Mention pain points companies face
|
| 294 |
+
- Show awareness of hiring trends
|
| 295 |
+
|
| 296 |
+
**Optimization Guidelines**:
|
| 297 |
+
- Keep length 300-800 words
|
| 298 |
+
- Use line breaks for readability
|
| 299 |
+
- Include 1-2 emojis strategically (optional based on tone)
|
| 300 |
+
- Add 3-5 relevant hashtags at the end
|
| 301 |
+
- Make it scannable (use bold or bullet points if helpful)
|
| 302 |
+
|
| 303 |
+
Output ONLY the optimized LinkedIn post:
|
| 304 |
+
=== OPTIMIZED LINKEDIN POST ===
|
| 305 |
+
[Your optimized post with SEO, hooks, portfolio mentions, and strong CTA]
|
| 306 |
+
""",
|
| 307 |
+
tools=[
|
| 308 |
+
generate_seo_keywords,
|
| 309 |
+
create_engagement_hooks,
|
| 310 |
+
search_industry_trends,
|
| 311 |
+
],
|
| 312 |
+
output_key="optimized_linkedin",
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def create_review_agent() -> LlmAgent:
|
| 317 |
+
"""Create the ReviewAgent that verifies claims and adds citations.
|
| 318 |
+
|
| 319 |
+
The ReviewAgent is responsible for:
|
| 320 |
+
- Verifying scientific accuracy
|
| 321 |
+
- Adding proper citations
|
| 322 |
+
- Checking tone and credibility
|
| 323 |
+
- Ensuring platform-appropriate formatting
|
| 324 |
+
- Final quality assurance
|
| 325 |
+
|
| 326 |
+
Returns:
|
| 327 |
+
LlmAgent configured for content review
|
| 328 |
+
"""
|
| 329 |
+
return LlmAgent(
|
| 330 |
+
name=REVIEW_AGENT_NAME,
|
| 331 |
+
model=Gemini(model=DEFAULT_MODEL, retry_options=RETRY_CONFIG),
|
| 332 |
+
description="Reviews content for accuracy, adds citations, and ensures quality",
|
| 333 |
+
instruction="""You are a scientific content reviewer ensuring accuracy, credibility, and opportunity appeal.
|
| 334 |
+
|
| 335 |
+
You will receive:
|
| 336 |
+
- Research findings with sources: {research_findings}
|
| 337 |
+
- Generated content for all platforms: {generated_content}
|
| 338 |
+
- Optimized LinkedIn post: {optimized_linkedin}
|
| 339 |
+
|
| 340 |
+
Your tasks:
|
| 341 |
+
|
| 342 |
+
1. **Verify Scientific Accuracy**:
|
| 343 |
+
- Check that claims match the research findings
|
| 344 |
+
- Ensure no overstatements or misleading interpretations
|
| 345 |
+
- Verify technical terminology is used correctly
|
| 346 |
+
|
| 347 |
+
2. **Add Proper Citations**:
|
| 348 |
+
- Use generate_citations() to create formatted citations from sources
|
| 349 |
+
- Add inline citations where claims reference specific papers
|
| 350 |
+
- Create a complete references section for the blog
|
| 351 |
+
- Add source links to LinkedIn and Twitter where appropriate
|
| 352 |
+
|
| 353 |
+
3. **Review Quality**:
|
| 354 |
+
- Check that tone is appropriate for each platform
|
| 355 |
+
- Ensure content builds author's credibility
|
| 356 |
+
- Verify engaging hooks and calls-to-action
|
| 357 |
+
- Check formatting (headings, line breaks, character limits)
|
| 358 |
+
|
| 359 |
+
4. **Opportunity Analysis** (use analyze_content_for_opportunities):
|
| 360 |
+
- Score the optimized LinkedIn post for opportunity appeal
|
| 361 |
+
- Provide actionable suggestions for improvement
|
| 362 |
+
- Ensure SEO keywords are present
|
| 363 |
+
- Verify engagement hooks are strong
|
| 364 |
+
|
| 365 |
+
5. **Final Polish**:
|
| 366 |
+
- Fix any grammar or style issues
|
| 367 |
+
- Ensure consistency across platforms
|
| 368 |
+
- Verify all hashtags are relevant
|
| 369 |
+
- Check that Twitter thread stays under character limits
|
| 370 |
+
|
| 371 |
+
Output the FINAL POLISHED CONTENT for all three platforms with citations and scores.
|
| 372 |
+
|
| 373 |
+
Format:
|
| 374 |
+
=== FINAL BLOG ARTICLE ===
|
| 375 |
+
[Blog with inline citations and references section]
|
| 376 |
+
|
| 377 |
+
=== FINAL LINKEDIN POST ===
|
| 378 |
+
[Use the optimized LinkedIn post, with any final improvements]
|
| 379 |
+
|
| 380 |
+
=== FINAL TWITTER THREAD ===
|
| 381 |
+
[Twitter thread with relevant citations]
|
| 382 |
+
|
| 383 |
+
=== CITATIONS ===
|
| 384 |
+
[Complete formatted citations for all sources]
|
| 385 |
+
|
| 386 |
+
=== OPPORTUNITY ANALYSIS ===
|
| 387 |
+
**Opportunity Score**: X/100
|
| 388 |
+
**SEO Score**: X/100
|
| 389 |
+
**Engagement Score**: X/100
|
| 390 |
+
**Suggestions**: [Key recommendations for improvement]
|
| 391 |
+
""",
|
| 392 |
+
tools=[generate_citations, analyze_content_for_opportunities],
|
| 393 |
+
output_key="final_content",
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
def create_content_generation_pipeline() -> SequentialAgent:
|
| 398 |
+
"""Create the complete content generation pipeline.
|
| 399 |
+
|
| 400 |
+
The pipeline runs agents in sequence:
|
| 401 |
+
1. ResearchAgent: Find papers and trends
|
| 402 |
+
2. StrategyAgent: Plan content approach
|
| 403 |
+
3. ContentGeneratorAgent: Create drafts
|
| 404 |
+
4. LinkedInOptimizationAgent: Optimize LinkedIn for opportunities
|
| 405 |
+
5. ReviewAgent: Verify, polish, and score
|
| 406 |
+
|
| 407 |
+
Design decision: We use SequentialAgent (not ParallelAgent) because each agent
|
| 408 |
+
depends on the outputs of previous agents. The state flows linearly through
|
| 409 |
+
the pipeline via the output_key/placeholder pattern, where each agent's
|
| 410 |
+
output_key becomes available as {placeholder} for subsequent agents.
|
| 411 |
+
|
| 412 |
+
The 5-agent architecture balances specialization with maintainability:
|
| 413 |
+
- Research: Academic credibility through paper sources
|
| 414 |
+
- Strategy: Professional positioning and audience targeting
|
| 415 |
+
- Content: Platform-specific format optimization
|
| 416 |
+
- LinkedIn: Opportunity generation (SEO, engagement, portfolio)
|
| 417 |
+
- Review: Quality assurance and scoring
|
| 418 |
+
|
| 419 |
+
Returns:
|
| 420 |
+
SequentialAgent orchestrating the complete workflow
|
| 421 |
+
"""
|
| 422 |
+
# Create all specialized agents
|
| 423 |
+
research_agent = create_research_agent()
|
| 424 |
+
strategy_agent = create_strategy_agent()
|
| 425 |
+
content_agent = create_content_generator_agent()
|
| 426 |
+
linkedin_optimizer = create_linkedin_optimization_agent()
|
| 427 |
+
review_agent = create_review_agent()
|
| 428 |
+
|
| 429 |
+
# Design decision: Order matters! Each agent builds on previous outputs.
|
| 430 |
+
# Do not reorder without updating placeholder references in instructions.
|
| 431 |
+
return SequentialAgent(
|
| 432 |
+
name=ROOT_AGENT_NAME,
|
| 433 |
+
description="Complete scientific content generation system with professional opportunity optimization",
|
| 434 |
+
sub_agents=[
|
| 435 |
+
research_agent,
|
| 436 |
+
strategy_agent,
|
| 437 |
+
content_agent,
|
| 438 |
+
linkedin_optimizer,
|
| 439 |
+
review_agent,
|
| 440 |
+
],
|
| 441 |
+
)
|
src/config.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration for the content generation agent system."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from google.genai import types
|
| 7 |
+
|
| 8 |
+
# Load environment variables
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
# API Configuration
|
| 12 |
+
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
| 13 |
+
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = os.getenv("GOOGLE_GENAI_USE_VERTEXAI", "FALSE")
|
| 14 |
+
|
| 15 |
+
# Model Configuration
|
| 16 |
+
DEFAULT_MODEL = "gemini-2.0-flash-exp"
|
| 17 |
+
|
| 18 |
+
# Retry configuration for transient failures
|
| 19 |
+
# Design decision: We use exponential backoff with 5 attempts to handle:
|
| 20 |
+
# - 429: Rate limiting (common with Gemini API free tier)
|
| 21 |
+
# - 500/503/504: Temporary server issues
|
| 22 |
+
# exp_base=7 gives: 1s, 7s, 49s... - aggressive enough for production use
|
| 23 |
+
# This ensures the agent completes tasks even with intermittent API issues
|
| 24 |
+
RETRY_CONFIG = types.HttpRetryOptions(
|
| 25 |
+
attempts=5, exp_base=7, initial_delay=1, http_status_codes=[429, 500, 503, 504]
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Agent Configuration
|
| 29 |
+
RESEARCH_AGENT_NAME = "ResearchAgent"
|
| 30 |
+
STRATEGY_AGENT_NAME = "StrategyAgent"
|
| 31 |
+
CONTENT_GENERATOR_AGENT_NAME = "ContentGeneratorAgent"
|
| 32 |
+
REVIEW_AGENT_NAME = "ReviewAgent"
|
| 33 |
+
ROOT_AGENT_NAME = "ScientificContentAgent"
|
| 34 |
+
|
| 35 |
+
# Content Configuration
|
| 36 |
+
SUPPORTED_PLATFORMS = ["blog", "linkedin", "twitter"]
|
| 37 |
+
MAX_PAPERS_PER_SEARCH = 5
|
| 38 |
+
CITATION_STYLE = "apa"
|
| 39 |
+
|
| 40 |
+
# Logging Configuration
|
| 41 |
+
LOG_LEVEL = "INFO"
|
| 42 |
+
LOG_FILE = "agent.log"
|
src/profile.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""User professional profile configuration for personalized content generation."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from dataclasses import dataclass, field
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import yaml
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class UserProfile:
|
| 13 |
+
"""Professional profile configuration for content personalization.
|
| 14 |
+
|
| 15 |
+
This profile helps tailor content to your expertise, positioning,
|
| 16 |
+
and professional goals for maximum opportunity generation.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
# Professional Identity
|
| 20 |
+
name: str = "Your Name"
|
| 21 |
+
target_role: str = "AI Consultant" # AI Consultant, ML Engineer, AI Architect, etc.
|
| 22 |
+
expertise_areas: list[str] = field(
|
| 23 |
+
default_factory=lambda: ["Machine Learning", "Artificial Intelligence", "Deep Learning"]
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Professional Goals
|
| 27 |
+
content_goals: list[str] = field(
|
| 28 |
+
default_factory=lambda: [
|
| 29 |
+
"opportunities", # Attract freelance/job opportunities
|
| 30 |
+
"credibility", # Build professional credibility
|
| 31 |
+
"visibility", # Increase visibility in the field
|
| 32 |
+
]
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# Geographic & Market
|
| 36 |
+
region: str = "Europe" # Europe, US, Asia, Global, etc.
|
| 37 |
+
languages: list[str] = field(default_factory=lambda: ["English"])
|
| 38 |
+
target_industries: list[str] = field(
|
| 39 |
+
default_factory=lambda: ["Technology", "Finance", "Healthcare", "Consulting"]
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Portfolio & Experience
|
| 43 |
+
github_username: str = "" # Your GitHub username
|
| 44 |
+
linkedin_url: str = "" # Your LinkedIn profile URL
|
| 45 |
+
portfolio_url: str = "" # Personal website/portfolio
|
| 46 |
+
kaggle_username: str = "" # Your Kaggle username
|
| 47 |
+
|
| 48 |
+
# Key Projects (to mention in content)
|
| 49 |
+
notable_projects: list[dict[str, str]] = field(
|
| 50 |
+
default_factory=lambda: [
|
| 51 |
+
{
|
| 52 |
+
"name": "Project Name",
|
| 53 |
+
"description": "Brief description of what you built",
|
| 54 |
+
"technologies": "PyTorch, FastAPI, Docker",
|
| 55 |
+
"url": "https://github.com/username/project",
|
| 56 |
+
}
|
| 57 |
+
]
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
# Technical Skills & Tools
|
| 61 |
+
primary_skills: list[str] = field(
|
| 62 |
+
default_factory=lambda: ["Python", "PyTorch", "TensorFlow", "Scikit-learn", "MLflow"]
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Content Preferences
|
| 66 |
+
content_tone: str = (
|
| 67 |
+
"professional-conversational" # professional-formal, professional-conversational, technical
|
| 68 |
+
)
|
| 69 |
+
use_emojis: bool = True # Use emojis in LinkedIn posts
|
| 70 |
+
posting_frequency: str = "2-3x per week" # daily, 2-3x per week, weekly
|
| 71 |
+
|
| 72 |
+
# SEO & Positioning
|
| 73 |
+
unique_value_proposition: str = (
|
| 74 |
+
"I help companies turn AI research into production-ready solutions"
|
| 75 |
+
)
|
| 76 |
+
key_differentiators: list[str] = field(
|
| 77 |
+
default_factory=lambda: [
|
| 78 |
+
"Bridging research and production",
|
| 79 |
+
"End-to-end AI implementation",
|
| 80 |
+
"Business-focused technical expertise",
|
| 81 |
+
]
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
def to_dict(self) -> dict[str, Any]:
|
| 85 |
+
"""Convert profile to dictionary for agent context."""
|
| 86 |
+
return {
|
| 87 |
+
"name": self.name,
|
| 88 |
+
"target_role": self.target_role,
|
| 89 |
+
"expertise_areas": self.expertise_areas,
|
| 90 |
+
"content_goals": self.content_goals,
|
| 91 |
+
"region": self.region,
|
| 92 |
+
"languages": self.languages,
|
| 93 |
+
"target_industries": self.target_industries,
|
| 94 |
+
"github_username": self.github_username,
|
| 95 |
+
"linkedin_url": self.linkedin_url,
|
| 96 |
+
"portfolio_url": self.portfolio_url,
|
| 97 |
+
"kaggle_username": self.kaggle_username,
|
| 98 |
+
"notable_projects": self.notable_projects,
|
| 99 |
+
"primary_skills": self.primary_skills,
|
| 100 |
+
"content_tone": self.content_tone,
|
| 101 |
+
"use_emojis": self.use_emojis,
|
| 102 |
+
"posting_frequency": self.posting_frequency,
|
| 103 |
+
"unique_value_proposition": self.unique_value_proposition,
|
| 104 |
+
"key_differentiators": self.key_differentiators,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
def get_profile_summary(self) -> str:
|
| 108 |
+
"""Generate a text summary of the profile for agent instructions."""
|
| 109 |
+
expertise_str = ", ".join(self.expertise_areas)
|
| 110 |
+
skills_str = ", ".join(self.primary_skills[:5])
|
| 111 |
+
goals_str = ", ".join(self.content_goals)
|
| 112 |
+
|
| 113 |
+
summary = f"""
|
| 114 |
+
**Professional Profile**:
|
| 115 |
+
- Role: {self.target_role}
|
| 116 |
+
- Expertise: {expertise_str}
|
| 117 |
+
- Key Skills: {skills_str}
|
| 118 |
+
- Region: {self.region}
|
| 119 |
+
- Content Goals: {goals_str}
|
| 120 |
+
- Value Proposition: {self.unique_value_proposition}
|
| 121 |
+
- Tone: {self.content_tone}
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
if self.github_username:
|
| 125 |
+
summary += f"- GitHub: github.com/{self.github_username}\n"
|
| 126 |
+
if self.linkedin_url:
|
| 127 |
+
summary += f"- LinkedIn: {self.linkedin_url}\n"
|
| 128 |
+
|
| 129 |
+
if self.notable_projects and self.notable_projects[0]["name"] != "Project Name":
|
| 130 |
+
summary += "\n**Notable Projects to Mention**:\n"
|
| 131 |
+
for project in self.notable_projects[:3]:
|
| 132 |
+
summary += (
|
| 133 |
+
f"- {project['name']}: {project['description']} ({project['technologies']})\n"
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
return summary
|
| 137 |
+
|
| 138 |
+
def validate(self) -> dict[str, list[str]]:
|
| 139 |
+
"""Validate profile completeness and correctness.
|
| 140 |
+
|
| 141 |
+
Returns:
|
| 142 |
+
Dictionary with 'errors' and 'warnings' lists
|
| 143 |
+
"""
|
| 144 |
+
errors = []
|
| 145 |
+
warnings = []
|
| 146 |
+
|
| 147 |
+
# Validate required fields
|
| 148 |
+
if self.name == "Your Name" or not self.name.strip():
|
| 149 |
+
warnings.append("β οΈ Name is not set. Please update 'name' field in profile.yaml")
|
| 150 |
+
|
| 151 |
+
if not self.expertise_areas or (
|
| 152 |
+
len(self.expertise_areas) == 3
|
| 153 |
+
and self.expertise_areas[0] == "Machine Learning"
|
| 154 |
+
and self.expertise_areas[1] == "Artificial Intelligence"
|
| 155 |
+
):
|
| 156 |
+
warnings.append(
|
| 157 |
+
"β οΈ Using default expertise areas. Update 'expertise_areas' with your specific skills"
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
# Validate URLs
|
| 161 |
+
url_pattern = re.compile(
|
| 162 |
+
r"^https?://" # http:// or https://
|
| 163 |
+
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|" # domain...
|
| 164 |
+
r"localhost|" # localhost...
|
| 165 |
+
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
|
| 166 |
+
r"(?::\d+)?" # optional port
|
| 167 |
+
r"(?:/?|[/?]\S+)$",
|
| 168 |
+
re.IGNORECASE,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
if self.linkedin_url and not url_pattern.match(self.linkedin_url):
|
| 172 |
+
errors.append(
|
| 173 |
+
f"β Invalid LinkedIn URL: '{self.linkedin_url}'. Must start with http:// or https://"
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
if self.portfolio_url and not url_pattern.match(self.portfolio_url):
|
| 177 |
+
errors.append(
|
| 178 |
+
f"β Invalid portfolio URL: '{self.portfolio_url}'. Must start with http:// or https://"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# Validate GitHub username (no special URL validation, just username)
|
| 182 |
+
if self.github_username and "/" in self.github_username:
|
| 183 |
+
warnings.append(
|
| 184 |
+
f"β οΈ GitHub username should be just the username, not a URL: '{self.github_username}'"
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# Validate Kaggle username
|
| 188 |
+
if self.kaggle_username and "/" in self.kaggle_username:
|
| 189 |
+
warnings.append(
|
| 190 |
+
f"β οΈ Kaggle username should be just the username, not a URL: '{self.kaggle_username}'"
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
# Validate content_tone enum
|
| 194 |
+
valid_tones = ["professional-formal", "professional-conversational", "technical", "casual"]
|
| 195 |
+
if self.content_tone not in valid_tones:
|
| 196 |
+
errors.append(
|
| 197 |
+
f"β Invalid content_tone: '{self.content_tone}'. "
|
| 198 |
+
f"Valid options: {', '.join(valid_tones)}"
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
# Validate content_goals
|
| 202 |
+
valid_goals = [
|
| 203 |
+
"opportunities",
|
| 204 |
+
"credibility",
|
| 205 |
+
"visibility",
|
| 206 |
+
"thought-leadership",
|
| 207 |
+
"networking",
|
| 208 |
+
]
|
| 209 |
+
invalid_goals = [g for g in self.content_goals if g not in valid_goals]
|
| 210 |
+
if invalid_goals:
|
| 211 |
+
warnings.append(
|
| 212 |
+
f"β οΈ Unrecognized content goals: {', '.join(invalid_goals)}. "
|
| 213 |
+
f"Valid options: {', '.join(valid_goals)}"
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
# Validate posting_frequency
|
| 217 |
+
valid_frequencies = ["daily", "2-3x per week", "weekly", "biweekly", "monthly"]
|
| 218 |
+
if self.posting_frequency not in valid_frequencies:
|
| 219 |
+
warnings.append(
|
| 220 |
+
f"β οΈ Unrecognized posting frequency: '{self.posting_frequency}'. "
|
| 221 |
+
f"Valid options: {', '.join(valid_frequencies)}"
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# Validate lists are not empty
|
| 225 |
+
if not self.expertise_areas:
|
| 226 |
+
errors.append(
|
| 227 |
+
"β 'expertise_areas' cannot be empty. Add at least one area of expertise"
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
if not self.primary_skills:
|
| 231 |
+
warnings.append("β οΈ 'primary_skills' is empty. Consider adding your technical skills")
|
| 232 |
+
|
| 233 |
+
if not self.target_industries:
|
| 234 |
+
warnings.append("β οΈ 'target_industries' is empty. Consider adding target industries")
|
| 235 |
+
|
| 236 |
+
# Validate notable_projects structure
|
| 237 |
+
for idx, project in enumerate(self.notable_projects):
|
| 238 |
+
required_keys = ["name", "description", "technologies", "url"]
|
| 239 |
+
missing_keys = [key for key in required_keys if key not in project]
|
| 240 |
+
if missing_keys:
|
| 241 |
+
warnings.append(f"β οΈ Project {idx + 1} missing keys: {', '.join(missing_keys)}")
|
| 242 |
+
|
| 243 |
+
# Check if still using default project
|
| 244 |
+
if project.get("name") == "Project Name":
|
| 245 |
+
warnings.append(
|
| 246 |
+
"β οΈ Using default project placeholder. Update 'notable_projects' with your actual projects"
|
| 247 |
+
)
|
| 248 |
+
break # Only warn once
|
| 249 |
+
|
| 250 |
+
# Validate unique_value_proposition
|
| 251 |
+
if (
|
| 252 |
+
self.unique_value_proposition
|
| 253 |
+
== "I help companies turn AI research into production-ready solutions"
|
| 254 |
+
):
|
| 255 |
+
warnings.append(
|
| 256 |
+
"β οΈ Using default value proposition. Update 'unique_value_proposition' with your unique offering"
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
return {"errors": errors, "warnings": warnings}
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
# Default profile (users should customize this)
|
| 263 |
+
DEFAULT_PROFILE = UserProfile()
|
| 264 |
+
|
| 265 |
+
# Path to user profile configuration
|
| 266 |
+
PROFILE_DIR = Path.home() / ".agentic-content-generation"
|
| 267 |
+
PROFILE_PATH = PROFILE_DIR / "profile.yaml"
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def load_profile_from_yaml(path: Path) -> UserProfile:
|
| 271 |
+
"""Load user profile from YAML file.
|
| 272 |
+
|
| 273 |
+
Args:
|
| 274 |
+
path: Path to the YAML file
|
| 275 |
+
|
| 276 |
+
Returns:
|
| 277 |
+
UserProfile instance
|
| 278 |
+
"""
|
| 279 |
+
if not path.exists():
|
| 280 |
+
return DEFAULT_PROFILE
|
| 281 |
+
|
| 282 |
+
try:
|
| 283 |
+
with open(path, encoding="utf-8") as f:
|
| 284 |
+
data = yaml.safe_load(f)
|
| 285 |
+
if not data:
|
| 286 |
+
return DEFAULT_PROFILE
|
| 287 |
+
# Filter out any keys that don't exist in UserProfile
|
| 288 |
+
valid_keys = UserProfile.__annotations__.keys()
|
| 289 |
+
filtered_data = {k: v for k, v in data.items() if k in valid_keys}
|
| 290 |
+
return UserProfile(**filtered_data)
|
| 291 |
+
except Exception as e:
|
| 292 |
+
print(f"Warning: Failed to load profile from {path}: {e}")
|
| 293 |
+
return DEFAULT_PROFILE
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def save_profile_to_yaml(profile: UserProfile, path: Path) -> None:
|
| 297 |
+
"""Save user profile to YAML file.
|
| 298 |
+
|
| 299 |
+
Args:
|
| 300 |
+
profile: UserProfile instance
|
| 301 |
+
path: Path to save the YAML file
|
| 302 |
+
"""
|
| 303 |
+
# Create directory if it doesn't exist
|
| 304 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 305 |
+
|
| 306 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 307 |
+
yaml.dump(profile.to_dict(), f, default_flow_style=False, sort_keys=False)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def load_user_profile(validate: bool = True) -> UserProfile:
|
| 311 |
+
"""Load user profile from configuration.
|
| 312 |
+
|
| 313 |
+
Checks ~/.agentic-content-generation/profile.yaml first.
|
| 314 |
+
Falls back to default profile if not found.
|
| 315 |
+
|
| 316 |
+
Args:
|
| 317 |
+
validate: Whether to run validation and display warnings/errors
|
| 318 |
+
|
| 319 |
+
Returns:
|
| 320 |
+
UserProfile instance
|
| 321 |
+
"""
|
| 322 |
+
if PROFILE_PATH.exists():
|
| 323 |
+
print(f"π€ Loading profile from {PROFILE_PATH}")
|
| 324 |
+
profile = load_profile_from_yaml(PROFILE_PATH)
|
| 325 |
+
else:
|
| 326 |
+
print("π€ Using default profile (no custom profile found)")
|
| 327 |
+
print(f"π‘ Run with --init-profile to create one at {PROFILE_PATH}")
|
| 328 |
+
profile = DEFAULT_PROFILE
|
| 329 |
+
|
| 330 |
+
# Validate profile if requested
|
| 331 |
+
if validate:
|
| 332 |
+
validation = profile.validate()
|
| 333 |
+
errors = validation["errors"]
|
| 334 |
+
warnings = validation["warnings"]
|
| 335 |
+
|
| 336 |
+
if errors:
|
| 337 |
+
print("\nβ Profile Validation Errors:")
|
| 338 |
+
for error in errors:
|
| 339 |
+
print(f" {error}")
|
| 340 |
+
print("\nβ οΈ Please fix these errors in your profile.yaml before continuing.\n")
|
| 341 |
+
raise ValueError(f"Profile validation failed with {len(errors)} error(s)")
|
| 342 |
+
|
| 343 |
+
if warnings:
|
| 344 |
+
print("\nπ Profile Validation Warnings:")
|
| 345 |
+
for warning in warnings:
|
| 346 |
+
print(f" {warning}")
|
| 347 |
+
print()
|
| 348 |
+
|
| 349 |
+
return profile
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def create_custom_profile(
|
| 353 |
+
name: str, target_role: str, expertise_areas: list[str], **kwargs
|
| 354 |
+
) -> UserProfile:
|
| 355 |
+
"""Create a custom user profile.
|
| 356 |
+
|
| 357 |
+
Args:
|
| 358 |
+
name: Your name
|
| 359 |
+
target_role: Target professional role
|
| 360 |
+
expertise_areas: List of expertise areas
|
| 361 |
+
**kwargs: Additional profile fields
|
| 362 |
+
|
| 363 |
+
Returns:
|
| 364 |
+
UserProfile instance
|
| 365 |
+
"""
|
| 366 |
+
return UserProfile(
|
| 367 |
+
name=name, target_role=target_role, expertise_areas=expertise_areas, **kwargs
|
| 368 |
+
)
|
src/profile_editor.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Interactive profile editor for the content generation agent."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import subprocess
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from src.profile import PROFILE_PATH, load_profile_from_yaml, save_profile_to_yaml
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def get_editor() -> str:
|
| 11 |
+
"""Get the user's preferred editor from environment variables.
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
Editor command (defaults to 'nano' if not set)
|
| 15 |
+
"""
|
| 16 |
+
# Check common editor environment variables
|
| 17 |
+
for env_var in ["VISUAL", "EDITOR"]:
|
| 18 |
+
editor = os.environ.get(env_var)
|
| 19 |
+
if editor:
|
| 20 |
+
return editor
|
| 21 |
+
|
| 22 |
+
# Platform-specific defaults
|
| 23 |
+
if os.name == "nt": # Windows
|
| 24 |
+
return "notepad"
|
| 25 |
+
return "nano" # Unix-like systems
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def edit_profile_interactive() -> bool:
|
| 29 |
+
"""Open the profile in an interactive editor.
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
True if profile was modified, False otherwise
|
| 33 |
+
"""
|
| 34 |
+
if not PROFILE_PATH.exists():
|
| 35 |
+
print(f"β Profile not found at {PROFILE_PATH}")
|
| 36 |
+
print("π‘ Run: python main.py --init-profile")
|
| 37 |
+
return False
|
| 38 |
+
|
| 39 |
+
# Read original content
|
| 40 |
+
with open(PROFILE_PATH, encoding="utf-8") as f:
|
| 41 |
+
original_content = f.read()
|
| 42 |
+
|
| 43 |
+
# Get editor
|
| 44 |
+
editor = get_editor()
|
| 45 |
+
print(f"π Opening profile in {editor}...")
|
| 46 |
+
print(f"π File: {PROFILE_PATH}\n")
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
# Open editor
|
| 50 |
+
subprocess.run([editor, str(PROFILE_PATH)], check=True)
|
| 51 |
+
|
| 52 |
+
# Read modified content
|
| 53 |
+
with open(PROFILE_PATH, encoding="utf-8") as f:
|
| 54 |
+
modified_content = f.read()
|
| 55 |
+
|
| 56 |
+
# Check if changed
|
| 57 |
+
if original_content == modified_content:
|
| 58 |
+
print("\nπ No changes made.")
|
| 59 |
+
return False
|
| 60 |
+
|
| 61 |
+
print("\nβ
Profile updated!")
|
| 62 |
+
return True
|
| 63 |
+
|
| 64 |
+
except subprocess.CalledProcessError as e:
|
| 65 |
+
print(f"\nβ Editor failed: {e}")
|
| 66 |
+
return False
|
| 67 |
+
except FileNotFoundError:
|
| 68 |
+
print(f"\nβ Editor '{editor}' not found.")
|
| 69 |
+
print("π‘ Set EDITOR environment variable to your preferred editor:")
|
| 70 |
+
print(" export EDITOR=vim")
|
| 71 |
+
print(" export EDITOR=code # VS Code")
|
| 72 |
+
print(" export EDITOR=emacs")
|
| 73 |
+
return False
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def show_profile_diff(path: Path) -> None:
|
| 77 |
+
"""Show a diff of profile changes.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
path: Path to the profile file
|
| 81 |
+
"""
|
| 82 |
+
# This is a placeholder for future implementation
|
| 83 |
+
# Could use difflib or external diff tool
|
| 84 |
+
pass
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def edit_profile_field(field_name: str, new_value: str) -> bool:
|
| 88 |
+
"""Edit a specific profile field programmatically.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
field_name: Name of the field to edit
|
| 92 |
+
new_value: New value for the field
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
True if successful, False otherwise
|
| 96 |
+
"""
|
| 97 |
+
if not PROFILE_PATH.exists():
|
| 98 |
+
print(f"β Profile not found at {PROFILE_PATH}")
|
| 99 |
+
return False
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
# Load profile
|
| 103 |
+
profile = load_profile_from_yaml(PROFILE_PATH)
|
| 104 |
+
|
| 105 |
+
# Update field
|
| 106 |
+
if not hasattr(profile, field_name):
|
| 107 |
+
print(f"β Unknown field: {field_name}")
|
| 108 |
+
return False
|
| 109 |
+
|
| 110 |
+
setattr(profile, field_name, new_value)
|
| 111 |
+
|
| 112 |
+
# Save profile
|
| 113 |
+
save_profile_to_yaml(profile, PROFILE_PATH)
|
| 114 |
+
print(f"β
Updated {field_name} to: {new_value}")
|
| 115 |
+
return True
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
print(f"β Failed to update profile: {e}")
|
| 119 |
+
return False
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def validate_after_edit() -> bool:
|
| 123 |
+
"""Validate profile after editing.
|
| 124 |
+
|
| 125 |
+
Returns:
|
| 126 |
+
True if validation passed (no errors), False otherwise
|
| 127 |
+
"""
|
| 128 |
+
from src.profile import load_user_profile
|
| 129 |
+
|
| 130 |
+
print("\nπ Validating profile...")
|
| 131 |
+
try:
|
| 132 |
+
load_user_profile(validate=True)
|
| 133 |
+
print("β
Profile is valid!\n")
|
| 134 |
+
return True
|
| 135 |
+
except ValueError as e:
|
| 136 |
+
print(f"β Validation failed: {e}\n")
|
| 137 |
+
print("π‘ Please fix the errors and try again.")
|
| 138 |
+
return False
|
src/requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
google-adk[eval]>=0.1.0
|
| 2 |
+
google-genai>=0.2.1
|
| 3 |
+
duckduckgo-search>=6.0.0
|
| 4 |
+
python-dotenv>=1.0.0
|
| 5 |
+
requests>=2.31.0
|
| 6 |
+
pyyaml>=6.0
|
| 7 |
+
sqlalchemy>=2.0.0
|
src/session_manager.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Session management utilities for the content generation agent."""
|
| 2 |
+
|
| 3 |
+
import sqlite3
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from src.profile import PROFILE_DIR
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def get_session_db_path() -> Path:
|
| 12 |
+
"""Get the path to the session database.
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
Path to sessions.db
|
| 16 |
+
"""
|
| 17 |
+
return PROFILE_DIR / "sessions.db"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def list_sessions(app_name: str = "scientific-content-agent") -> list[dict[str, Any]]:
|
| 21 |
+
"""List all sessions in the database.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
app_name: Application name to filter sessions
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
List of session dictionaries with metadata
|
| 28 |
+
"""
|
| 29 |
+
db_path = get_session_db_path()
|
| 30 |
+
|
| 31 |
+
if not db_path.exists():
|
| 32 |
+
return []
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
conn = sqlite3.connect(db_path)
|
| 36 |
+
conn.row_factory = sqlite3.Row
|
| 37 |
+
cursor = conn.cursor()
|
| 38 |
+
|
| 39 |
+
# Query sessions table
|
| 40 |
+
# Note: ADK's DatabaseSessionService uses these columns
|
| 41 |
+
query = """
|
| 42 |
+
SELECT
|
| 43 |
+
session_id,
|
| 44 |
+
app_name,
|
| 45 |
+
user_id,
|
| 46 |
+
created_at,
|
| 47 |
+
updated_at
|
| 48 |
+
FROM sessions
|
| 49 |
+
WHERE app_name = ?
|
| 50 |
+
ORDER BY updated_at DESC
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
cursor.execute(query, (app_name,))
|
| 54 |
+
rows = cursor.fetchall()
|
| 55 |
+
|
| 56 |
+
sessions = []
|
| 57 |
+
for row in rows:
|
| 58 |
+
session = {
|
| 59 |
+
"session_id": row["session_id"],
|
| 60 |
+
"app_name": row["app_name"],
|
| 61 |
+
"user_id": row["user_id"],
|
| 62 |
+
"created_at": row["created_at"],
|
| 63 |
+
"updated_at": row["updated_at"],
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
# Count messages in this session
|
| 67 |
+
cursor.execute(
|
| 68 |
+
"""
|
| 69 |
+
SELECT COUNT(*) as count
|
| 70 |
+
FROM messages
|
| 71 |
+
WHERE session_id = ?
|
| 72 |
+
""",
|
| 73 |
+
(row["session_id"],),
|
| 74 |
+
)
|
| 75 |
+
message_row = cursor.fetchone()
|
| 76 |
+
session["message_count"] = message_row["count"] if message_row else 0
|
| 77 |
+
|
| 78 |
+
sessions.append(session)
|
| 79 |
+
|
| 80 |
+
conn.close()
|
| 81 |
+
return sessions
|
| 82 |
+
|
| 83 |
+
except sqlite3.Error as e:
|
| 84 |
+
print(f"Database error: {e}")
|
| 85 |
+
return []
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def delete_session(session_id: str, app_name: str = "scientific-content-agent") -> dict[str, Any]:
|
| 89 |
+
"""Delete a session and its messages.
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
session_id: The session ID to delete
|
| 93 |
+
app_name: Application name for verification
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
Dictionary with status and message
|
| 97 |
+
"""
|
| 98 |
+
db_path = get_session_db_path()
|
| 99 |
+
|
| 100 |
+
if not db_path.exists():
|
| 101 |
+
return {"status": "error", "message": "Session database not found"}
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
conn = sqlite3.connect(db_path)
|
| 105 |
+
cursor = conn.cursor()
|
| 106 |
+
|
| 107 |
+
# Verify session exists and belongs to this app
|
| 108 |
+
cursor.execute(
|
| 109 |
+
"""
|
| 110 |
+
SELECT session_id
|
| 111 |
+
FROM sessions
|
| 112 |
+
WHERE session_id = ? AND app_name = ?
|
| 113 |
+
""",
|
| 114 |
+
(session_id, app_name),
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
if not cursor.fetchone():
|
| 118 |
+
conn.close()
|
| 119 |
+
return {"status": "error", "message": f"Session '{session_id}' not found"}
|
| 120 |
+
|
| 121 |
+
# Delete messages first (foreign key constraint)
|
| 122 |
+
cursor.execute("DELETE FROM messages WHERE session_id = ?", (session_id,))
|
| 123 |
+
messages_deleted = cursor.rowcount
|
| 124 |
+
|
| 125 |
+
# Delete session
|
| 126 |
+
cursor.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
|
| 127 |
+
session_deleted = cursor.rowcount
|
| 128 |
+
|
| 129 |
+
conn.commit()
|
| 130 |
+
conn.close()
|
| 131 |
+
|
| 132 |
+
if session_deleted > 0:
|
| 133 |
+
return {
|
| 134 |
+
"status": "success",
|
| 135 |
+
"message": f"Deleted session '{session_id}' and {messages_deleted} message(s)",
|
| 136 |
+
}
|
| 137 |
+
return {"status": "error", "message": "Failed to delete session"}
|
| 138 |
+
|
| 139 |
+
except sqlite3.Error as e:
|
| 140 |
+
return {"status": "error", "message": f"Database error: {str(e)}"}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def get_session_info(
|
| 144 |
+
session_id: str, app_name: str = "scientific-content-agent"
|
| 145 |
+
) -> dict[str, Any] | None:
|
| 146 |
+
"""Get detailed information about a specific session.
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
session_id: The session ID to query
|
| 150 |
+
app_name: Application name for verification
|
| 151 |
+
|
| 152 |
+
Returns:
|
| 153 |
+
Dictionary with session details or None if not found
|
| 154 |
+
"""
|
| 155 |
+
db_path = get_session_db_path()
|
| 156 |
+
|
| 157 |
+
if not db_path.exists():
|
| 158 |
+
return None
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
conn = sqlite3.connect(db_path)
|
| 162 |
+
conn.row_factory = sqlite3.Row
|
| 163 |
+
cursor = conn.cursor()
|
| 164 |
+
|
| 165 |
+
# Get session info
|
| 166 |
+
cursor.execute(
|
| 167 |
+
"""
|
| 168 |
+
SELECT
|
| 169 |
+
session_id,
|
| 170 |
+
app_name,
|
| 171 |
+
user_id,
|
| 172 |
+
created_at,
|
| 173 |
+
updated_at
|
| 174 |
+
FROM sessions
|
| 175 |
+
WHERE session_id = ? AND app_name = ?
|
| 176 |
+
""",
|
| 177 |
+
(session_id, app_name),
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
row = cursor.fetchone()
|
| 181 |
+
if not row:
|
| 182 |
+
conn.close()
|
| 183 |
+
return None
|
| 184 |
+
|
| 185 |
+
session = dict(row)
|
| 186 |
+
|
| 187 |
+
# Get messages
|
| 188 |
+
cursor.execute(
|
| 189 |
+
"""
|
| 190 |
+
SELECT
|
| 191 |
+
content,
|
| 192 |
+
role,
|
| 193 |
+
created_at
|
| 194 |
+
FROM messages
|
| 195 |
+
WHERE session_id = ?
|
| 196 |
+
ORDER BY created_at ASC
|
| 197 |
+
""",
|
| 198 |
+
(session_id,),
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
messages = [dict(msg) for msg in cursor.fetchall()]
|
| 202 |
+
session["messages"] = messages
|
| 203 |
+
session["message_count"] = len(messages)
|
| 204 |
+
|
| 205 |
+
conn.close()
|
| 206 |
+
return session
|
| 207 |
+
|
| 208 |
+
except sqlite3.Error as e:
|
| 209 |
+
print(f"Database error: {e}")
|
| 210 |
+
return None
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def format_session_list(sessions: list[dict[str, Any]]) -> str:
|
| 214 |
+
"""Format sessions list as a pretty table.
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
sessions: List of session dictionaries
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
Formatted string table
|
| 221 |
+
"""
|
| 222 |
+
if not sessions:
|
| 223 |
+
return "No sessions found."
|
| 224 |
+
|
| 225 |
+
# Calculate column widths
|
| 226 |
+
max_user_len = max((len(s.get("user_id", "")) for s in sessions), default=10)
|
| 227 |
+
max_user_len = max(max_user_len, 10) # Minimum width
|
| 228 |
+
|
| 229 |
+
output = []
|
| 230 |
+
output.append("\n" + "=" * 100)
|
| 231 |
+
output.append(
|
| 232 |
+
f"{'Session ID':<40} {'User':<{max_user_len}} {'Messages':<10} {'Last Updated':<20}"
|
| 233 |
+
)
|
| 234 |
+
output.append("=" * 100)
|
| 235 |
+
|
| 236 |
+
for session in sessions:
|
| 237 |
+
session_id = session["session_id"][:37] + "..." # Truncate long UUIDs
|
| 238 |
+
user_id = session.get("user_id", "Unknown")[:max_user_len]
|
| 239 |
+
message_count = str(session.get("message_count", 0))
|
| 240 |
+
updated_at = session.get("updated_at", "Unknown")
|
| 241 |
+
|
| 242 |
+
# Parse timestamp if it's in ISO format
|
| 243 |
+
try:
|
| 244 |
+
if "T" in updated_at:
|
| 245 |
+
dt = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
|
| 246 |
+
updated_at = dt.strftime("%Y-%m-%d %H:%M:%S")
|
| 247 |
+
except (ValueError, AttributeError):
|
| 248 |
+
pass
|
| 249 |
+
|
| 250 |
+
output.append(
|
| 251 |
+
f"{session_id:<40} {user_id:<{max_user_len}} {message_count:<10} {updated_at:<20}"
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
output.append("=" * 100 + "\n")
|
| 255 |
+
|
| 256 |
+
return "\n".join(output)
|
src/tools.py
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Custom tools for the content generation agent system."""
|
| 2 |
+
|
| 3 |
+
import xml.etree.ElementTree as ET
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
import requests
|
| 7 |
+
from duckduckgo_search import DDGS
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def search_papers(topic: str, max_results: int = 5) -> dict[str, Any]:
|
| 11 |
+
"""Search for academic papers and research articles on a given topic.
|
| 12 |
+
|
| 13 |
+
This tool searches for recent academic papers, research articles, and
|
| 14 |
+
scientific publications related to the specified topic. It provides
|
| 15 |
+
summaries and links to help build credible, research-backed content.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
topic: The research topic or subject to search for (e.g., "machine learning interpretability")
|
| 19 |
+
max_results: Maximum number of papers to return (default: 5)
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
A dictionary containing:
|
| 23 |
+
- status: "success" or "error"
|
| 24 |
+
- papers: List of paper dictionaries with title, authors, summary, link
|
| 25 |
+
- error_message: Error description if status is "error"
|
| 26 |
+
"""
|
| 27 |
+
try:
|
| 28 |
+
# Use arXiv API for academic papers
|
| 29 |
+
# Format: http://export.arxiv.org/api/query?search_query=all:{topic}&max_results={max_results}
|
| 30 |
+
base_url = "http://export.arxiv.org/api/query"
|
| 31 |
+
params = {
|
| 32 |
+
"search_query": f"all:{topic}",
|
| 33 |
+
"max_results": max_results,
|
| 34 |
+
"sortBy": "submittedDate",
|
| 35 |
+
"sortOrder": "descending",
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
response = requests.get(base_url, params=params, timeout=10)
|
| 39 |
+
response.raise_for_status()
|
| 40 |
+
|
| 41 |
+
# Parse XML response using proper XML parser
|
| 42 |
+
# Design decision: We use ElementTree instead of string parsing for robustness
|
| 43 |
+
# and proper handling of XML namespaces, encoding, and malformed entries.
|
| 44 |
+
try:
|
| 45 |
+
root = ET.fromstring(response.content)
|
| 46 |
+
except ET.ParseError as e:
|
| 47 |
+
return {
|
| 48 |
+
"status": "error",
|
| 49 |
+
"error_message": f"Failed to parse arXiv XML response: {str(e)}",
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
# arXiv API uses Atom namespace
|
| 53 |
+
namespace = {"atom": "http://www.w3.org/2005/Atom"}
|
| 54 |
+
|
| 55 |
+
# Extract papers from XML entries
|
| 56 |
+
papers = []
|
| 57 |
+
entries = root.findall("atom:entry", namespace)
|
| 58 |
+
|
| 59 |
+
for entry in entries[:max_results]:
|
| 60 |
+
try:
|
| 61 |
+
# Extract title (remove extra whitespace and newlines)
|
| 62 |
+
title_elem = entry.find("atom:title", namespace)
|
| 63 |
+
title = (
|
| 64 |
+
" ".join(title_elem.text.strip().split())
|
| 65 |
+
if title_elem is not None
|
| 66 |
+
else "Untitled"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# Extract summary (limit to 300 chars for readability)
|
| 70 |
+
summary_elem = entry.find("atom:summary", namespace)
|
| 71 |
+
if summary_elem is not None:
|
| 72 |
+
summary = " ".join(summary_elem.text.strip().split())
|
| 73 |
+
summary = summary[:300] + ("..." if len(summary) > 300 else "")
|
| 74 |
+
else:
|
| 75 |
+
summary = "No summary available"
|
| 76 |
+
|
| 77 |
+
# Extract paper ID/link
|
| 78 |
+
id_elem = entry.find("atom:id", namespace)
|
| 79 |
+
link = id_elem.text.strip() if id_elem is not None else ""
|
| 80 |
+
|
| 81 |
+
# Extract authors (first 3 authors for brevity)
|
| 82 |
+
authors = []
|
| 83 |
+
author_elems = entry.findall("atom:author", namespace)
|
| 84 |
+
for author_elem in author_elems[:3]:
|
| 85 |
+
name_elem = author_elem.find("atom:name", namespace)
|
| 86 |
+
if name_elem is not None:
|
| 87 |
+
authors.append(name_elem.text.strip())
|
| 88 |
+
|
| 89 |
+
papers.append(
|
| 90 |
+
{
|
| 91 |
+
"title": title,
|
| 92 |
+
"authors": ", ".join(authors) if authors else "Unknown",
|
| 93 |
+
"summary": summary,
|
| 94 |
+
"link": link,
|
| 95 |
+
}
|
| 96 |
+
)
|
| 97 |
+
except Exception:
|
| 98 |
+
# Skip malformed entries but continue processing
|
| 99 |
+
continue
|
| 100 |
+
|
| 101 |
+
if not papers:
|
| 102 |
+
return {"status": "error", "error_message": f"No papers found for topic: {topic}"}
|
| 103 |
+
|
| 104 |
+
return {"status": "success", "papers": papers, "count": len(papers)}
|
| 105 |
+
|
| 106 |
+
except requests.RequestException as e:
|
| 107 |
+
return {"status": "error", "error_message": f"Failed to search papers: {str(e)}"}
|
| 108 |
+
except Exception as e:
|
| 109 |
+
return {"status": "error", "error_message": f"Unexpected error: {str(e)}"}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def search_web(query: str, max_results: int = 5) -> dict[str, Any]:
|
| 113 |
+
"""Search the web for information using DuckDuckGo.
|
| 114 |
+
|
| 115 |
+
Use this tool to find:
|
| 116 |
+
- Recent news and industry trends
|
| 117 |
+
- Blog posts and technical articles
|
| 118 |
+
- Company information and market data
|
| 119 |
+
- Real-world examples and case studies
|
| 120 |
+
|
| 121 |
+
Args:
|
| 122 |
+
query: The search query
|
| 123 |
+
max_results: Maximum number of results to return (default: 5)
|
| 124 |
+
|
| 125 |
+
Returns:
|
| 126 |
+
A dictionary containing:
|
| 127 |
+
- status: "success" or "error"
|
| 128 |
+
- results: List of search results (title, link, snippet)
|
| 129 |
+
- error_message: Error description if status is "error"
|
| 130 |
+
"""
|
| 131 |
+
try:
|
| 132 |
+
with DDGS() as ddgs:
|
| 133 |
+
results = list(ddgs.text(query, max_results=max_results))
|
| 134 |
+
|
| 135 |
+
if not results:
|
| 136 |
+
return {"status": "success", "results": [], "count": 0}
|
| 137 |
+
|
| 138 |
+
formatted_results = []
|
| 139 |
+
for r in results:
|
| 140 |
+
formatted_results.append(
|
| 141 |
+
{
|
| 142 |
+
"title": r.get("title", ""),
|
| 143 |
+
"link": r.get("href", ""),
|
| 144 |
+
"snippet": r.get("body", ""),
|
| 145 |
+
}
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
return {"status": "success", "results": formatted_results, "count": len(formatted_results)}
|
| 149 |
+
|
| 150 |
+
except Exception as e:
|
| 151 |
+
return {"status": "error", "error_message": f"Web search error: {str(e)}"}
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def format_for_platform(content: str, platform: str, topic: str = "") -> dict[str, Any]:
|
| 155 |
+
"""Format content appropriately for different social media platforms.
|
| 156 |
+
|
| 157 |
+
Adjusts content length, structure, and style based on platform requirements:
|
| 158 |
+
- Blog: Long-form, structured with headings (1000-2000 words)
|
| 159 |
+
- LinkedIn: Professional, medium-length with key takeaways (300-800 words)
|
| 160 |
+
- Twitter: Concise thread format, engaging hooks (280 chars per tweet)
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
content: The raw content to format
|
| 164 |
+
platform: Target platform ("blog", "linkedin", or "twitter")
|
| 165 |
+
topic: Optional topic for context (used for hashtags, etc.)
|
| 166 |
+
|
| 167 |
+
Returns:
|
| 168 |
+
A dictionary containing:
|
| 169 |
+
- status: "success" or "error"
|
| 170 |
+
- formatted_content: Platform-optimized content
|
| 171 |
+
- metadata: Platform-specific metadata (hashtags, structure, etc.)
|
| 172 |
+
- error_message: Error description if status is "error"
|
| 173 |
+
"""
|
| 174 |
+
try:
|
| 175 |
+
platform = platform.lower()
|
| 176 |
+
|
| 177 |
+
if platform not in ["blog", "linkedin", "twitter"]:
|
| 178 |
+
return {
|
| 179 |
+
"status": "error",
|
| 180 |
+
"error_message": f"Unsupported platform: {platform}. Use 'blog', 'linkedin', or 'twitter'.",
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
metadata = {}
|
| 184 |
+
|
| 185 |
+
if platform == "blog":
|
| 186 |
+
# Blog: Add structure with markdown
|
| 187 |
+
metadata = {
|
| 188 |
+
"format": "markdown",
|
| 189 |
+
"target_length": "1000-2000 words",
|
| 190 |
+
"structure": "Title β Introduction β Main sections with H2/H3 β Conclusion β References",
|
| 191 |
+
}
|
| 192 |
+
formatted = f"""# {topic if topic else "Article Title"}
|
| 193 |
+
|
| 194 |
+
{content}
|
| 195 |
+
|
| 196 |
+
## References
|
| 197 |
+
[Add citations here]
|
| 198 |
+
"""
|
| 199 |
+
|
| 200 |
+
elif platform == "linkedin":
|
| 201 |
+
# LinkedIn: Professional tone with emojis and key takeaways
|
| 202 |
+
metadata = {
|
| 203 |
+
"format": "plain text with limited formatting",
|
| 204 |
+
"target_length": "300-800 words",
|
| 205 |
+
"best_practices": "Start with hook, use line breaks, end with call-to-action",
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
# Add structure
|
| 209 |
+
formatted = f"""π¬ {topic if topic else "Professional Insight"}
|
| 210 |
+
|
| 211 |
+
{content}
|
| 212 |
+
|
| 213 |
+
π‘ Key Takeaways:
|
| 214 |
+
[Summarize 3-5 bullet points]
|
| 215 |
+
|
| 216 |
+
What are your thoughts? Share in the comments below! π
|
| 217 |
+
|
| 218 |
+
#Research #Science #Innovation
|
| 219 |
+
"""
|
| 220 |
+
|
| 221 |
+
elif platform == "twitter":
|
| 222 |
+
# Twitter: Break into thread
|
| 223 |
+
metadata = {
|
| 224 |
+
"format": "thread (multiple tweets)",
|
| 225 |
+
"target_length": "280 characters per tweet",
|
| 226 |
+
"best_practices": "Number tweets (1/n), use hooks, add relevant hashtags",
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
# Basic thread structure
|
| 230 |
+
formatted = f"""π§΅ Thread: {topic if topic else "Key Insights"}
|
| 231 |
+
|
| 232 |
+
1/π§΅ {content[:250]}...
|
| 233 |
+
|
| 234 |
+
[Continue thread - AI will expand this into full thread]
|
| 235 |
+
|
| 236 |
+
#Research #Science
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
return {
|
| 240 |
+
"status": "success",
|
| 241 |
+
"formatted_content": formatted,
|
| 242 |
+
"platform": platform,
|
| 243 |
+
"metadata": metadata,
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
except Exception as e:
|
| 247 |
+
return {"status": "error", "error_message": f"Formatting error: {str(e)}"}
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def generate_citations(sources: list[dict[str, str]], style: str = "apa") -> dict[str, Any]:
|
| 251 |
+
"""Generate properly formatted citations from source information.
|
| 252 |
+
|
| 253 |
+
Creates academic-style citations from paper/article metadata to ensure
|
| 254 |
+
content credibility and proper attribution.
|
| 255 |
+
|
| 256 |
+
Args:
|
| 257 |
+
sources: List of source dictionaries with keys: title, authors, link, year (optional)
|
| 258 |
+
style: Citation style ("apa", "mla", or "chicago") - default is "apa"
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
A dictionary containing:
|
| 262 |
+
- status: "success" or "error"
|
| 263 |
+
- citations: List of formatted citation strings
|
| 264 |
+
- inline_format: Example of how to cite inline
|
| 265 |
+
- error_message: Error description if status is "error"
|
| 266 |
+
"""
|
| 267 |
+
try:
|
| 268 |
+
if not sources:
|
| 269 |
+
return {"status": "error", "error_message": "No sources provided for citation"}
|
| 270 |
+
|
| 271 |
+
style = style.lower()
|
| 272 |
+
if style not in ["apa", "mla", "chicago"]:
|
| 273 |
+
style = "apa" # Default to APA
|
| 274 |
+
|
| 275 |
+
citations = []
|
| 276 |
+
|
| 277 |
+
for i, source in enumerate(sources, 1):
|
| 278 |
+
title = source.get("title", "Untitled")
|
| 279 |
+
authors = source.get("authors", "Unknown")
|
| 280 |
+
link = source.get("link", "")
|
| 281 |
+
year = source.get("year", "n.d.")
|
| 282 |
+
|
| 283 |
+
if style == "apa":
|
| 284 |
+
# APA: Authors (Year). Title. Retrieved from URL
|
| 285 |
+
citation = f"{authors} ({year}). {title}. {link}"
|
| 286 |
+
elif style == "mla":
|
| 287 |
+
# MLA: Authors. "Title." Web. URL
|
| 288 |
+
citation = f'{authors}. "{title}." Web. {link}'
|
| 289 |
+
else: # chicago
|
| 290 |
+
# Chicago: Authors. "Title." Accessed URL
|
| 291 |
+
citation = f'{authors}. "{title}." {link}'
|
| 292 |
+
|
| 293 |
+
citations.append(f"[{i}] {citation}")
|
| 294 |
+
|
| 295 |
+
inline_format = {"apa": "(Author, Year)", "mla": "(Author)", "chicago": "(Author Year)"}
|
| 296 |
+
|
| 297 |
+
return {
|
| 298 |
+
"status": "success",
|
| 299 |
+
"citations": citations,
|
| 300 |
+
"style": style,
|
| 301 |
+
"inline_format": inline_format.get(style, "(Author, Year)"),
|
| 302 |
+
"count": len(citations),
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
except Exception as e:
|
| 306 |
+
return {"status": "error", "error_message": f"Citation generation error: {str(e)}"}
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def extract_key_findings(research_text: str, max_findings: int = 5) -> dict[str, Any]:
|
| 310 |
+
"""Extract key findings and insights from research text.
|
| 311 |
+
|
| 312 |
+
Parses research summaries to identify the most important findings,
|
| 313 |
+
conclusions, and actionable insights for content creation.
|
| 314 |
+
|
| 315 |
+
Args:
|
| 316 |
+
research_text: Raw research text to analyze
|
| 317 |
+
max_findings: Maximum number of key findings to extract (default: 5)
|
| 318 |
+
|
| 319 |
+
Returns:
|
| 320 |
+
A dictionary containing:
|
| 321 |
+
- status: "success" or "error"
|
| 322 |
+
- findings: List of key finding strings
|
| 323 |
+
- summary: Brief overall summary
|
| 324 |
+
- error_message: Error description if status is "error"
|
| 325 |
+
"""
|
| 326 |
+
try:
|
| 327 |
+
if not research_text or len(research_text.strip()) < 50:
|
| 328 |
+
return {"status": "error", "error_message": "Insufficient research text provided"}
|
| 329 |
+
|
| 330 |
+
# Simple keyword-based extraction (in production, use NLP/LLM)
|
| 331 |
+
sentences = research_text.replace("\n", " ").split(". ")
|
| 332 |
+
|
| 333 |
+
# Look for sentences with key indicator words
|
| 334 |
+
indicators = [
|
| 335 |
+
"found",
|
| 336 |
+
"discovered",
|
| 337 |
+
"showed",
|
| 338 |
+
"demonstrated",
|
| 339 |
+
"revealed",
|
| 340 |
+
"concluded",
|
| 341 |
+
"suggests",
|
| 342 |
+
"indicates",
|
| 343 |
+
"proves",
|
| 344 |
+
"confirms",
|
| 345 |
+
"important",
|
| 346 |
+
"significant",
|
| 347 |
+
"key",
|
| 348 |
+
"main",
|
| 349 |
+
"primary",
|
| 350 |
+
]
|
| 351 |
+
|
| 352 |
+
findings = []
|
| 353 |
+
for sentence in sentences:
|
| 354 |
+
sentence = sentence.strip()
|
| 355 |
+
if any(indicator in sentence.lower() for indicator in indicators):
|
| 356 |
+
findings.append(sentence if sentence.endswith(".") else sentence + ".")
|
| 357 |
+
if len(findings) >= max_findings:
|
| 358 |
+
break
|
| 359 |
+
|
| 360 |
+
# If not enough findings, take first few substantial sentences
|
| 361 |
+
if len(findings) < max_findings:
|
| 362 |
+
for sentence in sentences:
|
| 363 |
+
sentence = sentence.strip()
|
| 364 |
+
if len(sentence) > 30 and sentence not in findings:
|
| 365 |
+
findings.append(sentence if sentence.endswith(".") else sentence + ".")
|
| 366 |
+
if len(findings) >= max_findings:
|
| 367 |
+
break
|
| 368 |
+
|
| 369 |
+
summary = f"Analysis of research text identified {len(findings)} key findings and insights."
|
| 370 |
+
|
| 371 |
+
return {
|
| 372 |
+
"status": "success",
|
| 373 |
+
"findings": findings[:max_findings],
|
| 374 |
+
"summary": summary,
|
| 375 |
+
"count": len(findings[:max_findings]),
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
except Exception as e:
|
| 379 |
+
return {"status": "error", "error_message": f"Key finding extraction error: {str(e)}"}
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def search_industry_trends(
|
| 383 |
+
field: str, region: str = "global", max_results: int = 5
|
| 384 |
+
) -> dict[str, Any]:
|
| 385 |
+
"""Search for industry trends, job market demands, and hiring patterns in AI/ML.
|
| 386 |
+
|
| 387 |
+
Identifies what companies are looking for, hot skills in demand, and
|
| 388 |
+
industry pain points that professionals can address. Useful for aligning
|
| 389 |
+
content with market opportunities.
|
| 390 |
+
|
| 391 |
+
Args:
|
| 392 |
+
field: The AI/ML field to analyze (e.g., "Machine Learning", "NLP", "Computer Vision")
|
| 393 |
+
region: Geographic region for job market analysis (default: "global")
|
| 394 |
+
max_results: Maximum number of trends to return (default: 5)
|
| 395 |
+
|
| 396 |
+
Returns:
|
| 397 |
+
A dictionary containing:
|
| 398 |
+
- status: "success" or "error"
|
| 399 |
+
- trends: List of current industry trends and demands
|
| 400 |
+
- hot_skills: Technologies/frameworks in high demand
|
| 401 |
+
- pain_points: Common business challenges to address
|
| 402 |
+
- error_message: Error description if status is "error"
|
| 403 |
+
"""
|
| 404 |
+
try:
|
| 405 |
+
# Use search_web to find real trends
|
| 406 |
+
search_query = f"latest trends in {field} {region} {2024}"
|
| 407 |
+
|
| 408 |
+
# We'll use the newly created search_web function
|
| 409 |
+
# Note: In a real circular dependency scenario, we might need to handle imports differently,
|
| 410 |
+
# but here they are in the same file.
|
| 411 |
+
search_results = search_web(search_query, max_results=max_results)
|
| 412 |
+
|
| 413 |
+
if search_results.get("status") == "error":
|
| 414 |
+
return search_results
|
| 415 |
+
|
| 416 |
+
results = search_results.get("results", [])
|
| 417 |
+
|
| 418 |
+
trends = []
|
| 419 |
+
for r in results:
|
| 420 |
+
trends.append(f"{r['title']}: {r['snippet']}")
|
| 421 |
+
|
| 422 |
+
if not trends:
|
| 423 |
+
# Fallback if search fails to return good results
|
| 424 |
+
trends = [
|
| 425 |
+
f"Growing demand for {field} expertise in {region}",
|
| 426 |
+
f"Companies seeking production-ready {field} solutions",
|
| 427 |
+
"Emphasis on practical implementation over pure research",
|
| 428 |
+
]
|
| 429 |
+
|
| 430 |
+
# Basic skill mapping is still useful as a baseline
|
| 431 |
+
skill_mapping = {
|
| 432 |
+
"machine learning": ["PyTorch", "TensorFlow", "Scikit-learn", "MLflow", "Kubeflow"],
|
| 433 |
+
"nlp": ["Transformers", "LangChain", "OpenAI API", "HuggingFace", "spaCy"],
|
| 434 |
+
"computer vision": ["OpenCV", "YOLO", "SAM", "Detectron2", "PIL"],
|
| 435 |
+
"llm": ["LangChain", "LlamaIndex", "Vector Databases", "Prompt Engineering", "RAG"],
|
| 436 |
+
"mlops": ["MLflow", "Kubeflow", "Docker", "Kubernetes", "AWS SageMaker"],
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
field_lower = field.lower()
|
| 440 |
+
hot_skills = []
|
| 441 |
+
for key in skill_mapping:
|
| 442 |
+
if key in field_lower:
|
| 443 |
+
hot_skills.extend(skill_mapping[key][:3])
|
| 444 |
+
|
| 445 |
+
if not hot_skills:
|
| 446 |
+
hot_skills = ["Python", "PyTorch", "Cloud Platforms", "API Development"]
|
| 447 |
+
|
| 448 |
+
pain_points = [
|
| 449 |
+
f"Difficulty finding experienced {field} professionals",
|
| 450 |
+
f"Bridging gap between research papers and production code in {field}",
|
| 451 |
+
f"Scaling {field} solutions from prototype to enterprise",
|
| 452 |
+
f"Explaining ROI of {field} investments to executives",
|
| 453 |
+
f"Maintaining and monitoring {field} systems in production",
|
| 454 |
+
]
|
| 455 |
+
|
| 456 |
+
return {
|
| 457 |
+
"status": "success",
|
| 458 |
+
"trends": trends[:max_results],
|
| 459 |
+
"hot_skills": list(set(hot_skills)),
|
| 460 |
+
"pain_points": pain_points[:max_results],
|
| 461 |
+
"region": region,
|
| 462 |
+
"field": field,
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
except Exception as e:
|
| 466 |
+
return {"status": "error", "error_message": f"Industry trends search error: {str(e)}"}
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def generate_seo_keywords(topic: str, role: str = "AI Consultant") -> dict[str, Any]:
|
| 470 |
+
"""Generate LinkedIn SEO keywords that recruiters search for.
|
| 471 |
+
|
| 472 |
+
Creates role-specific keywords and technology terms that improve
|
| 473 |
+
visibility in recruiter searches and LinkedIn's algorithm.
|
| 474 |
+
|
| 475 |
+
Args:
|
| 476 |
+
topic: The content topic or expertise area
|
| 477 |
+
role: Target professional role (e.g., "AI Consultant", "ML Engineer")
|
| 478 |
+
|
| 479 |
+
Returns:
|
| 480 |
+
A dictionary containing:
|
| 481 |
+
- status: "success" or "error"
|
| 482 |
+
- primary_keywords: Main role-based keywords
|
| 483 |
+
- technical_keywords: Technology and framework terms
|
| 484 |
+
- action_keywords: Skill-based action verbs
|
| 485 |
+
- combined_phrases: Optimized keyword combinations
|
| 486 |
+
- error_message: Error description if status is "error"
|
| 487 |
+
"""
|
| 488 |
+
try:
|
| 489 |
+
# Role-based keywords
|
| 490 |
+
role_keywords = {
|
| 491 |
+
"consultant": ["AI Consultant", "ML Consultant", "AI Strategy", "Technical Advisor"],
|
| 492 |
+
"engineer": ["ML Engineer", "AI Engineer", "Machine Learning Engineer"],
|
| 493 |
+
"specialist": ["AI Specialist", "ML Specialist", "Data Science Specialist"],
|
| 494 |
+
"expert": ["AI Expert", "ML Expert", "Subject Matter Expert"],
|
| 495 |
+
"architect": ["AI Architect", "ML Architect", "Solutions Architect"],
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
role_lower = role.lower()
|
| 499 |
+
primary_keywords = [role]
|
| 500 |
+
for key in role_keywords:
|
| 501 |
+
if key in role_lower:
|
| 502 |
+
primary_keywords.extend(role_keywords[key][:2])
|
| 503 |
+
|
| 504 |
+
# Technical keywords based on topic
|
| 505 |
+
technical_keywords = []
|
| 506 |
+
topic_lower = topic.lower()
|
| 507 |
+
|
| 508 |
+
tech_mapping = {
|
| 509 |
+
"language": ["NLP", "LLM", "Transformers", "GPT", "BERT"],
|
| 510 |
+
"vision": ["Computer Vision", "CNN", "Object Detection", "Image Recognition"],
|
| 511 |
+
"learning": ["Deep Learning", "Neural Networks", "PyTorch", "TensorFlow"],
|
| 512 |
+
"agent": ["AI Agents", "Multi-Agent Systems", "LangChain", "Autonomous Systems"],
|
| 513 |
+
"data": ["Data Science", "Feature Engineering", "Model Training"],
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
for key in tech_mapping:
|
| 517 |
+
if key in topic_lower:
|
| 518 |
+
technical_keywords.extend(tech_mapping[key][:3])
|
| 519 |
+
|
| 520 |
+
if not technical_keywords:
|
| 521 |
+
technical_keywords = ["Machine Learning", "Artificial Intelligence", "Python"]
|
| 522 |
+
|
| 523 |
+
# Action keywords (skills)
|
| 524 |
+
action_keywords = [
|
| 525 |
+
"AI Development",
|
| 526 |
+
"Model Deployment",
|
| 527 |
+
"MLOps",
|
| 528 |
+
"Production ML",
|
| 529 |
+
"Algorithm Design",
|
| 530 |
+
"Technical Leadership",
|
| 531 |
+
"AI Strategy",
|
| 532 |
+
]
|
| 533 |
+
|
| 534 |
+
# Combined optimized phrases
|
| 535 |
+
combined_phrases = [
|
| 536 |
+
f"{primary_keywords[0]} | {technical_keywords[0]}",
|
| 537 |
+
f"Expert in {technical_keywords[0]} and {technical_keywords[1] if len(technical_keywords) > 1 else 'ML'}",
|
| 538 |
+
f"{action_keywords[0]} | {action_keywords[1]}",
|
| 539 |
+
]
|
| 540 |
+
|
| 541 |
+
return {
|
| 542 |
+
"status": "success",
|
| 543 |
+
"primary_keywords": list(set(primary_keywords))[:5],
|
| 544 |
+
"technical_keywords": list(set(technical_keywords))[:5],
|
| 545 |
+
"action_keywords": action_keywords[:5],
|
| 546 |
+
"combined_phrases": combined_phrases,
|
| 547 |
+
"total_keywords": len(set(primary_keywords + technical_keywords + action_keywords)),
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
except Exception as e:
|
| 551 |
+
return {"status": "error", "error_message": f"SEO keyword generation error: {str(e)}"}
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
def create_engagement_hooks(topic: str, goal: str = "opportunities") -> dict[str, Any]:
|
| 555 |
+
"""Create engagement hooks that invite professional connections and opportunities.
|
| 556 |
+
|
| 557 |
+
Generates calls-to-action, questions, and portfolio mentions that
|
| 558 |
+
encourage recruiters and potential clients to connect.
|
| 559 |
+
|
| 560 |
+
Args:
|
| 561 |
+
topic: The content topic
|
| 562 |
+
goal: Content goal ("opportunities", "discussion", "credibility", "visibility")
|
| 563 |
+
|
| 564 |
+
Returns:
|
| 565 |
+
A dictionary containing:
|
| 566 |
+
- status: "success" or "error"
|
| 567 |
+
- opening_hooks: Attention-grabbing opening lines
|
| 568 |
+
- closing_ctas: Strong calls-to-action
|
| 569 |
+
- discussion_questions: Questions that spark engagement
|
| 570 |
+
- portfolio_prompts: Ways to mention your work
|
| 571 |
+
- error_message: Error description if status is "error"
|
| 572 |
+
"""
|
| 573 |
+
try:
|
| 574 |
+
goal = goal.lower()
|
| 575 |
+
|
| 576 |
+
# Opening hooks based on goal
|
| 577 |
+
opening_hooks = {
|
| 578 |
+
"opportunities": [
|
| 579 |
+
f"Working with companies on {topic}? Here's what I've learned...",
|
| 580 |
+
f"After implementing {topic} for multiple clients, one thing is clear:",
|
| 581 |
+
f"Most {topic} projects fail because of this one mistake:",
|
| 582 |
+
],
|
| 583 |
+
"discussion": [
|
| 584 |
+
f"Hot take on {topic}:",
|
| 585 |
+
f"Here's what nobody tells you about {topic}:",
|
| 586 |
+
f"The {topic} landscape just shifted. Here's why it matters:",
|
| 587 |
+
],
|
| 588 |
+
"credibility": [
|
| 589 |
+
f"Deep dive into {topic} based on hands-on experience:",
|
| 590 |
+
f"Technical breakdown of {topic} that actually works in production:",
|
| 591 |
+
f"What I learned implementing {topic} at scale:",
|
| 592 |
+
],
|
| 593 |
+
"visibility": [
|
| 594 |
+
f"π₯ {topic} is evolving faster than ever. Here's what you need to know:",
|
| 595 |
+
f"Everyone's talking about {topic}, but here's what they're missing:",
|
| 596 |
+
f"3 things about {topic} that changed how I work:",
|
| 597 |
+
],
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
# Closing CTAs based on goal
|
| 601 |
+
closing_ctas = {
|
| 602 |
+
"opportunities": [
|
| 603 |
+
"Looking to implement this in your organization? Let's connect and discuss your needs.",
|
| 604 |
+
"Need help with your {topic} project? DM me to explore collaboration.",
|
| 605 |
+
"Building something similar? I'd love to hear about your approach. Drop a comment or message me.",
|
| 606 |
+
],
|
| 607 |
+
"discussion": [
|
| 608 |
+
"What's your take on this? Agree or disagree? Let's discuss in the comments!",
|
| 609 |
+
"Have you encountered this in your work? Share your experience below.",
|
| 610 |
+
"Curious how this applies to your use case? Let's chat!",
|
| 611 |
+
],
|
| 612 |
+
"credibility": [
|
| 613 |
+
"Want to dive deeper into the technical details? Connect with me.",
|
| 614 |
+
"Questions about the implementation? Happy to share insights.",
|
| 615 |
+
"Follow for more technical deep-dives on {topic}.",
|
| 616 |
+
],
|
| 617 |
+
"visibility": [
|
| 618 |
+
"π Follow for more insights on {topic} and AI/ML trends.",
|
| 619 |
+
"π Repost if you found this valuable. Tag someone who needs to see this.",
|
| 620 |
+
"π¬ What would you add to this list? Comment below!",
|
| 621 |
+
],
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
# Discussion questions
|
| 625 |
+
discussion_questions = [
|
| 626 |
+
f"What's been your biggest challenge with {topic}?",
|
| 627 |
+
f"Are you seeing similar trends with {topic} in your industry?",
|
| 628 |
+
f"Which aspect of {topic} should I cover next?",
|
| 629 |
+
f"What's your hot take on the future of {topic}?",
|
| 630 |
+
f"Have you tried implementing {topic}? What were your results?",
|
| 631 |
+
]
|
| 632 |
+
|
| 633 |
+
# Portfolio prompts
|
| 634 |
+
portfolio_prompts = [
|
| 635 |
+
f"In my recent project on {topic}, I discovered...",
|
| 636 |
+
f"While building a {topic} solution, here's what worked:",
|
| 637 |
+
f"My open-source work on {topic} taught me...",
|
| 638 |
+
f"Check out my GitHub for {topic} implementations that...",
|
| 639 |
+
f"Drawing from my Kaggle competition on {topic}...",
|
| 640 |
+
]
|
| 641 |
+
|
| 642 |
+
return {
|
| 643 |
+
"status": "success",
|
| 644 |
+
"opening_hooks": opening_hooks.get(goal, opening_hooks["credibility"])[:3],
|
| 645 |
+
"closing_ctas": [
|
| 646 |
+
cta.replace("{topic}", topic)
|
| 647 |
+
for cta in closing_ctas.get(goal, closing_ctas["opportunities"])[:3]
|
| 648 |
+
],
|
| 649 |
+
"discussion_questions": discussion_questions[:3],
|
| 650 |
+
"portfolio_prompts": portfolio_prompts[:3],
|
| 651 |
+
"goal": goal,
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
except Exception as e:
|
| 655 |
+
return {"status": "error", "error_message": f"Engagement hook creation error: {str(e)}"}
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
def analyze_content_for_opportunities(
|
| 659 |
+
content: str, target_role: str = "AI Consultant"
|
| 660 |
+
) -> dict[str, Any]:
|
| 661 |
+
"""Analyze content for recruiter appeal and opportunity generation potential.
|
| 662 |
+
|
| 663 |
+
Scores content based on factors that attract professional opportunities:
|
| 664 |
+
SEO keywords, engagement hooks, portfolio mentions, and business value.
|
| 665 |
+
|
| 666 |
+
Args:
|
| 667 |
+
content: The content to analyze
|
| 668 |
+
target_role: Target professional role for scoring
|
| 669 |
+
|
| 670 |
+
Returns:
|
| 671 |
+
A dictionary containing:
|
| 672 |
+
- status: "success" or "error"
|
| 673 |
+
- opportunity_score: Overall score (0-100)
|
| 674 |
+
- seo_score: SEO keyword presence (0-100)
|
| 675 |
+
- engagement_score: Engagement hook effectiveness (0-100)
|
| 676 |
+
- value_score: Business value communication (0-100)
|
| 677 |
+
- suggestions: List of improvement suggestions
|
| 678 |
+
- error_message: Error description if status is "error"
|
| 679 |
+
"""
|
| 680 |
+
try:
|
| 681 |
+
if not content or len(content) < 100:
|
| 682 |
+
return {
|
| 683 |
+
"status": "error",
|
| 684 |
+
"error_message": "Content too short for meaningful analysis (minimum 100 characters)",
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
content_lower = content.lower()
|
| 688 |
+
|
| 689 |
+
# SEO keyword scoring
|
| 690 |
+
# Design decision: We check for both role-based keywords (consultant, engineer)
|
| 691 |
+
# and technical terms (PyTorch, TensorFlow) because recruiters search using both.
|
| 692 |
+
# The multiplier of 200 ensures that hitting ~50% of keywords gives a good score.
|
| 693 |
+
seo_keywords = [
|
| 694 |
+
"ai",
|
| 695 |
+
"machine learning",
|
| 696 |
+
"ml",
|
| 697 |
+
"deep learning",
|
| 698 |
+
"neural network",
|
| 699 |
+
"python",
|
| 700 |
+
"tensorflow",
|
| 701 |
+
"pytorch",
|
| 702 |
+
"consulting",
|
| 703 |
+
"engineer",
|
| 704 |
+
"architect",
|
| 705 |
+
"specialist",
|
| 706 |
+
"expert",
|
| 707 |
+
]
|
| 708 |
+
seo_hits = sum(1 for keyword in seo_keywords if keyword in content_lower)
|
| 709 |
+
seo_score = min(100, (seo_hits / len(seo_keywords)) * 200)
|
| 710 |
+
|
| 711 |
+
# Engagement hooks scoring
|
| 712 |
+
# Design decision: We look for calls-to-action, questions, and invitation words
|
| 713 |
+
# because these are proven to increase LinkedIn engagement and prompt connections.
|
| 714 |
+
# Target of 5 indicators gives 100 score - this is based on LinkedIn best practices.
|
| 715 |
+
engagement_indicators = [
|
| 716 |
+
"?",
|
| 717 |
+
"let's",
|
| 718 |
+
"connect",
|
| 719 |
+
"dm",
|
| 720 |
+
"message",
|
| 721 |
+
"discuss",
|
| 722 |
+
"share",
|
| 723 |
+
"comment",
|
| 724 |
+
"what's your",
|
| 725 |
+
"have you",
|
| 726 |
+
"follow",
|
| 727 |
+
]
|
| 728 |
+
engagement_hits = sum(
|
| 729 |
+
1 for indicator in engagement_indicators if indicator in content_lower
|
| 730 |
+
)
|
| 731 |
+
engagement_score = min(100, (engagement_hits / 5) * 100)
|
| 732 |
+
|
| 733 |
+
# Business value scoring
|
| 734 |
+
# Design decision: Recruiters and clients care about business outcomes, not just tech.
|
| 735 |
+
# We prioritize words that show real-world impact and problem-solving ability.
|
| 736 |
+
# This distinguishes professional content from purely academic content.
|
| 737 |
+
value_indicators = [
|
| 738 |
+
"production",
|
| 739 |
+
"scale",
|
| 740 |
+
"roi",
|
| 741 |
+
"business",
|
| 742 |
+
"solution",
|
| 743 |
+
"impact",
|
| 744 |
+
"results",
|
| 745 |
+
"improve",
|
| 746 |
+
"optimize",
|
| 747 |
+
"problem",
|
| 748 |
+
"challenge",
|
| 749 |
+
]
|
| 750 |
+
value_hits = sum(1 for indicator in value_indicators if indicator in content_lower)
|
| 751 |
+
value_score = min(100, (value_hits / 5) * 100)
|
| 752 |
+
|
| 753 |
+
# Portfolio mention detection
|
| 754 |
+
# Design decision: Mentioning projects demonstrates hands-on experience.
|
| 755 |
+
# This is critical for converting interest into opportunities.
|
| 756 |
+
# We use a lower threshold (3 mentions = 100) since portfolios are mentioned sparingly.
|
| 757 |
+
portfolio_indicators = ["project", "github", "kaggle", "built", "developed", "implemented"]
|
| 758 |
+
portfolio_mentions = sum(
|
| 759 |
+
1 for indicator in portfolio_indicators if indicator in content_lower
|
| 760 |
+
)
|
| 761 |
+
portfolio_score = min(100, (portfolio_mentions / 3) * 100)
|
| 762 |
+
|
| 763 |
+
# Calculate overall opportunity score
|
| 764 |
+
# Design decision: Weighted scoring gives highest priority to SEO and engagement (30% each)
|
| 765 |
+
# because these directly impact visibility and connection rate. Business value (25%) and
|
| 766 |
+
# portfolio (15%) are supporting factors. This weighting was designed based on LinkedIn's
|
| 767 |
+
# algorithm priorities and recruiter behavior patterns.
|
| 768 |
+
opportunity_score = int(
|
| 769 |
+
seo_score * 0.3 + engagement_score * 0.3 + value_score * 0.25 + portfolio_score * 0.15
|
| 770 |
+
)
|
| 771 |
+
|
| 772 |
+
# Generate suggestions
|
| 773 |
+
suggestions = []
|
| 774 |
+
if seo_score < 50:
|
| 775 |
+
suggestions.append(
|
| 776 |
+
f"Add more {target_role} keywords and technical terms for better visibility"
|
| 777 |
+
)
|
| 778 |
+
if engagement_score < 50:
|
| 779 |
+
suggestions.append(
|
| 780 |
+
"Include stronger calls-to-action and questions to invite connections"
|
| 781 |
+
)
|
| 782 |
+
if value_score < 50:
|
| 783 |
+
suggestions.append("Emphasize business value and practical impact over pure theory")
|
| 784 |
+
if portfolio_mentions == 0:
|
| 785 |
+
suggestions.append(
|
| 786 |
+
"Mention your projects or portfolio to demonstrate hands-on expertise"
|
| 787 |
+
)
|
| 788 |
+
if len(content) < 300:
|
| 789 |
+
suggestions.append(
|
| 790 |
+
"Consider expanding content for better engagement (aim for 300+ words)"
|
| 791 |
+
)
|
| 792 |
+
|
| 793 |
+
return {
|
| 794 |
+
"status": "success",
|
| 795 |
+
"opportunity_score": opportunity_score,
|
| 796 |
+
"seo_score": int(seo_score),
|
| 797 |
+
"engagement_score": int(engagement_score),
|
| 798 |
+
"value_score": int(value_score),
|
| 799 |
+
"portfolio_score": int(portfolio_score),
|
| 800 |
+
"suggestions": suggestions
|
| 801 |
+
if suggestions
|
| 802 |
+
else ["Content looks great for opportunities!"],
|
| 803 |
+
"grade": "Excellent"
|
| 804 |
+
if opportunity_score >= 80
|
| 805 |
+
else "Good"
|
| 806 |
+
if opportunity_score >= 60
|
| 807 |
+
else "Needs Improvement",
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
except Exception as e:
|
| 811 |
+
return {"status": "error", "error_message": f"Content analysis error: {str(e)}"}
|
ui_app.py
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gradio web interface for the Scientific Content Generation Agent."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import json
|
| 5 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
from main import run_content_generation
|
| 11 |
+
from src.config import CITATION_STYLE, DEFAULT_MODEL, MAX_PAPERS_PER_SEARCH, SUPPORTED_PLATFORMS
|
| 12 |
+
from src.profile import (
|
| 13 |
+
DEFAULT_PROFILE,
|
| 14 |
+
PROFILE_PATH,
|
| 15 |
+
UserProfile,
|
| 16 |
+
load_user_profile,
|
| 17 |
+
save_profile_to_yaml,
|
| 18 |
+
)
|
| 19 |
+
from src.session_manager import delete_session, get_session_info, list_sessions
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ============================================================================
|
| 23 |
+
# Tab 1: Content Generation
|
| 24 |
+
# ============================================================================
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
async def async_generate_with_progress(
|
| 28 |
+
topic: str,
|
| 29 |
+
platforms: List[str],
|
| 30 |
+
tone: str,
|
| 31 |
+
audience: str,
|
| 32 |
+
session_id: str,
|
| 33 |
+
progress: gr.Progress = gr.Progress(),
|
| 34 |
+
) -> str:
|
| 35 |
+
"""Generate content with progress tracking.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
topic: Research topic
|
| 39 |
+
platforms: List of platforms (Blog, LinkedIn, Twitter)
|
| 40 |
+
tone: Content tone
|
| 41 |
+
audience: Target audience
|
| 42 |
+
session_id: Optional session ID to resume
|
| 43 |
+
progress: Gradio progress tracker
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
Generated content as formatted string
|
| 47 |
+
"""
|
| 48 |
+
try:
|
| 49 |
+
# Validate inputs
|
| 50 |
+
if not topic or not topic.strip():
|
| 51 |
+
return "β Error: Please enter a topic."
|
| 52 |
+
|
| 53 |
+
if not platforms:
|
| 54 |
+
return "β Error: Please select at least one platform."
|
| 55 |
+
|
| 56 |
+
# Convert UI platform names to internal format
|
| 57 |
+
platform_map = {"Blog": "blog", "LinkedIn": "linkedin", "Twitter": "twitter"}
|
| 58 |
+
platforms_internal = [platform_map[p] for p in platforms]
|
| 59 |
+
|
| 60 |
+
# Build preferences
|
| 61 |
+
preferences = {
|
| 62 |
+
"platforms": platforms_internal,
|
| 63 |
+
"tone": tone,
|
| 64 |
+
"target_audience": audience if audience.strip() else "researchers and professionals",
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
# Use session ID if provided
|
| 68 |
+
session = session_id.strip() if session_id and session_id.strip() else None
|
| 69 |
+
|
| 70 |
+
# Progress tracking with fixed milestones
|
| 71 |
+
progress(0.0, desc="π Initializing agent pipeline...")
|
| 72 |
+
await asyncio.sleep(0.5) # Brief pause for UI feedback
|
| 73 |
+
|
| 74 |
+
progress(0.1, desc="π¬ ResearchAgent: Searching academic papers and trends...")
|
| 75 |
+
# Run the actual content generation (2-5 minutes)
|
| 76 |
+
# We can't track real progress without hooking into ADK events, so we'll use milestones
|
| 77 |
+
|
| 78 |
+
# Start the generation in a separate task so we can update progress
|
| 79 |
+
generation_task = asyncio.create_task(run_content_generation(topic, preferences, session))
|
| 80 |
+
|
| 81 |
+
# Simulate progress while generation runs
|
| 82 |
+
# These are approximate milestones based on agent pipeline
|
| 83 |
+
milestones = [
|
| 84 |
+
(0.2, "π― StrategyAgent: Planning content strategy..."),
|
| 85 |
+
(0.4, "βοΈ ContentGeneratorAgent: Creating content..."),
|
| 86 |
+
(0.7, "π LinkedInOptimizationAgent: Optimizing for opportunities..."),
|
| 87 |
+
(0.85, "β
ReviewAgent: Final review and citations..."),
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
# Update progress while waiting for completion
|
| 91 |
+
for milestone_progress, desc in milestones:
|
| 92 |
+
# Check if generation is complete
|
| 93 |
+
if generation_task.done():
|
| 94 |
+
break
|
| 95 |
+
progress(milestone_progress, desc=desc)
|
| 96 |
+
# Wait a bit before next milestone (total ~30 seconds for progress updates)
|
| 97 |
+
await asyncio.sleep(7)
|
| 98 |
+
|
| 99 |
+
# Wait for generation to complete
|
| 100 |
+
result = await generation_task
|
| 101 |
+
|
| 102 |
+
progress(1.0, desc="β
Generation complete!")
|
| 103 |
+
|
| 104 |
+
# Format the result nicely
|
| 105 |
+
if result and isinstance(result, str):
|
| 106 |
+
return f"""# Content Generation Complete! π
|
| 107 |
+
|
| 108 |
+
{result}
|
| 109 |
+
|
| 110 |
+
---
|
| 111 |
+
πΎ Content saved to output directory
|
| 112 |
+
π Session ID: {session or "New session created"}
|
| 113 |
+
"""
|
| 114 |
+
else:
|
| 115 |
+
return "β
Content generation completed. Check the logs for details."
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
error_msg = f"β Error during content generation: {str(e)}"
|
| 119 |
+
print(error_msg)
|
| 120 |
+
import traceback
|
| 121 |
+
|
| 122 |
+
traceback.print_exc()
|
| 123 |
+
return error_msg
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def generate_content_sync(
|
| 127 |
+
topic: str, platforms: List[str], tone: str, audience: str, session_id: str
|
| 128 |
+
) -> str:
|
| 129 |
+
"""Synchronous wrapper for async content generation.
|
| 130 |
+
|
| 131 |
+
This is needed because Gradio requires sync functions unless we use .then() chaining.
|
| 132 |
+
"""
|
| 133 |
+
return asyncio.run(async_generate_with_progress(topic, platforms, tone, audience, session_id))
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# ============================================================================
|
| 137 |
+
# Tab 2: Profile Editor
|
| 138 |
+
# ============================================================================
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def load_profile_ui() -> Tuple:
|
| 142 |
+
"""Load current profile for form population.
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
Tuple of all profile field values in form order
|
| 146 |
+
"""
|
| 147 |
+
try:
|
| 148 |
+
profile = load_user_profile(validate=False)
|
| 149 |
+
return (
|
| 150 |
+
profile.name,
|
| 151 |
+
profile.target_role,
|
| 152 |
+
", ".join(profile.expertise_areas),
|
| 153 |
+
", ".join(profile.content_goals),
|
| 154 |
+
profile.region,
|
| 155 |
+
", ".join(profile.languages),
|
| 156 |
+
", ".join(profile.target_industries),
|
| 157 |
+
profile.github_username,
|
| 158 |
+
profile.linkedin_url,
|
| 159 |
+
profile.portfolio_url,
|
| 160 |
+
profile.kaggle_username,
|
| 161 |
+
json.dumps(profile.notable_projects, indent=2),
|
| 162 |
+
", ".join(profile.primary_skills),
|
| 163 |
+
profile.content_tone,
|
| 164 |
+
profile.use_emojis,
|
| 165 |
+
profile.posting_frequency,
|
| 166 |
+
profile.unique_value_proposition,
|
| 167 |
+
", ".join(profile.key_differentiators),
|
| 168 |
+
"β
Profile loaded successfully!",
|
| 169 |
+
)
|
| 170 |
+
except Exception as e:
|
| 171 |
+
return (
|
| 172 |
+
DEFAULT_PROFILE.name,
|
| 173 |
+
DEFAULT_PROFILE.target_role,
|
| 174 |
+
", ".join(DEFAULT_PROFILE.expertise_areas),
|
| 175 |
+
", ".join(DEFAULT_PROFILE.content_goals),
|
| 176 |
+
DEFAULT_PROFILE.region,
|
| 177 |
+
", ".join(DEFAULT_PROFILE.languages),
|
| 178 |
+
", ".join(DEFAULT_PROFILE.target_industries),
|
| 179 |
+
DEFAULT_PROFILE.github_username,
|
| 180 |
+
DEFAULT_PROFILE.linkedin_url,
|
| 181 |
+
DEFAULT_PROFILE.portfolio_url,
|
| 182 |
+
DEFAULT_PROFILE.kaggle_username,
|
| 183 |
+
json.dumps(DEFAULT_PROFILE.notable_projects, indent=2),
|
| 184 |
+
", ".join(DEFAULT_PROFILE.primary_skills),
|
| 185 |
+
DEFAULT_PROFILE.content_tone,
|
| 186 |
+
DEFAULT_PROFILE.use_emojis,
|
| 187 |
+
DEFAULT_PROFILE.posting_frequency,
|
| 188 |
+
DEFAULT_PROFILE.unique_value_proposition,
|
| 189 |
+
", ".join(DEFAULT_PROFILE.key_differentiators),
|
| 190 |
+
f"β οΈ Error loading profile: {str(e)}. Showing defaults.",
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def validate_profile_ui(
|
| 195 |
+
name: str,
|
| 196 |
+
target_role: str,
|
| 197 |
+
expertise_areas: str,
|
| 198 |
+
content_goals: str,
|
| 199 |
+
region: str,
|
| 200 |
+
languages: str,
|
| 201 |
+
target_industries: str,
|
| 202 |
+
github: str,
|
| 203 |
+
linkedin: str,
|
| 204 |
+
portfolio: str,
|
| 205 |
+
kaggle: str,
|
| 206 |
+
projects_json: str,
|
| 207 |
+
skills: str,
|
| 208 |
+
tone: str,
|
| 209 |
+
emojis: bool,
|
| 210 |
+
frequency: str,
|
| 211 |
+
uvp: str,
|
| 212 |
+
differentiators: str,
|
| 213 |
+
) -> str:
|
| 214 |
+
"""Validate profile fields without saving.
|
| 215 |
+
|
| 216 |
+
Returns:
|
| 217 |
+
Validation result message
|
| 218 |
+
"""
|
| 219 |
+
try:
|
| 220 |
+
# Parse list fields
|
| 221 |
+
expertise_list = [x.strip() for x in expertise_areas.split(",") if x.strip()]
|
| 222 |
+
goals_list = [x.strip() for x in content_goals.split(",") if x.strip()]
|
| 223 |
+
languages_list = [x.strip() for x in languages.split(",") if x.strip()]
|
| 224 |
+
industries_list = [x.strip() for x in target_industries.split(",") if x.strip()]
|
| 225 |
+
skills_list = [x.strip() for x in skills.split(",") if x.strip()]
|
| 226 |
+
diff_list = [x.strip() for x in differentiators.split(",") if x.strip()]
|
| 227 |
+
|
| 228 |
+
# Parse projects JSON
|
| 229 |
+
try:
|
| 230 |
+
projects = json.loads(projects_json) if projects_json.strip() else []
|
| 231 |
+
except json.JSONDecodeError as e:
|
| 232 |
+
return f"β Invalid JSON in Notable Projects: {str(e)}"
|
| 233 |
+
|
| 234 |
+
# Create profile object
|
| 235 |
+
profile = UserProfile(
|
| 236 |
+
name=name,
|
| 237 |
+
target_role=target_role,
|
| 238 |
+
expertise_areas=expertise_list,
|
| 239 |
+
content_goals=goals_list,
|
| 240 |
+
region=region,
|
| 241 |
+
languages=languages_list,
|
| 242 |
+
target_industries=industries_list,
|
| 243 |
+
github_username=github,
|
| 244 |
+
linkedin_url=linkedin,
|
| 245 |
+
portfolio_url=portfolio,
|
| 246 |
+
kaggle_username=kaggle,
|
| 247 |
+
notable_projects=projects,
|
| 248 |
+
primary_skills=skills_list,
|
| 249 |
+
content_tone=tone,
|
| 250 |
+
use_emojis=emojis,
|
| 251 |
+
posting_frequency=frequency,
|
| 252 |
+
unique_value_proposition=uvp,
|
| 253 |
+
key_differentiators=diff_list,
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
# Validate
|
| 257 |
+
validation = profile.validate()
|
| 258 |
+
|
| 259 |
+
if validation["errors"]:
|
| 260 |
+
error_msg = "β Validation Errors:\n" + "\n".join(
|
| 261 |
+
f" β’ {err}" for err in validation["errors"]
|
| 262 |
+
)
|
| 263 |
+
if validation["warnings"]:
|
| 264 |
+
error_msg += "\n\nβ οΈ Warnings:\n" + "\n".join(
|
| 265 |
+
f" β’ {warn}" for warn in validation["warnings"]
|
| 266 |
+
)
|
| 267 |
+
return error_msg
|
| 268 |
+
|
| 269 |
+
if validation["warnings"]:
|
| 270 |
+
return "β οΈ Validation Warnings:\n" + "\n".join(
|
| 271 |
+
f" β’ {warn}" for warn in validation["warnings"]
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
return "β
Profile is valid!"
|
| 275 |
+
|
| 276 |
+
except Exception as e:
|
| 277 |
+
return f"β Validation error: {str(e)}"
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def save_profile_ui(
|
| 281 |
+
name: str,
|
| 282 |
+
target_role: str,
|
| 283 |
+
expertise_areas: str,
|
| 284 |
+
content_goals: str,
|
| 285 |
+
region: str,
|
| 286 |
+
languages: str,
|
| 287 |
+
target_industries: str,
|
| 288 |
+
github: str,
|
| 289 |
+
linkedin: str,
|
| 290 |
+
portfolio: str,
|
| 291 |
+
kaggle: str,
|
| 292 |
+
projects_json: str,
|
| 293 |
+
skills: str,
|
| 294 |
+
tone: str,
|
| 295 |
+
emojis: bool,
|
| 296 |
+
frequency: str,
|
| 297 |
+
uvp: str,
|
| 298 |
+
differentiators: str,
|
| 299 |
+
) -> str:
|
| 300 |
+
"""Save profile to YAML file.
|
| 301 |
+
|
| 302 |
+
Returns:
|
| 303 |
+
Save result message
|
| 304 |
+
"""
|
| 305 |
+
try:
|
| 306 |
+
# Parse list fields
|
| 307 |
+
expertise_list = [x.strip() for x in expertise_areas.split(",") if x.strip()]
|
| 308 |
+
goals_list = [x.strip() for x in content_goals.split(",") if x.strip()]
|
| 309 |
+
languages_list = [x.strip() for x in languages.split(",") if x.strip()]
|
| 310 |
+
industries_list = [x.strip() for x in target_industries.split(",") if x.strip()]
|
| 311 |
+
skills_list = [x.strip() for x in skills.split(",") if x.strip()]
|
| 312 |
+
diff_list = [x.strip() for x in differentiators.split(",") if x.strip()]
|
| 313 |
+
|
| 314 |
+
# Parse projects JSON
|
| 315 |
+
try:
|
| 316 |
+
projects = json.loads(projects_json) if projects_json.strip() else []
|
| 317 |
+
except json.JSONDecodeError as e:
|
| 318 |
+
return f"β Invalid JSON in Notable Projects: {str(e)}"
|
| 319 |
+
|
| 320 |
+
# Create profile object
|
| 321 |
+
profile = UserProfile(
|
| 322 |
+
name=name,
|
| 323 |
+
target_role=target_role,
|
| 324 |
+
expertise_areas=expertise_list,
|
| 325 |
+
content_goals=goals_list,
|
| 326 |
+
region=region,
|
| 327 |
+
languages=languages_list,
|
| 328 |
+
target_industries=industries_list,
|
| 329 |
+
github_username=github,
|
| 330 |
+
linkedin_url=linkedin,
|
| 331 |
+
portfolio_url=portfolio,
|
| 332 |
+
kaggle_username=kaggle,
|
| 333 |
+
notable_projects=projects,
|
| 334 |
+
primary_skills=skills_list,
|
| 335 |
+
content_tone=tone,
|
| 336 |
+
use_emojis=emojis,
|
| 337 |
+
posting_frequency=frequency,
|
| 338 |
+
unique_value_proposition=uvp,
|
| 339 |
+
key_differentiators=diff_list,
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
# Validate before saving
|
| 343 |
+
validation = profile.validate()
|
| 344 |
+
if validation["errors"]:
|
| 345 |
+
return "β Cannot save profile with errors:\n" + "\n".join(
|
| 346 |
+
f" β’ {err}" for err in validation["errors"]
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
# Save to YAML
|
| 350 |
+
save_profile_to_yaml(profile, PROFILE_PATH)
|
| 351 |
+
|
| 352 |
+
msg = f"β
Profile saved to {PROFILE_PATH}"
|
| 353 |
+
if validation["warnings"]:
|
| 354 |
+
msg += "\n\nβ οΈ Warnings:\n" + "\n".join(f" β’ {warn}" for warn in validation["warnings"])
|
| 355 |
+
return msg
|
| 356 |
+
|
| 357 |
+
except Exception as e:
|
| 358 |
+
return f"β Error saving profile: {str(e)}"
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
# ============================================================================
|
| 362 |
+
# Tab 3: Session History
|
| 363 |
+
# ============================================================================
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def list_sessions_ui() -> pd.DataFrame:
|
| 367 |
+
"""List all sessions as a DataFrame.
|
| 368 |
+
|
| 369 |
+
Returns:
|
| 370 |
+
DataFrame with session information
|
| 371 |
+
"""
|
| 372 |
+
try:
|
| 373 |
+
sessions = list_sessions()
|
| 374 |
+
if not sessions:
|
| 375 |
+
return pd.DataFrame(columns=["Session ID", "User", "Messages", "Last Updated"])
|
| 376 |
+
|
| 377 |
+
df = pd.DataFrame(
|
| 378 |
+
[
|
| 379 |
+
{
|
| 380 |
+
"Session ID": s["session_id"],
|
| 381 |
+
"User": s["user_id"],
|
| 382 |
+
"Messages": s["message_count"],
|
| 383 |
+
"Last Updated": s["updated_at"],
|
| 384 |
+
}
|
| 385 |
+
for s in sessions
|
| 386 |
+
]
|
| 387 |
+
)
|
| 388 |
+
return df
|
| 389 |
+
except Exception as e:
|
| 390 |
+
print(f"Error listing sessions: {e}")
|
| 391 |
+
return pd.DataFrame(columns=["Session ID", "User", "Messages", "Last Updated"])
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
def get_session_details_ui(session_id: str) -> str:
|
| 395 |
+
"""Get detailed information about a session.
|
| 396 |
+
|
| 397 |
+
Args:
|
| 398 |
+
session_id: Session ID to retrieve
|
| 399 |
+
|
| 400 |
+
Returns:
|
| 401 |
+
Formatted session details or error message
|
| 402 |
+
"""
|
| 403 |
+
if not session_id or not session_id.strip():
|
| 404 |
+
return "Please select a session from the table."
|
| 405 |
+
|
| 406 |
+
try:
|
| 407 |
+
info = get_session_info(session_id.strip())
|
| 408 |
+
if not info:
|
| 409 |
+
return f"β Session not found: {session_id}"
|
| 410 |
+
|
| 411 |
+
# Format the information nicely
|
| 412 |
+
details = f"""# Session Details
|
| 413 |
+
|
| 414 |
+
**Session ID**: {info["session_id"]}
|
| 415 |
+
**User**: {info["user_id"]}
|
| 416 |
+
**Created**: {info["created_at"]}
|
| 417 |
+
**Last Updated**: {info["updated_at"]}
|
| 418 |
+
**Message Count**: {info["message_count"]}
|
| 419 |
+
|
| 420 |
+
## Messages
|
| 421 |
+
|
| 422 |
+
"""
|
| 423 |
+
if info.get("messages"):
|
| 424 |
+
for i, msg in enumerate(info["messages"], 1):
|
| 425 |
+
details += f"### Message {i}\n```\n{msg}\n```\n\n"
|
| 426 |
+
else:
|
| 427 |
+
details += "*No messages in this session*\n"
|
| 428 |
+
|
| 429 |
+
return details
|
| 430 |
+
|
| 431 |
+
except Exception as e:
|
| 432 |
+
return f"β Error retrieving session: {str(e)}"
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def delete_session_ui(session_id: str) -> Tuple[pd.DataFrame, str]:
|
| 436 |
+
"""Delete a session.
|
| 437 |
+
|
| 438 |
+
Args:
|
| 439 |
+
session_id: Session ID to delete
|
| 440 |
+
|
| 441 |
+
Returns:
|
| 442 |
+
Tuple of (updated sessions DataFrame, status message)
|
| 443 |
+
"""
|
| 444 |
+
if not session_id or not session_id.strip():
|
| 445 |
+
return list_sessions_ui(), "Please select a session to delete."
|
| 446 |
+
|
| 447 |
+
try:
|
| 448 |
+
delete_session(session_id.strip())
|
| 449 |
+
return list_sessions_ui(), f"β
Session deleted: {session_id}"
|
| 450 |
+
except Exception as e:
|
| 451 |
+
return list_sessions_ui(), f"β Error deleting session: {str(e)}"
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
# ============================================================================
|
| 455 |
+
# Tab 4: Settings
|
| 456 |
+
# ============================================================================
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def save_settings_ui(api_key: str, model: str, max_papers: int, citation_style: str) -> str:
|
| 460 |
+
"""Save settings (placeholder - would need to update config).
|
| 461 |
+
|
| 462 |
+
Args:
|
| 463 |
+
api_key: Google API key
|
| 464 |
+
model: Model name
|
| 465 |
+
max_papers: Max papers to search
|
| 466 |
+
citation_style: Citation style
|
| 467 |
+
|
| 468 |
+
Returns:
|
| 469 |
+
Status message
|
| 470 |
+
"""
|
| 471 |
+
# Note: This is a simplified version. In production, you'd want to:
|
| 472 |
+
# 1. Update .env file for API key
|
| 473 |
+
# 2. Update config file for other settings
|
| 474 |
+
# 3. Or use a dedicated settings storage mechanism
|
| 475 |
+
|
| 476 |
+
messages = []
|
| 477 |
+
|
| 478 |
+
if api_key and api_key.strip():
|
| 479 |
+
messages.append("β οΈ API key changes require restart to take effect")
|
| 480 |
+
|
| 481 |
+
if model != DEFAULT_MODEL:
|
| 482 |
+
messages.append(f"β οΈ Model changed to {model} (requires restart)")
|
| 483 |
+
|
| 484 |
+
if max_papers != MAX_PAPERS_PER_SEARCH:
|
| 485 |
+
messages.append(f"β οΈ Max papers changed to {max_papers} (requires restart)")
|
| 486 |
+
|
| 487 |
+
if citation_style != CITATION_STYLE:
|
| 488 |
+
messages.append(f"β οΈ Citation style changed to {citation_style} (requires restart)")
|
| 489 |
+
|
| 490 |
+
if not messages:
|
| 491 |
+
return "βΉοΈ No settings changes detected"
|
| 492 |
+
|
| 493 |
+
return "\n".join(messages) + "\n\nπ‘ Settings saved (restart app to apply)"
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
# ============================================================================
|
| 497 |
+
# Main UI Creation
|
| 498 |
+
# ============================================================================
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
def create_ui() -> gr.Blocks:
|
| 502 |
+
"""Create the main Gradio UI.
|
| 503 |
+
|
| 504 |
+
Returns:
|
| 505 |
+
Gradio Blocks application
|
| 506 |
+
"""
|
| 507 |
+
with gr.Blocks(title="Scientific Content Generation Agent") as app:
|
| 508 |
+
gr.Markdown(
|
| 509 |
+
"""
|
| 510 |
+
# π¬ Scientific Content Generation Agent
|
| 511 |
+
|
| 512 |
+
Generate research-backed content for blogs, LinkedIn, and Twitter using AI-powered multi-agent system.
|
| 513 |
+
"""
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
with gr.Tabs():
|
| 517 |
+
# ===== TAB 1: GENERATE CONTENT =====
|
| 518 |
+
with gr.Tab("π Generate Content"):
|
| 519 |
+
gr.Markdown("### Create Scientific Content")
|
| 520 |
+
|
| 521 |
+
with gr.Row():
|
| 522 |
+
with gr.Column():
|
| 523 |
+
topic_input = gr.Textbox(
|
| 524 |
+
label="Research Topic",
|
| 525 |
+
placeholder="e.g., AI Agents and Multi-Agent Systems",
|
| 526 |
+
lines=2,
|
| 527 |
+
)
|
| 528 |
+
platform_checkboxes = gr.CheckboxGroup(
|
| 529 |
+
choices=["Blog", "LinkedIn", "Twitter"],
|
| 530 |
+
value=["Blog", "LinkedIn", "Twitter"],
|
| 531 |
+
label="Target Platforms",
|
| 532 |
+
)
|
| 533 |
+
tone_dropdown = gr.Dropdown(
|
| 534 |
+
choices=[
|
| 535 |
+
"professional-formal",
|
| 536 |
+
"professional-conversational",
|
| 537 |
+
"technical",
|
| 538 |
+
],
|
| 539 |
+
value="professional-conversational",
|
| 540 |
+
label="Content Tone",
|
| 541 |
+
)
|
| 542 |
+
audience_input = gr.Textbox(
|
| 543 |
+
label="Target Audience",
|
| 544 |
+
value="researchers and professionals",
|
| 545 |
+
lines=1,
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
with gr.Accordion("Advanced Options", open=False):
|
| 549 |
+
session_id_input = gr.Textbox(
|
| 550 |
+
label="Session ID (optional - leave empty for new session)",
|
| 551 |
+
placeholder="Enter session ID to resume",
|
| 552 |
+
lines=1,
|
| 553 |
+
)
|
| 554 |
+
|
| 555 |
+
generate_btn = gr.Button("Generate Content", variant="primary", size="lg")
|
| 556 |
+
|
| 557 |
+
with gr.Column():
|
| 558 |
+
output_display = gr.Textbox(
|
| 559 |
+
label="Generated Content",
|
| 560 |
+
lines=25,
|
| 561 |
+
max_lines=50,
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
generate_btn.click(
|
| 565 |
+
fn=generate_content_sync,
|
| 566 |
+
inputs=[
|
| 567 |
+
topic_input,
|
| 568 |
+
platform_checkboxes,
|
| 569 |
+
tone_dropdown,
|
| 570 |
+
audience_input,
|
| 571 |
+
session_id_input,
|
| 572 |
+
],
|
| 573 |
+
outputs=output_display,
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
# ===== TAB 2: PROFILE EDITOR =====
|
| 577 |
+
with gr.Tab("π€ Profile Editor"):
|
| 578 |
+
gr.Markdown("### Edit Your Professional Profile")
|
| 579 |
+
|
| 580 |
+
with gr.Row():
|
| 581 |
+
with gr.Column():
|
| 582 |
+
gr.Markdown("#### Professional Identity")
|
| 583 |
+
name_input = gr.Textbox(label="Name", value="Your Name")
|
| 584 |
+
target_role_input = gr.Textbox(label="Target Role", value="AI Consultant")
|
| 585 |
+
expertise_input = gr.Textbox(
|
| 586 |
+
label="Expertise Areas (comma-separated)",
|
| 587 |
+
value="Machine Learning, AI",
|
| 588 |
+
lines=2,
|
| 589 |
+
)
|
| 590 |
+
|
| 591 |
+
gr.Markdown("#### Professional Goals")
|
| 592 |
+
goals_input = gr.Textbox(
|
| 593 |
+
label="Content Goals (comma-separated)",
|
| 594 |
+
value="opportunities, credibility, visibility",
|
| 595 |
+
lines=2,
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
gr.Markdown("#### Geographic & Market")
|
| 599 |
+
region_dropdown = gr.Dropdown(
|
| 600 |
+
choices=["Europe", "US", "Asia", "Global"],
|
| 601 |
+
value="Europe",
|
| 602 |
+
label="Region",
|
| 603 |
+
)
|
| 604 |
+
languages_input = gr.Textbox(
|
| 605 |
+
label="Languages (comma-separated)", value="English"
|
| 606 |
+
)
|
| 607 |
+
industries_input = gr.Textbox(
|
| 608 |
+
label="Target Industries (comma-separated)",
|
| 609 |
+
value="Technology, Finance",
|
| 610 |
+
lines=2,
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
with gr.Column():
|
| 614 |
+
gr.Markdown("#### Portfolio & Links")
|
| 615 |
+
github_input = gr.Textbox(label="GitHub Username")
|
| 616 |
+
linkedin_input = gr.Textbox(label="LinkedIn URL")
|
| 617 |
+
portfolio_input = gr.Textbox(label="Portfolio URL")
|
| 618 |
+
kaggle_input = gr.Textbox(label="Kaggle Username")
|
| 619 |
+
|
| 620 |
+
gr.Markdown("#### Technical Skills")
|
| 621 |
+
skills_input = gr.Textbox(
|
| 622 |
+
label="Primary Skills (comma-separated)",
|
| 623 |
+
value="Python, PyTorch, TensorFlow",
|
| 624 |
+
lines=2,
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
gr.Markdown("#### Content Preferences")
|
| 628 |
+
tone_radio = gr.Radio(
|
| 629 |
+
choices=[
|
| 630 |
+
"professional-formal",
|
| 631 |
+
"professional-conversational",
|
| 632 |
+
"technical",
|
| 633 |
+
],
|
| 634 |
+
value="professional-conversational",
|
| 635 |
+
label="Content Tone",
|
| 636 |
+
)
|
| 637 |
+
emojis_checkbox = gr.Checkbox(label="Use Emojis", value=True)
|
| 638 |
+
frequency_dropdown = gr.Dropdown(
|
| 639 |
+
choices=["daily", "2-3x per week", "weekly"],
|
| 640 |
+
value="2-3x per week",
|
| 641 |
+
label="Posting Frequency",
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
with gr.Row():
|
| 645 |
+
with gr.Column():
|
| 646 |
+
gr.Markdown("#### SEO & Positioning")
|
| 647 |
+
uvp_input = gr.Textbox(
|
| 648 |
+
label="Unique Value Proposition",
|
| 649 |
+
value="I help companies turn AI research into production",
|
| 650 |
+
lines=2,
|
| 651 |
+
)
|
| 652 |
+
diff_input = gr.Textbox(
|
| 653 |
+
label="Key Differentiators (comma-separated)",
|
| 654 |
+
value="Research to production, End-to-end AI",
|
| 655 |
+
lines=2,
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
with gr.Column():
|
| 659 |
+
gr.Markdown("#### Notable Projects (JSON)")
|
| 660 |
+
projects_input = gr.Code(
|
| 661 |
+
label="Projects",
|
| 662 |
+
language="json",
|
| 663 |
+
value=json.dumps(
|
| 664 |
+
[
|
| 665 |
+
{
|
| 666 |
+
"name": "Project Name",
|
| 667 |
+
"description": "Description",
|
| 668 |
+
"technologies": "Tech stack",
|
| 669 |
+
"url": "https://github.com/...",
|
| 670 |
+
}
|
| 671 |
+
],
|
| 672 |
+
indent=2,
|
| 673 |
+
),
|
| 674 |
+
lines=10,
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
with gr.Row():
|
| 678 |
+
load_btn = gr.Button("Load Profile")
|
| 679 |
+
validate_btn = gr.Button("Validate Profile")
|
| 680 |
+
save_btn = gr.Button("Save Profile", variant="primary")
|
| 681 |
+
|
| 682 |
+
profile_status = gr.Textbox(label="Status", lines=5)
|
| 683 |
+
|
| 684 |
+
# Wire up profile buttons
|
| 685 |
+
load_btn.click(
|
| 686 |
+
fn=load_profile_ui,
|
| 687 |
+
inputs=[],
|
| 688 |
+
outputs=[
|
| 689 |
+
name_input,
|
| 690 |
+
target_role_input,
|
| 691 |
+
expertise_input,
|
| 692 |
+
goals_input,
|
| 693 |
+
region_dropdown,
|
| 694 |
+
languages_input,
|
| 695 |
+
industries_input,
|
| 696 |
+
github_input,
|
| 697 |
+
linkedin_input,
|
| 698 |
+
portfolio_input,
|
| 699 |
+
kaggle_input,
|
| 700 |
+
projects_input,
|
| 701 |
+
skills_input,
|
| 702 |
+
tone_radio,
|
| 703 |
+
emojis_checkbox,
|
| 704 |
+
frequency_dropdown,
|
| 705 |
+
uvp_input,
|
| 706 |
+
diff_input,
|
| 707 |
+
profile_status,
|
| 708 |
+
],
|
| 709 |
+
)
|
| 710 |
+
|
| 711 |
+
validate_btn.click(
|
| 712 |
+
fn=validate_profile_ui,
|
| 713 |
+
inputs=[
|
| 714 |
+
name_input,
|
| 715 |
+
target_role_input,
|
| 716 |
+
expertise_input,
|
| 717 |
+
goals_input,
|
| 718 |
+
region_dropdown,
|
| 719 |
+
languages_input,
|
| 720 |
+
industries_input,
|
| 721 |
+
github_input,
|
| 722 |
+
linkedin_input,
|
| 723 |
+
portfolio_input,
|
| 724 |
+
kaggle_input,
|
| 725 |
+
projects_input,
|
| 726 |
+
skills_input,
|
| 727 |
+
tone_radio,
|
| 728 |
+
emojis_checkbox,
|
| 729 |
+
frequency_dropdown,
|
| 730 |
+
uvp_input,
|
| 731 |
+
diff_input,
|
| 732 |
+
],
|
| 733 |
+
outputs=profile_status,
|
| 734 |
+
)
|
| 735 |
+
|
| 736 |
+
save_btn.click(
|
| 737 |
+
fn=save_profile_ui,
|
| 738 |
+
inputs=[
|
| 739 |
+
name_input,
|
| 740 |
+
target_role_input,
|
| 741 |
+
expertise_input,
|
| 742 |
+
goals_input,
|
| 743 |
+
region_dropdown,
|
| 744 |
+
languages_input,
|
| 745 |
+
industries_input,
|
| 746 |
+
github_input,
|
| 747 |
+
linkedin_input,
|
| 748 |
+
portfolio_input,
|
| 749 |
+
kaggle_input,
|
| 750 |
+
projects_input,
|
| 751 |
+
skills_input,
|
| 752 |
+
tone_radio,
|
| 753 |
+
emojis_checkbox,
|
| 754 |
+
frequency_dropdown,
|
| 755 |
+
uvp_input,
|
| 756 |
+
diff_input,
|
| 757 |
+
],
|
| 758 |
+
outputs=profile_status,
|
| 759 |
+
)
|
| 760 |
+
|
| 761 |
+
# ===== TAB 3: SESSION HISTORY =====
|
| 762 |
+
with gr.Tab("π Session History"):
|
| 763 |
+
gr.Markdown("### View and Manage Sessions")
|
| 764 |
+
|
| 765 |
+
with gr.Row():
|
| 766 |
+
refresh_btn = gr.Button("Refresh Sessions")
|
| 767 |
+
|
| 768 |
+
sessions_table = gr.Dataframe(
|
| 769 |
+
label="Sessions",
|
| 770 |
+
value=list_sessions_ui(),
|
| 771 |
+
interactive=False,
|
| 772 |
+
)
|
| 773 |
+
|
| 774 |
+
with gr.Row():
|
| 775 |
+
session_selector = gr.Textbox(
|
| 776 |
+
label="Session ID (paste from table)",
|
| 777 |
+
placeholder="Enter session ID",
|
| 778 |
+
)
|
| 779 |
+
|
| 780 |
+
session_details = gr.Markdown(label="Session Details")
|
| 781 |
+
|
| 782 |
+
with gr.Row():
|
| 783 |
+
view_details_btn = gr.Button("View Details")
|
| 784 |
+
delete_btn = gr.Button("Delete Session", variant="stop")
|
| 785 |
+
resume_btn = gr.Button("Resume Session")
|
| 786 |
+
|
| 787 |
+
session_status = gr.Textbox(label="Status", lines=2)
|
| 788 |
+
|
| 789 |
+
# Wire up session buttons
|
| 790 |
+
refresh_btn.click(fn=list_sessions_ui, inputs=[], outputs=sessions_table)
|
| 791 |
+
|
| 792 |
+
view_details_btn.click(
|
| 793 |
+
fn=get_session_details_ui, inputs=session_selector, outputs=session_details
|
| 794 |
+
)
|
| 795 |
+
|
| 796 |
+
delete_btn.click(
|
| 797 |
+
fn=delete_session_ui,
|
| 798 |
+
inputs=session_selector,
|
| 799 |
+
outputs=[sessions_table, session_status],
|
| 800 |
+
)
|
| 801 |
+
|
| 802 |
+
# Resume session - switches to Tab 1 and populates session ID
|
| 803 |
+
def resume_session(session_id):
|
| 804 |
+
return session_id
|
| 805 |
+
|
| 806 |
+
resume_btn.click(
|
| 807 |
+
fn=resume_session, inputs=session_selector, outputs=session_id_input
|
| 808 |
+
)
|
| 809 |
+
|
| 810 |
+
# ===== TAB 4: SETTINGS =====
|
| 811 |
+
with gr.Tab("βοΈ Settings"):
|
| 812 |
+
gr.Markdown("### Configure API and Content Settings")
|
| 813 |
+
|
| 814 |
+
gr.Markdown("#### API Configuration")
|
| 815 |
+
api_key_input = gr.Textbox(
|
| 816 |
+
label="Google API Key",
|
| 817 |
+
type="password",
|
| 818 |
+
placeholder="Enter your API key from https://aistudio.google.com/app/api_keys",
|
| 819 |
+
)
|
| 820 |
+
gr.Markdown(
|
| 821 |
+
"*Your API key is stored locally and never shared. Get one at [Google AI Studio](https://aistudio.google.com/app/api_keys)*"
|
| 822 |
+
)
|
| 823 |
+
|
| 824 |
+
model_dropdown = gr.Dropdown(
|
| 825 |
+
choices=["gemini-2.0-flash-exp", "gemini-1.5-pro", "gemini-1.5-flash"],
|
| 826 |
+
value=DEFAULT_MODEL,
|
| 827 |
+
label="Model",
|
| 828 |
+
)
|
| 829 |
+
|
| 830 |
+
gr.Markdown("#### Content Configuration")
|
| 831 |
+
max_papers_slider = gr.Slider(
|
| 832 |
+
minimum=1,
|
| 833 |
+
maximum=20,
|
| 834 |
+
value=MAX_PAPERS_PER_SEARCH,
|
| 835 |
+
step=1,
|
| 836 |
+
label="Max Papers per Search",
|
| 837 |
+
)
|
| 838 |
+
citation_radio = gr.Radio(
|
| 839 |
+
choices=["apa", "mla", "chicago"], value=CITATION_STYLE, label="Citation Style"
|
| 840 |
+
)
|
| 841 |
+
|
| 842 |
+
save_settings_btn = gr.Button("Save Settings", variant="primary")
|
| 843 |
+
settings_status = gr.Textbox(label="Status", lines=3)
|
| 844 |
+
|
| 845 |
+
save_settings_btn.click(
|
| 846 |
+
fn=save_settings_ui,
|
| 847 |
+
inputs=[api_key_input, model_dropdown, max_papers_slider, citation_radio],
|
| 848 |
+
outputs=settings_status,
|
| 849 |
+
)
|
| 850 |
+
|
| 851 |
+
gr.Markdown(
|
| 852 |
+
"""
|
| 853 |
+
---
|
| 854 |
+
π‘ **Tips**:
|
| 855 |
+
- Generate Content: Enter a topic and click Generate (takes 2-5 minutes)
|
| 856 |
+
- Profile Editor: Customize your professional profile for personalized content
|
| 857 |
+
- Session History: Resume previous generations or delete old sessions
|
| 858 |
+
- Settings: Configure API key and content preferences
|
| 859 |
+
|
| 860 |
+
π [Documentation](https://github.com/anthropics/agentic-content-generation) |
|
| 861 |
+
π [Report Issues](https://github.com/anthropics/agentic-content-generation/issues)
|
| 862 |
+
"""
|
| 863 |
+
)
|
| 864 |
+
|
| 865 |
+
return app
|
| 866 |
+
|
| 867 |
+
|
| 868 |
+
# ============================================================================
|
| 869 |
+
# Main Entry Point
|
| 870 |
+
# ============================================================================
|
| 871 |
+
|
| 872 |
+
if __name__ == "__main__":
|
| 873 |
+
print("π Launching Scientific Content Generation Agent UI...")
|
| 874 |
+
print("π Access at: http://localhost:7860")
|
| 875 |
+
print()
|
| 876 |
+
|
| 877 |
+
app = create_ui()
|
| 878 |
+
app.queue() # Enable queueing for long-running tasks
|
| 879 |
+
app.launch(server_name="0.0.0.0", server_port=7860, share=False)
|