pommes-assistant 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 (142) hide show
  1. pommes_assistant-0.1.0/LICENSE +21 -0
  2. pommes_assistant-0.1.0/MANIFEST.in +5 -0
  3. pommes_assistant-0.1.0/PKG-INFO +157 -0
  4. pommes_assistant-0.1.0/README.md +101 -0
  5. pommes_assistant-0.1.0/pyproject.toml +83 -0
  6. pommes_assistant-0.1.0/setup.cfg +4 -0
  7. pommes_assistant-0.1.0/src/pommes_assistant/__init__.py +13 -0
  8. pommes_assistant-0.1.0/src/pommes_assistant/assistant.py +179 -0
  9. pommes_assistant-0.1.0/src/pommes_assistant/audit/__init__.py +5 -0
  10. pommes_assistant-0.1.0/src/pommes_assistant/audit/audit.py +146 -0
  11. pommes_assistant-0.1.0/src/pommes_assistant/audit/references.py +111 -0
  12. pommes_assistant-0.1.0/src/pommes_assistant/audit/thresholds.yaml +21 -0
  13. pommes_assistant-0.1.0/src/pommes_assistant/bench/__init__.py +9 -0
  14. pommes_assistant-0.1.0/src/pommes_assistant/bench/backend.py +306 -0
  15. pommes_assistant-0.1.0/src/pommes_assistant/bench/dialogues.py +496 -0
  16. pommes_assistant-0.1.0/src/pommes_assistant/bench/rejudge.py +115 -0
  17. pommes_assistant-0.1.0/src/pommes_assistant/bench/report.py +247 -0
  18. pommes_assistant-0.1.0/src/pommes_assistant/bench/runner.py +259 -0
  19. pommes_assistant-0.1.0/src/pommes_assistant/bench/score.py +309 -0
  20. pommes_assistant-0.1.0/src/pommes_assistant/catalogue/__init__.py +1 -0
  21. pommes_assistant-0.1.0/src/pommes_assistant/catalogue/card.py +110 -0
  22. pommes_assistant-0.1.0/src/pommes_assistant/catalogue/cards/sltyndp_2024.yaml +775 -0
  23. pommes_assistant-0.1.0/src/pommes_assistant/catalogue/loaders/__init__.py +1 -0
  24. pommes_assistant-0.1.0/src/pommes_assistant/catalogue/loaders/sltyndp.py +177 -0
  25. pommes_assistant-0.1.0/src/pommes_assistant/catalogue/rules.py +334 -0
  26. pommes_assistant-0.1.0/src/pommes_assistant/catalogue/search.py +111 -0
  27. pommes_assistant-0.1.0/src/pommes_assistant/cli.py +1586 -0
  28. pommes_assistant-0.1.0/src/pommes_assistant/client/__init__.py +11 -0
  29. pommes_assistant-0.1.0/src/pommes_assistant/client/agent.py +508 -0
  30. pommes_assistant-0.1.0/src/pommes_assistant/client/llm.py +332 -0
  31. pommes_assistant-0.1.0/src/pommes_assistant/client/toolbox.py +196 -0
  32. pommes_assistant-0.1.0/src/pommes_assistant/compile/__init__.py +18 -0
  33. pommes_assistant-0.1.0/src/pommes_assistant/compile/assemble.py +146 -0
  34. pommes_assistant-0.1.0/src/pommes_assistant/compile/build.py +221 -0
  35. pommes_assistant-0.1.0/src/pommes_assistant/compile/compare.py +114 -0
  36. pommes_assistant-0.1.0/src/pommes_assistant/compile/craft.py +289 -0
  37. pommes_assistant-0.1.0/src/pommes_assistant/compile/errors.py +18 -0
  38. pommes_assistant-0.1.0/src/pommes_assistant/compile/overrides.py +50 -0
  39. pommes_assistant-0.1.0/src/pommes_assistant/compile/table_transforms.py +223 -0
  40. pommes_assistant-0.1.0/src/pommes_assistant/compile/write.py +149 -0
  41. pommes_assistant-0.1.0/src/pommes_assistant/dialogue/__init__.py +5 -0
  42. pommes_assistant-0.1.0/src/pommes_assistant/dialogue/policy.py +432 -0
  43. pommes_assistant-0.1.0/src/pommes_assistant/dialogue/quotes.py +159 -0
  44. pommes_assistant-0.1.0/src/pommes_assistant/estimate/__init__.py +21 -0
  45. pommes_assistant-0.1.0/src/pommes_assistant/estimate/journal.py +98 -0
  46. pommes_assistant-0.1.0/src/pommes_assistant/estimate/machine.py +61 -0
  47. pommes_assistant-0.1.0/src/pommes_assistant/estimate/size.py +172 -0
  48. pommes_assistant-0.1.0/src/pommes_assistant/estimate/time.py +95 -0
  49. pommes_assistant-0.1.0/src/pommes_assistant/guess/__init__.py +46 -0
  50. pommes_assistant-0.1.0/src/pommes_assistant/guess/adapters/__init__.py +27 -0
  51. pommes_assistant-0.1.0/src/pommes_assistant/guess/adapters/battery.py +238 -0
  52. pommes_assistant-0.1.0/src/pommes_assistant/guess/adapters/dispatchable.py +165 -0
  53. pommes_assistant-0.1.0/src/pommes_assistant/guess/adapters/electrolyser_hub.py +156 -0
  54. pommes_assistant-0.1.0/src/pommes_assistant/guess/adapters/h2_cavity.py +286 -0
  55. pommes_assistant-0.1.0/src/pommes_assistant/guess/adapters/hydrogen.py +209 -0
  56. pommes_assistant-0.1.0/src/pommes_assistant/guess/adapters/shiftable_demand.py +117 -0
  57. pommes_assistant-0.1.0/src/pommes_assistant/guess/brent.py +108 -0
  58. pommes_assistant-0.1.0/src/pommes_assistant/guess/context.py +300 -0
  59. pommes_assistant-0.1.0/src/pommes_assistant/guess/conventions.py +72 -0
  60. pommes_assistant-0.1.0/src/pommes_assistant/guess/curves.py +244 -0
  61. pommes_assistant-0.1.0/src/pommes_assistant/guess/domain.py +51 -0
  62. pommes_assistant-0.1.0/src/pommes_assistant/guess/engine.py +152 -0
  63. pommes_assistant-0.1.0/src/pommes_assistant/guess/errors.py +22 -0
  64. pommes_assistant-0.1.0/src/pommes_assistant/guess/finance.py +91 -0
  65. pommes_assistant-0.1.0/src/pommes_assistant/guess/inputs.py +622 -0
  66. pommes_assistant-0.1.0/src/pommes_assistant/guess/prices.py +391 -0
  67. pommes_assistant-0.1.0/src/pommes_assistant/guess/report.py +351 -0
  68. pommes_assistant-0.1.0/src/pommes_assistant/guess/rules/__init__.py +27 -0
  69. pommes_assistant-0.1.0/src/pommes_assistant/guess/rules/battery.py +252 -0
  70. pommes_assistant-0.1.0/src/pommes_assistant/guess/rules/dispatchable.py +75 -0
  71. pommes_assistant-0.1.0/src/pommes_assistant/guess/rules/electrolyser.py +522 -0
  72. pommes_assistant-0.1.0/src/pommes_assistant/guess/rules/h2_cavity.py +234 -0
  73. pommes_assistant-0.1.0/src/pommes_assistant/guess/rules/shiftable_demand.py +242 -0
  74. pommes_assistant-0.1.0/src/pommes_assistant/guess/settings.yaml +279 -0
  75. pommes_assistant-0.1.0/src/pommes_assistant/guess/validation.yaml +1279 -0
  76. pommes_assistant-0.1.0/src/pommes_assistant/mcp/__init__.py +6 -0
  77. pommes_assistant-0.1.0/src/pommes_assistant/mcp/prompts.py +61 -0
  78. pommes_assistant-0.1.0/src/pommes_assistant/mcp/server.py +135 -0
  79. pommes_assistant-0.1.0/src/pommes_assistant/mcp/skill/SKILL.md +113 -0
  80. pommes_assistant-0.1.0/src/pommes_assistant/mcp/tools.py +1062 -0
  81. pommes_assistant-0.1.0/src/pommes_assistant/results/__init__.py +10 -0
  82. pommes_assistant-0.1.0/src/pommes_assistant/results/checks.py +268 -0
  83. pommes_assistant-0.1.0/src/pommes_assistant/results/compare.py +106 -0
  84. pommes_assistant-0.1.0/src/pommes_assistant/results/load.py +228 -0
  85. pommes_assistant-0.1.0/src/pommes_assistant/results/report.py +674 -0
  86. pommes_assistant-0.1.0/src/pommes_assistant/run/__init__.py +25 -0
  87. pommes_assistant-0.1.0/src/pommes_assistant/run/guards.py +36 -0
  88. pommes_assistant-0.1.0/src/pommes_assistant/run/llm.py +355 -0
  89. pommes_assistant-0.1.0/src/pommes_assistant/run/llm_data/relay.py +321 -0
  90. pommes_assistant-0.1.0/src/pommes_assistant/run/llm_data/serve.sbatch +77 -0
  91. pommes_assistant-0.1.0/src/pommes_assistant/run/local.py +147 -0
  92. pommes_assistant-0.1.0/src/pommes_assistant/run/profile.py +126 -0
  93. pommes_assistant-0.1.0/src/pommes_assistant/run/remote.py +272 -0
  94. pommes_assistant-0.1.0/src/pommes_assistant/run/runner.py +185 -0
  95. pommes_assistant-0.1.0/src/pommes_assistant/sketch/__init__.py +19 -0
  96. pommes_assistant-0.1.0/src/pommes_assistant/sketch/errors.py +16 -0
  97. pommes_assistant-0.1.0/src/pommes_assistant/sketch/flexibility.py +258 -0
  98. pommes_assistant-0.1.0/src/pommes_assistant/sketch/l0.py +220 -0
  99. pommes_assistant-0.1.0/src/pommes_assistant/sketch/l1.py +148 -0
  100. pommes_assistant-0.1.0/src/pommes_assistant/sketch/reduce.py +341 -0
  101. pommes_assistant-0.1.0/src/pommes_assistant/sketch/regret.py +514 -0
  102. pommes_assistant-0.1.0/src/pommes_assistant/sketch/summary.py +427 -0
  103. pommes_assistant-0.1.0/src/pommes_assistant/sketch/thresholds.yaml +23 -0
  104. pommes_assistant-0.1.0/src/pommes_assistant/sketch/worlds.py +392 -0
  105. pommes_assistant-0.1.0/src/pommes_assistant/spec/__init__.py +0 -0
  106. pommes_assistant-0.1.0/src/pommes_assistant/spec/edit.py +1017 -0
  107. pommes_assistant-0.1.0/src/pommes_assistant/spec/expected_units.py +106 -0
  108. pommes_assistant-0.1.0/src/pommes_assistant/spec/findings.py +199 -0
  109. pommes_assistant-0.1.0/src/pommes_assistant/spec/generate.py +116 -0
  110. pommes_assistant-0.1.0/src/pommes_assistant/spec/importer.py +179 -0
  111. pommes_assistant-0.1.0/src/pommes_assistant/spec/inherit.py +247 -0
  112. pommes_assistant-0.1.0/src/pommes_assistant/spec/io.py +132 -0
  113. pommes_assistant-0.1.0/src/pommes_assistant/spec/model.py +268 -0
  114. pommes_assistant-0.1.0/src/pommes_assistant/spec/paths.py +45 -0
  115. pommes_assistant-0.1.0/src/pommes_assistant/spec/pommes_schema.py +110 -0
  116. pommes_assistant-0.1.0/src/pommes_assistant/spec/replay.py +257 -0
  117. pommes_assistant-0.1.0/src/pommes_assistant/spec/rules.py +646 -0
  118. pommes_assistant-0.1.0/src/pommes_assistant/spec/schema_data/expected_units.yaml +107 -0
  119. pommes_assistant-0.1.0/src/pommes_assistant/spec/schema_data/pommes-0.5.0.json +1759 -0
  120. pommes_assistant-0.1.0/src/pommes_assistant/spec/schema_data/pommes-0.6.0.json +1945 -0
  121. pommes_assistant-0.1.0/src/pommes_assistant/spec/sources.py +135 -0
  122. pommes_assistant-0.1.0/src/pommes_assistant/spec/tables.py +105 -0
  123. pommes_assistant-0.1.0/src/pommes_assistant/spec/transforms.py +302 -0
  124. pommes_assistant-0.1.0/src/pommes_assistant/spec/units.py +236 -0
  125. pommes_assistant-0.1.0/src/pommes_assistant/spec/validate.py +72 -0
  126. pommes_assistant-0.1.0/src/pommes_assistant/spec/values.py +145 -0
  127. pommes_assistant-0.1.0/src/pommes_assistant/spec/walk.py +144 -0
  128. pommes_assistant-0.1.0/src/pommes_assistant/views/__init__.py +95 -0
  129. pommes_assistant-0.1.0/src/pommes_assistant/views/areas.yaml +40 -0
  130. pommes_assistant-0.1.0/src/pommes_assistant/views/chart.py +126 -0
  131. pommes_assistant-0.1.0/src/pommes_assistant/views/data.py +667 -0
  132. pommes_assistant-0.1.0/src/pommes_assistant/views/palette.py +116 -0
  133. pommes_assistant-0.1.0/src/pommes_assistant/views/palette.yaml +89 -0
  134. pommes_assistant-0.1.0/src/pommes_assistant/views/render_mpl.py +221 -0
  135. pommes_assistant-0.1.0/src/pommes_assistant/views/render_plotly.py +190 -0
  136. pommes_assistant-0.1.0/src/pommes_assistant/views/standard.py +572 -0
  137. pommes_assistant-0.1.0/src/pommes_assistant.egg-info/PKG-INFO +157 -0
  138. pommes_assistant-0.1.0/src/pommes_assistant.egg-info/SOURCES.txt +140 -0
  139. pommes_assistant-0.1.0/src/pommes_assistant.egg-info/dependency_links.txt +1 -0
  140. pommes_assistant-0.1.0/src/pommes_assistant.egg-info/entry_points.txt +2 -0
  141. pommes_assistant-0.1.0/src/pommes_assistant.egg-info/requires.txt +44 -0
  142. pommes_assistant-0.1.0/src/pommes_assistant.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Robin Girard, MINES Paris – PSL
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,5 @@
1
+ prune tests
2
+ prune docs
3
+ prune examples
4
+ prune output
5
+ global-exclude *.pyc __pycache__
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: pommes-assistant
3
+ Version: 0.1.0
4
+ Summary: Deterministic backbone of a conversational assistant for pommes energy-system studies: StudySpec with provenance, validator, catalogue, tools
5
+ Author-email: Robin Girard <robin.girard@minesparis.psl.eu>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://git.persee.minesparis.psl.eu/energy-alternatives/pommes-assistant
8
+ Keywords: energy,energy system modelling,pommes,provenance,LLM tools,MCP
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: Scientific/Engineering
14
+ Classifier: Natural Language :: French
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: pydantic<3,>=2.7
19
+ Requires-Dist: pyyaml<7,>=6
20
+ Provides-Extra: pommes
21
+ Requires-Dist: pommes<0.7,>=0.5; extra == "pommes"
22
+ Provides-Extra: craft
23
+ Requires-Dist: pommes_craft>=0.5.1; extra == "craft"
24
+ Requires-Dist: polars>=0.20; extra == "craft"
25
+ Provides-Extra: data
26
+ Requires-Dist: polars>=0.20; extra == "data"
27
+ Requires-Dist: xarray>=2024.1; extra == "data"
28
+ Requires-Dist: h5netcdf>=1.3; extra == "data"
29
+ Requires-Dist: numpy>=1.26; extra == "data"
30
+ Provides-Extra: mcp
31
+ Requires-Dist: mcp<3,>=2.2; extra == "mcp"
32
+ Provides-Extra: views
33
+ Requires-Dist: plotly>=6; extra == "views"
34
+ Requires-Dist: matplotlib>=3.8; extra == "views"
35
+ Requires-Dist: xarray>=2024.1; extra == "views"
36
+ Requires-Dist: h5netcdf>=1.3; extra == "views"
37
+ Requires-Dist: numpy>=1.26; extra == "views"
38
+ Requires-Dist: hdf5plugin>=4; extra == "views"
39
+ Provides-Extra: all
40
+ Requires-Dist: pommes<0.7,>=0.6; extra == "all"
41
+ Requires-Dist: pommes_craft>=0.5.1; extra == "all"
42
+ Requires-Dist: linopy>=0.8; extra == "all"
43
+ Requires-Dist: highspy>=1.7; extra == "all"
44
+ Requires-Dist: polars>=0.20; extra == "all"
45
+ Requires-Dist: xarray>=2024.1; extra == "all"
46
+ Requires-Dist: h5netcdf>=1.3; extra == "all"
47
+ Requires-Dist: numpy>=1.26; extra == "all"
48
+ Requires-Dist: mcp<3,>=2.2; extra == "all"
49
+ Requires-Dist: plotly>=6; extra == "all"
50
+ Requires-Dist: matplotlib>=3.8; extra == "all"
51
+ Requires-Dist: hdf5plugin>=4; extra == "all"
52
+ Provides-Extra: dev
53
+ Requires-Dist: pytest>=8; extra == "dev"
54
+ Requires-Dist: anyio>=4; extra == "dev"
55
+ Dynamic: license-file
56
+
57
+ # pommes-assistant
58
+
59
+ Ossature déterministe d'un assistant conversationnel pour les études
60
+ [pommes](https://git.persee.minesparis.psl.eu/energy-alternatives/pommes) (planification de
61
+ systèmes multi-énergies, MINES Paris – PSL / PERSEE). Le LLM n'est qu'une interface
62
+ remplaçable ; la valeur est dans les outils typés, utilisables sans lui.
63
+
64
+ Vous décrivez votre étude en français ; l'assistant remplit le fichier d'étude (le
65
+ *StudySpec*), pose les questions qui manquent, esquisse le modèle, estime son coût de calcul, le
66
+ résout et lit les résultats. **Aucun nombre n'est inventé** : toute valeur vient de vos mots
67
+ (cités), d'un jeu de données du catalogue, d'un document cité ou d'un défaut de pommes marqué
68
+ comme tel. L'outil refuse toute écriture qui ne cite pas sa source.
69
+
70
+ **Mode d'emploi complet : `docs/guide.md`** (dans le dépôt).
71
+
72
+ ## Installation
73
+
74
+ ```bash
75
+ pip install "pommes-assistant[all]" # l'assistant, pommes, pommes_craft, HiGHS, les vues
76
+ pip install pommes-assistant # le cœur seul : lire et valider un StudySpec
77
+ ```
78
+
79
+ Python 3.12 au moins. Pour un modèle local : [Ollama](https://ollama.com), puis
80
+ `ollama pull gemma4:26b` (≈ 20 Go de mémoire).
81
+
82
+ ## Trois façons de s'en servir
83
+
84
+ - **Claude Code** (VS Code, PyCharm, terminal ; abonnement Claude), la voie la plus sûre (29 dialogues
85
+ de test sur 30) :
86
+
87
+ ```bash
88
+ claude mcp add pommes-assistant -- pommes-assistant mcp # dans votre projet
89
+ pommes-assistant skill --install . # la marche de l'assistant
90
+ ```
91
+
92
+ - **Python ou un notebook**, avec un modèle ouvert servi par Ollama (gemma4:26b : 28 à 30 sur 30) :
93
+
94
+ ```python
95
+ from pommes_assistant import Assistant
96
+
97
+ assistant = Assistant()
98
+ assistant("Mon étude est le fichier mon_etude.study.yaml. Dis-moi ce qui manque.")
99
+ assistant("esquisse-la")
100
+ ```
101
+
102
+ - **Le terminal** : `pommes-assistant chat --spec mon_etude.study.yaml`.
103
+
104
+ Au banc, aucun modèle n'a jamais écrit de nombre inventé : c'est l'ossature qui le refuse, pas le
105
+ modèle qui s'en abstient.
106
+
107
+ ## Ce que fait l'ossature
108
+
109
+ - le **StudySpec** porte une source par valeur numérique ; le **validateur** rend des écarts typés,
110
+ et les questions de l'assistant viennent de lui ;
111
+ - le **catalogue** de jeux certifiés (SL-TYNDP 2024 et PECD) et l'**audit** d'ordres de grandeur ;
112
+ - le **compilateur** produit une étude pommes ordinaire (le cas TYNDP 8 nœuds à l'identique) ;
113
+ - l'**esquisse** (une heure ou quelques semaines représentatives, en secondes), l'**estimation par
114
+ règle** (électrolyseur, cavité d'hydrogène, batterie, demande déplaçable, pilotable) et le
115
+ **regret** d'une règle dans son monde, sous le bandeau « pas un résultat » ;
116
+ - l'**estimateur** de taille, de temps et de mémoire ; l'**exécution** locale ou sur un serveur
117
+ (soumettre, suivre, rapatrier, sans shell libre) ; le modèle de langage servi par un job GPU
118
+ Slurm (`run llm`) ;
119
+ - les **vues** (neuf, en HTML ou PDF) et le **rapport** Quarto ;
120
+ - le **serveur MCP**, une fonction par action ; le **client** local (Ollama ou compatible OpenAI) ;
121
+ le **banc** de 30 dialogues, qui mesure les inventions et les questions superflues.
122
+
123
+ ## Développement
124
+
125
+ ```bash
126
+ git clone https://git.persee.minesparis.psl.eu/energy-alternatives/pommes-assistant.git
127
+ pip install -e ".[all,dev]"
128
+ pytest -q # sans données ni LLM ; les tests « data » lisent la racine DATA
129
+ ```
130
+
131
+ ## Commandes
132
+
133
+ ```bash
134
+ pommes-assistant validate mon_etude.study.yaml # écarts typés (--json)
135
+ pommes-assistant questions mon_etude.study.yaml # les questions du prochain tour
136
+ pommes-assistant spec set-user mon_etude.study.yaml "components[Carbon/carbon].tax" 100 \
137
+ --who "Camille Martin" --quote "une taxe carbone de 100 €/t"
138
+ pommes-assistant sketch mon_etude.study.yaml # esquisse L0 (ou --level L1)
139
+ pommes-assistant guess mon_etude.study.yaml # estimation par règle, sans résolution
140
+ pommes-assistant regret mon_etude.study.yaml # regret d'une règle dans son monde
141
+ pommes-assistant estimate mon_etude.study.yaml # taille, temps et RAM
142
+ pommes-assistant build mon_etude.study.yaml -o etude # une étude pommes ordinaire
143
+ pommes-assistant run local etude --dry-run # ou run submit etude --host …
144
+ pommes-assistant results etude/output/<run> --checks --view all --format html,pdf
145
+ pommes-assistant import-craft modele_sauve -o mon_etude.study.yaml
146
+ pommes-assistant chat --spec mon_etude.study.yaml # l'assistant au terminal (Ollama)
147
+ pommes-assistant mcp # le serveur MCP sur stdio
148
+ ```
149
+
150
+ Documentation dans le dépôt : `docs/guide.md` (mode d'emploi), `docs/studyspec.md` (format),
151
+ `docs/compile.md`, `docs/sketch.md`, `docs/run.md`, `docs/views.md`, `docs/mcp.md`,
152
+ `docs/client.md`, `docs/bench.md` ; exemples dans `examples/` (TYNDP 8 nœuds,
153
+ heat_electrification, hub H2 de Fos, dorsale H2, notebook `assistant.ipynb`).
154
+
155
+ ## Licence
156
+
157
+ MIT — Robin Girard, MINES Paris – PSL.
@@ -0,0 +1,101 @@
1
+ # pommes-assistant
2
+
3
+ Ossature déterministe d'un assistant conversationnel pour les études
4
+ [pommes](https://git.persee.minesparis.psl.eu/energy-alternatives/pommes) (planification de
5
+ systèmes multi-énergies, MINES Paris – PSL / PERSEE). Le LLM n'est qu'une interface
6
+ remplaçable ; la valeur est dans les outils typés, utilisables sans lui.
7
+
8
+ Vous décrivez votre étude en français ; l'assistant remplit le fichier d'étude (le
9
+ *StudySpec*), pose les questions qui manquent, esquisse le modèle, estime son coût de calcul, le
10
+ résout et lit les résultats. **Aucun nombre n'est inventé** : toute valeur vient de vos mots
11
+ (cités), d'un jeu de données du catalogue, d'un document cité ou d'un défaut de pommes marqué
12
+ comme tel. L'outil refuse toute écriture qui ne cite pas sa source.
13
+
14
+ **Mode d'emploi complet : `docs/guide.md`** (dans le dépôt).
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install "pommes-assistant[all]" # l'assistant, pommes, pommes_craft, HiGHS, les vues
20
+ pip install pommes-assistant # le cœur seul : lire et valider un StudySpec
21
+ ```
22
+
23
+ Python 3.12 au moins. Pour un modèle local : [Ollama](https://ollama.com), puis
24
+ `ollama pull gemma4:26b` (≈ 20 Go de mémoire).
25
+
26
+ ## Trois façons de s'en servir
27
+
28
+ - **Claude Code** (VS Code, PyCharm, terminal ; abonnement Claude), la voie la plus sûre (29 dialogues
29
+ de test sur 30) :
30
+
31
+ ```bash
32
+ claude mcp add pommes-assistant -- pommes-assistant mcp # dans votre projet
33
+ pommes-assistant skill --install . # la marche de l'assistant
34
+ ```
35
+
36
+ - **Python ou un notebook**, avec un modèle ouvert servi par Ollama (gemma4:26b : 28 à 30 sur 30) :
37
+
38
+ ```python
39
+ from pommes_assistant import Assistant
40
+
41
+ assistant = Assistant()
42
+ assistant("Mon étude est le fichier mon_etude.study.yaml. Dis-moi ce qui manque.")
43
+ assistant("esquisse-la")
44
+ ```
45
+
46
+ - **Le terminal** : `pommes-assistant chat --spec mon_etude.study.yaml`.
47
+
48
+ Au banc, aucun modèle n'a jamais écrit de nombre inventé : c'est l'ossature qui le refuse, pas le
49
+ modèle qui s'en abstient.
50
+
51
+ ## Ce que fait l'ossature
52
+
53
+ - le **StudySpec** porte une source par valeur numérique ; le **validateur** rend des écarts typés,
54
+ et les questions de l'assistant viennent de lui ;
55
+ - le **catalogue** de jeux certifiés (SL-TYNDP 2024 et PECD) et l'**audit** d'ordres de grandeur ;
56
+ - le **compilateur** produit une étude pommes ordinaire (le cas TYNDP 8 nœuds à l'identique) ;
57
+ - l'**esquisse** (une heure ou quelques semaines représentatives, en secondes), l'**estimation par
58
+ règle** (électrolyseur, cavité d'hydrogène, batterie, demande déplaçable, pilotable) et le
59
+ **regret** d'une règle dans son monde, sous le bandeau « pas un résultat » ;
60
+ - l'**estimateur** de taille, de temps et de mémoire ; l'**exécution** locale ou sur un serveur
61
+ (soumettre, suivre, rapatrier, sans shell libre) ; le modèle de langage servi par un job GPU
62
+ Slurm (`run llm`) ;
63
+ - les **vues** (neuf, en HTML ou PDF) et le **rapport** Quarto ;
64
+ - le **serveur MCP**, une fonction par action ; le **client** local (Ollama ou compatible OpenAI) ;
65
+ le **banc** de 30 dialogues, qui mesure les inventions et les questions superflues.
66
+
67
+ ## Développement
68
+
69
+ ```bash
70
+ git clone https://git.persee.minesparis.psl.eu/energy-alternatives/pommes-assistant.git
71
+ pip install -e ".[all,dev]"
72
+ pytest -q # sans données ni LLM ; les tests « data » lisent la racine DATA
73
+ ```
74
+
75
+ ## Commandes
76
+
77
+ ```bash
78
+ pommes-assistant validate mon_etude.study.yaml # écarts typés (--json)
79
+ pommes-assistant questions mon_etude.study.yaml # les questions du prochain tour
80
+ pommes-assistant spec set-user mon_etude.study.yaml "components[Carbon/carbon].tax" 100 \
81
+ --who "Camille Martin" --quote "une taxe carbone de 100 €/t"
82
+ pommes-assistant sketch mon_etude.study.yaml # esquisse L0 (ou --level L1)
83
+ pommes-assistant guess mon_etude.study.yaml # estimation par règle, sans résolution
84
+ pommes-assistant regret mon_etude.study.yaml # regret d'une règle dans son monde
85
+ pommes-assistant estimate mon_etude.study.yaml # taille, temps et RAM
86
+ pommes-assistant build mon_etude.study.yaml -o etude # une étude pommes ordinaire
87
+ pommes-assistant run local etude --dry-run # ou run submit etude --host …
88
+ pommes-assistant results etude/output/<run> --checks --view all --format html,pdf
89
+ pommes-assistant import-craft modele_sauve -o mon_etude.study.yaml
90
+ pommes-assistant chat --spec mon_etude.study.yaml # l'assistant au terminal (Ollama)
91
+ pommes-assistant mcp # le serveur MCP sur stdio
92
+ ```
93
+
94
+ Documentation dans le dépôt : `docs/guide.md` (mode d'emploi), `docs/studyspec.md` (format),
95
+ `docs/compile.md`, `docs/sketch.md`, `docs/run.md`, `docs/views.md`, `docs/mcp.md`,
96
+ `docs/client.md`, `docs/bench.md` ; exemples dans `examples/` (TYNDP 8 nœuds,
97
+ heat_electrification, hub H2 de Fos, dorsale H2, notebook `assistant.ipynb`).
98
+
99
+ ## Licence
100
+
101
+ MIT — Robin Girard, MINES Paris – PSL.
@@ -0,0 +1,83 @@
1
+ [build-system]
2
+ # setuptools>=77: the [project] table uses the PEP 639 licence form (SPDX string +
3
+ # license-files), which older setuptools rejects.
4
+ requires = ["setuptools>=77", "wheel"]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "pommes-assistant"
9
+ # Single source of truth: `pommes_assistant.__version__`, read by setuptools without import.
10
+ dynamic = ["version"]
11
+ description = "Deterministic backbone of a conversational assistant for pommes energy-system studies: StudySpec with provenance, validator, catalogue, tools"
12
+ readme = "README.md"
13
+ requires-python = ">=3.12"
14
+ license = "MIT"
15
+ license-files = ["LICENSE"]
16
+ authors = [{ name = "Robin Girard", email = "robin.girard@minesparis.psl.eu" }]
17
+ keywords = ["energy", "energy system modelling", "pommes", "provenance", "LLM tools", "MCP"]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Intended Audience :: Science/Research",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Scientific/Engineering",
24
+ "Natural Language :: French",
25
+ ]
26
+ # The core (spec + validate) must stay light and import neither pommes nor linopy:
27
+ # the CLI answers in well under 2 s. pommes and pommes_craft are optional extras.
28
+ dependencies = [
29
+ "pydantic>=2.7,<3",
30
+ "pyyaml>=6,<7",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ # Regenerate the bundled parameter schema from an installed pommes.
35
+ # Snapshots are bundled for 0.5.0 and 0.6.0 ; a StudySpec is validated against its own.
36
+ pommes = ["pommes>=0.5,<0.7"]
37
+ # Import a pommes_craft model.yaml. pommes_craft 0.3.7 pins linopy<0.8, which conflicts with
38
+ # pommes>=0.5 (install it with `pip install --no-deps pommes_craft`); 0.3.8 lifts the pin
39
+ # (pommes_craft!11, 2026-10-05).
40
+ craft = ["pommes_craft>=0.5.1", "polars>=0.20"]
41
+ # Read data: `validate --tables`, catalogue loaders, audit of a pommes Dataset.
42
+ data = ["polars>=0.20", "xarray>=2024.1", "h5netcdf>=1.3", "numpy>=1.26"]
43
+ # The MCP server (``pommes-assistant mcp``); the tools themselves need no mcp.
44
+ mcp = ["mcp>=2.2,<3"]
45
+ # The views (S7): HTML with plotly, PDF/SVG/PNG with matplotlib; zstd result files need
46
+ # hdf5plugin.
47
+ views = ["plotly>=6", "matplotlib>=3.8", "xarray>=2024.1", "h5netcdf>=1.3", "numpy>=1.26",
48
+ "hdf5plugin>=4"]
49
+ # Everything a user needs : the assistant (MCP tools in process, Ollama or an OpenAI-compatible
50
+ # server), compilation, sketch, guess, regret, runs, views.
51
+ all = ["pommes>=0.6,<0.7", "pommes_craft>=0.5.1", "linopy>=0.8", "highspy>=1.7", "polars>=0.20",
52
+ "xarray>=2024.1", "h5netcdf>=1.3", "numpy>=1.26", "mcp>=2.2,<3", "plotly>=6",
53
+ "matplotlib>=3.8", "hdf5plugin>=4"]
54
+ dev = ["pytest>=8", "anyio>=4"]
55
+
56
+ [project.scripts]
57
+ pommes-assistant = "pommes_assistant.cli:main"
58
+
59
+ [project.urls]
60
+ Repository = "https://git.persee.minesparis.psl.eu/energy-alternatives/pommes-assistant"
61
+
62
+ [tool.setuptools.dynamic]
63
+ version = { attr = "pommes_assistant.__version__" }
64
+
65
+ [tool.setuptools.packages.find]
66
+ where = ["src"]
67
+
68
+ [tool.setuptools.package-data]
69
+ "pommes_assistant.spec" = ["schema_data/*.json", "schema_data/*.yaml"]
70
+ "pommes_assistant.catalogue" = ["cards/*.yaml"]
71
+ "pommes_assistant.audit" = ["thresholds.yaml"]
72
+ "pommes_assistant.sketch" = ["thresholds.yaml"]
73
+ "pommes_assistant.run" = ["llm_data/*"]
74
+ "pommes_assistant.mcp" = ["skill/SKILL.md"]
75
+ "pommes_assistant.guess" = ["settings.yaml", "validation.yaml"]
76
+ "pommes_assistant.views" = ["*.yaml"]
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = ["tests"]
80
+ addopts = "-ra"
81
+ markers = [
82
+ "data: reads the real datasets under ${DATA} (skipped when the root is not configured)",
83
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ """pommes-assistant: deterministic backbone for describing, building, running and
2
+ exploiting pommes energy-system studies, with the provenance of every number."""
3
+
4
+ __version__ = "0.1.0"
5
+
6
+
7
+ def __getattr__(name: str):
8
+ """``from pommes_assistant import Assistant`` without loading the client at import time."""
9
+ if name in ("Assistant", "Answer"):
10
+ from pommes_assistant import assistant
11
+
12
+ return getattr(assistant, name)
13
+ raise AttributeError(f"module 'pommes_assistant' has no attribute {name!r}")
@@ -0,0 +1,179 @@
1
+ """The assistant from Python : a notebook, a script, an IDE console (S35).
2
+
3
+ .. code-block:: python
4
+
5
+ from pommes_assistant import Assistant
6
+
7
+ assistant = Assistant(host="slurm") # gemma4:26b served from a GPU job (run llm)
8
+ assistant("Mon étude est le fichier etude.study.yaml. Esquisse-la.")
9
+ assistant("combien de temps prendrait le modèle complet ?")
10
+ assistant.close() # the job it submitted is cancelled
11
+
12
+ The same tool loop as ``pommes-assistant chat`` and the bench (:class:`~pommes_assistant.
13
+ client.Agent`), with the same policy and the same guards : no number without a source, the
14
+ questions from the validator. A local model by default (Ollama on this machine), or one served
15
+ from a host of the profile (``host=``, :class:`~pommes_assistant.run.llm.Serving` : the job is
16
+ submitted on the first call, held, and cancelled by :meth:`Assistant.close`, by a ``with``
17
+ block, or at the end of the process), or any OpenAI-compatible server (``backend="openai"``,
18
+ ``base_url=``). Claude itself is reached through Claude Code (VS Code, PyCharm : the MCP
19
+ server of ``.mcp.json``), not from here : no API key (decision of 2026-09-27).
20
+
21
+ Each call returns an :class:`Answer`, which a notebook shows as Markdown : the answer, then the
22
+ tools called (refused ones marked), the time and, if any, the wait for an unreachable server.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import atexit
28
+ import html
29
+ import json
30
+ from dataclasses import dataclass, field
31
+ from pathlib import Path
32
+ from typing import Any
33
+
34
+ #: as each model was measured (``cli.DEFAULT_THINK``) : the 26B thinks, the 12B does not
35
+ DEFAULT_THINK = {"gemma4:26b": True}
36
+
37
+
38
+ @dataclass
39
+ class Answer:
40
+ """One answer of the assistant."""
41
+
42
+ text: str
43
+ tool_uses: list[dict[str, Any]] = field(default_factory=list)
44
+ error: str | None = None
45
+ duration_s: float = 0.0
46
+ waited_s: float = 0.0
47
+
48
+ def __str__(self) -> str:
49
+ return self.text + (f"\n\n[erreur : {self.error}]" if self.error else "")
50
+
51
+ def _repr_markdown_(self) -> str:
52
+ parts = [self.text or "*(pas de réponse)*"]
53
+ if self.error:
54
+ parts.append(f"> **Erreur** : {self.error}")
55
+ if self.tool_uses:
56
+ rows = []
57
+ for u in self.tool_uses:
58
+ args = {k: v for k, v in (u.get("args") or {}).items() if k != "spec_file"} \
59
+ if isinstance(u.get("args"), dict) else u.get("args")
60
+ shown = html.escape(json.dumps(args, ensure_ascii=False, default=str))
61
+ if len(shown) > 160:
62
+ shown = shown[:157] + "…"
63
+ rows.append(f"<li><code>{u['tool']}</code>{'' if u.get('ok') else ' — refusé'}"
64
+ f" <small>{shown}</small></li>")
65
+ parts.append(f"<details><summary>{len(self.tool_uses)} appel(s) d'outil</summary>"
66
+ f"<ul>{''.join(rows)}</ul></details>")
67
+ tail = f"{self.duration_s:.0f} s"
68
+ if self.waited_s:
69
+ tail += f", dont {self.waited_s:.0f} s d'attente du serveur"
70
+ parts.append(f"<small>{tail}</small>")
71
+ return "\n\n".join(parts)
72
+
73
+
74
+ class Assistant:
75
+ """A conversation with the assistant (see the module's docstring).
76
+
77
+ Args:
78
+ model: the model's name on the server (``gemma4:26b`` by default, the measured one).
79
+ host: a host of the profile that serves the model from a GPU job (``run llm``) ; the
80
+ job is submitted on the first call if none runs, held, and cancelled on close.
81
+ base_url: the server's address (default : Ollama on this machine, or the tunnel of
82
+ ``host``).
83
+ backend: ``ollama`` (native API : context length and thinking per request) or
84
+ ``openai`` (any OpenAI-compatible server).
85
+ think: thinking on or off ; default : as the model was measured.
86
+ workdir: the folder relative paths are read from (default : the current one).
87
+ wait_minutes: how long a call waits for an unreachable server (VPN, a job starting).
88
+ verbose: print each tool call as it happens.
89
+ chat: a chat model already made (tests) ; ``model``, ``backend`` and ``base_url`` are
90
+ then not used.
91
+ """
92
+
93
+ def __init__(self, model: str = "gemma4:26b", *, host: str | None = None,
94
+ base_url: str | None = None, backend: str = "ollama",
95
+ think: bool | str | None = None, workdir: str | Path | None = None,
96
+ wait_minutes: float = 10.0, max_tokens: int | None = 8192,
97
+ num_ctx: int = 65536, job_time: str | None = None, verbose: bool = False,
98
+ chat: Any = None) -> None:
99
+ self.model, self.host, self.base_url, self.backend = model, host, base_url, backend
100
+ self.think = DEFAULT_THINK.get(model, False) if think is None else think
101
+ self.workdir = Path(workdir or Path.cwd()).resolve()
102
+ self.wait_minutes, self.max_tokens, self.num_ctx = wait_minutes, max_tokens, num_ctx
103
+ self.job_time, self.verbose = job_time, verbose
104
+ self.answers: list[Answer] = []
105
+ self._chat, self._agent, self._serving = chat, None, None
106
+ self._closed = False
107
+ atexit.register(self.close)
108
+
109
+ # -------------------------------------------------------------------- lifecycle
110
+ def _serve(self) -> str | None:
111
+ """The base URL of the host's tunnel, the job submitted and held if needed."""
112
+ if self.host is None:
113
+ return None
114
+ if self._serving is None:
115
+ from pommes_assistant.run.llm import Serving
116
+ from pommes_assistant.run.profile import host as host_of
117
+
118
+ serving = Serving(host_of(self.host), time_limit=self.job_time,
119
+ log=lambda m: print(f"[run llm] {m}", flush=True))
120
+ serving.__enter__()
121
+ self._serving = serving
122
+ return self._serving.base_url
123
+
124
+ def _make_agent(self) -> Any:
125
+ from pommes_assistant.client import Agent, ToolBox, chat_model
126
+
127
+ chat = self._chat
128
+ if chat is None:
129
+ url = self.base_url or self._serve()
130
+ options: dict[str, Any] = {"max_tokens": self.max_tokens,
131
+ "wait_s": 60.0 * self.wait_minutes,
132
+ "on_wait": lambda m: print(f"[serveur] {m}", flush=True)}
133
+ if self.backend == "ollama":
134
+ options.update(num_ctx=self.num_ctx, think=self.think)
135
+ chat = chat_model(self.backend, self.model, url, **options)
136
+ elif self.host is not None:
137
+ self._serve()
138
+
139
+ def show(kind: str, data: dict[str, Any]) -> None:
140
+ if kind == "tool":
141
+ state = "ok" if data["ok"] else "refusé"
142
+ print(f" · {data['name']} → {state}", flush=True)
143
+
144
+ return Agent(chat, ToolBox(self.workdir), on_event=show if self.verbose else None)
145
+
146
+ def close(self) -> None:
147
+ """End the conversation ; the job this assistant submitted is cancelled."""
148
+ if self._closed:
149
+ return
150
+ self._closed = True
151
+ if self._serving is not None:
152
+ self._serving.__exit__(None, None, None)
153
+ self._serving = None
154
+
155
+ def __enter__(self) -> Assistant:
156
+ return self
157
+
158
+ def __exit__(self, *exc: Any) -> None:
159
+ self.close()
160
+
161
+ # -------------------------------------------------------------------- conversation
162
+ def __call__(self, message: str) -> Answer:
163
+ """Send one message ; the answer comes back when the model has finished its turn."""
164
+ if self._closed:
165
+ raise RuntimeError("cet assistant est fermé : en créer un nouveau")
166
+ if self._agent is None:
167
+ self._agent = self._make_agent()
168
+ turn = self._agent.send(message)
169
+ answer = Answer(turn.text, list(turn.tool_uses), turn.error, turn.duration_s,
170
+ getattr(turn, "waited_s", 0.0))
171
+ self.answers.append(answer)
172
+ return answer
173
+
174
+ ask = __call__
175
+
176
+ def __repr__(self) -> str:
177
+ where = self.host or self.base_url or "Ollama local"
178
+ return (f"Assistant({self.model}, {where}, {len(self.answers)} réponse(s)"
179
+ + (", fermé" if self._closed else "") + ")")
@@ -0,0 +1,5 @@
1
+ """Audit of the inputs (F10): indicators on an assembled pommes Dataset, compared to sourced
2
+ references with sourced thresholds. Never a correction: error / warning / info lines only.
3
+
4
+ Needs the ``data`` extra (xarray; polars for references read from the catalogue).
5
+ """