expAge commited on
Commit ·
e45232e
1
Parent(s): 9e8fbdf
feat(enrich): inverser le format du nom de soumission — horodatage en tête
Browse filesAvant : 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 +4 -4
- src/jdm_agent/tools/jdm_tools.py +9 -4
- tests/test_tools.py +2 -2
- tests/test_uploader.py +11 -10
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 : `
|
| 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-
|
| 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"
|
| 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 |
-
`
|
| 1050 |
-
trace quel LLM a produit
|
| 1051 |
-
|
| 1052 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"].
|
| 387 |
-
"
|
| 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-
|
| 19 |
-
assert name == "
|
| 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
|
| 25 |
# Les espaces → '_', le point conservé (autorisé en filename), pas de doublon.
|
| 26 |
-
assert name == "
|
| 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 == "
|
| 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 == "
|
| 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"]
|
|
|
|
| 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-
|
| 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
|
| 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 "
|
| 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 |
|