llamagent 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. llamagent-0.1.0/.github/workflows/ci.yml +47 -0
  2. llamagent-0.1.0/.github/workflows/release.yml +64 -0
  3. llamagent-0.1.0/.gitignore +10 -0
  4. llamagent-0.1.0/.python-version +1 -0
  5. llamagent-0.1.0/LICENSE +21 -0
  6. llamagent-0.1.0/PKG-INFO +147 -0
  7. llamagent-0.1.0/README.md +121 -0
  8. llamagent-0.1.0/demo/stats.py +19 -0
  9. llamagent-0.1.0/docs/interface.png +0 -0
  10. llamagent-0.1.0/pyproject.toml +44 -0
  11. llamagent-0.1.0/scripts/check_package.py +38 -0
  12. llamagent-0.1.0/src/llamagent/__init__.py +1 -0
  13. llamagent-0.1.0/src/llamagent/agent.py +124 -0
  14. llamagent-0.1.0/src/llamagent/cli.py +119 -0
  15. llamagent-0.1.0/src/llamagent/commands.py +250 -0
  16. llamagent-0.1.0/src/llamagent/config.py +123 -0
  17. llamagent-0.1.0/src/llamagent/context.py +109 -0
  18. llamagent-0.1.0/src/llamagent/guards.py +66 -0
  19. llamagent-0.1.0/src/llamagent/history.py +82 -0
  20. llamagent-0.1.0/src/llamagent/llm.py +49 -0
  21. llamagent-0.1.0/src/llamagent/prompts/AGENT.md +22 -0
  22. llamagent-0.1.0/src/llamagent/prompts/COMPACT.md +12 -0
  23. llamagent-0.1.0/src/llamagent/session.py +147 -0
  24. llamagent-0.1.0/src/llamagent/skills/corriger-un-bug.md +12 -0
  25. llamagent-0.1.0/src/llamagent/skills/ecrire-tests-pytest.md +14 -0
  26. llamagent-0.1.0/src/llamagent/tools.py +317 -0
  27. llamagent-0.1.0/src/llamagent/ui.py +317 -0
  28. llamagent-0.1.0/steps/step1_hello.py +67 -0
  29. llamagent-0.1.0/steps/step2_chat.py +66 -0
  30. llamagent-0.1.0/steps/step3_tool.py +78 -0
  31. llamagent-0.1.0/steps/step4_loop.py +96 -0
  32. llamagent-0.1.0/tests/test_agent.py +120 -0
  33. llamagent-0.1.0/tests/test_cli.py +26 -0
  34. llamagent-0.1.0/tests/test_commands.py +135 -0
  35. llamagent-0.1.0/tests/test_config.py +67 -0
  36. llamagent-0.1.0/tests/test_context.py +73 -0
  37. llamagent-0.1.0/tests/test_guards.py +69 -0
  38. llamagent-0.1.0/tests/test_history.py +94 -0
  39. llamagent-0.1.0/tests/test_session.py +64 -0
  40. llamagent-0.1.0/tests/test_tools.py +186 -0
  41. llamagent-0.1.0/tests/test_ui.py +115 -0
  42. llamagent-0.1.0/try_step5.py +27 -0
  43. llamagent-0.1.0/uv.lock +272 -0
@@ -0,0 +1,47 @@
1
+ # Intégration continue (étape 18) : à chaque push sur main et à chaque pull
2
+ # request, les tests tournent sous Linux, Windows et macOS, avec Python 3.11
3
+ # à 3.13. Puis le paquet est construit, vérifié et installé comme le ferait
4
+ # un utilisateur.
5
+ name: CI
6
+
7
+ on:
8
+ push:
9
+ branches: [main]
10
+ pull_request:
11
+
12
+ jobs:
13
+ tests:
14
+ name: Tests (${{ matrix.os }}, Python ${{ matrix.python }})
15
+ runs-on: ${{ matrix.os }}
16
+ strategy:
17
+ fail-fast: false # voir tous les systèmes en échec, pas seulement le premier
18
+ matrix:
19
+ os: [ubuntu-latest, windows-latest, macos-latest]
20
+ python: ["3.11", "3.12", "3.13"]
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: astral-sh/setup-uv@v6
24
+ with:
25
+ python-version: ${{ matrix.python }}
26
+ - name: Installer les dépendances (versions exactes de uv.lock)
27
+ run: uv sync --locked
28
+ - name: Tests
29
+ run: uv run pytest -q
30
+
31
+ package:
32
+ name: Paquet
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ - uses: astral-sh/setup-uv@v6
37
+ - name: Construire
38
+ run: uv build
39
+ - name: Vérifier le contenu
40
+ run: python3 scripts/check_package.py
41
+ - name: Installer le paquet construit et le lancer
42
+ run: |
43
+ uv tool install dist/*.whl
44
+ LLAMAGENT="$(uv tool dir --bin)/llamagent"
45
+ "$LLAMAGENT" --version
46
+ mkdir "$RUNNER_TEMP/projet" && cd "$RUNNER_TEMP/projet"
47
+ echo /quit | "$LLAMAGENT"
@@ -0,0 +1,64 @@
1
+ # Publication (étape 18) : quand une étiquette de version (v0.1.0...) est
2
+ # poussée, le paquet est testé, construit et vérifié, publié sur TestPyPI,
3
+ # puis sur PyPI après validation manuelle (environnement « pypi » protégé).
4
+ # Aucun mot de passe ni jeton : PyPI fait confiance à ce workflow précis
5
+ # (Trusted Publishing), qui prouve son identité avec un jeton OIDC de GitHub.
6
+ name: Publication
7
+
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+
12
+ jobs:
13
+ build:
14
+ name: Tester et construire
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v6
19
+ - name: Tests
20
+ run: |
21
+ uv sync --locked
22
+ uv run pytest -q
23
+ - name: Construire
24
+ run: uv build
25
+ - name: Vérifier le paquet et la version
26
+ run: python3 scripts/check_package.py "$GITHUB_REF_NAME"
27
+ - uses: actions/upload-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ testpypi:
33
+ name: Publier sur TestPyPI
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment:
37
+ name: testpypi
38
+ url: https://test.pypi.org/p/llamagent
39
+ permissions:
40
+ id-token: write # nécessaire au Trusted Publishing
41
+ steps:
42
+ - uses: actions/download-artifact@v4
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+ - uses: pypa/gh-action-pypi-publish@release/v1
47
+ with:
48
+ repository-url: https://test.pypi.org/legacy/
49
+
50
+ pypi:
51
+ name: Publier sur PyPI
52
+ needs: testpypi
53
+ runs-on: ubuntu-latest
54
+ environment:
55
+ name: pypi
56
+ url: https://pypi.org/p/llamagent
57
+ permissions:
58
+ id-token: write
59
+ steps:
60
+ - uses: actions/download-artifact@v4
61
+ with:
62
+ name: dist
63
+ path: dist/
64
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nils Malmberg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.5
2
+ Name: llamagent
3
+ Version: 0.1.0
4
+ Summary: Un agent de code minimal autour d'Ollama, écrit à la main pour comprendre chaque brique.
5
+ Project-URL: Documentation, https://nils-malmberg.fr/project8.html
6
+ Project-URL: Repository, https://github.com/nils-malmberg/llamagent
7
+ Project-URL: Issues, https://github.com/nils-malmberg/llamagent/issues
8
+ Author: Nils Malmberg
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,coding-agent,llm,ollama,tutoriel
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Education
16
+ Classifier: Natural Language :: French
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: requests>=2.34.2
25
+ Description-Content-Type: text/markdown
26
+
27
+ # llamagent
28
+
29
+ Un agent de code minimal en Python, écrit à la main pour comprendre comment fonctionne un agent de code : boucle agentique, harness, outils, prompt système, skills et garde-fous.
30
+
31
+ Il pilote un LLM local servi par [Ollama](https://ollama.com) (par défaut `qwen3.5:4b`) via l'API native `/api/chat`. Aucun framework d'agent : une seule dépendance, `requests`.
32
+
33
+ ![Interface de llamagent : la mascotte, puis chaque tour de la boucle avec le modèle, le harness et l'outil](https://nils-malmberg.fr/media/projets/project8/interface.png)
34
+
35
+ 📖 Tutoriel complet, étape par étape : [nils-malmberg.fr/project8.html](https://nils-malmberg.fr/project8.html)
36
+
37
+ ## Installation
38
+
39
+ Prérequis : un serveur [Ollama](https://ollama.com) avec un modèle qui gère les outils (`ollama pull qwen3.5:4b`).
40
+
41
+ ```bash
42
+ uv tool install llamagent # ou : pipx install llamagent
43
+ ```
44
+
45
+ Si Ollama ne tourne pas sur la même machine, indiquez son adresse dans `~/.llamagent/config.toml` (sous Windows : `C:\Users\<vous>\.llamagent\config.toml`) :
46
+
47
+ ```toml
48
+ url = "http://192.168.1.76:11434"
49
+ ```
50
+
51
+ ## Utilisation
52
+
53
+ ```bash
54
+ cd chemin/vers/mon-projet
55
+ llamagent # mode conversation
56
+ llamagent "Trouve le bug dans @calc.py" # une seule demande, puis on quitte
57
+ llamagent -c # reprendre la dernière conversation
58
+ llamagent -m qwen3.5:9b -t 0.5 # choisir le modèle et la température
59
+ llamagent --help
60
+ ```
61
+
62
+ Le dossier depuis lequel on lance `llamagent` devient le dossier de travail : les outils de fichiers ne peuvent ni lire ni écrire ailleurs.
63
+
64
+ Chaque tour de la boucle est affiché avec ses trois acteurs : le **modèle** (ce qu'il demande), le **harness** (ce que les garde-fous en font) et l'**outil** (ce qu'il renvoie).
65
+
66
+ ### Commandes
67
+
68
+ | Commande | Rôle |
69
+ |---|---|
70
+ | `/help` | liste des commandes |
71
+ | `/init` | préparer le projet : crée `AGENTS.md` et le dossier `.llamagent/` |
72
+ | `/config` | réglages actuels |
73
+ | `/models`, `/model qwen3.5:9b` | modèles installés, changer de modèle |
74
+ | `/temp 0.5`, `/ctx 16384` | changer la température, la taille du contexte |
75
+ | `/skills`, `/skill corriger-un-bug` | skills disponibles, charger une skill |
76
+ | `/context`, `/compact` | ce qui occupe le contexte, le compacter (automatique à 80 %) |
77
+ | `/sessions`, `/resume 2`, `/delete 2` | conversations sauvegardées |
78
+ | `/allow python -m pytest` | lancer une commande sans confirmation pour la session |
79
+ | `/clear`, `/quit` | effacer la conversation, quitter |
80
+
81
+ `@chemin` dans une demande joint le contenu du fichier.
82
+
83
+ ## Configuration
84
+
85
+ Les réglages viennent de cinq sources ; chacune remplace la précédente :
86
+
87
+ 1. les valeurs par défaut ;
88
+ 2. le fichier global `~/.llamagent/config.toml` ;
89
+ 3. le fichier du projet `.llamagent/config.toml` ;
90
+ 4. les variables d'environnement `OLLAMA_URL` et `OLLAMA_MODEL` ;
91
+ 5. les options de la ligne de commande.
92
+
93
+ ```toml
94
+ url = "http://localhost:11434"
95
+ model = "qwen3.5:4b"
96
+ temperature = 0.2
97
+ num_ctx = 8192
98
+ allowed_commands = ["python -m pytest", "git status", "git diff"]
99
+ ```
100
+
101
+ Autres variables : `LLAMAGENT_WORKDIR` (dossier de travail) et `NO_COLOR` (affichage sans couleurs).
102
+
103
+ ## Personnaliser un projet
104
+
105
+ ```
106
+ mon-projet/
107
+ ├── AGENTS.md # consignes du projet, ajoutées au prompt système
108
+ └── .llamagent/
109
+ ├── .gitignore # exclut sessions/ de git
110
+ ├── config.toml # réglages du projet
111
+ ├── sessions/ # conversations sauvegardées
112
+ └── skills/ # skills du projet (remplacent les globales de même nom)
113
+ ```
114
+
115
+ `/init` crée ces fichiers. Une skill est un fichier Markdown avec un en-tête `description:` ; le modèle la charge avec l'outil `load_skill` quand la demande correspond.
116
+
117
+ ```markdown
118
+ ---
119
+ description: Écrire des tests pytest pour un module Python.
120
+ ---
121
+ # Écrire des tests pytest
122
+
123
+ 1. Lis le module à tester avec read_file.
124
+ 2. ...
125
+ ```
126
+
127
+ ## Limites
128
+
129
+ - Seuls les outils de fichiers sont confinés au dossier de travail. Une commande lancée par `run_command` peut écrire ailleurs : la protection est la confirmation `[o/N]`. Les commandes autorisées sont un confort, pas une barrière de sécurité.
130
+ - Avec un modèle 4B et 8192 tokens, le contexte se remplit vite : `/compact` le résume.
131
+ - Un petit modèle peut inventer un outil, se tromper de chemin ou répéter un appel. Les erreurs lui sont renvoyées pour qu'il se corrige, mais il n'y arrive pas toujours.
132
+
133
+ ## Développement
134
+
135
+ ```bash
136
+ git clone https://github.com/nils-malmberg/llamagent.git
137
+ cd llamagent
138
+ uv sync
139
+ uv run pytest -q
140
+ uv tool install -e . # la commande llamagent utilise ce dossier
141
+ ```
142
+
143
+ Chaque push lance les tests sous Windows, macOS et Linux (Python 3.11 à 3.13). Pour publier une version : mettre à jour `version` dans `pyproject.toml`, puis créer et pousser l'étiquette correspondante (`git tag v0.1.0 && git push origin v0.1.0`) ; le paquet est publié sur TestPyPI, puis sur PyPI après validation.
144
+
145
+ ## Licence
146
+
147
+ MIT, voir [LICENSE](https://github.com/nils-malmberg/llamagent/blob/main/LICENSE).
@@ -0,0 +1,121 @@
1
+ # llamagent
2
+
3
+ Un agent de code minimal en Python, écrit à la main pour comprendre comment fonctionne un agent de code : boucle agentique, harness, outils, prompt système, skills et garde-fous.
4
+
5
+ Il pilote un LLM local servi par [Ollama](https://ollama.com) (par défaut `qwen3.5:4b`) via l'API native `/api/chat`. Aucun framework d'agent : une seule dépendance, `requests`.
6
+
7
+ ![Interface de llamagent : la mascotte, puis chaque tour de la boucle avec le modèle, le harness et l'outil](https://nils-malmberg.fr/media/projets/project8/interface.png)
8
+
9
+ 📖 Tutoriel complet, étape par étape : [nils-malmberg.fr/project8.html](https://nils-malmberg.fr/project8.html)
10
+
11
+ ## Installation
12
+
13
+ Prérequis : un serveur [Ollama](https://ollama.com) avec un modèle qui gère les outils (`ollama pull qwen3.5:4b`).
14
+
15
+ ```bash
16
+ uv tool install llamagent # ou : pipx install llamagent
17
+ ```
18
+
19
+ Si Ollama ne tourne pas sur la même machine, indiquez son adresse dans `~/.llamagent/config.toml` (sous Windows : `C:\Users\<vous>\.llamagent\config.toml`) :
20
+
21
+ ```toml
22
+ url = "http://192.168.1.76:11434"
23
+ ```
24
+
25
+ ## Utilisation
26
+
27
+ ```bash
28
+ cd chemin/vers/mon-projet
29
+ llamagent # mode conversation
30
+ llamagent "Trouve le bug dans @calc.py" # une seule demande, puis on quitte
31
+ llamagent -c # reprendre la dernière conversation
32
+ llamagent -m qwen3.5:9b -t 0.5 # choisir le modèle et la température
33
+ llamagent --help
34
+ ```
35
+
36
+ Le dossier depuis lequel on lance `llamagent` devient le dossier de travail : les outils de fichiers ne peuvent ni lire ni écrire ailleurs.
37
+
38
+ Chaque tour de la boucle est affiché avec ses trois acteurs : le **modèle** (ce qu'il demande), le **harness** (ce que les garde-fous en font) et l'**outil** (ce qu'il renvoie).
39
+
40
+ ### Commandes
41
+
42
+ | Commande | Rôle |
43
+ |---|---|
44
+ | `/help` | liste des commandes |
45
+ | `/init` | préparer le projet : crée `AGENTS.md` et le dossier `.llamagent/` |
46
+ | `/config` | réglages actuels |
47
+ | `/models`, `/model qwen3.5:9b` | modèles installés, changer de modèle |
48
+ | `/temp 0.5`, `/ctx 16384` | changer la température, la taille du contexte |
49
+ | `/skills`, `/skill corriger-un-bug` | skills disponibles, charger une skill |
50
+ | `/context`, `/compact` | ce qui occupe le contexte, le compacter (automatique à 80 %) |
51
+ | `/sessions`, `/resume 2`, `/delete 2` | conversations sauvegardées |
52
+ | `/allow python -m pytest` | lancer une commande sans confirmation pour la session |
53
+ | `/clear`, `/quit` | effacer la conversation, quitter |
54
+
55
+ `@chemin` dans une demande joint le contenu du fichier.
56
+
57
+ ## Configuration
58
+
59
+ Les réglages viennent de cinq sources ; chacune remplace la précédente :
60
+
61
+ 1. les valeurs par défaut ;
62
+ 2. le fichier global `~/.llamagent/config.toml` ;
63
+ 3. le fichier du projet `.llamagent/config.toml` ;
64
+ 4. les variables d'environnement `OLLAMA_URL` et `OLLAMA_MODEL` ;
65
+ 5. les options de la ligne de commande.
66
+
67
+ ```toml
68
+ url = "http://localhost:11434"
69
+ model = "qwen3.5:4b"
70
+ temperature = 0.2
71
+ num_ctx = 8192
72
+ allowed_commands = ["python -m pytest", "git status", "git diff"]
73
+ ```
74
+
75
+ Autres variables : `LLAMAGENT_WORKDIR` (dossier de travail) et `NO_COLOR` (affichage sans couleurs).
76
+
77
+ ## Personnaliser un projet
78
+
79
+ ```
80
+ mon-projet/
81
+ ├── AGENTS.md # consignes du projet, ajoutées au prompt système
82
+ └── .llamagent/
83
+ ├── .gitignore # exclut sessions/ de git
84
+ ├── config.toml # réglages du projet
85
+ ├── sessions/ # conversations sauvegardées
86
+ └── skills/ # skills du projet (remplacent les globales de même nom)
87
+ ```
88
+
89
+ `/init` crée ces fichiers. Une skill est un fichier Markdown avec un en-tête `description:` ; le modèle la charge avec l'outil `load_skill` quand la demande correspond.
90
+
91
+ ```markdown
92
+ ---
93
+ description: Écrire des tests pytest pour un module Python.
94
+ ---
95
+ # Écrire des tests pytest
96
+
97
+ 1. Lis le module à tester avec read_file.
98
+ 2. ...
99
+ ```
100
+
101
+ ## Limites
102
+
103
+ - Seuls les outils de fichiers sont confinés au dossier de travail. Une commande lancée par `run_command` peut écrire ailleurs : la protection est la confirmation `[o/N]`. Les commandes autorisées sont un confort, pas une barrière de sécurité.
104
+ - Avec un modèle 4B et 8192 tokens, le contexte se remplit vite : `/compact` le résume.
105
+ - Un petit modèle peut inventer un outil, se tromper de chemin ou répéter un appel. Les erreurs lui sont renvoyées pour qu'il se corrige, mais il n'y arrive pas toujours.
106
+
107
+ ## Développement
108
+
109
+ ```bash
110
+ git clone https://github.com/nils-malmberg/llamagent.git
111
+ cd llamagent
112
+ uv sync
113
+ uv run pytest -q
114
+ uv tool install -e . # la commande llamagent utilise ce dossier
115
+ ```
116
+
117
+ Chaque push lance les tests sous Windows, macOS et Linux (Python 3.11 à 3.13). Pour publier une version : mettre à jour `version` dans `pyproject.toml`, puis créer et pousser l'étiquette correspondante (`git tag v0.1.0 && git push origin v0.1.0`) ; le paquet est publié sur TestPyPI, puis sur PyPI après validation.
118
+
119
+ ## Licence
120
+
121
+ MIT, voir [LICENSE](https://github.com/nils-malmberg/llamagent/blob/main/LICENSE).
@@ -0,0 +1,19 @@
1
+ """Petit script de statistiques sur des notes.
2
+
3
+ Il contient un bug volontaire : c'est la démo de llamagent.
4
+ """
5
+
6
+
7
+ def moyenne(notes: list[float]) -> float:
8
+ total = 0
9
+ for i in range(1, len(notes)):
10
+ total += notes[i]
11
+ return total / len(notes)
12
+
13
+
14
+ if __name__ == "__main__":
15
+ notes = [12, 15, 9, 18]
16
+ resultat = moyenne(notes)
17
+ print(f"Moyenne des notes {notes} : {resultat}")
18
+ assert resultat == 13.5, f"moyenne attendue : 13.5, obtenue : {resultat}"
19
+ print("OK")
Binary file
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "llamagent"
3
+ version = "0.1.0"
4
+ description = "Un agent de code minimal autour d'Ollama, écrit à la main pour comprendre chaque brique."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Nils Malmberg" }]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "requests>=2.34.2",
12
+ ]
13
+ keywords = ["agent", "coding-agent", "llm", "ollama", "tutoriel"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Education",
19
+ "Natural Language :: French",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Software Development",
26
+ ]
27
+
28
+ [project.urls]
29
+ Documentation = "https://nils-malmberg.fr/project8.html"
30
+ Repository = "https://github.com/nils-malmberg/llamagent"
31
+ Issues = "https://github.com/nils-malmberg/llamagent/issues"
32
+
33
+ [project.scripts]
34
+ llamagent = "llamagent.cli:main"
35
+
36
+ [build-system]
37
+ # 1.27 : première version qui comprend « license = "MIT" » (PEP 639).
38
+ requires = ["hatchling>=1.27"]
39
+ build-backend = "hatchling.build"
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ "pytest>=9.1.1",
44
+ ]
@@ -0,0 +1,38 @@
1
+ """Vérifie le paquet construit avant de le publier (étape 18).
2
+
3
+ python scripts/check_package.py # le contenu du paquet (dist/)
4
+ python scripts/check_package.py v0.1.0 # et l'étiquette correspond à la version
5
+ """
6
+
7
+ import sys
8
+ import tomllib
9
+ import zipfile
10
+ from pathlib import Path
11
+
12
+ # Fichiers sans lesquels llamagent ne démarre pas : s'ils manquent dans le
13
+ # paquet, il s'installe sans erreur mais plante au premier lancement.
14
+ REQUIRED = [
15
+ "llamagent/cli.py",
16
+ "llamagent/prompts/AGENT.md",
17
+ "llamagent/prompts/COMPACT.md",
18
+ "llamagent/skills/corriger-un-bug.md",
19
+ "llamagent/skills/ecrire-tests-pytest.md",
20
+ ]
21
+
22
+
23
+ def main() -> None:
24
+ version = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]["version"]
25
+ if len(sys.argv) > 1 and sys.argv[1] != f"v{version}":
26
+ sys.exit(f"L'étiquette {sys.argv[1]} ne correspond pas à la version v{version} de pyproject.toml.")
27
+ wheels = list(Path("dist").glob(f"llamagent-{version}-*.whl"))
28
+ if len(wheels) != 1:
29
+ sys.exit(f"Un seul paquet llamagent-{version} attendu dans dist/, trouvé : {[w.name for w in wheels]}")
30
+ names = zipfile.ZipFile(wheels[0]).namelist()
31
+ missing = [name for name in REQUIRED if name not in names]
32
+ if missing:
33
+ sys.exit(f"Fichiers absents du paquet : {', '.join(missing)}")
34
+ print(f"{wheels[0].name} : {len(names)} fichiers, prompts et skills présents.")
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()
@@ -0,0 +1 @@
1
+ """llamagent : un agent de code minimal autour d'Ollama."""
@@ -0,0 +1,124 @@
1
+ """La boucle agentique : le cœur de llamagent.
2
+
3
+ tant que le modèle demande des outils :
4
+ exécuter chaque outil demandé
5
+ ajouter le résultat à la conversation
6
+ rappeler le modèle
7
+ sinon : renvoyer sa réponse finale
8
+
9
+ La boucle ne connaît aucun outil en particulier : elle passe par le registre
10
+ de tools.py. Elle ne parle pas non plus directement à Ollama : on lui donne
11
+ une fonction `chat`, ce qui permet de la tester avec un faux modèle.
12
+ """
13
+
14
+ import json
15
+ import os
16
+ import platform
17
+ from pathlib import Path
18
+ from typing import Callable
19
+
20
+ from llamagent import ui
21
+ from llamagent.config import MAX_OUTPUT_CHARS, MAX_TOURS
22
+ from llamagent.guards import is_allowed_command, truncate
23
+ from llamagent.tools import TOOLS, ToolContext, execute_tool, tool_schemas
24
+
25
+ # Une fonction qui reçoit (messages, schémas des outils) et renvoie le message
26
+ # de l'assistant. En vrai : llm.chat. Dans les tests : un faux modèle.
27
+ ChatFunction = Callable[[list[dict], list[dict]], dict]
28
+
29
+
30
+ # ─────────────────────────── Prompt système ───────────────────────────
31
+
32
+
33
+ def build_system_prompt(template: str, workdir: Path, skills: dict[str, str], project_instructions: str = "") -> str:
34
+ """Remplit le modèle de prompt (prompts/AGENT.md) avec l'environnement réel.
35
+
36
+ Si le projet a un fichier AGENTS.md (étape 9), son contenu est ajouté à la fin.
37
+ """
38
+ shell = "cmd.exe" if os.name == "nt" else "/bin/sh"
39
+ skills_text = "\n".join(f"- {name} : {description}" for name, description in skills.items())
40
+ # str.replace plutôt que str.format : le Markdown peut contenir des { }.
41
+ prompt = (
42
+ template.replace("{os}", platform.system())
43
+ .replace("{shell}", shell)
44
+ .replace("{workdir}", str(workdir))
45
+ .replace("{skills}", skills_text or "(aucune)")
46
+ )
47
+ if project_instructions:
48
+ prompt += f"\n\n## Instructions du projet (AGENTS.md)\n{project_instructions}"
49
+ return prompt
50
+
51
+
52
+ # ─────────────────────────── La boucle ───────────────────────────
53
+
54
+
55
+ def describe_harness(name: str, arguments, ctx: ToolContext, repeated: bool) -> str:
56
+ """Ce que le harness fait de la demande du modèle, en une phrase (étape 17).
57
+
58
+ Seulement pour l'affichage : on rend visible le rôle du harness entre
59
+ le modèle, qui demande, et l'outil, qui exécute.
60
+ """
61
+ if repeated:
62
+ return "appel identique au précédent : pas exécuté, le modèle est prévenu"
63
+ if name not in TOOLS:
64
+ return "outil inconnu : l'erreur est renvoyée au modèle"
65
+ if name == "edit_file":
66
+ return "vérifie le chemin, montre le diff et demande ta confirmation"
67
+ if name == "run_command":
68
+ command = arguments.get("command", "") if isinstance(arguments, dict) else ""
69
+ if is_allowed_command(command, ctx.allowed_commands):
70
+ return "commande autorisée : lancée sans confirmation"
71
+ return "demande ta confirmation avant de lancer la commande"
72
+ if name == "load_skill":
73
+ return "vérifie que la skill existe et la charge"
74
+ return f"vérifie le chemin et exécute {name}"
75
+
76
+
77
+ def run_turn(chat: ChatFunction, messages: list[dict], ctx: ToolContext, max_tours: int = MAX_TOURS) -> str:
78
+ """Traite le dernier message utilisateur et renvoie la réponse finale.
79
+
80
+ `messages` est modifiée sur place : on y ajoute les messages de
81
+ l'assistant et les résultats des outils, pour garder tout l'historique.
82
+ L'affichage montre chaque tour : le modèle demande, le harness décide,
83
+ l'outil répond.
84
+ """
85
+ previous_call = None
86
+ for tour in range(1, max_tours + 1):
87
+ print(ui.tour_header(tour))
88
+ reply = chat(messages, tool_schemas())
89
+ # 1) On garde le message de l'assistant TEL QUEL, avec ses tool_calls :
90
+ # au prochain appel, le modèle doit voir ce qu'il a demandé.
91
+ messages.append(reply)
92
+
93
+ tool_calls = reply.get("tool_calls") or []
94
+ if not tool_calls:
95
+ print(ui.role_line("modèle", "répond sans demander d'outil : fin de la boucle"))
96
+ print(ui.answer_header(tour))
97
+ return reply.get("content", "")
98
+
99
+ for call in tool_calls:
100
+ name = call["function"]["name"]
101
+ arguments = call["function"].get("arguments", {})
102
+ print(ui.model_asks(name, arguments))
103
+
104
+ # Détection de boucle : le même appel deux fois de suite ne
105
+ # donnera pas un résultat différent, on le signale au modèle.
106
+ signature = (name, json.dumps(arguments, sort_keys=True, ensure_ascii=False))
107
+ repeated = signature == previous_call
108
+ print(ui.role_line("harness", describe_harness(name, arguments, ctx, repeated)))
109
+ if repeated:
110
+ result = (
111
+ f"Erreur : tu viens déjà d'appeler {name} avec exactement ces arguments. "
112
+ "Le résultat sera le même : change d'approche ou donne ta réponse finale."
113
+ )
114
+ else:
115
+ result = execute_tool(name, arguments, ctx)
116
+ previous_call = signature
117
+
118
+ shortened = truncate(result, MAX_OUTPUT_CHARS)
119
+ print(ui.tool_result(shortened, truncated=len(shortened) < len(result)))
120
+ # 2) Puis le résultat de l'outil, avec le rôle "tool".
121
+ messages.append({"role": "tool", "tool_name": name, "content": shortened})
122
+
123
+ print(ui.role_line("harness", f"limite de {max_tours} tours atteinte : la boucle s'arrête", ui.RED))
124
+ return f"Arrêt : limite de {max_tours} tours atteinte sans réponse finale."