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.
- llamagent-0.1.0/.github/workflows/ci.yml +47 -0
- llamagent-0.1.0/.github/workflows/release.yml +64 -0
- llamagent-0.1.0/.gitignore +10 -0
- llamagent-0.1.0/.python-version +1 -0
- llamagent-0.1.0/LICENSE +21 -0
- llamagent-0.1.0/PKG-INFO +147 -0
- llamagent-0.1.0/README.md +121 -0
- llamagent-0.1.0/demo/stats.py +19 -0
- llamagent-0.1.0/docs/interface.png +0 -0
- llamagent-0.1.0/pyproject.toml +44 -0
- llamagent-0.1.0/scripts/check_package.py +38 -0
- llamagent-0.1.0/src/llamagent/__init__.py +1 -0
- llamagent-0.1.0/src/llamagent/agent.py +124 -0
- llamagent-0.1.0/src/llamagent/cli.py +119 -0
- llamagent-0.1.0/src/llamagent/commands.py +250 -0
- llamagent-0.1.0/src/llamagent/config.py +123 -0
- llamagent-0.1.0/src/llamagent/context.py +109 -0
- llamagent-0.1.0/src/llamagent/guards.py +66 -0
- llamagent-0.1.0/src/llamagent/history.py +82 -0
- llamagent-0.1.0/src/llamagent/llm.py +49 -0
- llamagent-0.1.0/src/llamagent/prompts/AGENT.md +22 -0
- llamagent-0.1.0/src/llamagent/prompts/COMPACT.md +12 -0
- llamagent-0.1.0/src/llamagent/session.py +147 -0
- llamagent-0.1.0/src/llamagent/skills/corriger-un-bug.md +12 -0
- llamagent-0.1.0/src/llamagent/skills/ecrire-tests-pytest.md +14 -0
- llamagent-0.1.0/src/llamagent/tools.py +317 -0
- llamagent-0.1.0/src/llamagent/ui.py +317 -0
- llamagent-0.1.0/steps/step1_hello.py +67 -0
- llamagent-0.1.0/steps/step2_chat.py +66 -0
- llamagent-0.1.0/steps/step3_tool.py +78 -0
- llamagent-0.1.0/steps/step4_loop.py +96 -0
- llamagent-0.1.0/tests/test_agent.py +120 -0
- llamagent-0.1.0/tests/test_cli.py +26 -0
- llamagent-0.1.0/tests/test_commands.py +135 -0
- llamagent-0.1.0/tests/test_config.py +67 -0
- llamagent-0.1.0/tests/test_context.py +73 -0
- llamagent-0.1.0/tests/test_guards.py +69 -0
- llamagent-0.1.0/tests/test_history.py +94 -0
- llamagent-0.1.0/tests/test_session.py +64 -0
- llamagent-0.1.0/tests/test_tools.py +186 -0
- llamagent-0.1.0/tests/test_ui.py +115 -0
- llamagent-0.1.0/try_step5.py +27 -0
- 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 @@
|
|
|
1
|
+
3.11
|
llamagent-0.1.0/LICENSE
ADDED
|
@@ -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.
|
llamagent-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
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."
|