Spaces:
Sleeping
Sleeping
Christophe Bourgoin
Claude
commited on
Commit
·
918983a
1
Parent(s):
a8a231d
Add development environment setup with uv, pyproject.toml, and Makefile
Browse files- Added pyproject.toml with project metadata and dependencies
- Created comprehensive Makefile for development tasks
- Set up uv virtual environment with all dependencies
- Added .gitignore for Python/uv projects
- Created CLAUDE.md documentation for future Claude instances
- Added basic test structure with pytest
- Configured ruff, mypy, and pytest in pyproject.toml
Development commands available via make:
- make setup: Create venv and install dependencies
- make test: Run all tests
- make lint: Run linter
- make format: Format code
- make run-ui: Run Gradio interface
- make deploy-github/deploy-hf: Deploy to repositories
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- .gitignore +175 -0
- CLAUDE.md +219 -0
- Makefile +230 -0
- pyproject.toml +180 -0
- tests/__init__.py +0 -0
- tests/test_tools.py +166 -0
.gitignore
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
share/python-wheels/
|
| 20 |
+
*.egg-info/
|
| 21 |
+
.installed.cfg
|
| 22 |
+
*.egg
|
| 23 |
+
MANIFEST
|
| 24 |
+
|
| 25 |
+
# Virtual Environments
|
| 26 |
+
.venv/
|
| 27 |
+
venv/
|
| 28 |
+
ENV/
|
| 29 |
+
env/
|
| 30 |
+
.env.local
|
| 31 |
+
|
| 32 |
+
# uv
|
| 33 |
+
.uv/
|
| 34 |
+
uv.lock
|
| 35 |
+
|
| 36 |
+
# PyInstaller
|
| 37 |
+
*.manifest
|
| 38 |
+
*.spec
|
| 39 |
+
|
| 40 |
+
# Unit test / coverage reports
|
| 41 |
+
htmlcov/
|
| 42 |
+
.tox/
|
| 43 |
+
.nox/
|
| 44 |
+
.coverage
|
| 45 |
+
.coverage.*
|
| 46 |
+
.cache
|
| 47 |
+
nosetests.xml
|
| 48 |
+
coverage.xml
|
| 49 |
+
*.cover
|
| 50 |
+
*.py,cover
|
| 51 |
+
.hypothesis/
|
| 52 |
+
.pytest_cache/
|
| 53 |
+
cover/
|
| 54 |
+
|
| 55 |
+
# Translations
|
| 56 |
+
*.mo
|
| 57 |
+
*.pot
|
| 58 |
+
|
| 59 |
+
# Django
|
| 60 |
+
*.log
|
| 61 |
+
local_settings.py
|
| 62 |
+
db.sqlite3
|
| 63 |
+
db.sqlite3-journal
|
| 64 |
+
|
| 65 |
+
# Flask
|
| 66 |
+
instance/
|
| 67 |
+
.webassets-cache
|
| 68 |
+
|
| 69 |
+
# Scrapy
|
| 70 |
+
.scrapy
|
| 71 |
+
|
| 72 |
+
# Sphinx documentation
|
| 73 |
+
docs/_build/
|
| 74 |
+
|
| 75 |
+
# PyBuilder
|
| 76 |
+
.pybuilder/
|
| 77 |
+
target/
|
| 78 |
+
|
| 79 |
+
# Jupyter Notebook
|
| 80 |
+
.ipynb_checkpoints
|
| 81 |
+
|
| 82 |
+
# IPython
|
| 83 |
+
profile_default/
|
| 84 |
+
ipython_config.py
|
| 85 |
+
|
| 86 |
+
# pyenv
|
| 87 |
+
.python-version
|
| 88 |
+
|
| 89 |
+
# pipenv
|
| 90 |
+
Pipfile.lock
|
| 91 |
+
|
| 92 |
+
# poetry
|
| 93 |
+
poetry.lock
|
| 94 |
+
|
| 95 |
+
# PEP 582
|
| 96 |
+
__pypackages__/
|
| 97 |
+
|
| 98 |
+
# Celery
|
| 99 |
+
celerybeat-schedule
|
| 100 |
+
celerybeat.pid
|
| 101 |
+
|
| 102 |
+
# SageMath
|
| 103 |
+
*.sage.py
|
| 104 |
+
|
| 105 |
+
# Environments
|
| 106 |
+
.env
|
| 107 |
+
.venv
|
| 108 |
+
env/
|
| 109 |
+
venv/
|
| 110 |
+
ENV/
|
| 111 |
+
env.bak/
|
| 112 |
+
venv.bak/
|
| 113 |
+
|
| 114 |
+
# Spyder
|
| 115 |
+
.spyderproject
|
| 116 |
+
.spyproject
|
| 117 |
+
|
| 118 |
+
# Rope
|
| 119 |
+
.ropeproject
|
| 120 |
+
|
| 121 |
+
# mkdocs
|
| 122 |
+
/site
|
| 123 |
+
|
| 124 |
+
# mypy
|
| 125 |
+
.mypy_cache/
|
| 126 |
+
.dmypy.json
|
| 127 |
+
dmypy.json
|
| 128 |
+
|
| 129 |
+
# Pyre
|
| 130 |
+
.pyre/
|
| 131 |
+
|
| 132 |
+
# pytype
|
| 133 |
+
.pytype/
|
| 134 |
+
|
| 135 |
+
# Cython
|
| 136 |
+
cython_debug/
|
| 137 |
+
|
| 138 |
+
# ruff
|
| 139 |
+
.ruff_cache/
|
| 140 |
+
|
| 141 |
+
# IDEs
|
| 142 |
+
.vscode/
|
| 143 |
+
.idea/
|
| 144 |
+
*.swp
|
| 145 |
+
*.swo
|
| 146 |
+
*~
|
| 147 |
+
.DS_Store
|
| 148 |
+
|
| 149 |
+
# Project specific
|
| 150 |
+
agent.log
|
| 151 |
+
output/
|
| 152 |
+
logs/
|
| 153 |
+
sessions.db
|
| 154 |
+
*.db
|
| 155 |
+
*.sqlite
|
| 156 |
+
|
| 157 |
+
# User profile (may contain sensitive info)
|
| 158 |
+
profile.yaml
|
| 159 |
+
|
| 160 |
+
# Temporary files
|
| 161 |
+
*.tmp
|
| 162 |
+
*.bak
|
| 163 |
+
*.swp
|
| 164 |
+
*~
|
| 165 |
+
|
| 166 |
+
# ADK/Gemini cache
|
| 167 |
+
.adk_cache/
|
| 168 |
+
.google_cache/
|
| 169 |
+
|
| 170 |
+
# Session data (local only)
|
| 171 |
+
~/.agentic-content-generation/
|
| 172 |
+
|
| 173 |
+
# Build artifacts
|
| 174 |
+
*.whl
|
| 175 |
+
*.tar.gz
|
CLAUDE.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CLAUDE.md
|
| 2 |
+
|
| 3 |
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
| 4 |
+
|
| 5 |
+
## Project Overview
|
| 6 |
+
|
| 7 |
+
**Scientific Content Generation Agent** - A 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) and deployed on Hugging Face Spaces.
|
| 8 |
+
|
| 9 |
+
**Framework**: Google Agent Development Kit (ADK)
|
| 10 |
+
**Model**: Gemini 2.0 Flash (`gemini-2.0-flash-exp`)
|
| 11 |
+
**UI**: Gradio 6.0
|
| 12 |
+
**Deployment**: Hugging Face Spaces (free hosting)
|
| 13 |
+
|
| 14 |
+
## Commands
|
| 15 |
+
|
| 16 |
+
### Local Development
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
# Run the Gradio UI locally
|
| 20 |
+
python app.py
|
| 21 |
+
|
| 22 |
+
# Run CLI version with default topic
|
| 23 |
+
python main.py
|
| 24 |
+
|
| 25 |
+
# Run CLI with custom topic
|
| 26 |
+
python main.py --topic "Your Research Topic"
|
| 27 |
+
|
| 28 |
+
# Initialize user profile
|
| 29 |
+
python main.py --init-profile
|
| 30 |
+
|
| 31 |
+
# Edit profile interactively
|
| 32 |
+
python main.py --edit-profile
|
| 33 |
+
|
| 34 |
+
# Validate profile
|
| 35 |
+
python main.py --validate-profile
|
| 36 |
+
|
| 37 |
+
# List all sessions
|
| 38 |
+
python main.py --list-sessions
|
| 39 |
+
|
| 40 |
+
# Resume a session
|
| 41 |
+
python main.py --session-id <SESSION_ID>
|
| 42 |
+
|
| 43 |
+
# Delete a session
|
| 44 |
+
python main.py --delete-session <SESSION_ID>
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### Hugging Face Deployment
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
# Add and commit changes
|
| 51 |
+
git add .
|
| 52 |
+
git commit -m "Your commit message"
|
| 53 |
+
|
| 54 |
+
# Push to Hugging Face Space (triggers automatic rebuild)
|
| 55 |
+
git push origin main
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
**Important**: Set `GOOGLE_API_KEY` as a secret in Hugging Face Space settings before deployment.
|
| 59 |
+
|
| 60 |
+
## Architecture
|
| 61 |
+
|
| 62 |
+
### Multi-Agent Pipeline
|
| 63 |
+
|
| 64 |
+
The system uses a **SequentialAgent** (not ParallelAgent) because each agent depends on outputs from previous agents. State flows linearly through the pipeline via the `output_key`/placeholder pattern.
|
| 65 |
+
|
| 66 |
+
**5-Agent Pipeline** (execution order matters):
|
| 67 |
+
|
| 68 |
+
1. **ResearchAgent** ([src/agents.py:28](src/agents.py#L28))
|
| 69 |
+
- Searches academic papers (arXiv API)
|
| 70 |
+
- Searches web trends (DuckDuckGo)
|
| 71 |
+
- Extracts key findings
|
| 72 |
+
- Outputs: `research_findings`
|
| 73 |
+
|
| 74 |
+
2. **StrategyAgent** ([src/agents.py:74](src/agents.py#L74))
|
| 75 |
+
- Analyzes research findings
|
| 76 |
+
- Plans content approach for each platform
|
| 77 |
+
- Defines target audience and key messages
|
| 78 |
+
- Focuses on professional positioning
|
| 79 |
+
- Outputs: `content_strategy`
|
| 80 |
+
|
| 81 |
+
3. **ContentGeneratorAgent** ([src/agents.py:160](src/agents.py#L160))
|
| 82 |
+
- Creates platform-specific content
|
| 83 |
+
- Blog (1000-2000 words)
|
| 84 |
+
- LinkedIn post (300-800 words)
|
| 85 |
+
- Twitter thread (8-12 tweets)
|
| 86 |
+
- Outputs: `generated_content`
|
| 87 |
+
|
| 88 |
+
4. **LinkedInOptimizationAgent** ([src/agents.py:234](src/agents.py#L234))
|
| 89 |
+
- Optimizes LinkedIn post for SEO
|
| 90 |
+
- Adds engagement hooks and CTAs
|
| 91 |
+
- Integrates portfolio mentions
|
| 92 |
+
- Emphasizes business value
|
| 93 |
+
- Outputs: `optimized_linkedin`
|
| 94 |
+
|
| 95 |
+
5. **ReviewAgent** ([src/agents.py:316](src/agents.py#L316))
|
| 96 |
+
- Verifies scientific accuracy
|
| 97 |
+
- Adds proper citations (APA format)
|
| 98 |
+
- Scores opportunity appeal
|
| 99 |
+
- Final polish
|
| 100 |
+
- Outputs: `final_content`
|
| 101 |
+
|
| 102 |
+
### Key Design Decisions
|
| 103 |
+
|
| 104 |
+
**Retry Configuration** ([src/config.py:24](src/config.py#L24)):
|
| 105 |
+
- 5 attempts with exponential backoff (exp_base=7)
|
| 106 |
+
- Handles rate limiting (429) and server errors (500/503/504)
|
| 107 |
+
- Critical for Gemini API free tier reliability
|
| 108 |
+
|
| 109 |
+
**XML Parsing** ([src/tools.py:44](src/tools.py#L44)):
|
| 110 |
+
- Uses ElementTree (not string parsing) for arXiv API responses
|
| 111 |
+
- Robust handling of namespaces and malformed entries
|
| 112 |
+
|
| 113 |
+
**Opportunity Scoring** ([src/tools.py:768](src/tools.py#L768)):
|
| 114 |
+
- Weighted: SEO (30%) + Engagement (30%) + Business Value (25%) + Portfolio (15%)
|
| 115 |
+
- Based on LinkedIn algorithm priorities and recruiter behavior
|
| 116 |
+
|
| 117 |
+
### File Structure
|
| 118 |
+
|
| 119 |
+
```
|
| 120 |
+
scientific-content-agent/
|
| 121 |
+
├── app.py # Entry point for HF Spaces (imports ui_app)
|
| 122 |
+
├── main.py # CLI entry point with async pipeline
|
| 123 |
+
├── ui_app.py # Gradio interface (4 tabs)
|
| 124 |
+
├── requirements.txt # Python dependencies
|
| 125 |
+
├── profile.example.yaml # User profile template
|
| 126 |
+
├── .env.example # API key template
|
| 127 |
+
└── src/
|
| 128 |
+
├── agents.py # Agent definitions and pipeline
|
| 129 |
+
├── config.py # Configuration and constants
|
| 130 |
+
├── tools.py # Custom tools (search_papers, search_web, etc.)
|
| 131 |
+
├── profile.py # UserProfile dataclass and management
|
| 132 |
+
├── profile_editor.py # Interactive profile editing
|
| 133 |
+
└── session_manager.py # Session persistence (SQLite)
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
### State Management
|
| 137 |
+
|
| 138 |
+
**Profile System**: User profiles stored in `~/.agentic-content-generation/profile.yaml` (local) or managed via Gradio UI (HF Spaces). Profiles inject professional context into agent instructions.
|
| 139 |
+
|
| 140 |
+
**Session Persistence**: SQLite database at `~/.agentic-content-generation/sessions.db` stores conversation history. Sessions can be resumed by ID.
|
| 141 |
+
|
| 142 |
+
**Agent State Flow**:
|
| 143 |
+
```
|
| 144 |
+
User Input → ResearchAgent → {research_findings}
|
| 145 |
+
→ StrategyAgent → {content_strategy}
|
| 146 |
+
→ ContentGeneratorAgent → {generated_content}
|
| 147 |
+
→ LinkedInOptimizationAgent → {optimized_linkedin}
|
| 148 |
+
→ ReviewAgent → {final_content}
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
Placeholders like `{research_findings}` in agent instructions are replaced with actual outputs from previous agents.
|
| 152 |
+
|
| 153 |
+
## Tools
|
| 154 |
+
|
| 155 |
+
All custom tools return `dict[str, Any]` with `status` field ("success" or "error").
|
| 156 |
+
|
| 157 |
+
**Research Tools**:
|
| 158 |
+
- `search_papers(topic, max_results)` - arXiv API search
|
| 159 |
+
- `search_web(query, max_results)` - DuckDuckGo search
|
| 160 |
+
- `extract_key_findings(research_text)` - Keyword-based extraction
|
| 161 |
+
|
| 162 |
+
**Content Tools**:
|
| 163 |
+
- `format_for_platform(content, platform, topic)` - Platform-specific formatting
|
| 164 |
+
- `generate_citations(sources, style)` - APA/MLA/Chicago citations
|
| 165 |
+
|
| 166 |
+
**Optimization Tools**:
|
| 167 |
+
- `generate_seo_keywords(topic, role)` - Recruiter-focused keywords
|
| 168 |
+
- `create_engagement_hooks(topic, goal)` - CTAs and discussion prompts
|
| 169 |
+
- `search_industry_trends(field, region)` - Market demands and hot skills
|
| 170 |
+
- `analyze_content_for_opportunities(content, target_role)` - Opportunity scoring
|
| 171 |
+
|
| 172 |
+
## Configuration
|
| 173 |
+
|
| 174 |
+
**Environment Variables** (`.env`):
|
| 175 |
+
```bash
|
| 176 |
+
GOOGLE_API_KEY=your_key_here
|
| 177 |
+
GOOGLE_GENAI_USE_VERTEXAI=FALSE
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
**Constants** ([src/config.py](src/config.py)):
|
| 181 |
+
- `DEFAULT_MODEL = "gemini-2.0-flash-exp"`
|
| 182 |
+
- `MAX_PAPERS_PER_SEARCH = 5`
|
| 183 |
+
- `CITATION_STYLE = "apa"`
|
| 184 |
+
- `SUPPORTED_PLATFORMS = ["blog", "linkedin", "twitter"]`
|
| 185 |
+
|
| 186 |
+
## Gradio UI
|
| 187 |
+
|
| 188 |
+
4 tabs in [ui_app.py](ui_app.py):
|
| 189 |
+
|
| 190 |
+
1. **Generate Content** - Main content generation interface
|
| 191 |
+
2. **Profile Editor** - Manage user professional profile
|
| 192 |
+
3. **Session History** - View and resume past sessions
|
| 193 |
+
4. **Settings** - Configure API key and preferences
|
| 194 |
+
|
| 195 |
+
Progress tracking uses fixed milestones (no real-time ADK event hooks).
|
| 196 |
+
|
| 197 |
+
## Common Issues
|
| 198 |
+
|
| 199 |
+
**Build fails on HF Spaces**: Ensure all dependencies in [requirements.txt](requirements.txt) are compatible with Python 3.11+.
|
| 200 |
+
|
| 201 |
+
**Rate limiting**: Retry config handles 429 errors automatically. If persistent, consider upgrading to Vertex AI.
|
| 202 |
+
|
| 203 |
+
**No content generated**: Check that `GOOGLE_API_KEY` is set as a Space secret or in Settings tab.
|
| 204 |
+
|
| 205 |
+
**arXiv API errors**: Tool gracefully handles XML parse errors and malformed entries (continues processing).
|
| 206 |
+
|
| 207 |
+
## API Key Management
|
| 208 |
+
|
| 209 |
+
**Local**: Create `.env` file with `GOOGLE_API_KEY=...`
|
| 210 |
+
**HF Spaces**: Add as secret in Space settings (name: `GOOGLE_API_KEY`)
|
| 211 |
+
**Get key**: https://aistudio.google.com/app/api_keys
|
| 212 |
+
|
| 213 |
+
## Testing
|
| 214 |
+
|
| 215 |
+
No formal test suite yet. Manual testing:
|
| 216 |
+
- Generate content for a topic
|
| 217 |
+
- Verify all 3 platforms (Blog, LinkedIn, Twitter)
|
| 218 |
+
- Check citations format
|
| 219 |
+
- Validate opportunity scores
|
Makefile
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.PHONY: help install install-dev test test-unit test-integration test-cov lint format type-check clean run run-cli run-ui setup venv sync update build deploy-hf deploy-github all check
|
| 2 |
+
|
| 3 |
+
# Default target
|
| 4 |
+
.DEFAULT_GOAL := help
|
| 5 |
+
|
| 6 |
+
# Variables
|
| 7 |
+
PYTHON := python3
|
| 8 |
+
UV := uv
|
| 9 |
+
VENV_DIR := .venv
|
| 10 |
+
SRC_DIR := src
|
| 11 |
+
TEST_DIR := tests
|
| 12 |
+
COVERAGE_DIR := htmlcov
|
| 13 |
+
|
| 14 |
+
# Colors for output
|
| 15 |
+
BLUE := \033[0;34m
|
| 16 |
+
GREEN := \033[0;32m
|
| 17 |
+
YELLOW := \033[0;33m
|
| 18 |
+
RED := \033[0;31m
|
| 19 |
+
NC := \033[0m # No Color
|
| 20 |
+
|
| 21 |
+
help: ## Show this help message
|
| 22 |
+
@echo "$(BLUE)Scientific Content Agent - Development Commands$(NC)"
|
| 23 |
+
@echo ""
|
| 24 |
+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(GREEN)%-20s$(NC) %s\n", $$1, $$2}'
|
| 25 |
+
|
| 26 |
+
# Setup and Installation
|
| 27 |
+
setup: venv install-dev ## Complete setup: create venv and install all dependencies
|
| 28 |
+
@echo "$(GREEN)✓ Setup complete! Activate with: source .venv/bin/activate$(NC)"
|
| 29 |
+
|
| 30 |
+
venv: ## Create uv virtual environment
|
| 31 |
+
@echo "$(BLUE)Creating uv virtual environment...$(NC)"
|
| 32 |
+
$(UV) venv $(VENV_DIR)
|
| 33 |
+
@echo "$(GREEN)✓ Virtual environment created$(NC)"
|
| 34 |
+
|
| 35 |
+
install: ## Install production dependencies
|
| 36 |
+
@echo "$(BLUE)Installing production dependencies...$(NC)"
|
| 37 |
+
$(UV) pip install -e .
|
| 38 |
+
@echo "$(GREEN)✓ Production dependencies installed$(NC)"
|
| 39 |
+
|
| 40 |
+
install-dev: ## Install development dependencies
|
| 41 |
+
@echo "$(BLUE)Installing development dependencies...$(NC)"
|
| 42 |
+
$(UV) pip install -e ".[dev]"
|
| 43 |
+
@echo "$(GREEN)✓ Development dependencies installed$(NC)"
|
| 44 |
+
|
| 45 |
+
sync: ## Sync dependencies with pyproject.toml
|
| 46 |
+
@echo "$(BLUE)Syncing dependencies...$(NC)"
|
| 47 |
+
$(UV) pip sync pyproject.toml
|
| 48 |
+
@echo "$(GREEN)✓ Dependencies synced$(NC)"
|
| 49 |
+
|
| 50 |
+
update: ## Update all dependencies to latest versions
|
| 51 |
+
@echo "$(BLUE)Updating dependencies...$(NC)"
|
| 52 |
+
$(UV) pip install --upgrade -e ".[dev]"
|
| 53 |
+
@echo "$(GREEN)✓ Dependencies updated$(NC)"
|
| 54 |
+
|
| 55 |
+
# Running the application
|
| 56 |
+
run: run-ui ## Run the Gradio UI (default)
|
| 57 |
+
|
| 58 |
+
run-ui: ## Run the Gradio web interface
|
| 59 |
+
@echo "$(BLUE)Starting Gradio UI...$(NC)"
|
| 60 |
+
$(UV) run python app.py
|
| 61 |
+
|
| 62 |
+
run-cli: ## Run the CLI version (use: make run-cli TOPIC="your topic")
|
| 63 |
+
@echo "$(BLUE)Running CLI...$(NC)"
|
| 64 |
+
@if [ -z "$(TOPIC)" ]; then \
|
| 65 |
+
$(UV) run python main.py; \
|
| 66 |
+
else \
|
| 67 |
+
$(UV) run python main.py --topic "$(TOPIC)"; \
|
| 68 |
+
fi
|
| 69 |
+
|
| 70 |
+
run-session: ## Resume a session (use: make run-session SESSION_ID=...)
|
| 71 |
+
@if [ -z "$(SESSION_ID)" ]; then \
|
| 72 |
+
echo "$(RED)Error: SESSION_ID not provided. Use: make run-session SESSION_ID=xxx$(NC)"; \
|
| 73 |
+
exit 1; \
|
| 74 |
+
fi
|
| 75 |
+
$(UV) run python main.py --session-id "$(SESSION_ID)"
|
| 76 |
+
|
| 77 |
+
# Profile Management
|
| 78 |
+
profile-init: ## Initialize default profile
|
| 79 |
+
@echo "$(BLUE)Initializing profile...$(NC)"
|
| 80 |
+
$(UV) run python main.py --init-profile
|
| 81 |
+
|
| 82 |
+
profile-edit: ## Edit profile interactively
|
| 83 |
+
@echo "$(BLUE)Opening profile editor...$(NC)"
|
| 84 |
+
$(UV) run python main.py --edit-profile
|
| 85 |
+
|
| 86 |
+
profile-validate: ## Validate current profile
|
| 87 |
+
@echo "$(BLUE)Validating profile...$(NC)"
|
| 88 |
+
$(UV) run python main.py --validate-profile
|
| 89 |
+
|
| 90 |
+
# Session Management
|
| 91 |
+
sessions-list: ## List all sessions
|
| 92 |
+
@echo "$(BLUE)Listing sessions...$(NC)"
|
| 93 |
+
$(UV) run python main.py --list-sessions
|
| 94 |
+
|
| 95 |
+
session-delete: ## Delete a session (use: make session-delete SESSION_ID=...)
|
| 96 |
+
@if [ -z "$(SESSION_ID)" ]; then \
|
| 97 |
+
echo "$(RED)Error: SESSION_ID not provided. Use: make session-delete SESSION_ID=xxx$(NC)"; \
|
| 98 |
+
exit 1; \
|
| 99 |
+
fi
|
| 100 |
+
$(UV) run python main.py --delete-session "$(SESSION_ID)"
|
| 101 |
+
|
| 102 |
+
# Testing
|
| 103 |
+
test: ## Run all tests
|
| 104 |
+
@echo "$(BLUE)Running all tests...$(NC)"
|
| 105 |
+
$(UV) run pytest
|
| 106 |
+
|
| 107 |
+
test-unit: ## Run unit tests only
|
| 108 |
+
@echo "$(BLUE)Running unit tests...$(NC)"
|
| 109 |
+
$(UV) run pytest -m unit
|
| 110 |
+
|
| 111 |
+
test-integration: ## Run integration tests only
|
| 112 |
+
@echo "$(BLUE)Running integration tests...$(NC)"
|
| 113 |
+
$(UV) run pytest -m integration
|
| 114 |
+
|
| 115 |
+
test-fast: ## Run tests without slow tests
|
| 116 |
+
@echo "$(BLUE)Running fast tests...$(NC)"
|
| 117 |
+
$(UV) run pytest -m "not slow"
|
| 118 |
+
|
| 119 |
+
test-cov: ## Run tests with coverage report
|
| 120 |
+
@echo "$(BLUE)Running tests with coverage...$(NC)"
|
| 121 |
+
$(UV) run pytest --cov=$(SRC_DIR) --cov-report=html --cov-report=term
|
| 122 |
+
@echo "$(GREEN)✓ Coverage report generated at $(COVERAGE_DIR)/index.html$(NC)"
|
| 123 |
+
|
| 124 |
+
test-watch: ## Run tests in watch mode (requires pytest-watch)
|
| 125 |
+
@echo "$(BLUE)Running tests in watch mode...$(NC)"
|
| 126 |
+
$(UV) run ptw -- -v
|
| 127 |
+
|
| 128 |
+
# Code Quality
|
| 129 |
+
lint: ## Run ruff linter
|
| 130 |
+
@echo "$(BLUE)Running ruff linter...$(NC)"
|
| 131 |
+
$(UV) run ruff check $(SRC_DIR) $(TEST_DIR) main.py ui_app.py app.py
|
| 132 |
+
|
| 133 |
+
lint-fix: ## Run ruff linter and fix issues
|
| 134 |
+
@echo "$(BLUE)Running ruff linter with auto-fix...$(NC)"
|
| 135 |
+
$(UV) run ruff check --fix $(SRC_DIR) $(TEST_DIR) main.py ui_app.py app.py
|
| 136 |
+
@echo "$(GREEN)✓ Linting complete$(NC)"
|
| 137 |
+
|
| 138 |
+
format: ## Format code with ruff
|
| 139 |
+
@echo "$(BLUE)Formatting code...$(NC)"
|
| 140 |
+
$(UV) run ruff format $(SRC_DIR) $(TEST_DIR) main.py ui_app.py app.py
|
| 141 |
+
@echo "$(GREEN)✓ Code formatted$(NC)"
|
| 142 |
+
|
| 143 |
+
type-check: ## Run mypy type checker
|
| 144 |
+
@echo "$(BLUE)Running mypy type checker...$(NC)"
|
| 145 |
+
$(UV) run mypy $(SRC_DIR)
|
| 146 |
+
|
| 147 |
+
check: lint type-check test-fast ## Run all checks (lint, type-check, fast tests)
|
| 148 |
+
@echo "$(GREEN)✓ All checks passed!$(NC)"
|
| 149 |
+
|
| 150 |
+
all: format lint type-check test ## Format, lint, type-check, and test everything
|
| 151 |
+
@echo "$(GREEN)✓ All tasks completed!$(NC)"
|
| 152 |
+
|
| 153 |
+
# Cleaning
|
| 154 |
+
clean: ## Clean build artifacts and cache files
|
| 155 |
+
@echo "$(BLUE)Cleaning build artifacts...$(NC)"
|
| 156 |
+
rm -rf build/
|
| 157 |
+
rm -rf dist/
|
| 158 |
+
rm -rf *.egg-info
|
| 159 |
+
rm -rf $(COVERAGE_DIR)
|
| 160 |
+
rm -rf .coverage
|
| 161 |
+
rm -rf .pytest_cache
|
| 162 |
+
rm -rf .mypy_cache
|
| 163 |
+
rm -rf .ruff_cache
|
| 164 |
+
rm -rf $(SRC_DIR)/__pycache__
|
| 165 |
+
rm -rf $(TEST_DIR)/__pycache__
|
| 166 |
+
rm -rf __pycache__
|
| 167 |
+
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
| 168 |
+
find . -type f -name "*.pyc" -delete
|
| 169 |
+
find . -type f -name "*.pyo" -delete
|
| 170 |
+
@echo "$(GREEN)✓ Cleaned$(NC)"
|
| 171 |
+
|
| 172 |
+
clean-all: clean ## Clean everything including venv
|
| 173 |
+
@echo "$(BLUE)Cleaning virtual environment...$(NC)"
|
| 174 |
+
rm -rf $(VENV_DIR)
|
| 175 |
+
rm -rf output/
|
| 176 |
+
rm -rf logs/
|
| 177 |
+
rm -f agent.log
|
| 178 |
+
@echo "$(GREEN)✓ Everything cleaned$(NC)"
|
| 179 |
+
|
| 180 |
+
# Building
|
| 181 |
+
build: ## Build the project
|
| 182 |
+
@echo "$(BLUE)Building project...$(NC)"
|
| 183 |
+
$(UV) build
|
| 184 |
+
@echo "$(GREEN)✓ Build complete$(NC)"
|
| 185 |
+
|
| 186 |
+
# Deployment
|
| 187 |
+
deploy-hf: ## Deploy to Hugging Face Spaces
|
| 188 |
+
@echo "$(BLUE)Deploying to Hugging Face Spaces...$(NC)"
|
| 189 |
+
@if [ -z "$(MSG)" ]; then \
|
| 190 |
+
echo "$(RED)Error: Commit message required. Use: make deploy-hf MSG='your message'$(NC)"; \
|
| 191 |
+
exit 1; \
|
| 192 |
+
fi
|
| 193 |
+
git add .
|
| 194 |
+
git commit -m "$(MSG)"
|
| 195 |
+
git push origin main
|
| 196 |
+
@echo "$(GREEN)✓ Pushed to Hugging Face. Check build status at:$(NC)"
|
| 197 |
+
@echo "$(BLUE)https://huggingface.co/spaces/Chris30/scientific-content-agent$(NC)"
|
| 198 |
+
|
| 199 |
+
deploy-github: ## Deploy to GitHub
|
| 200 |
+
@echo "$(BLUE)Deploying to GitHub...$(NC)"
|
| 201 |
+
@if [ -z "$(MSG)" ]; then \
|
| 202 |
+
echo "$(RED)Error: Commit message required. Use: make deploy-github MSG='your message'$(NC)"; \
|
| 203 |
+
exit 1; \
|
| 204 |
+
fi
|
| 205 |
+
git add .
|
| 206 |
+
git commit -m "$(MSG)"
|
| 207 |
+
git push github main
|
| 208 |
+
@echo "$(GREEN)✓ Pushed to GitHub$(NC)"
|
| 209 |
+
|
| 210 |
+
deploy-both: ## Deploy to both Hugging Face and GitHub
|
| 211 |
+
@echo "$(BLUE)Deploying to both remotes...$(NC)"
|
| 212 |
+
@if [ -z "$(MSG)" ]; then \
|
| 213 |
+
echo "$(RED)Error: Commit message required. Use: make deploy-both MSG='your message'$(NC)"; \
|
| 214 |
+
exit 1; \
|
| 215 |
+
fi
|
| 216 |
+
git add .
|
| 217 |
+
git commit -m "$(MSG)"
|
| 218 |
+
git push origin main
|
| 219 |
+
git push github main
|
| 220 |
+
@echo "$(GREEN)✓ Pushed to both Hugging Face and GitHub$(NC)"
|
| 221 |
+
|
| 222 |
+
# Development workflow
|
| 223 |
+
dev: format lint-fix ## Format and lint code
|
| 224 |
+
@echo "$(GREEN)✓ Code formatted and linted$(NC)"
|
| 225 |
+
|
| 226 |
+
pre-commit: format lint type-check test-fast ## Pre-commit checks (format, lint, type-check, fast tests)
|
| 227 |
+
@echo "$(GREEN)✓ Pre-commit checks passed!$(NC)"
|
| 228 |
+
|
| 229 |
+
ci: lint type-check test-cov ## Run CI checks (lint, type-check, test with coverage)
|
| 230 |
+
@echo "$(GREEN)✓ CI checks passed!$(NC)"
|
pyproject.toml
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "scientific-content-agent"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "AI-powered multi-agent system for generating research-backed content"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.11"
|
| 7 |
+
license = { text = "MIT" }
|
| 8 |
+
authors = [
|
| 9 |
+
{ name = "Christophe Bourgoin", email = "chris@example.com" }
|
| 10 |
+
]
|
| 11 |
+
keywords = [
|
| 12 |
+
"ai",
|
| 13 |
+
"agents",
|
| 14 |
+
"content-generation",
|
| 15 |
+
"multi-agent",
|
| 16 |
+
"google-adk",
|
| 17 |
+
"gradio",
|
| 18 |
+
"research",
|
| 19 |
+
]
|
| 20 |
+
classifiers = [
|
| 21 |
+
"Development Status :: 3 - Alpha",
|
| 22 |
+
"Intended Audience :: Developers",
|
| 23 |
+
"License :: OSI Approved :: MIT License",
|
| 24 |
+
"Programming Language :: Python :: 3",
|
| 25 |
+
"Programming Language :: Python :: 3.11",
|
| 26 |
+
"Programming Language :: Python :: 3.12",
|
| 27 |
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
dependencies = [
|
| 31 |
+
"google-adk>=0.1.0",
|
| 32 |
+
"google-genai>=0.1.0",
|
| 33 |
+
"google-cloud-aiplatform>=1.38.0",
|
| 34 |
+
"gradio>=6.0.1",
|
| 35 |
+
"python-dotenv>=1.0.0",
|
| 36 |
+
"requests>=2.31.0",
|
| 37 |
+
"duckduckgo-search>=6.0.0",
|
| 38 |
+
"pyyaml>=6.0",
|
| 39 |
+
"pandas>=2.0.0",
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
[project.optional-dependencies]
|
| 43 |
+
dev = [
|
| 44 |
+
"pytest>=8.0.0",
|
| 45 |
+
"pytest-asyncio>=0.23.0",
|
| 46 |
+
"pytest-cov>=4.1.0",
|
| 47 |
+
"ruff>=0.1.0",
|
| 48 |
+
"mypy>=1.8.0",
|
| 49 |
+
"types-requests>=2.31.0",
|
| 50 |
+
"types-PyYAML>=6.0.0",
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
[project.urls]
|
| 54 |
+
Homepage = "https://github.com/ChrisBg/scientific-content-agent-interface"
|
| 55 |
+
Repository = "https://github.com/ChrisBg/scientific-content-agent-interface"
|
| 56 |
+
"Bug Tracker" = "https://github.com/ChrisBg/scientific-content-agent-interface/issues"
|
| 57 |
+
"Hugging Face Space" = "https://huggingface.co/spaces/Chris30/scientific-content-agent"
|
| 58 |
+
|
| 59 |
+
[project.scripts]
|
| 60 |
+
scientific-content-agent = "main:main"
|
| 61 |
+
|
| 62 |
+
[build-system]
|
| 63 |
+
requires = ["hatchling"]
|
| 64 |
+
build-backend = "hatchling.build"
|
| 65 |
+
|
| 66 |
+
[tool.hatch.build.targets.wheel]
|
| 67 |
+
packages = ["src"]
|
| 68 |
+
|
| 69 |
+
# Ruff configuration
|
| 70 |
+
[tool.ruff]
|
| 71 |
+
line-length = 100
|
| 72 |
+
target-version = "py311"
|
| 73 |
+
exclude = [
|
| 74 |
+
".git",
|
| 75 |
+
".venv",
|
| 76 |
+
"__pycache__",
|
| 77 |
+
"build",
|
| 78 |
+
"dist",
|
| 79 |
+
"output",
|
| 80 |
+
"logs",
|
| 81 |
+
".pytest_cache",
|
| 82 |
+
]
|
| 83 |
+
|
| 84 |
+
[tool.ruff.lint]
|
| 85 |
+
select = [
|
| 86 |
+
"E", # pycodestyle errors
|
| 87 |
+
"W", # pycodestyle warnings
|
| 88 |
+
"F", # pyflakes
|
| 89 |
+
"I", # isort
|
| 90 |
+
"B", # flake8-bugbear
|
| 91 |
+
"C4", # flake8-comprehensions
|
| 92 |
+
"UP", # pyupgrade
|
| 93 |
+
"ARG", # flake8-unused-arguments
|
| 94 |
+
"SIM", # flake8-simplify
|
| 95 |
+
]
|
| 96 |
+
ignore = [
|
| 97 |
+
"E501", # line too long (handled by formatter)
|
| 98 |
+
"B008", # do not perform function calls in argument defaults
|
| 99 |
+
"B905", # zip without strict parameter
|
| 100 |
+
]
|
| 101 |
+
|
| 102 |
+
[tool.ruff.lint.per-file-ignores]
|
| 103 |
+
"__init__.py" = ["F401"] # unused imports
|
| 104 |
+
"tests/**/*.py" = ["ARG"] # unused arguments in tests
|
| 105 |
+
|
| 106 |
+
[tool.ruff.format]
|
| 107 |
+
quote-style = "double"
|
| 108 |
+
indent-style = "space"
|
| 109 |
+
line-ending = "auto"
|
| 110 |
+
|
| 111 |
+
# Mypy configuration
|
| 112 |
+
[tool.mypy]
|
| 113 |
+
python_version = "3.11"
|
| 114 |
+
warn_return_any = true
|
| 115 |
+
warn_unused_configs = true
|
| 116 |
+
disallow_untyped_defs = false
|
| 117 |
+
disallow_incomplete_defs = false
|
| 118 |
+
check_untyped_defs = true
|
| 119 |
+
disallow_untyped_decorators = false
|
| 120 |
+
no_implicit_optional = true
|
| 121 |
+
warn_redundant_casts = true
|
| 122 |
+
warn_unused_ignores = true
|
| 123 |
+
warn_no_return = true
|
| 124 |
+
strict_equality = true
|
| 125 |
+
ignore_missing_imports = true
|
| 126 |
+
|
| 127 |
+
[[tool.mypy.overrides]]
|
| 128 |
+
module = [
|
| 129 |
+
"google.adk.*",
|
| 130 |
+
"google.genai.*",
|
| 131 |
+
"duckduckgo_search.*",
|
| 132 |
+
]
|
| 133 |
+
ignore_missing_imports = true
|
| 134 |
+
|
| 135 |
+
# Pytest configuration
|
| 136 |
+
[tool.pytest.ini_options]
|
| 137 |
+
minversion = "8.0"
|
| 138 |
+
testpaths = ["tests"]
|
| 139 |
+
python_files = ["test_*.py"]
|
| 140 |
+
python_classes = ["Test*"]
|
| 141 |
+
python_functions = ["test_*"]
|
| 142 |
+
addopts = [
|
| 143 |
+
"-v",
|
| 144 |
+
"--strict-markers",
|
| 145 |
+
"--tb=short",
|
| 146 |
+
"--cov=src",
|
| 147 |
+
"--cov-report=term-missing",
|
| 148 |
+
"--cov-report=html",
|
| 149 |
+
"--cov-report=xml",
|
| 150 |
+
]
|
| 151 |
+
asyncio_mode = "auto"
|
| 152 |
+
markers = [
|
| 153 |
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
| 154 |
+
"integration: marks tests as integration tests",
|
| 155 |
+
"unit: marks tests as unit tests",
|
| 156 |
+
]
|
| 157 |
+
|
| 158 |
+
# Coverage configuration
|
| 159 |
+
[tool.coverage.run]
|
| 160 |
+
source = ["src"]
|
| 161 |
+
omit = [
|
| 162 |
+
"*/tests/*",
|
| 163 |
+
"*/__pycache__/*",
|
| 164 |
+
"*/site-packages/*",
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
[tool.coverage.report]
|
| 168 |
+
precision = 2
|
| 169 |
+
show_missing = true
|
| 170 |
+
skip_covered = false
|
| 171 |
+
exclude_lines = [
|
| 172 |
+
"pragma: no cover",
|
| 173 |
+
"def __repr__",
|
| 174 |
+
"raise AssertionError",
|
| 175 |
+
"raise NotImplementedError",
|
| 176 |
+
"if __name__ == .__main__.:",
|
| 177 |
+
"if TYPE_CHECKING:",
|
| 178 |
+
"class .*\\bProtocol\\):",
|
| 179 |
+
"@(abc\\.)?abstractmethod",
|
| 180 |
+
]
|
tests/__init__.py
ADDED
|
File without changes
|
tests/test_tools.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for custom tools."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from src.tools import (
|
| 6 |
+
analyze_content_for_opportunities,
|
| 7 |
+
create_engagement_hooks,
|
| 8 |
+
extract_key_findings,
|
| 9 |
+
format_for_platform,
|
| 10 |
+
generate_citations,
|
| 11 |
+
generate_seo_keywords,
|
| 12 |
+
search_industry_trends,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class TestFormatForPlatform:
|
| 17 |
+
"""Tests for format_for_platform tool."""
|
| 18 |
+
|
| 19 |
+
@pytest.mark.unit
|
| 20 |
+
def test_format_blog(self):
|
| 21 |
+
"""Test blog formatting."""
|
| 22 |
+
result = format_for_platform("Test content", "blog", "AI Research")
|
| 23 |
+
assert result["status"] == "success"
|
| 24 |
+
assert result["platform"] == "blog"
|
| 25 |
+
assert "markdown" in result["metadata"]["format"]
|
| 26 |
+
assert "AI Research" in result["formatted_content"]
|
| 27 |
+
|
| 28 |
+
@pytest.mark.unit
|
| 29 |
+
def test_format_linkedin(self):
|
| 30 |
+
"""Test LinkedIn formatting."""
|
| 31 |
+
result = format_for_platform("Test content", "linkedin", "ML Topic")
|
| 32 |
+
assert result["status"] == "success"
|
| 33 |
+
assert result["platform"] == "linkedin"
|
| 34 |
+
assert "Key Takeaways" in result["formatted_content"]
|
| 35 |
+
|
| 36 |
+
@pytest.mark.unit
|
| 37 |
+
def test_format_twitter(self):
|
| 38 |
+
"""Test Twitter formatting."""
|
| 39 |
+
result = format_for_platform("Test content", "twitter", "AI News")
|
| 40 |
+
assert result["status"] == "success"
|
| 41 |
+
assert result["platform"] == "twitter"
|
| 42 |
+
assert "Thread" in result["formatted_content"]
|
| 43 |
+
|
| 44 |
+
@pytest.mark.unit
|
| 45 |
+
def test_invalid_platform(self):
|
| 46 |
+
"""Test invalid platform error."""
|
| 47 |
+
result = format_for_platform("Test content", "invalid", "Topic")
|
| 48 |
+
assert result["status"] == "error"
|
| 49 |
+
assert "Unsupported platform" in result["error_message"]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class TestGenerateCitations:
|
| 53 |
+
"""Tests for generate_citations tool."""
|
| 54 |
+
|
| 55 |
+
@pytest.mark.unit
|
| 56 |
+
def test_apa_citations(self):
|
| 57 |
+
"""Test APA citation generation."""
|
| 58 |
+
sources = [
|
| 59 |
+
{
|
| 60 |
+
"title": "Test Paper",
|
| 61 |
+
"authors": "Smith, J.",
|
| 62 |
+
"link": "https://arxiv.org/abs/123",
|
| 63 |
+
"year": "2024",
|
| 64 |
+
}
|
| 65 |
+
]
|
| 66 |
+
result = generate_citations(sources, "apa")
|
| 67 |
+
assert result["status"] == "success"
|
| 68 |
+
assert len(result["citations"]) == 1
|
| 69 |
+
assert "Smith, J." in result["citations"][0]
|
| 70 |
+
assert "(2024)" in result["citations"][0]
|
| 71 |
+
|
| 72 |
+
@pytest.mark.unit
|
| 73 |
+
def test_empty_sources(self):
|
| 74 |
+
"""Test error with no sources."""
|
| 75 |
+
result = generate_citations([])
|
| 76 |
+
assert result["status"] == "error"
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class TestExtractKeyFindings:
|
| 80 |
+
"""Tests for extract_key_findings tool."""
|
| 81 |
+
|
| 82 |
+
@pytest.mark.unit
|
| 83 |
+
def test_extract_findings(self):
|
| 84 |
+
"""Test key findings extraction."""
|
| 85 |
+
text = "Research found that AI improves efficiency. Studies showed significant results."
|
| 86 |
+
result = extract_key_findings(text, max_findings=2)
|
| 87 |
+
assert result["status"] == "success"
|
| 88 |
+
assert len(result["findings"]) <= 2
|
| 89 |
+
|
| 90 |
+
@pytest.mark.unit
|
| 91 |
+
def test_insufficient_text(self):
|
| 92 |
+
"""Test error with short text."""
|
| 93 |
+
result = extract_key_findings("Too short", max_findings=5)
|
| 94 |
+
assert result["status"] == "error"
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class TestGenerateSeoKeywords:
|
| 98 |
+
"""Tests for generate_seo_keywords tool."""
|
| 99 |
+
|
| 100 |
+
@pytest.mark.unit
|
| 101 |
+
def test_keyword_generation(self):
|
| 102 |
+
"""Test SEO keyword generation."""
|
| 103 |
+
result = generate_seo_keywords("Machine Learning", "AI Consultant")
|
| 104 |
+
assert result["status"] == "success"
|
| 105 |
+
assert len(result["primary_keywords"]) > 0
|
| 106 |
+
assert len(result["technical_keywords"]) > 0
|
| 107 |
+
assert "AI Consultant" in result["primary_keywords"]
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class TestCreateEngagementHooks:
|
| 111 |
+
"""Tests for create_engagement_hooks tool."""
|
| 112 |
+
|
| 113 |
+
@pytest.mark.unit
|
| 114 |
+
def test_opportunities_goal(self):
|
| 115 |
+
"""Test hooks for opportunities goal."""
|
| 116 |
+
result = create_engagement_hooks("AI Agents", "opportunities")
|
| 117 |
+
assert result["status"] == "success"
|
| 118 |
+
assert len(result["opening_hooks"]) > 0
|
| 119 |
+
assert len(result["closing_ctas"]) > 0
|
| 120 |
+
assert result["goal"] == "opportunities"
|
| 121 |
+
|
| 122 |
+
@pytest.mark.unit
|
| 123 |
+
def test_discussion_goal(self):
|
| 124 |
+
"""Test hooks for discussion goal."""
|
| 125 |
+
result = create_engagement_hooks("NLP", "discussion")
|
| 126 |
+
assert result["status"] == "success"
|
| 127 |
+
assert len(result["discussion_questions"]) > 0
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class TestAnalyzeContentForOpportunities:
|
| 131 |
+
"""Tests for analyze_content_for_opportunities tool."""
|
| 132 |
+
|
| 133 |
+
@pytest.mark.unit
|
| 134 |
+
def test_content_analysis(self):
|
| 135 |
+
"""Test content opportunity analysis."""
|
| 136 |
+
content = """
|
| 137 |
+
As an AI Consultant specializing in Machine Learning, I've built production systems
|
| 138 |
+
using PyTorch and TensorFlow. Let's connect to discuss how AI can solve your business problems.
|
| 139 |
+
Check out my GitHub for real-world implementations.
|
| 140 |
+
"""
|
| 141 |
+
result = analyze_content_for_opportunities(content, "AI Consultant")
|
| 142 |
+
assert result["status"] == "success"
|
| 143 |
+
assert "opportunity_score" in result
|
| 144 |
+
assert "seo_score" in result
|
| 145 |
+
assert "engagement_score" in result
|
| 146 |
+
assert 0 <= result["opportunity_score"] <= 100
|
| 147 |
+
|
| 148 |
+
@pytest.mark.unit
|
| 149 |
+
def test_short_content_error(self):
|
| 150 |
+
"""Test error with too short content."""
|
| 151 |
+
result = analyze_content_for_opportunities("Too short")
|
| 152 |
+
assert result["status"] == "error"
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class TestSearchIndustryTrends:
|
| 156 |
+
"""Tests for search_industry_trends tool."""
|
| 157 |
+
|
| 158 |
+
@pytest.mark.integration
|
| 159 |
+
@pytest.mark.slow
|
| 160 |
+
def test_trend_search(self):
|
| 161 |
+
"""Test industry trend search (requires internet)."""
|
| 162 |
+
result = search_industry_trends("Machine Learning", "global", max_results=3)
|
| 163 |
+
assert result["status"] == "success"
|
| 164 |
+
assert "trends" in result
|
| 165 |
+
assert "hot_skills" in result
|
| 166 |
+
assert len(result["hot_skills"]) > 0
|