Add AGENTS.md with coding guidelines for AI agents
Comprehensive guide covering: - Build/lint/test commands including single test execution - Code style: imports, formatting, types, naming, error handling - Discord patterns: @logged_command, autocomplete, embed emojis - Service layer abstraction rules - Model patterns (from_api_data, required IDs) - Testing with aioresponses and complete model data - Critical rules for git, services, and commits 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2881207ca4
commit
d6ec2a11ec
190
AGENTS.md
Normal file
190
AGENTS.md
Normal file
@ -0,0 +1,190 @@
|
||||
# AGENTS.md - Discord Bot v2.0
|
||||
|
||||
Guidelines for AI coding agents working in this repository.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Start bot**: `python bot.py`
|
||||
**Run all tests**: `python -m pytest --tb=short -q`
|
||||
**Run single test file**: `python -m pytest tests/test_models.py -v`
|
||||
**Run single test**: `python -m pytest tests/test_models.py::TestTeamModel::test_team_creation_minimal -v`
|
||||
**Run tests matching pattern**: `python -m pytest -k "test_player" -v`
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `bot.py` - Main entry point
|
||||
- `commands/` - Discord slash commands (package-based)
|
||||
- `services/` - API service layer (BaseService pattern)
|
||||
- `models/` - Pydantic data models
|
||||
- `views/` - Discord UI components (embeds, modals)
|
||||
- `utils/` - Logging, decorators, caching
|
||||
- `tests/` - pytest test suite
|
||||
|
||||
## Code Style
|
||||
|
||||
### Imports
|
||||
Order: stdlib, third-party, local. Separate groups with blank lines.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from typing import Optional, List
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from services.player_service import player_service
|
||||
from utils.decorators import logged_command
|
||||
```
|
||||
|
||||
### Formatting
|
||||
- Line length: 100 characters max
|
||||
- Docstrings: Google style with triple quotes
|
||||
- Indentation: 4 spaces
|
||||
- Trailing commas in multi-line structures
|
||||
|
||||
### Type Hints
|
||||
Always use type hints for function signatures:
|
||||
|
||||
```python
|
||||
async def get_player(self, player_id: int) -> Optional[Player]:
|
||||
async def search_players(self, query: str, limit: int = 10) -> List[Player]:
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
- Classes: `PascalCase` (PlayerService, TeamInfoCommands)
|
||||
- Functions/methods: `snake_case` (get_player, search_players)
|
||||
- Constants: `UPPER_SNAKE_CASE` (SBA_CURRENT_SEASON)
|
||||
- Private: prefix with `_` (_client, _team_service)
|
||||
|
||||
### Error Handling
|
||||
Use custom exceptions from `exceptions.py`. Prefer "raise or return" over Optional:
|
||||
|
||||
```python
|
||||
from exceptions import APIException, PlayerNotFoundError
|
||||
|
||||
async def get_player(self, player_id: int) -> Player:
|
||||
result = await self.get_by_id(player_id)
|
||||
if result is None:
|
||||
raise PlayerNotFoundError(f"Player {player_id} not found")
|
||||
return result
|
||||
```
|
||||
|
||||
## Discord Command Patterns
|
||||
|
||||
### Always use @logged_command decorator
|
||||
Eliminates boilerplate logging. Class must have `self.logger` attribute:
|
||||
|
||||
```python
|
||||
class PlayerInfoCommands(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.logger = get_contextual_logger(f'{__name__}.PlayerInfoCommands')
|
||||
|
||||
@discord.app_commands.command(name="player")
|
||||
@logged_command("/player")
|
||||
async def player_command(self, interaction, name: str):
|
||||
# Business logic only - no try/catch boilerplate needed
|
||||
player = await player_service.get_player_by_name(name)
|
||||
await interaction.followup.send(embed=create_embed(player))
|
||||
```
|
||||
|
||||
### Autocomplete: Use standalone functions (not methods)
|
||||
```python
|
||||
async def player_name_autocomplete(
|
||||
interaction: discord.Interaction,
|
||||
current: str,
|
||||
) -> List[discord.app_commands.Choice[str]]:
|
||||
if len(current) < 2:
|
||||
return []
|
||||
try:
|
||||
players = await player_service.search_players(current, limit=25)
|
||||
return [discord.app_commands.Choice(name=p.name, value=p.name) for p in players]
|
||||
except Exception:
|
||||
return [] # Never break autocomplete
|
||||
|
||||
class MyCommands(commands.Cog):
|
||||
@discord.app_commands.command()
|
||||
@discord.app_commands.autocomplete(name=player_name_autocomplete)
|
||||
async def my_command(self, interaction, name: str): ...
|
||||
```
|
||||
|
||||
### Embed emoji rules
|
||||
Template methods auto-add emojis. Never double up:
|
||||
|
||||
```python
|
||||
# CORRECT - template adds emoji
|
||||
embed = EmbedTemplate.success(title="Operation Completed") # Results in: "Operation Completed"
|
||||
|
||||
# WRONG - double emoji
|
||||
embed = EmbedTemplate.success(title="Operation Completed") # Results in: " Operation Completed"
|
||||
|
||||
# For custom emoji, use create_base_embed
|
||||
embed = EmbedTemplate.create_base_embed(title="Custom Title", color=EmbedColors.SUCCESS)
|
||||
```
|
||||
|
||||
## Service Layer
|
||||
|
||||
### Never bypass services for API calls
|
||||
```python
|
||||
# CORRECT
|
||||
player = await player_service.get_player(player_id)
|
||||
|
||||
# WRONG - never do this
|
||||
client = await player_service.get_client()
|
||||
await client.get(f'players/{player_id}')
|
||||
```
|
||||
|
||||
### Key service methods
|
||||
- `TeamService.get_team(team_id)` - not `get_team_by_id()`
|
||||
- `PlayerService.search_players(query, limit, all_seasons=True)` - cross-season search
|
||||
|
||||
## Models
|
||||
|
||||
### Use from_api_data() classmethod
|
||||
```python
|
||||
player = Player.from_api_data(api_response)
|
||||
```
|
||||
|
||||
### Database entities require id field
|
||||
```python
|
||||
class Player(SBABaseModel):
|
||||
id: int = Field(..., description="Player ID from database") # Required, not Optional
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Use aioresponses for HTTP mocking
|
||||
```python
|
||||
from aioresponses import aioresponses
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_player():
|
||||
with aioresponses() as m:
|
||||
m.get("https://api.example.com/v3/players/1", payload={"id": 1, "name": "Test"})
|
||||
result = await api_client.get("players", object_id=1)
|
||||
assert result["name"] == "Test"
|
||||
```
|
||||
|
||||
### Provide complete model data
|
||||
Pydantic validates all fields. Use helper functions for test data:
|
||||
|
||||
```python
|
||||
def create_player_data(player_id: int, name: str, **kwargs):
|
||||
return {"id": player_id, "name": name, "wara": 2.5, "season": 13, "pos_1": "CF", **kwargs}
|
||||
```
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Git**: Never commit directly to `main`. Create feature branches.
|
||||
2. **Services**: Always use service layer methods, never direct API client access.
|
||||
3. **Embeds**: Don't add emojis to titles when using template methods (success/error/warning/info).
|
||||
4. **Tests**: Include docstrings explaining "what" and "why" for each test.
|
||||
5. **Commits**: Do not commit without user approval.
|
||||
|
||||
## Documentation
|
||||
|
||||
Check `CLAUDE.md` files in directories for detailed patterns:
|
||||
- `commands/CLAUDE.md` - Command architecture
|
||||
- `services/CLAUDE.md` - Service patterns
|
||||
- `models/CLAUDE.md` - Model validation
|
||||
- `tests/CLAUDE.md` - Testing strategies
|
||||
Loading…
Reference in New Issue
Block a user