expAge commited on
Commit
e45232e
·
1 Parent(s): 9e8fbdf

feat(enrich): inverser le format du nom de soumission — horodatage en tête

Browse files

Avant : from_{model}_automatic_submission_{HHhMM}_{DD-MM-YY}.enrich
Après : {HHhMM}_{DD-MM-YY}_automatic_submission_from_{model}.enrich

Avantage : tri chronologique naturel des fichiers dans un dossier
(alphabétique = chronologique pour HH:MM, après tri par date c'est même
le bon ordre quand on regarde une seule journée).

Côté docstring write_submission_file, durcissement sur model_name :
le LLM doit passer son VRAI identifiant tel qu'affiché par Claude Code
(« Opus 4.7 » → "claude-opus-4-7"), ne pas deviner ; mieux vaut laisser
vide (fallback "mcp_client") qu'inventer un mauvais nom.

Tests mis à jour, 89/89 verts.

src/jdm_agent/enrich/pipeline.py CHANGED
@@ -18,11 +18,12 @@ def compute_submission_filename(model_name: str, *,
18
  now: Optional[datetime] = None) -> str:
19
  """Nom standardisé d'un fichier de soumission LLMDrops.
20
 
21
- Format : `from_{model_slug}_automatic_submission_{HH}h{MM}_{DD}-{MM}-{YY}.enrich`
22
 
 
23
  Le `model_name` est slugifié : espaces et caractères non-sûrs (URL/shell)
24
  sont remplacés par `_`, le reste est conservé (on garde les tirets et le
25
- point usuels des noms de modèles type `claude-sonnet-4-7` ou `gpt-4.1`).
26
  Le timestamp utilise l'heure LOCALE (cohérent avec le contexte utilisateur).
27
 
28
  Args:
@@ -39,8 +40,7 @@ def compute_submission_filename(model_name: str, *,
39
  slug = re.sub(r"[^A-Za-z0-9._-]+", "_", model_name).strip("_") or "unknown"
40
  ts = (now or datetime.now())
41
  return (
42
- f"from_{slug}_automatic_submission_"
43
- f"{ts:%Hh%M}_{ts:%d-%m-%y}.enrich"
44
  )
45
 
46
 
 
18
  now: Optional[datetime] = None) -> str:
19
  """Nom standardisé d'un fichier de soumission LLMDrops.
20
 
21
+ Format : `{HH}h{MM}_{DD}-{MM}-{YY}_automatic_submission_from_{model_slug}.enrich`
22
 
23
+ L'horodatage en tête sert au tri chronologique naturel dans un dossier.
24
  Le `model_name` est slugifié : espaces et caractères non-sûrs (URL/shell)
25
  sont remplacés par `_`, le reste est conservé (on garde les tirets et le
26
+ point usuels des noms de modèles type `claude-opus-4-7` ou `gpt-4.1`).
27
  Le timestamp utilise l'heure LOCALE (cohérent avec le contexte utilisateur).
28
 
29
  Args:
 
40
  slug = re.sub(r"[^A-Za-z0-9._-]+", "_", model_name).strip("_") or "unknown"
41
  ts = (now or datetime.now())
42
  return (
43
+ f"{ts:%Hh%M}_{ts:%d-%m-%y}_automatic_submission_from_{slug}.enrich"
 
44
  )
45
 
46
 
src/jdm_agent/tools/jdm_tools.py CHANGED
@@ -1046,10 +1046,15 @@ def write_submission_file(
1046
  SOUMISSION AUTOMATIQUE au LLMDrops (Phase 12, opt-in) :
1047
  - `upload=True` : après écriture locale, POST le fichier au endpoint
1048
  LLMDrops JDM. Le fichier est uploadé sous un nom standardisé
1049
- `from_{model}_automatic_submission_{HHhMM}_{DD-MM-YY}.enrich` qui
1050
- trace quel LLM a produit la soumission et quand.
1051
- - `model_name` : nom du LLM source (ex. "claude-sonnet-4-7", "gpt-5").
1052
- Vide fallback sur l'env `LLM_MODEL` puis `"mcp_client"`.
 
 
 
 
 
1053
  - `api_key` : clé API LLMDrops. Vide → lue dans `JDM_DROPS_API_KEY`.
1054
  - Défaut `upload=False` : pas de transmission silencieuse, le LLM
1055
  doit demander l'upload explicitement.
 
1046
  SOUMISSION AUTOMATIQUE au LLMDrops (Phase 12, opt-in) :
1047
  - `upload=True` : après écriture locale, POST le fichier au endpoint
1048
  LLMDrops JDM. Le fichier est uploadé sous un nom standardisé
1049
+ `{HHhMM}_{DD-MM-YY}_automatic_submission_from_{model}.enrich` qui
1050
+ permet le tri chronologique naturel et trace quel LLM a produit
1051
+ la soumission.
1052
+ - `model_name` : nom EXACT du LLM source (ex. "claude-opus-4-7",
1053
+ "claude-sonnet-4-5", "gpt-5"). ⚠️ Ne DEVINE PAS, ne MÉLANGE PAS les
1054
+ versions : tu DOIS passer ton VRAI identifiant tel qu'il apparaît
1055
+ dans ta config (Claude Code l'affiche en bas du terminal). Si tu
1056
+ n'es pas sûr, laisse vide → fallback sur env `LLM_MODEL` puis
1057
+ `"mcp_client"`. Mieux vaut "mcp_client" qu'un nom inventé.
1058
  - `api_key` : clé API LLMDrops. Vide → lue dans `JDM_DROPS_API_KEY`.
1059
  - Défaut `upload=False` : pas de transmission silencieuse, le LLM
1060
  doit demander l'upload explicitement.
tests/test_tools.py CHANGED
@@ -383,8 +383,8 @@ def test_write_submission_file_with_upload_success(tmp_path, monkeypatch):
383
  assert out["count"] == 1
384
  assert out["upload"]["ok"] is True
385
  assert out["upload"]["status_code"] == 200
386
- assert out["upload"]["uploaded_as"].startswith(
387
- "from_claude-sonnet-4-7_automatic_submission_"
388
  )
389
  assert out["upload"]["response"] == {"status": "ok", "id": 7}
390
 
 
383
  assert out["count"] == 1
384
  assert out["upload"]["ok"] is True
385
  assert out["upload"]["status_code"] == 200
386
+ assert out["upload"]["uploaded_as"].endswith(
387
+ "_automatic_submission_from_claude-sonnet-4-7.enrich"
388
  )
389
  assert out["upload"]["response"] == {"status": "ok", "id": 7}
390
 
tests/test_uploader.py CHANGED
@@ -15,28 +15,28 @@ from jdm_agent.enrich.uploader import DEFAULT_ENDPOINT_URL
15
 
16
  def test_filename_basic_format():
17
  now = datetime(2026, 5, 27, 14, 32, 0)
18
- name = compute_submission_filename("claude-sonnet-4-7", now=now)
19
- assert name == "from_claude-sonnet-4-7_automatic_submission_14h32_27-05-26.enrich"
20
 
21
 
22
  def test_filename_slug_spaces_to_underscores():
23
  now = datetime(2026, 1, 3, 9, 5, 0)
24
- name = compute_submission_filename("Claude Sonnet 4.7", now=now)
25
  # Les espaces → '_', le point conservé (autorisé en filename), pas de doublon.
26
- assert name == "from_Claude_Sonnet_4.7_automatic_submission_09h05_03-01-26.enrich"
27
 
28
 
29
  def test_filename_slug_strips_dangerous_chars():
30
  now = datetime(2026, 12, 31, 23, 59, 0)
31
  name = compute_submission_filename("gpt-5/turbo:beta", now=now)
32
  # Slashes et deux-points → '_' (sécurité URL / shell).
33
- assert name == "from_gpt-5_turbo_beta_automatic_submission_23h59_31-12-26.enrich"
34
 
35
 
36
  def test_filename_empty_model_falls_back():
37
  now = datetime(2026, 5, 27, 14, 32, 0)
38
  name = compute_submission_filename("", now=now)
39
- assert name == "from_unknown_automatic_submission_14h32_27-05-26.enrich"
40
 
41
 
42
  def test_filename_only_unsafe_chars_falls_back():
@@ -64,7 +64,8 @@ def test_submit_missing_api_key(tmp_path, monkeypatch):
64
  assert out["ok"] is False
65
  assert "API" in out["error"] or "JDM_DROPS_API_KEY" in out["error"]
66
  # Mais le filename uploadé est quand même calculé (utile pour log).
67
- assert out["uploaded_as"].startswith("from_")
 
68
 
69
 
70
  @respx.mock
@@ -76,13 +77,13 @@ def test_submit_success_with_json_response(tmp_path):
76
  return_value=httpx.Response(200, json={"status": "ok", "drop_id": 42})
77
  )
78
 
79
- out = submit_to_jdm(p, api_key="secret-key", model_name="claude-sonnet-4-7")
80
 
81
  assert route.called
82
  assert out["ok"] is True
83
  assert out["status_code"] == 200
84
  assert out["response"] == {"status": "ok", "drop_id": 42}
85
- assert out["uploaded_as"].startswith("from_claude-sonnet-4-7_automatic_submission_")
86
  assert out["endpoint"] == DEFAULT_ENDPOINT_URL
87
  assert out["error"] is None
88
 
@@ -91,7 +92,7 @@ def test_submit_success_with_json_response(tmp_path):
91
  assert request.headers["X-API-Key"] == "secret-key"
92
  body = request.content.decode("utf-8", errors="replace")
93
  # Multipart contient le filename uploadé et le format=json.
94
- assert "from_claude-sonnet-4-7_automatic_submission_" in body
95
  assert "format" in body and "json" in body
96
 
97
 
 
15
 
16
  def test_filename_basic_format():
17
  now = datetime(2026, 5, 27, 14, 32, 0)
18
+ name = compute_submission_filename("claude-opus-4-7", now=now)
19
+ assert name == "14h32_27-05-26_automatic_submission_from_claude-opus-4-7.enrich"
20
 
21
 
22
  def test_filename_slug_spaces_to_underscores():
23
  now = datetime(2026, 1, 3, 9, 5, 0)
24
+ name = compute_submission_filename("Claude Opus 4.7", now=now)
25
  # Les espaces → '_', le point conservé (autorisé en filename), pas de doublon.
26
+ assert name == "09h05_03-01-26_automatic_submission_from_Claude_Opus_4.7.enrich"
27
 
28
 
29
  def test_filename_slug_strips_dangerous_chars():
30
  now = datetime(2026, 12, 31, 23, 59, 0)
31
  name = compute_submission_filename("gpt-5/turbo:beta", now=now)
32
  # Slashes et deux-points → '_' (sécurité URL / shell).
33
+ assert name == "23h59_31-12-26_automatic_submission_from_gpt-5_turbo_beta.enrich"
34
 
35
 
36
  def test_filename_empty_model_falls_back():
37
  now = datetime(2026, 5, 27, 14, 32, 0)
38
  name = compute_submission_filename("", now=now)
39
+ assert name == "14h32_27-05-26_automatic_submission_from_unknown.enrich"
40
 
41
 
42
  def test_filename_only_unsafe_chars_falls_back():
 
64
  assert out["ok"] is False
65
  assert "API" in out["error"] or "JDM_DROPS_API_KEY" in out["error"]
66
  # Mais le filename uploadé est quand même calculé (utile pour log).
67
+ assert "_automatic_submission_from_" in out["uploaded_as"]
68
+ assert out["uploaded_as"].endswith(".enrich")
69
 
70
 
71
  @respx.mock
 
77
  return_value=httpx.Response(200, json={"status": "ok", "drop_id": 42})
78
  )
79
 
80
+ out = submit_to_jdm(p, api_key="secret-key", model_name="claude-opus-4-7")
81
 
82
  assert route.called
83
  assert out["ok"] is True
84
  assert out["status_code"] == 200
85
  assert out["response"] == {"status": "ok", "drop_id": 42}
86
+ assert "_automatic_submission_from_claude-opus-4-7.enrich" in out["uploaded_as"]
87
  assert out["endpoint"] == DEFAULT_ENDPOINT_URL
88
  assert out["error"] is None
89
 
 
92
  assert request.headers["X-API-Key"] == "secret-key"
93
  body = request.content.decode("utf-8", errors="replace")
94
  # Multipart contient le filename uploadé et le format=json.
95
+ assert "_automatic_submission_from_claude-opus-4-7.enrich" in body
96
  assert "format" in body and "json" in body
97
 
98