python

Par elophanto · elophanto

npx skills add https://github.com/elophanto/elophanto --skill python

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

  1. Lisez le code existant dans la zone cible (self_read_source ou file_read).
  2. Respectez les patterns déjà en place — nommage, gestion d'erreurs, imports.
  3. Vérifiez si quelque chose de similaire existe (self_list_capabilities pour les tools, file_list avec le pattern *.py pour le code général).
  4. 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 | None et non Optional[str]
  • from __future__ import annotations en 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.Path au lieu de os.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, retournez ToolResult(success=False, error=...)
  • Utilisez async/await pour TOUS les I/O (fichier, réseau, subprocess)
  • description est ce que le LLM lit — écrivez-la comme une chaîne d'aide, pas un commentaire de code
  • input_schema doit ê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.asyncio sur les fonctions de test async
  • Structure de test : propriétés de l'interface → chemin heureux → cas d'erreur
  • Exécution : self_run_tests ou python -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 with ou 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 .env ne 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.

Skills similaires