Développement Python
Description
Guide de codage Python pour EloPhanto — couvre le développement de plugins, les patterns async, la gestion d'erreurs, les tests avec pytest, et l'interface BaseTool.
Triggers
- python
- build plugin
- create plugin
- create tool
- modify source
- pytest
- async
- pip
- pyproject
Instructions
1. Avant d'écrire du code
- Lisez le code existant dans la zone cible (self_read_source ou file_read).
- Respectez les patterns déjà en place — nommage, gestion d'erreurs, imports.
- Vérifiez si quelque chose de similaire existe (self_list_capabilities pour les tools,
file_list avec le pattern
*.pypour le code général). - Identifiez les cas limites en amont : entrée vide, paramètres manquants, fichier non trouvé, permission refusée, timeouts.
2. Style
- Python 3.12+ — utilisez
str | Noneet nonOptional[str] from __future__ import annotationsen haut de chaque fichier- Annotations de type sur TOUTES les signatures de fonction (paramètres ET types de retour)
- Ordre des imports : stdlib → tiers → projet (ruff l'impose)
- Utilisez
pathlib.Pathau lieu deos.path - Utilisez les f-strings pour le formatage
- Longueur de ligne : 100 caractères maximum
- Pas de code mort — supprimez les blocs commentés et les imports inutilisés
3. Interface de Plugin EloPhanto
Chaque tool doit implémenter la classe abstraite BaseTool :
from __future__ import annotations
from typing import Any
from tools.base import BaseTool, PermissionLevel, ToolResult
class MyTool(BaseTool):
@property
def name(self) -> str:
return "my_tool" # snake_case, unique parmi tous les tools
@property
def description(self) -> str:
return "Description claire et actinelle que le LLM lit pour décider d'utiliser ce tool."
@property
def input_schema(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"param": {"type": "string", "description": "Ce que cela fait"},
},
"required": ["param"],
}
@property
def permission_level(self) -> PermissionLevel:
return PermissionLevel.MODERATE # SAFE | MODERATE | DESTRUCTIVE | CRITICAL
async def execute(self, params: dict[str, Any]) -> ToolResult:
try:
result = await do_something(params["param"])
return ToolResult(success=True, data={"output": result})
except Exception as e:
return ToolResult(success=False, error=f"Failed: {e}")
Règles critiques :
- NE JAMAIS lever d'exception depuis
execute()— capturez toutes les exceptions, retournezToolResult(success=False, error=...) - Utilisez
async/awaitpour TOUS les I/O (fichier, réseau, subprocess) descriptionest ce que le LLM lit — écrivez-la comme une chaîne d'aide, pas un commentaire de codeinput_schemadoit être un JSON Schema valide avec descriptions pour chaque propriété- Gardez les dépendances minimales — préférez stdlib aux packages externes
4. Gestion d'erreurs
# Exceptions spécifiques avec messages informatifs
try:
data = json.loads(response)
except json.JSONDecodeError as e:
return ToolResult(success=False, error=f"Invalid JSON: {e}")
# Early returns pour la validation (clauses de garde)
async def execute(self, params):
path = Path(params["path"])
if not path.exists():
return ToolResult(success=False, error=f"Not found: {path}")
if not path.is_file():
return ToolResult(success=False, error=f"Not a file: {path}")
# logique principale après que les gardes passent
Anti-patterns :
- Bare
except:— toujours capturer les exceptions spécifiques - Avaler les erreurs en silence — toujours enregistrer ou retourner l'erreur
- Lever une exception depuis execute() — la boucle d'agent s'attend à ToolResult, pas à des exceptions
5. Patterns Async
import asyncio
# Subprocess avec timeout
proc = await asyncio.create_subprocess_exec(
"command", "arg1",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30)
except asyncio.TimeoutError:
proc.kill()
await proc.communicate()
return ToolResult(success=False, error="Command timed out")
# Opérations parallèles
results = await asyncio.gather(task_a(), task_b(), return_exceptions=True)
# I/O fichier (sync est correct pour les petits fichiers dans un contexte asyncio)
content = Path("file.txt").read_text(encoding="utf-8")
6. Tests
- Framework : pytest avec
pytest-asyncio(asyncio_mode="auto") @pytest.mark.asynciosur les fonctions de test async- Structure de test : propriétés de l'interface → chemin heureux → cas d'erreur
- Exécution :
self_run_testsoupython -m pytest tests/ -v --tb=short - Isolez les tests — pas de dépendances de service externe
import pytest
from plugins.my_tool.plugin import MyTool
@pytest.mark.asyncio
async def test_execute_success():
tool = MyTool()
result = await tool.execute({"param": "valid_input"})
assert result.success
assert "output" in result.data
@pytest.mark.asyncio
async def test_execute_missing_param():
tool = MyTool()
result = await tool.execute({})
assert not result.success
7. Checklist de Révision de Code
- Sécurité : Validation des entrées, pas de fuite de credentials, vérifications de traversée de répertoire
- Ressources : Fichiers/connexions fermés (utilisez
withou try/finally) - Cas limites : Entrée vide, paramètres manquants, timeouts, fichiers volumineux
- Gestion d'erreurs : Défaillances gracieuses, messages d'erreur informatifs
- Types : Toutes les signatures typées, mypy passe
- Tests : Le nouveau code a une couverture de test
8. Outillage
- ruff pour le linting (règles : E, F, I, UP, B) — exécutez avec
ruff check . - mypy pour la vérification de type (cible Python 3.12, strict=false)
- pytest pour les tests (asyncio_mode="auto")
- uv pour la gestion des packages
Vérifier
- Le code a été réellement exécuté (ou type-vérifié / linté selon les besoins) et la sortie de la commande est capturée
- Les dépendances et versions de runtime utilisées sont épinglées et enregistrées (par ex. requirements.txt, package.json + lockfile, .nvmrc)
- Les erreurs ou avertissements émis par l'exécution sont traités ou explicitement acceptés avec une raison
- Les nouveaux I/O externes (réseau, filesystem, DB) ont des timeouts et une gestion d'erreurs, pas de défaillance silencieuse
- Les tests pour la modification ont été exécutés et le décompte des réussites/échecs est dans la transcript
- Les secrets et credentials sont lus depuis env/secret store, pas en dur, et les fichiers
.envne sont pas committes
Notes
Les plugins EloPhanto se trouvent dans plugins/<name>/plugin.py. Ils sont enregistrés dans
core/registry.py et leurs dépendances injectées dans core/agent.py. Utilisez
self_read_source pour étudier les tools existants avant d'en créer de nouveaux.