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>

Files changed (6) hide show
  1. .gitignore +175 -0
  2. CLAUDE.md +219 -0
  3. Makefile +230 -0
  4. pyproject.toml +180 -0
  5. tests/__init__.py +0 -0
  6. 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