websemantic 0.2.5__py3-none-any.whl
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.
- websemantic/__init__.py +3 -0
- websemantic/adapters/__init__.py +1 -0
- websemantic/adapters/lql.py +118 -0
- websemantic/adapters/pyrcel.py +104 -0
- websemantic/adapters/tls.py +130 -0
- websemantic/adapters/tls_outputs.py +70 -0
- websemantic/cli.py +592 -0
- websemantic/comparison.py +223 -0
- websemantic/conversation.py +44 -0
- websemantic/core/__init__.py +1 -0
- websemantic/core/validation.py +172 -0
- websemantic/environments.json +23 -0
- websemantic/gemini.py +222 -0
- websemantic/geography.py +223 -0
- websemantic/lql_comparison.py +138 -0
- websemantic/qualitative.py +60 -0
- websemantic/registry.py +45 -0
- websemantic/replay.py +68 -0
- websemantic/semantics.py +266 -0
- websemantic/session.py +271 -0
- websemantic/units.py +92 -0
- websemantic/web_research.py +146 -0
- websemantic-0.2.5.dist-info/METADATA +64 -0
- websemantic-0.2.5.dist-info/RECORD +28 -0
- websemantic-0.2.5.dist-info/WHEEL +5 -0
- websemantic-0.2.5.dist-info/entry_points.txt +2 -0
- websemantic-0.2.5.dist-info/licenses/LICENSE +135 -0
- websemantic-0.2.5.dist-info/top_level.txt +1 -0
websemantic/cli.py
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"""PowerShell-friendly terminal proof of concept. No generated code execution."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from websemantic import web_research
|
|
12
|
+
from websemantic.conversation import explicit_consent, requested_actions
|
|
13
|
+
from websemantic.gemini import GeminiError, extract, load_private_key
|
|
14
|
+
from websemantic.geography import apply_report, location, research, show_report
|
|
15
|
+
from websemantic.registry import environments, execute, load_descriptor
|
|
16
|
+
from websemantic.semantics import describe
|
|
17
|
+
from websemantic.session import ClarificationNeeded, Session
|
|
18
|
+
|
|
19
|
+
ENVIRONMENTS = {str(i): (item['id'], item['namespace'], item['definition'])
|
|
20
|
+
for i, item in enumerate(environments(), 1)}
|
|
21
|
+
AVAILABLE = {item['id'] for item in environments() if item['available']}
|
|
22
|
+
DEFAULT_MODEL = next(item['id'] for item in environments() if item['available'])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def choose_environment():
|
|
26
|
+
while True:
|
|
27
|
+
print('\nWebSemantic — choisissez votre environnement :')
|
|
28
|
+
for number, (_, namespace, definition) in ENVIRONMENTS.items():
|
|
29
|
+
print(f'{number} — {namespace} — {definition}')
|
|
30
|
+
print('q — quitter')
|
|
31
|
+
try:
|
|
32
|
+
choice = input('Votre choix (' + ', '.join(ENVIRONMENTS) + ') > ').strip().lower()
|
|
33
|
+
except (EOFError, KeyboardInterrupt):
|
|
34
|
+
print('\nFin de session.')
|
|
35
|
+
return None
|
|
36
|
+
if choice in ('q', '/q', 'quit', 'exit'):
|
|
37
|
+
return None
|
|
38
|
+
if choice not in ENVIRONMENTS:
|
|
39
|
+
print('Saisissez ' + ', '.join(ENVIRONMENTS) + '.')
|
|
40
|
+
continue
|
|
41
|
+
model, namespace, _ = ENVIRONMENTS[choice]
|
|
42
|
+
if model not in AVAILABLE:
|
|
43
|
+
print(f'{namespace} — Work in progress.')
|
|
44
|
+
continue
|
|
45
|
+
return model
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def presentation(session):
|
|
49
|
+
return session.descriptor.get('presentation', {})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def show(session):
|
|
53
|
+
if session.scenarios:
|
|
54
|
+
questions = []
|
|
55
|
+
seen = set()
|
|
56
|
+
for label, child in session.scenarios.items():
|
|
57
|
+
for question in child.questions:
|
|
58
|
+
key = question['question']
|
|
59
|
+
if question['blocking'] and key not in seen:
|
|
60
|
+
questions.append((label, question))
|
|
61
|
+
seen.add(key)
|
|
62
|
+
if questions:
|
|
63
|
+
for label, question in questions[:2]:
|
|
64
|
+
print(f'{label} : {question["question"]}')
|
|
65
|
+
return
|
|
66
|
+
for label, child in session.scenarios.items():
|
|
67
|
+
print(label + ' :')
|
|
68
|
+
show(child)
|
|
69
|
+
for issue in session.result().issues:
|
|
70
|
+
if issue.code == 'comparison_control':
|
|
71
|
+
print('À harmoniser :', describe(issue.field.split('.')[-1], session.descriptor)[0], '— même valeur pour les deux scénarios.')
|
|
72
|
+
return
|
|
73
|
+
if session.pending_clarification:
|
|
74
|
+
print(session.pending_clarification)
|
|
75
|
+
return
|
|
76
|
+
result = session.result()
|
|
77
|
+
if result.decision == 'refuse':
|
|
78
|
+
print('Le logiciel ne permet pas de traiter cette demande.')
|
|
79
|
+
missing = [issue.field for issue in result.issues if issue.code == 'missing']
|
|
80
|
+
if missing:
|
|
81
|
+
names = [describe(field.split('.')[1], session.descriptor)[0] for field in missing[:4]]
|
|
82
|
+
print('À préciser : ' + ', '.join(names) + (f' et {len(missing)-4} autres champs.' if len(missing)>4 else '.'))
|
|
83
|
+
if any('default' in spec for group in ('inputs', 'experiment') for spec in session.descriptor.get(group, {}).values()):
|
|
84
|
+
print('Pour un premier essai : « prends les valeurs par défaut ». Le calcul demandé attend les informations suffisantes.')
|
|
85
|
+
unaccepted = [issue for issue in result.issues if issue.code == 'unaccepted_assumption']
|
|
86
|
+
if unaccepted:
|
|
87
|
+
print(f'{len(unaccepted)} hypothèses attendent votre accord. Demandez le tableau pour les examiner, puis dites « J’accepte les hypothèses proposées » si vous souhaitez les utiliser.')
|
|
88
|
+
for issue in result.issues:
|
|
89
|
+
if issue.code not in ('missing', 'unaccepted_assumption'):
|
|
90
|
+
print(f'À vérifier : {issue.field} ({issue.code}).')
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def details(session, full=False):
|
|
94
|
+
if session.scenarios:
|
|
95
|
+
for label, child in session.scenarios.items():
|
|
96
|
+
print('\n' + label)
|
|
97
|
+
details(child, full=full)
|
|
98
|
+
return
|
|
99
|
+
print(f"{'Paramètre':28} | {'Défaut':16} | {'Retenue':16} | {'Unité':16} | Origine / état")
|
|
100
|
+
print('-' * 110)
|
|
101
|
+
for group in ('inputs', 'experiment'):
|
|
102
|
+
for name, spec in session.descriptor.get(group, {}).items():
|
|
103
|
+
primary = presentation(session).get('primary_fields')
|
|
104
|
+
if not full and (spec.get('display_hidden') or (primary and f'{group}.{name}' not in primary)):
|
|
105
|
+
continue
|
|
106
|
+
record = getattr(session.scenario, group).get(name)
|
|
107
|
+
label, unit, _ = describe(name, session.descriptor)
|
|
108
|
+
status = 'manquant' if not record else ('fourni' if record.origin == 'provided' else ('hypothèse validée' if record.accepted else 'à valider'))
|
|
109
|
+
print(f"{label:28} | {spec.get('default', '—')!s:16} | {record.value if record else 'à préciser'!s:16} | {unit:16} | {status}")
|
|
110
|
+
print('Pour aller plus loin, demandez le détail complet, une définition ou une modification en une phrase.')
|
|
111
|
+
for source in sorted({record.source for group in ('inputs', 'experiment') for name, record in getattr(session.scenario, group).items() if record.source and (full or not session.descriptor[group][name].get('operational_default'))}):
|
|
112
|
+
print('Source des hypothèses :', source)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def formulas(session):
|
|
116
|
+
if session.scenarios:
|
|
117
|
+
for label, child in session.scenarios.items():
|
|
118
|
+
print('\n' + label)
|
|
119
|
+
formulas(child)
|
|
120
|
+
return
|
|
121
|
+
entries = session.descriptor.get('model_equations', [])
|
|
122
|
+
if not entries:
|
|
123
|
+
print('Les formules de cet environnement ne sont pas encore documentées.')
|
|
124
|
+
return
|
|
125
|
+
for entry in entries:
|
|
126
|
+
print(f"{entry['label']} [{entry['unit']}] : {entry['expression']}")
|
|
127
|
+
print(entry['meaning'])
|
|
128
|
+
for table in session.descriptor.get('coefficient_display', []):
|
|
129
|
+
group, name = table['field'].split('.')
|
|
130
|
+
record = getattr(session.scenario, group).get(name)
|
|
131
|
+
if record and record.value in table['values']:
|
|
132
|
+
print(table['template'].format(choice=record.value, **table['values'][record.value]))
|
|
133
|
+
print(presentation(session).get('formula_note', 'Les formules et leurs limites sont documentées dans le référentiel du modèle.'))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def ask_questions(session, questions=None):
|
|
137
|
+
for question in session.questions if questions is None else questions:
|
|
138
|
+
if not question['blocking']:
|
|
139
|
+
print('Pour préciser :', question['question'])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def explain(session, name):
|
|
143
|
+
name = name.split('.')[-1]
|
|
144
|
+
from websemantic.units import fold
|
|
145
|
+
|
|
146
|
+
matches = [field for group in ('inputs', 'experiment')
|
|
147
|
+
for field, spec in session.descriptor.get(group, {}).items()
|
|
148
|
+
if fold(name) in {fold(field), fold(spec.get('label', field)),
|
|
149
|
+
*(fold(alias) for alias in spec.get('aliases', []))}]
|
|
150
|
+
if len(matches) == 1:
|
|
151
|
+
name = matches[0]
|
|
152
|
+
for group in ('inputs', 'experiment'):
|
|
153
|
+
if name in session.descriptor.get(group, {}):
|
|
154
|
+
label, unit, definition = describe(name, session.descriptor)
|
|
155
|
+
print(f'{label} — unité : {unit}. {definition}')
|
|
156
|
+
spec = session.descriptor[group][name]
|
|
157
|
+
if 'values' in spec:
|
|
158
|
+
print('Choix possibles : ' + ', '.join(spec['values']))
|
|
159
|
+
print(f'Pour le modifier, indiquez le nouveau choix en précisant {label.lower()} et son unité.')
|
|
160
|
+
record = getattr(session.scenario, group).get(name)
|
|
161
|
+
if record and record.source:
|
|
162
|
+
print('Source :', record.source)
|
|
163
|
+
return
|
|
164
|
+
print('Paramètre non reconnu. Demandez le tableau des paramètres pour retrouver son nom.')
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def results_table(session, target, medians):
|
|
168
|
+
if medians.get('comparison') is True:
|
|
169
|
+
print('\nComparaison des médianes des réalisations :')
|
|
170
|
+
print(f"{'Indicateur':24} | {'Scénario 1':16} | {'Scénario 2':16} | {'Écart 1−2':16} | Unité")
|
|
171
|
+
manifest = json.loads((target / 'manifest.json').read_text(encoding='utf-8'))
|
|
172
|
+
for indicator, values in medians['differences'].items():
|
|
173
|
+
spec = session.descriptor['comparison']['indicator_labels'][indicator]
|
|
174
|
+
print(f"{spec['label']:24} | {values['scenario_1']:16.3f} | {values['scenario_2']:16.3f} | {values['difference_1_minus_2']:16.3f} | {spec['unit']}")
|
|
175
|
+
primary = medians['differences'][session.descriptor['comparison']['primary_indicator']]
|
|
176
|
+
difference = primary['difference_1_minus_2']
|
|
177
|
+
if difference == 0:
|
|
178
|
+
print('Les deux scénarios ont la même consommation médiane sur la période simulée.')
|
|
179
|
+
else:
|
|
180
|
+
direction = 'plus' if difference > 0 else 'moins'
|
|
181
|
+
relative = primary['relative_percent_vs_2']
|
|
182
|
+
suffix = f'{abs(relative):.1f} % de {direction}' if relative is not None else f'{direction} (référence nulle : pourcentage indéfini)'
|
|
183
|
+
print(f'Dans cette simulation, le scénario 1 consomme {suffix} que le scénario 2.')
|
|
184
|
+
print('La comparaison dépend de tous les paramètres retenus ; elle ne démontre pas l’effet isolé d’un seul équipement. Données synthétiques ; annualisation par extrapolation. Mêmes graines, sans garantie de tirages identiques après changement de configuration.')
|
|
185
|
+
print('Dossier des résultats :', target)
|
|
186
|
+
for path in manifest['controlled_fields']:
|
|
187
|
+
group, name = path.split('.')
|
|
188
|
+
record = manifest['scenarios']['scenario_1'][group][name]
|
|
189
|
+
if not session.descriptor[group][name].get('display_hidden'):
|
|
190
|
+
label, unit, _ = describe(name, session.descriptor)
|
|
191
|
+
print(f'Commun aux deux scénarios : {label} = {record["value"]} {unit}.')
|
|
192
|
+
return
|
|
193
|
+
config = presentation(session).get('results', {})
|
|
194
|
+
manifest = json.loads((target / 'manifest.json').read_text(encoding='utf-8'))
|
|
195
|
+
scenario = manifest['scenario']
|
|
196
|
+
context_path = target / 'geographic-context.json'
|
|
197
|
+
context = json.loads(context_path.read_text(encoding='utf-8')) if context_path.is_file() else {}
|
|
198
|
+
headline = config.get('headline')
|
|
199
|
+
if headline:
|
|
200
|
+
location_text = headline.get('location_template', ' à {city}').format(city=context['city']) if context.get('city') else ''
|
|
201
|
+
print('\n' + headline['template'].format(value=medians[headline['indicator']], location=location_text))
|
|
202
|
+
recap = []
|
|
203
|
+
for group, names in (('inputs', config.get('recap_fields', [])), ('experiment', config.get('context_fields', []))):
|
|
204
|
+
for name in names:
|
|
205
|
+
if name in scenario[group]:
|
|
206
|
+
label, unit, _ = describe(name, session.descriptor)
|
|
207
|
+
record = scenario[group][name]
|
|
208
|
+
value = record['value']
|
|
209
|
+
if type(value) in (int, float):
|
|
210
|
+
value = f'{value:g}'
|
|
211
|
+
recap.append(f'{label.lower()} {value}' + ('' if unit in ('nombre', 'catégorie', 'identifiant') else ' ' + unit))
|
|
212
|
+
if recap:
|
|
213
|
+
print('Hypothèses retenues : ' + '; '.join(recap) + '.')
|
|
214
|
+
if config.get('indicators') and (not config.get('table') or config.get('deterministic')):
|
|
215
|
+
print(f"{'Indicateur':30} | {'Valeur':14} | Unité")
|
|
216
|
+
for name, label, unit in config['indicators']:
|
|
217
|
+
value = medians.get(name)
|
|
218
|
+
shown = f'{value:.3f}' if type(value) in (int, float) else 'non valide'
|
|
219
|
+
print(f'{label:30} | {shown:14} | {unit}')
|
|
220
|
+
if config.get('table') and not config.get('deterministic'):
|
|
221
|
+
import pandas as pd
|
|
222
|
+
|
|
223
|
+
means = pd.read_csv(target / config['table']).mean(numeric_only=True)
|
|
224
|
+
print(f"{'Indicateur':30} | {'Médiane':12} | {'Moyenne':12} | Unité")
|
|
225
|
+
selected = config.get('display_indicators')
|
|
226
|
+
for name, label, unit in config.get('indicators', []):
|
|
227
|
+
if selected is None or name in selected:
|
|
228
|
+
value = medians.get(name)
|
|
229
|
+
average = means.get(name)
|
|
230
|
+
shown = f'{value:.3f}' if type(value) in (int, float) else 'non valide'
|
|
231
|
+
mean_shown = f'{average:.3f}' if average is not None and pd.notna(average) else 'non valide'
|
|
232
|
+
print(f'{label:30} | {shown:12} | {mean_shown:12} | {unit}')
|
|
233
|
+
for note in config.get('notes', []):
|
|
234
|
+
print(note)
|
|
235
|
+
print('Dossier des résultats :', target)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def suggestions(session, has_results=False):
|
|
239
|
+
ui = presentation(session)
|
|
240
|
+
missing = [issue for issue in session.result().issues if issue.code == 'missing']
|
|
241
|
+
if missing:
|
|
242
|
+
field = missing[0].field.split('.')[-1]
|
|
243
|
+
label, unit, _ = describe(field, session.descriptor)
|
|
244
|
+
fourth = (f'Préciser {label.lower()} ({unit}) et comprendre son rôle.', '/e ' + field)
|
|
245
|
+
else:
|
|
246
|
+
fourth = ('Revoir les hypothèses, leurs unités et leurs sources avant de calculer.', '/details')
|
|
247
|
+
fifth = tuple(ui.get('event_suggestion', ['Comprendre les limites du modèle.', '/uncertainty']))
|
|
248
|
+
if session.geographic_context:
|
|
249
|
+
fifth = (f"Vérifier les hypothèses pour {session.geographic_context['city']}, les unités et leurs sources.", '/details')
|
|
250
|
+
if has_results:
|
|
251
|
+
fifth = ('Comprendre la dispersion et les limites des résultats déjà calculés.', '/uncertainty')
|
|
252
|
+
initial = [tuple(item) for item in ui.get('suggestions', [])[:3]]
|
|
253
|
+
fallback = [('Comprendre le périmètre du logiciel.', '/show'), ('Examiner les paramètres déclarés.', '/details'), ('Comprendre les hypothèses du profil.', '/profile')]
|
|
254
|
+
initial += fallback[len(initial):]
|
|
255
|
+
return initial + [fourth, fifth]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def run_if_ready(session, args, descriptor):
|
|
259
|
+
if not session.run_requested or session.pending_clarification or session.geographic_pending or any(item['blocking'] for item in session.questions) or session.result().decision != 'execute':
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
print('Calcul en cours…')
|
|
263
|
+
if session.scenarios:
|
|
264
|
+
from websemantic.comparison import run
|
|
265
|
+
|
|
266
|
+
target, medians = run(session.scenarios, descriptor, args.workspace, args.output_dir)
|
|
267
|
+
for child in session.scenarios.values():
|
|
268
|
+
child.run_requested = False
|
|
269
|
+
else:
|
|
270
|
+
target, medians = execute(session.scenario, descriptor, args.workspace, args.output_dir)
|
|
271
|
+
session.run_requested = False
|
|
272
|
+
if getattr(session, 'geographic_context', None):
|
|
273
|
+
(target / 'geographic-context.json').write_text(json.dumps(session.geographic_context, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
274
|
+
if session.web_reports:
|
|
275
|
+
(target / 'web-context.json').write_text(json.dumps(session.web_reports, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
276
|
+
results_table(session, target, medians)
|
|
277
|
+
(target / 'conversation.json').write_text(json.dumps({
|
|
278
|
+
'environment': presentation(session).get('namespace', args.model), 'llm': args.llm, 'calls': session.calls, 'history': session.history,
|
|
279
|
+
'validation': asdict(session.result()),
|
|
280
|
+
}, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
281
|
+
if args.open_results and os.name == 'nt':
|
|
282
|
+
os.startfile(target)
|
|
283
|
+
return target, medians
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def answer_from_web(session, question, args):
|
|
287
|
+
if args.max_calls and session.calls >= args.max_calls:
|
|
288
|
+
raise GeminiError('Plafond local atteint ; la recherche documentaire nécessite un appel Gemini.')
|
|
289
|
+
load_private_key(args.workspace)
|
|
290
|
+
session.calls += 1
|
|
291
|
+
state = session.state()
|
|
292
|
+
report = web_research.research(question, args.llm, state, descriptor=session.descriptor)
|
|
293
|
+
session.add_questions(report.get('questions', []))
|
|
294
|
+
session.web_reports.append(report)
|
|
295
|
+
session.history.append({'user': question, 'assistant': report['answer'], 'web_context': report})
|
|
296
|
+
output_root = Path(getattr(args, 'output_dir', None) or args.workspace / 'runs')
|
|
297
|
+
output_root.mkdir(parents=True, exist_ok=True)
|
|
298
|
+
with (output_root / 'documentation.jsonl').open('a', encoding='utf-8') as journal:
|
|
299
|
+
journal.write(json.dumps(report, ensure_ascii=False) + '\n')
|
|
300
|
+
web_research.show_report(report, presentation(session).get("short_name", "WebSemantic"))
|
|
301
|
+
for item in report.get('questions', []):
|
|
302
|
+
print('Pour préciser :', item['question'])
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def main(argv=None):
|
|
306
|
+
parser = argparse.ArgumentParser(
|
|
307
|
+
description="Websemantic : conversation et simulation scientifique locale."
|
|
308
|
+
)
|
|
309
|
+
parser.add_argument("command", choices=["models", "describe", "chat"])
|
|
310
|
+
parser.add_argument("name", nargs="?", default=DEFAULT_MODEL)
|
|
311
|
+
parser.add_argument("--model", choices=[item[0] for item in ENVIRONMENTS.values()], help="Environnement pour un démarrage direct ou une demande non interactive.")
|
|
312
|
+
parser.add_argument('--direct', action='store_true', help='Démarrage sans menu pour les scripts.')
|
|
313
|
+
parser.add_argument("--llm", default="gemini-3.5-flash-lite")
|
|
314
|
+
parser.add_argument("--max-calls", type=int, default=0, help="0 : sans plafond local (quota fournisseur inchangé).")
|
|
315
|
+
parser.add_argument('--output-dir', type=Path, default=os.environ.get('WEBSEMANTIC_OUTPUT_DIR'))
|
|
316
|
+
parser.add_argument('--open-results', action='store_true')
|
|
317
|
+
parser.add_argument(
|
|
318
|
+
"--workspace",
|
|
319
|
+
type=Path,
|
|
320
|
+
default=Path(
|
|
321
|
+
os.environ.get("WEBSEMANTIC_WORKSPACE", Path(__file__).resolve().parents[2])
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
parser.add_argument(
|
|
325
|
+
"--once", help="Une seule demande sans menu interactif ; le calcul explicitement demandé reste soumis aux contrôles."
|
|
326
|
+
)
|
|
327
|
+
args = parser.parse_args(argv)
|
|
328
|
+
if args.command == "models":
|
|
329
|
+
for model, namespace, definition in ENVIRONMENTS.values():
|
|
330
|
+
print(f"{namespace} — {definition} — " + ('disponible' if model in AVAILABLE else 'Work in progress'))
|
|
331
|
+
return 0
|
|
332
|
+
if args.max_calls < 0:
|
|
333
|
+
parser.error("--max-calls doit être positif ou zéro.")
|
|
334
|
+
if args.name != DEFAULT_MODEL and args.model is None:
|
|
335
|
+
args.model = args.name
|
|
336
|
+
if args.model is not None and args.model not in AVAILABLE:
|
|
337
|
+
print(f'websemantic.{args.model} — Work in progress.')
|
|
338
|
+
if args.once or args.direct or args.command != 'chat':
|
|
339
|
+
return 2
|
|
340
|
+
args.model = None
|
|
341
|
+
if args.command == 'chat' and not args.once and not args.direct:
|
|
342
|
+
args.model = choose_environment()
|
|
343
|
+
if args.model is None:
|
|
344
|
+
return 0
|
|
345
|
+
args.model = args.model or DEFAULT_MODEL
|
|
346
|
+
try:
|
|
347
|
+
descriptor = load_descriptor(args.workspace, args.model)
|
|
348
|
+
except (OSError, ValueError, TypeError):
|
|
349
|
+
print(
|
|
350
|
+
"Descripteur introuvable : utiliser --workspace avec le dossier semantic-sim-layer."
|
|
351
|
+
)
|
|
352
|
+
return 1
|
|
353
|
+
if args.command == "describe":
|
|
354
|
+
print(yaml.safe_dump(descriptor, allow_unicode=True, sort_keys=False))
|
|
355
|
+
return 0
|
|
356
|
+
session = Session(descriptor)
|
|
357
|
+
namespace = presentation(session).get("namespace", "websemantic." + args.model)
|
|
358
|
+
last_output = None
|
|
359
|
+
choices = []
|
|
360
|
+
print(f"WebSemantic — {presentation(session).get('short_name', args.model)}")
|
|
361
|
+
print('Écrivez votre demande : je peux proposer des hypothèses, expliquer les paramètres ou lancer le calcul après validation.')
|
|
362
|
+
print(
|
|
363
|
+
"La validation ou une modification déclenche le calcul dès que le scénario est complet."
|
|
364
|
+
)
|
|
365
|
+
while True:
|
|
366
|
+
try:
|
|
367
|
+
may_run = False
|
|
368
|
+
line = args.once if args.once else input("\n" + namespace + " > ").strip()
|
|
369
|
+
except (EOFError, KeyboardInterrupt):
|
|
370
|
+
print("\nFin de session.")
|
|
371
|
+
return 0
|
|
372
|
+
try:
|
|
373
|
+
if not line:
|
|
374
|
+
continue
|
|
375
|
+
if choices and line in ('1', '2', '3', '4', '5'):
|
|
376
|
+
line = choices[int(line)-1][1]
|
|
377
|
+
choices = []
|
|
378
|
+
print('Choix >', line)
|
|
379
|
+
elif line not in ('/s', '\\s'):
|
|
380
|
+
choices = []
|
|
381
|
+
if line.startswith('\\'):
|
|
382
|
+
line = '/' + line[1:]
|
|
383
|
+
aliases = {'/r': '/run', '/v': '/accept', '/d': '/details', '/p': '/profile', '/a': '/help', '/q': '/quit', '/s': '/suggest'}
|
|
384
|
+
if line.startswith('/') and not line.startswith(('/set ', '/e ', '/web ')):
|
|
385
|
+
line = aliases.get(line.split()[0], line.split()[0])
|
|
386
|
+
if line in ("/quit", "/exit"):
|
|
387
|
+
return 0
|
|
388
|
+
if line == '/suggest':
|
|
389
|
+
choices = suggestions(session, last_output is not None)
|
|
390
|
+
print('Pour préciser votre objectif ou le scénario :')
|
|
391
|
+
for number, (label, _) in enumerate(choices, 1):
|
|
392
|
+
print(f'{number}. {label}')
|
|
393
|
+
print('Saisissez 1 à 5, ou écrivez votre question.')
|
|
394
|
+
elif line == '/events':
|
|
395
|
+
for field in presentation(session).get('events', []):
|
|
396
|
+
explain(session, field)
|
|
397
|
+
elif line == '/uncertainty':
|
|
398
|
+
print(presentation(session).get('uncertainty_text', 'Consultez les limites et incertitudes déclarées du modèle.'))
|
|
399
|
+
elif line == "/help":
|
|
400
|
+
print(
|
|
401
|
+
"/s suggestions | /show résumé | /d tableau | /details-all tous les paramètres | "
|
|
402
|
+
"/e NOM définition | /formulas formules | /p proposer | /v accepter | "
|
|
403
|
+
"/web QUESTION sources | /set GROUPE.NOM VALEUR modifier | /r calculer | /q quitter"
|
|
404
|
+
)
|
|
405
|
+
elif line == "/show":
|
|
406
|
+
show(session)
|
|
407
|
+
elif line == "/details":
|
|
408
|
+
details(session)
|
|
409
|
+
elif line == '/details-all':
|
|
410
|
+
details(session, full=True)
|
|
411
|
+
elif line == '/formulas':
|
|
412
|
+
formulas(session)
|
|
413
|
+
elif line.startswith('/e '):
|
|
414
|
+
explain(session, line.split(maxsplit=1)[1])
|
|
415
|
+
elif line.startswith('/web '):
|
|
416
|
+
answer_from_web(session, line.split(maxsplit=1)[1], args)
|
|
417
|
+
elif line == '/web':
|
|
418
|
+
print('Exemple : /web ' + presentation(session).get('web_example', 'sources scientifiques du modèle'))
|
|
419
|
+
elif line == "/profile":
|
|
420
|
+
session.propose_profile()
|
|
421
|
+
session.history.append({'user': '/profile', 'assistant': 'Profil de démonstration proposé, à valider.'})
|
|
422
|
+
may_run = True
|
|
423
|
+
print(
|
|
424
|
+
"Profil DEMONSTRATION propose, jamais des mesures reelles. Verifiez ci-dessous puis /accept."
|
|
425
|
+
)
|
|
426
|
+
show(session)
|
|
427
|
+
elif line == "/accept":
|
|
428
|
+
if session.geographic_pending:
|
|
429
|
+
print('Le contexte géographique n’a pas abouti. Reformulez la demande avec la ville pour réessayer ; /v ne remplace pas la consultation des sources.')
|
|
430
|
+
if args.once:
|
|
431
|
+
return 1
|
|
432
|
+
continue
|
|
433
|
+
if session.pending_clarification:
|
|
434
|
+
print(session.pending_clarification)
|
|
435
|
+
if args.once:
|
|
436
|
+
return 1
|
|
437
|
+
continue
|
|
438
|
+
session.accept_profile()
|
|
439
|
+
session.run_requested = True
|
|
440
|
+
session.history.append({'user': '/accept', 'assistant': 'Hypothèses acceptées ; calcul automatique soumis aux contrôles.'})
|
|
441
|
+
print(
|
|
442
|
+
"Hypothèses proposées acceptées."
|
|
443
|
+
)
|
|
444
|
+
show(session)
|
|
445
|
+
may_run = True
|
|
446
|
+
elif line.startswith("/set "):
|
|
447
|
+
_, path, text = line.split(" ", 2)
|
|
448
|
+
session.set_value(path, text)
|
|
449
|
+
session.history.append({'user': line, 'assistant': 'Paramètre modifié ; calcul automatique soumis aux contrôles.'})
|
|
450
|
+
show(session)
|
|
451
|
+
may_run = True
|
|
452
|
+
elif line == "/run":
|
|
453
|
+
session.run_requested = True
|
|
454
|
+
may_run = True
|
|
455
|
+
show(session)
|
|
456
|
+
elif line.startswith("/"):
|
|
457
|
+
print("Commande inconnue. /help")
|
|
458
|
+
else:
|
|
459
|
+
if web_research.explicitly_requested(line):
|
|
460
|
+
answer_from_web(session, line, args)
|
|
461
|
+
if args.once:
|
|
462
|
+
return 0
|
|
463
|
+
continue
|
|
464
|
+
previous_run_requested = session.run_requested
|
|
465
|
+
session.request_calculation(line)
|
|
466
|
+
if line.lower().strip() in ('annule le calcul', 'ne calcule pas', 'stop le calcul'):
|
|
467
|
+
print('Demande de calcul en attente annulée. Le scénario est conservé.')
|
|
468
|
+
if args.once:
|
|
469
|
+
return 0
|
|
470
|
+
continue
|
|
471
|
+
if line.lower().strip() in ('calcule', 'calcule maintenant', 'lance le calcul', 'simule', 'fais le calcul', 'recalcule'):
|
|
472
|
+
show(session)
|
|
473
|
+
output = run_if_ready(session, args, descriptor)
|
|
474
|
+
if output:
|
|
475
|
+
last_output = output
|
|
476
|
+
if args.once:
|
|
477
|
+
return 0
|
|
478
|
+
continue
|
|
479
|
+
if last_output and not session.run_requested and ('moyenne' in line.lower() or 'résultat' in line.lower()) and not any(word in line.lower() for word in ('prend', 'utilise', 'profil', 'mix')):
|
|
480
|
+
results_table(session, *last_output)
|
|
481
|
+
print('Ces résultats concernent le dernier calcul enregistré ; /r recalcule le scénario actuel.')
|
|
482
|
+
if args.once:
|
|
483
|
+
return 0
|
|
484
|
+
continue
|
|
485
|
+
place = location(line, descriptor)
|
|
486
|
+
if place and session.geographic_context and session.geographic_context.get('city') == descriptor['geography']['places'][place]['city'] and not any(word in line.lower() for word in ('recherche', 'internet', 'sources', 'actualise', 'documente')):
|
|
487
|
+
place = None
|
|
488
|
+
if place:
|
|
489
|
+
session.geographic_pending = True
|
|
490
|
+
session.pending_clarification = 'Le contexte géographique demandé doit être consulté et validé avant le calcul.'
|
|
491
|
+
if args.max_calls and session.calls + 2 > args.max_calls:
|
|
492
|
+
raise GeminiError('Ce contexte nécessite deux appels Gemini ; plafond local insuffisant.')
|
|
493
|
+
load_private_key(args.workspace)
|
|
494
|
+
session.calls += 1
|
|
495
|
+
parsed, _usage = extract(line, descriptor, session.history, args.llm, state=session.state())
|
|
496
|
+
session.apply(line, parsed)
|
|
497
|
+
session.pending_clarification = 'Analyse géographique en attente ; aucun calcul.'
|
|
498
|
+
session.calls += 1
|
|
499
|
+
report = research(place, args.llm, descriptor)
|
|
500
|
+
apply_report(session, line, report)
|
|
501
|
+
show_report(session)
|
|
502
|
+
show(session)
|
|
503
|
+
if args.once:
|
|
504
|
+
return 0
|
|
505
|
+
continue
|
|
506
|
+
local_message = session.local_intent(line)
|
|
507
|
+
if local_message:
|
|
508
|
+
print(presentation(session).get('short_name', 'WebSemantic') + ' >', local_message)
|
|
509
|
+
show(session)
|
|
510
|
+
output = run_if_ready(session, args, descriptor)
|
|
511
|
+
if output:
|
|
512
|
+
last_output = output
|
|
513
|
+
if args.once:
|
|
514
|
+
return 0
|
|
515
|
+
continue
|
|
516
|
+
if args.max_calls and session.calls >= args.max_calls:
|
|
517
|
+
print(
|
|
518
|
+
"Plafond d'appels atteint. Les commandes locales restent disponibles."
|
|
519
|
+
)
|
|
520
|
+
if args.once:
|
|
521
|
+
return 1
|
|
522
|
+
continue
|
|
523
|
+
session.calls += 1
|
|
524
|
+
load_private_key(args.workspace)
|
|
525
|
+
state = session.state()
|
|
526
|
+
parsed, _usage = extract(line, descriptor, session.history, args.llm, state=state)
|
|
527
|
+
if parsed.get('needs_web') is True:
|
|
528
|
+
if session.run_requested and not previous_run_requested:
|
|
529
|
+
session.pending_clarification = 'Cette demande de calcul nécessite des informations externes. Précisez et validez les paramètres concernés ; /v seul ne résout pas cette demande.'
|
|
530
|
+
session.run_requested = previous_run_requested
|
|
531
|
+
answer_from_web(session, line, args)
|
|
532
|
+
if args.once:
|
|
533
|
+
return 0
|
|
534
|
+
continue
|
|
535
|
+
actions = requested_actions(parsed)
|
|
536
|
+
unresolved = session.pending_clarification
|
|
537
|
+
session.apply(line, parsed)
|
|
538
|
+
if unresolved and not session.scenarios and not parsed['updates'] and actions:
|
|
539
|
+
session.pending_clarification = unresolved
|
|
540
|
+
if 'quit' in actions:
|
|
541
|
+
return 0
|
|
542
|
+
if 'propose' in actions:
|
|
543
|
+
session.propose_profile()
|
|
544
|
+
if 'accept' in actions:
|
|
545
|
+
if session.geographic_pending or session.pending_clarification:
|
|
546
|
+
raise ClarificationNeeded(session.pending_clarification or 'Le contexte documentaire doit être résolu avant validation.')
|
|
547
|
+
if not explicit_consent(line, parsed.get('acceptance_evidence')):
|
|
548
|
+
session.run_requested = False
|
|
549
|
+
raise ClarificationNeeded('Les hypothèses restent non acceptées. Confirmez explicitement votre accord si vous souhaitez les utiliser.')
|
|
550
|
+
session.accept_profile()
|
|
551
|
+
session.run_requested = True
|
|
552
|
+
session.history.append({'user': line, 'assistant': 'Hypothèses explicitement acceptées après extraction des valeurs fournies.'})
|
|
553
|
+
if 'details' in actions:
|
|
554
|
+
details(session, full=parsed.get('detail_level') == 'full')
|
|
555
|
+
if 'explain' in actions:
|
|
556
|
+
explain(session, parsed.get('parameter', ''))
|
|
557
|
+
if 'suggest' in actions:
|
|
558
|
+
for number, (label, _) in enumerate(suggestions(session, last_output is not None), 1):
|
|
559
|
+
print(f'{number}. {label}')
|
|
560
|
+
if 'formulas' in actions:
|
|
561
|
+
formulas(session)
|
|
562
|
+
if not (session.run_requested and session.result().decision == 'execute'):
|
|
563
|
+
print("WebSemantic >", parsed.get("message", ""))
|
|
564
|
+
if parsed["updates"] or session.run_requested or actions & {"propose", "accept"}:
|
|
565
|
+
show(session)
|
|
566
|
+
elif session.pending_clarification:
|
|
567
|
+
print(session.pending_clarification)
|
|
568
|
+
ask_questions(session, parsed.get('questions', []))
|
|
569
|
+
for chunk in parsed.get('scenario_updates', []):
|
|
570
|
+
child = session.scenarios.get(chunk['scenario'])
|
|
571
|
+
if child and any(not item['blocking'] for item in chunk.get('questions', [])):
|
|
572
|
+
print(chunk['scenario'] + ' :')
|
|
573
|
+
ask_questions(child, chunk.get('questions', []))
|
|
574
|
+
may_run = True
|
|
575
|
+
if may_run:
|
|
576
|
+
output = run_if_ready(session, args, descriptor)
|
|
577
|
+
if output:
|
|
578
|
+
last_output = output
|
|
579
|
+
if args.once:
|
|
580
|
+
return 0
|
|
581
|
+
except ClarificationNeeded as exc:
|
|
582
|
+
print(f"\nWebsemantic > {exc}")
|
|
583
|
+
if args.once:
|
|
584
|
+
return 1
|
|
585
|
+
except (GeminiError, ValueError, KeyError, TypeError, OSError) as exc:
|
|
586
|
+
print(f"Erreur : {exc}")
|
|
587
|
+
if args.once:
|
|
588
|
+
return 1
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
if __name__ == "__main__":
|
|
592
|
+
raise SystemExit(main())
|